pax_global_header00006660000000000000000000000064151214466450014522gustar00rootroot0000000000000052 comment=898beb369a8be32686dcf656ec4f842b1cd432aa sdp-3.0.17/000077500000000000000000000000001512144664500124005ustar00rootroot00000000000000sdp-3.0.17/.github/000077500000000000000000000000001512144664500137405ustar00rootroot00000000000000sdp-3.0.17/.github/.ci.conf000066400000000000000000000002231512144664500152550ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT EXCLUDED_CONTRIBUTORS=('Josh Bleecher Snyder') sdp-3.0.17/.github/.gitignore000066400000000000000000000001561512144664500157320ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT .goassets sdp-3.0.17/.github/fetch-scripts.sh000077500000000000000000000016001512144664500170520ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT set -eu SCRIPT_PATH="$(realpath "$(dirname "$0")")" GOASSETS_PATH="${SCRIPT_PATH}/.goassets" GOASSETS_REF=${GOASSETS_REF:-master} if [ -d "${GOASSETS_PATH}" ]; then if ! git -C "${GOASSETS_PATH}" diff --exit-code; then echo "${GOASSETS_PATH} has uncommitted changes" >&2 exit 1 fi git -C "${GOASSETS_PATH}" fetch origin git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} else git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" fi sdp-3.0.17/.github/install-hooks.sh000077500000000000000000000012421512144664500170650ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT SCRIPT_PATH="$(realpath "$(dirname "$0")")" . ${SCRIPT_PATH}/fetch-scripts.sh cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" sdp-3.0.17/.github/workflows/000077500000000000000000000000001512144664500157755ustar00rootroot00000000000000sdp-3.0.17/.github/workflows/api.yaml000066400000000000000000000011141512144664500174270ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: API on: pull_request: jobs: check: uses: pion/.goassets/.github/workflows/api.reusable.yml@master sdp-3.0.17/.github/workflows/codeql-analysis.yml000066400000000000000000000013201512144664500216040ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: CodeQL on: workflow_dispatch: schedule: - cron: '23 5 * * 0' pull_request: branches: - master paths: - '**.go' jobs: analyze: uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master sdp-3.0.17/.github/workflows/fuzz.yaml000066400000000000000000000013421512144664500176570ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Fuzz on: push: branches: - master schedule: - cron: "0 */8 * * *" jobs: fuzz: uses: pion/.goassets/.github/workflows/fuzz.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version fuzz-time: "60s" sdp-3.0.17/.github/workflows/lint.yaml000066400000000000000000000011151512144664500176250ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Lint on: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/lint.reusable.yml@master sdp-3.0.17/.github/workflows/release.yml000066400000000000000000000012501512144664500201360ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Release on: push: tags: - 'v*' jobs: release: uses: pion/.goassets/.github/workflows/release.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version sdp-3.0.17/.github/workflows/renovate-go-sum-fix.yaml000066400000000000000000000012671512144664500225030ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Fix go.sum on: push: branches: - renovate/* jobs: fix: uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master secrets: token: ${{ secrets.PIONBOT_PRIVATE_KEY }} sdp-3.0.17/.github/workflows/reuse.yml000066400000000000000000000011511512144664500176410ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: REUSE Compliance Check on: push: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master sdp-3.0.17/.github/workflows/test.yaml000066400000000000000000000033271512144664500176450ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Test on: push: branches: - master pull_request: jobs: test: uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} secrets: inherit test-i386: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-windows: uses: pion/.goassets/.github/workflows/test-windows.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-macos: uses: pion/.goassets/.github/workflows/test-macos.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-wasm: uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version secrets: inherit sdp-3.0.17/.github/workflows/tidy-check.yaml000066400000000000000000000013021512144664500207010ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Go mod tidy on: pull_request: push: branches: - master jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version sdp-3.0.17/.gitignore000066400000000000000000000006321512144664500143710ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT ### JetBrains IDE ### ##################### .idea/ ### Emacs Temporary Files ### ############################# *~ ### Folders ### ############### bin/ vendor/ node_modules/ ### Files ### ############# *.ivf *.ogg tags cover.out *.sw[poe] *.wasm examples/sfu-ws/cert.pem examples/sfu-ws/key.pem wasm_exec.js sdp-3.0.17/.golangci.yml000066400000000000000000000202661512144664500147720ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT version: "2" linters: enable: - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers - bidichk # Checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - containedctx # containedctx is a linter that detects struct contained context.Context field - contextcheck # check the function whether use a non-inherited context - cyclop # checks function and package cyclomatic complexity - decorder # check declaration order and count of types, constants, variables and functions - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dupl # Tool for code clone detection - durationcheck # check for two durations multiplied together - err113 # Golang linter to check the errors handling expressions - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. - exhaustive # check exhaustiveness of enum switch statements - forbidigo # Forbids identifiers - forcetypeassert # finds forced type assertions - gochecknoglobals # Checks that no globals are present in Go code - gocognit # Computes and checks the cognitive complexity of functions - goconst # Finds repeated strings that could be replaced by a constant - gocritic # The most opinionated Go source code linter - gocyclo # Computes and checks the cyclomatic complexity of functions - godot # Check if comments end in a period - godox # Tool for detection of FIXME, TODO and other comment keywords - goheader # Checks is file header matches to pattern - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - goprintffuncname # Checks that printf-like functions are named with `f` at the end - gosec # Inspects source code for security problems - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - grouper # An analyzer to analyze expression groups. - importas # Enforces consistent import aliases - ineffassign # Detects when assignments to existing variables are not used - lll # Reports long lines - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - misspell # Finds commonly misspelled English words in comments - nakedret # Finds naked returns in functions greater than a specified function length - nestif # Reports deeply nested if statements - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - noctx # noctx finds sending http request without context.Context - predeclared # find code that shadows one of Go's predeclared identifiers - revive # golint replacement, finds style mistakes - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks - tagliatelle # Checks the struct tags. - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers - unconvert # Remove unnecessary type conversions - unparam # Reports unused function parameters - unused # Checks Go code for unused constants, variables, functions and types - varnamelen # checks that the length of a variable's name matches its scope - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: - depguard # Go linter that checks if package imports are in a list of acceptable packages - funlen # Tool for detection of long functions - gochecknoinits # Checks that no init functions are present in Go code - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. - interfacebloat # A linter that checks length of interface. - ireturn # Accept Interfaces, Return Concrete Types - mnd # An analyzer to detect magic numbers - nolintlint # Reports ill-formed or insufficient nolint directives - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test - prealloc # Finds slice declarations that could potentially be preallocated - promlinter # Check Prometheus metrics naming via promlint - rowserrcheck # checks whether Err of rows is checked successfully - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. - testpackage # linter that makes you use a separate _test package - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - wrapcheck # Checks that errors returned from external packages are wrapped - wsl # Whitespace Linter - Forces you to use empty lines! settings: staticcheck: checks: - all - -QF1008 # "could remove embedded field", to keep it explicit! - -QF1003 # "could use tagged switch on enum", Cases conflicts with exhaustive! exhaustive: default-signifies-exhaustive: true forbidigo: forbid: - pattern: ^fmt.Print(f|ln)?$ - pattern: ^log.(Panic|Fatal|Print)(f|ln)?$ - pattern: ^os.Exit$ - pattern: ^panic$ - pattern: ^print(ln)?$ - pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$ pkg: ^testing$ msg: use testify/assert instead analyze-types: true gomodguard: blocked: modules: - github.com/pkg/errors: recommendations: - errors govet: enable: - shadow revive: rules: # Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility - name: use-any severity: warning disabled: false misspell: locale: US varnamelen: max-distance: 12 min-name-length: 2 ignore-type-assert-ok: true ignore-map-index-ok: true ignore-chan-recv-ok: true ignore-decls: - i int - n int - w io.Writer - r io.Reader - b []byte exclusions: generated: lax rules: - linters: - forbidigo - gocognit path: (examples|main\.go) - linters: - gocognit path: _test\.go - linters: - forbidigo path: cmd formatters: enable: - gci # Gci control golang package import order and make it always deterministic. - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification - gofumpt # Gofumpt checks whether code was gofumpt-ed. - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports exclusions: generated: lax sdp-3.0.17/.goreleaser.yml000066400000000000000000000001711512144664500153300ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT builds: - skip: true sdp-3.0.17/.reuse/000077500000000000000000000000001512144664500136015ustar00rootroot00000000000000sdp-3.0.17/.reuse/dep5000066400000000000000000000011141512144664500143560ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Pion Source: https://github.com/pion/ Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json yarn.lock Copyright: 2023 The Pion community License: MIT Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt Copyright: 2023 The Pion community License: CC0-1.0 sdp-3.0.17/LICENSE000066400000000000000000000021051512144664500134030ustar00rootroot00000000000000MIT License Copyright (c) 2023 The Pion community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sdp-3.0.17/LICENSES/000077500000000000000000000000001512144664500136055ustar00rootroot00000000000000sdp-3.0.17/LICENSES/MIT.txt000066400000000000000000000020661512144664500150030ustar00rootroot00000000000000MIT License Copyright (c) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sdp-3.0.17/README.md000066400000000000000000000044741512144664500136700ustar00rootroot00000000000000


Pion SDP

A Go implementation of the SDP

Pion SDP Sourcegraph Widget join us on Discord Follow us on Bluesky
GitHub Workflow Status Go Reference Coverage Status Go Report Card License: MIT


### Roadmap The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. ### Community Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. We are always looking to support **your projects**. Please reach out if you have something to build! If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) ### Contributing Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible ### License MIT License - see [LICENSE](LICENSE) for full text sdp-3.0.17/base_lexer.go000066400000000000000000000071541512144664500150470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "errors" "fmt" "io" "slices" "strconv" ) var errDocumentStart = errors.New("already on document start") type syntaxError struct { s string i int } func (e syntaxError) Error() string { if e.i < 0 { e.i = 0 } return fmt.Sprintf("sdp: syntax error at pos %d: %s", e.i, strconv.QuoteToASCII(e.s[e.i:e.i+1])) } type baseLexer struct { value string pos int } func (l baseLexer) syntaxError() error { return syntaxError{s: l.value, i: l.pos - 1} } func (l *baseLexer) unreadByte() error { if l.pos <= 0 { return errDocumentStart } l.pos-- return nil } func (l *baseLexer) readByte() (byte, error) { if l.pos >= len(l.value) { return byte(0), io.EOF } ch := l.value[l.pos] l.pos++ return ch, nil } func (l *baseLexer) nextLine() error { for { ch, err := l.readByte() if errors.Is(err, io.EOF) { return nil } else if err != nil { return err } if !isNewline(ch) { return l.unreadByte() } } } func (l *baseLexer) readWhitespace() error { //notlint:cyclop for { ch, err := l.readByte() if errors.Is(err, io.EOF) { return nil } else if err != nil { return err } if !isWhitespace(ch) { return l.unreadByte() } } } func (l *baseLexer) readUint64Field() (i uint64, err error) { //nolint:cyclop for { ch, err := l.readByte() if errors.Is(err, io.EOF) && i > 0 { break } else if err != nil { return i, err } if isNewline(ch) { if err := l.unreadByte(); err != nil { return i, err } break } if isWhitespace(ch) { if err := l.readWhitespace(); err != nil { return i, err } break } switch ch { case '0': i *= 10 case '1': i = i*10 + 1 case '2': i = i*10 + 2 case '3': i = i*10 + 3 case '4': i = i*10 + 4 case '5': i = i*10 + 5 case '6': i = i*10 + 6 case '7': i = i*10 + 7 case '8': i = i*10 + 8 case '9': i = i*10 + 9 default: return i, l.syntaxError() } } return i, nil } // Returns next field on this line or empty string if no more fields on line. func (l *baseLexer) readField() (string, error) { start := l.pos var stop int for { stop = l.pos ch, err := l.readByte() if errors.Is(err, io.EOF) && stop > start { break } else if err != nil { return "", err } if isNewline(ch) { if err := l.unreadByte(); err != nil { return "", err } break } if isWhitespace(ch) { if err := l.readWhitespace(); err != nil { return "", err } break } } return l.value[start:stop], nil } func (l *lexer) readRequiredField() (string, error) { field, err := l.readField() if err != nil { return "", err } if field == "" { return "", errFieldMissing } return field, nil } // Returns symbols until line end. func (l *baseLexer) readLine() (string, error) { start := l.pos trim := 1 for { ch, err := l.readByte() if err != nil { return "", err } if ch == '\r' { trim++ } if ch == '\n' { return l.value[start : l.pos-trim], nil } } } func (l *baseLexer) readType() (byte, error) { for { firstByte, err := l.readByte() if err != nil { return 0, err } if isNewline(firstByte) { continue } secondByte, err := l.readByte() if err != nil { return 0, err } if secondByte != '=' { return firstByte, l.syntaxError() } return firstByte, nil } } func isNewline(ch byte) bool { return ch == '\n' || ch == '\r' } func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' } func anyOf(element string, data ...string) bool { return slices.Contains(data, element) } sdp-3.0.17/base_lexer_test.go000066400000000000000000000122111512144664500160740ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "io" "testing" "github.com/stretchr/testify/assert" ) func TestLexer(t *testing.T) { t.Run("single field", func(t *testing.T) { for k, value := range map[string]string{ "clean": "aaa", "with extra space": "aaa ", "with linebreak": "aaa \n", "with linebreak 2": "aaa \r\n", } { l := &baseLexer{value: value} field, err := l.readField() assert.NoError(t, err) assert.Equalf(t, "aaa", field, "%s: aaa not parsed, got: '%v'", k, field) } }) t.Run("syntax error", func(t *testing.T) { l := &baseLexer{value: "12NaN"} _, err := l.readUint64Field() assert.Error(t, err) }) t.Run("many fields", func(t *testing.T) { lex := &baseLexer{value: "aaa 123\nf1 f2\nlast"} t.Run("first line", func(t *testing.T) { field, err := lex.readField() assert.NoError(t, err) assert.Equal(t, "aaa", field) value, err := lex.readUint64Field() assert.NoError(t, err) assert.Equal(t, value, uint64(123)) assert.NoError(t, lex.nextLine()) }) t.Run("second line", func(t *testing.T) { field, err := lex.readField() assert.NoError(t, err) assert.Equal(t, "f1", field) field, err = lex.readField() assert.NoError(t, err) assert.Equal(t, "f2", field) field, err = lex.readField() assert.NoError(t, err) assert.Empty(t, field) assert.NoError(t, lex.nextLine()) }) t.Run("last line", func(t *testing.T) { field, err := lex.readField() assert.NoError(t, err) assert.Equal(t, "last", field) }) }) } func TestSyntaxError_Error(t *testing.T) { t.Run("index in range", func(t *testing.T) { e := syntaxError{s: "hello", i: 1} assert.Equal(t, byte('e'), e.s[e.i]) }) t.Run("negative index coerced to zero", func(t *testing.T) { e := syntaxError{s: "hello", i: -2} assert.NotPanics(t, func() { _ = e.Error() }) }) t.Run("escaped newline", func(t *testing.T) { e := syntaxError{s: "a\nb", i: 1} // points to '\n' assert.Equal(t, byte('\n'), e.s[e.i]) }) } func TestUnreadByte_ErrorAtStart(t *testing.T) { l := &baseLexer{value: "", pos: 0} err := l.unreadByte() assert.ErrorIs(t, err, errDocumentStart) assert.Equal(t, 0, l.pos, "pos should remain at 0 after failed unread") } func TestReadByte_ReturnsEOFAtEnd(t *testing.T) { l := &baseLexer{value: "a", pos: 1} // already at end b, err := l.readByte() assert.Equal(t, byte(0), b) assert.ErrorIs(t, err, io.EOF) assert.Equal(t, 1, l.pos, "pos should not advance on EOF") } func TestNextLine_NoErrorOnEOF(t *testing.T) { l := &baseLexer{value: "", pos: 0} err := l.nextLine() assert.NoError(t, err) assert.Equal(t, 0, l.pos, "pos should remain at 0 on empty input") } func TestReadWhitespace_NoErrorOnEOF(t *testing.T) { l := &baseLexer{value: "", pos: 0} err := l.readWhitespace() assert.NoError(t, err) assert.Equal(t, 0, l.pos, "pos should remain at 0 on empty input") } func TestReadUint64Field_Errors(t *testing.T) { t.Run("empty input -> EOF", func(t *testing.T) { l := &baseLexer{value: "", pos: 0} _, err := l.readUint64Field() assert.ErrorIs(t, err, io.EOF) }) t.Run("non-digit at start -> syntaxError", func(t *testing.T) { l := &baseLexer{value: "x123", pos: 0} _, err := l.readUint64Field() var se syntaxError assert.ErrorAs(t, err, &se) }) } func TestReadField_Errors(t *testing.T) { t.Run("empty input -> returns EOF", func(t *testing.T) { l := &baseLexer{value: "", pos: 0} s, err := l.readField() assert.Empty(t, s) assert.ErrorIs(t, err, io.EOF) assert.Equal(t, 0, l.pos) }) t.Run("starting at end of input -> returns EOF", func(t *testing.T) { l := &baseLexer{value: "abc", pos: len("abc")} s, err := l.readField() assert.Empty(t, s) assert.ErrorIs(t, err, io.EOF) assert.Equal(t, len("abc"), l.pos) }) } func TestReadRequiredField_PropagatesReadFieldError(t *testing.T) { // Start at end/empty so readField() returns EOF. l := &lexer{baseLexer: baseLexer{value: "", pos: 0}} got, err := l.readRequiredField() assert.Empty(t, got) assert.ErrorIs(t, err, io.EOF) } func TestReadRequiredField_FieldMissingOnLeadingWhitespace(t *testing.T) { // Leading whitespace makes readField() return "" with nil error, // which should trigger errFieldMissing in readRequiredField(). l := &lexer{baseLexer: baseLexer{value: " \t"}} got, err := l.readRequiredField() assert.Empty(t, got) assert.ErrorIs(t, err, errFieldMissing) } func TestReadLine_CRLFTrimsCorrectly(t *testing.T) { l := &baseLexer{value: "abc\r\nx", pos: 0} s, err := l.readLine() assert.NoError(t, err) assert.Equal(t, "abc", s) assert.Equal(t, len("abc\r\n"), l.pos, "pos should be after the newline sequence") } func TestReadLine_EOFOnEmptyInput(t *testing.T) { l := &baseLexer{value: "", pos: 0} s, err := l.readLine() assert.Empty(t, s) assert.ErrorIs(t, err, io.EOF) assert.Equal(t, 0, l.pos, "pos should remain at 0 on empty input") } func TestReadLine_EOFWhenNoNewlinePresent(t *testing.T) { l := &baseLexer{value: "tail", pos: 0} s, err := l.readLine() assert.Empty(t, s) assert.ErrorIs(t, err, io.EOF) assert.Equal(t, len("tail"), l.pos, "pos should advance to end on EOF") } sdp-3.0.17/codecov.yml000066400000000000000000000007151512144664500145500ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT coverage: status: project: default: # Allow decreasing 2% of total coverage to avoid noise. threshold: 2% patch: default: target: 70% only_pulls: true ignore: - "examples/*" - "examples/**/*" sdp-3.0.17/common_description.go000066400000000000000000000075701512144664500166330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "strconv" ) // Information describes the "i=" field which provides textual information // about the session. type Information string func (i Information) String() string { return stringFromMarshal(i.marshalInto, i.marshalSize) } func (i Information) marshalInto(b []byte) []byte { return append(b, i...) } func (i Information) marshalSize() (size int) { return len(i) } // ConnectionInformation defines the representation for the "c=" field // containing connection data. type ConnectionInformation struct { NetworkType string AddressType string Address *Address } func (c ConnectionInformation) String() string { return stringFromMarshal(c.marshalInto, c.marshalSize) } func (c ConnectionInformation) marshalInto(b []byte) []byte { b = append(append(b, c.NetworkType...), ' ') b = append(b, c.AddressType...) if c.Address != nil { b = append(b, ' ') b = c.Address.marshalInto(b) } return b } func (c ConnectionInformation) marshalSize() (size int) { size = len(c.NetworkType) size += 1 + len(c.AddressType) if c.Address != nil { size += 1 + c.Address.marshalSize() } return } // Address desribes a structured address token from within the "c=" field. type Address struct { Address string TTL *int Range *int } func (c *Address) String() string { return stringFromMarshal(c.marshalInto, c.marshalSize) } func (c *Address) marshalInto(b []byte) []byte { b = append(b, c.Address...) if c.TTL != nil { b = append(b, '/') b = strconv.AppendInt(b, int64(*c.TTL), 10) } if c.Range != nil { b = append(b, '/') b = strconv.AppendInt(b, int64(*c.Range), 10) } return b } func (c Address) marshalSize() (size int) { size = len(c.Address) if c.TTL != nil { size += 1 + lenUint(uint64(*c.TTL)) //nolint:gosec // G115 } if c.Range != nil { size += 1 + lenUint(uint64(*c.Range)) //nolint:gosec // G115 } return } // Bandwidth describes an optional field which denotes the proposed bandwidth // to be used by the session or media. type Bandwidth struct { Experimental bool Type string Bandwidth uint64 } func (b Bandwidth) String() string { return stringFromMarshal(b.marshalInto, b.marshalSize) } func (b Bandwidth) marshalInto(d []byte) []byte { if b.Experimental { d = append(d, "X-"...) } d = append(append(d, b.Type...), ':') return strconv.AppendUint(d, b.Bandwidth, 10) } func (b Bandwidth) marshalSize() (size int) { if b.Experimental { size += 2 } size += len(b.Type) + 1 + lenUint(b.Bandwidth) return } // EncryptionKey describes the "k=" which conveys encryption key information. type EncryptionKey string func (e EncryptionKey) String() string { return stringFromMarshal(e.marshalInto, e.marshalSize) } func (e EncryptionKey) marshalInto(b []byte) []byte { return append(b, e...) } func (e EncryptionKey) marshalSize() (size int) { return len(e) } // Attribute describes the "a=" field which represents the primary means for // extending SDP. type Attribute struct { Key string Value string } // NewPropertyAttribute constructs a new attribute. func NewPropertyAttribute(key string) Attribute { return Attribute{ Key: key, } } // NewAttribute constructs a new attribute. func NewAttribute(key, value string) Attribute { return Attribute{ Key: key, Value: value, } } func (a Attribute) String() string { return stringFromMarshal(a.marshalInto, a.marshalSize) } func (a Attribute) marshalInto(b []byte) []byte { b = append(b, a.Key...) if len(a.Value) > 0 { b = append(append(b, ':'), a.Value...) } return b } func (a Attribute) marshalSize() (size int) { size = len(a.Key) if len(a.Value) > 0 { size += 1 + len(a.Value) } return size } // IsICECandidate returns true if the attribute key equals "candidate". func (a Attribute) IsICECandidate() bool { return a.Key == "candidate" } sdp-3.0.17/common_description_test.go000066400000000000000000000054421512144664500176660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "testing" "github.com/stretchr/testify/assert" ) func TestInformation_String(t *testing.T) { i := Information("About this session") assert.Equal(t, "About this session", i.String()) } func TestConnectionInformation_String(t *testing.T) { t.Run("without address", func(t *testing.T) { c := ConnectionInformation{NetworkType: "IN", AddressType: "IP4", Address: nil} assert.Equal(t, "IN IP4", c.String()) }) t.Run("with address + TTL + Range", func(t *testing.T) { ttl, rg := 127, 3 addr := &Address{Address: "224.2.17.12", TTL: &ttl, Range: &rg} c := ConnectionInformation{NetworkType: "IN", AddressType: "IP4", Address: addr} assert.Equal(t, "IN IP4 224.2.17.12/127/3", c.String()) }) } func TestAddress_String_Variants(t *testing.T) { t.Run("only address", func(t *testing.T) { a := &Address{Address: "239.255.255.250"} assert.Equal(t, "239.255.255.250", a.String()) }) t.Run("TTL only", func(t *testing.T) { ttl := 5 a := &Address{Address: "239.255.255.250", TTL: &ttl} assert.Equal(t, "239.255.255.250/5", a.String()) }) t.Run("Range only", func(t *testing.T) { rg := 7 a := &Address{Address: "239.255.255.250", Range: &rg} assert.Equal(t, "239.255.255.250/7", a.String()) }) t.Run("TTL and Range", func(t *testing.T) { ttl, rg := 5, 7 a := &Address{Address: "239.255.255.250", TTL: &ttl, Range: &rg} assert.Equal(t, "239.255.255.250/5/7", a.String()) }) } func TestAddress_marshal_RangePaths(t *testing.T) { t.Run("Range only size matches", func(t *testing.T) { rg := 9 a := Address{Address: "a", Range: &rg} out := a.marshalInto(nil) want := "a/9" assert.Equal(t, want, string(out)) assert.Equal(t, len(want), a.marshalSize()) }) t.Run("TTL and Range size matches", func(t *testing.T) { ttl, rg := 2, 11 a := Address{Address: "addr", TTL: &ttl, Range: &rg} s := (&a).String() assert.Equal(t, "addr/2/11", s) assert.Equal(t, len(s), a.marshalSize()) }) } func TestBandwidth_String(t *testing.T) { t.Run("standard", func(t *testing.T) { b := Bandwidth{Experimental: false, Type: "AS", Bandwidth: 512} assert.Equal(t, "AS:512", b.String()) }) t.Run("experimental", func(t *testing.T) { b := Bandwidth{Experimental: true, Type: "AS", Bandwidth: 512} assert.Equal(t, "X-AS:512", b.String()) }) } func TestEncryptionKey_String(t *testing.T) { e := EncryptionKey("clear:hunter2") assert.Equal(t, "clear:hunter2", e.String()) } func TestAttribute_IsICECandidate(t *testing.T) { assert.True(t, Attribute{Key: "candidate"}.IsICECandidate()) assert.False(t, Attribute{Key: "Candidate"}.IsICECandidate()) assert.False(t, Attribute{Key: "ice-candidate"}.IsICECandidate()) assert.False(t, Attribute{Key: ""}.IsICECandidate()) } sdp-3.0.17/direction.go000066400000000000000000000030161512144664500147070ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import "errors" // Direction is a marker for transmission directon of an endpoint. type Direction int const ( // DirectionSendRecv is for bidirectional communication. DirectionSendRecv Direction = iota + 1 // DirectionSendOnly is for outgoing communication. DirectionSendOnly // DirectionRecvOnly is for incoming communication. DirectionRecvOnly // DirectionInactive is for no communication. DirectionInactive ) const ( directionSendRecvStr = "sendrecv" directionSendOnlyStr = "sendonly" directionRecvOnlyStr = "recvonly" directionInactiveStr = "inactive" directionUnknownStr = "" ) var errDirectionString = errors.New("invalid direction string") // NewDirection defines a procedure for creating a new direction from a raw // string. func NewDirection(raw string) (Direction, error) { switch raw { case directionSendRecvStr: return DirectionSendRecv, nil case directionSendOnlyStr: return DirectionSendOnly, nil case directionRecvOnlyStr: return DirectionRecvOnly, nil case directionInactiveStr: return DirectionInactive, nil default: return Direction(unknown), errDirectionString } } func (t Direction) String() string { switch t { case DirectionSendRecv: return directionSendRecvStr case DirectionSendOnly: return directionSendOnlyStr case DirectionRecvOnly: return directionRecvOnlyStr case DirectionInactive: return directionInactiveStr default: return directionUnknownStr } } sdp-3.0.17/direction_test.go000066400000000000000000000021411512144664500157440ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewDirection(t *testing.T) { passingtests := []struct { value string expected Direction }{ {"sendrecv", DirectionSendRecv}, {"sendonly", DirectionSendOnly}, {"recvonly", DirectionRecvOnly}, {"inactive", DirectionInactive}, } failingtests := []string{ "", "notadirection", } for i, u := range passingtests { dir, err := NewDirection(u.value) assert.NoError(t, err) assert.Equalf(t, u.expected, dir, "%d: %+v", i, u) } for _, u := range failingtests { _, err := NewDirection(u) assert.Error(t, err) } } func TestDirection_String(t *testing.T) { tests := []struct { actual Direction expected string }{ {Direction(unknown), directionUnknownStr}, {DirectionSendRecv, "sendrecv"}, {DirectionSendOnly, "sendonly"}, {DirectionRecvOnly, "recvonly"}, {DirectionInactive, "inactive"}, } for i, u := range tests { assert.Equalf(t, u.expected, u.actual.String(), "%d: %+v", i, u) } } sdp-3.0.17/extmap.go000066400000000000000000000051211512144664500142240ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "fmt" "net/url" "strconv" "strings" ) // Default ext values. const ( DefExtMapValueABSSendTime = 1 DefExtMapValueTransportCC = 2 DefExtMapValueSDESMid = 3 DefExtMapValueSDESRTPStreamID = 4 ABSSendTimeURI = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" TransportCCURI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" SDESMidURI = "urn:ietf:params:rtp-hdrext:sdes:mid" SDESRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id" SDESRepairRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id" AudioLevelURI = "urn:ietf:params:rtp-hdrext:ssrc-audio-level" ) // ExtMap represents the activation of a single RTP header extension. type ExtMap struct { Value int Direction Direction URI *url.URL ExtAttr *string } // Clone converts this object to an Attribute. func (e *ExtMap) Clone() Attribute { return Attribute{Key: "extmap", Value: e.string()} } // Unmarshal creates an Extmap from a string. func (e *ExtMap) Unmarshal(raw string) error { parts := strings.SplitN(raw, ":", 2) if len(parts) != 2 { return fmt.Errorf("%w: %v", errSyntaxError, raw) } fields := strings.Fields(parts[1]) if len(fields) < 2 { return fmt.Errorf("%w: %v", errSyntaxError, raw) } valdir := strings.Split(fields[0], "/") value, err := strconv.ParseInt(valdir[0], 10, 64) if (value < 1) || (value > 246) { return fmt.Errorf("%w: %v -- extmap key must be in the range 1-256", errSyntaxError, valdir[0]) } if err != nil { return fmt.Errorf("%w: %v", errSyntaxError, valdir[0]) } var direction Direction if len(valdir) == 2 { direction, err = NewDirection(valdir[1]) if err != nil { return err } } uri, err := url.Parse(fields[1]) if err != nil { return err } if len(fields) == 3 { tmp := fields[2] e.ExtAttr = &tmp } e.Value = int(value) e.Direction = direction e.URI = uri return nil } // Marshal creates a string from an ExtMap. func (e *ExtMap) Marshal() string { return e.Name() + ":" + e.string() } func (e *ExtMap) string() string { output := fmt.Sprintf("%d", e.Value) dirstring := e.Direction.String() if dirstring != directionUnknownStr { output += "/" + dirstring } if e.URI != nil { output += " " + e.URI.String() } if e.ExtAttr != nil { output += " " + *e.ExtAttr } return output } // Name returns the constant name of this object. func (e *ExtMap) Name() string { return "extmap" } sdp-3.0.17/extmap_test.go000066400000000000000000000043451512144664500152720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "net/url" "testing" "github.com/stretchr/testify/assert" ) func TestExtmap(t *testing.T) { passingtests := []struct { parameter string expected string }{ {exampleAttrExtmap1, exampleAttrExtmap1Line}, {exampleAttrExtmap2, exampleAttrExtmap2Line}, } failingtests := []struct { parameter string expected string }{ {failingAttrExtmap1, failingAttrExtmap1Line}, {failingAttrExtmap2, failingAttrExtmap2Line}, } for i, u := range passingtests { actual := ExtMap{} assert.NoError(t, actual.Unmarshal(u.parameter)) assert.Equalf(t, u.expected, actual.Marshal(), "%d: %+v", i, u) } for _, u := range failingtests { actual := ExtMap{} assert.Error(t, actual.Unmarshal(u.parameter)) } } func TestTransportCCExtMap(t *testing.T) { // a=extmap:["/"] // a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 uri, _ := url.Parse("http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01") e := ExtMap{ Value: 3, URI: uri, } assert.NotEqual( t, "3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", e.Marshal(), "TestTransportCC failed", ) } func TestExtMap_Clone(t *testing.T) { u, _ := url.Parse(AudioLevelURI) ext := "vad" em := &ExtMap{Value: 5, URI: u, ExtAttr: &ext} got := em.Clone() assert.Equal(t, "extmap", got.Key) assert.Equal(t, "5 "+AudioLevelURI+" "+ext, got.Value) } func TestExtMap_Unmarshal_Error_LenParts(t *testing.T) { var em ExtMap err := em.Unmarshal("extmap 1 example.com") assert.ErrorIs(t, err, errSyntaxError) err = em.Unmarshal("") assert.ErrorIs(t, err, errSyntaxError) } func TestExtMap_Unmarshal_Error_LenFields(t *testing.T) { var em ExtMap err := em.Unmarshal("extmap:1") assert.ErrorIs(t, err, errSyntaxError) } func TestExtMap_Unmarshal_Error_NewDirection(t *testing.T) { var em ExtMap err := em.Unmarshal("extmap:1/not-a-dir http://example.com") assert.Error(t, err) } func TestExtMap_Unmarshal_Error_URLParse(t *testing.T) { var em ExtMap err := em.Unmarshal("extmap:1 http://example.com/%zz") assert.Error(t, err) } sdp-3.0.17/fuzz_test.go000066400000000000000000000010621512144664500147630ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "testing" "github.com/stretchr/testify/assert" ) func FuzzUnmarshal(f *testing.F) { f.Add("") f.Add(CanonicalUnmarshalSDP) f.Fuzz(func(t *testing.T, data string) { // Check that unmarshalling any byte slice does not panic. var sd SessionDescription if err := sd.UnmarshalString(data); err != nil { return } // Check that we can marshal anything we unmarshalled. _, err := sd.Marshal() assert.NoError(t, err) }) } sdp-3.0.17/go.mod000066400000000000000000000004151512144664500135060ustar00rootroot00000000000000module github.com/pion/sdp/v3 go 1.21 require ( github.com/pion/randutil v0.1.0 github.com/stretchr/testify v1.11.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) sdp-3.0.17/go.sum000066400000000000000000000020321512144664500135300ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sdp-3.0.17/jsep.go000066400000000000000000000176531512144664500137040ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "fmt" "net/url" "strconv" "time" ) // Constants for SDP attributes used in JSEP. const ( AttrKeyCandidate = "candidate" AttrKeyEndOfCandidates = "end-of-candidates" AttrKeyIdentity = "identity" AttrKeyGroup = "group" AttrKeySSRC = "ssrc" AttrKeySSRCGroup = "ssrc-group" AttrKeyMsid = "msid" AttrKeyMsidSemantic = "msid-semantic" AttrKeyConnectionSetup = "setup" AttrKeyMID = "mid" AttrKeyICELite = "ice-lite" AttrKeyICEOptions = "ice-options" AttrKeyRTCPMux = "rtcp-mux" AttrKeyRTCPRsize = "rtcp-rsize" AttrKeyInactive = "inactive" AttrKeyRecvOnly = "recvonly" AttrKeySendOnly = "sendonly" AttrKeySendRecv = "sendrecv" AttrKeyExtMap = "extmap" AttrKeyExtMapAllowMixed = "extmap-allow-mixed" AttrKeyCryptex = "cryptex" ) // Constants for semantic tokens used in JSEP. const ( SemanticTokenLipSynchronization = "LS" SemanticTokenFlowIdentification = "FID" SemanticTokenForwardErrorCorrection = "FEC" // https://datatracker.ietf.org/doc/html/rfc5956#section-4.1 SemanticTokenForwardErrorCorrectionFramework = "FEC-FR" SemanticTokenWebRTCMediaStreams = "WMS" ) // Constants for extmap key. const ( ExtMapValueTransportCC = 3 ) func extMapURI() map[int]string { return map[int]string{ ExtMapValueTransportCC: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", } } // API to match draft-ietf-rtcweb-jsep // Move to webrtc or its own package? // NewJSEPSessionDescription creates a new SessionDescription with // some settings that are required by the JSEP spec. // // Note: Since v2.4.0, session ID has been fixed to use crypto random according to // // JSEP spec, so that NewJSEPSessionDescription now returns error as a second // return value. func NewJSEPSessionDescription(identity bool) (*SessionDescription, error) { sid, err := newSessionID() if err != nil { return nil, err } descr := &SessionDescription{ Version: 0, Origin: Origin{ Username: "-", SessionID: sid, SessionVersion: uint64(time.Now().Unix()), //nolint:gosec // G115 NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0", }, SessionName: "-", TimeDescriptions: []TimeDescription{ { Timing: Timing{ StartTime: 0, StopTime: 0, }, RepeatTimes: nil, }, }, Attributes: []Attribute{ // "Attribute(ice-options:trickle)", // TODO: implement trickle ICE }, } if identity { descr.WithPropertyAttribute(AttrKeyIdentity) } return descr, nil } // WithPropertyAttribute adds a property attribute 'a=key' to the session description. func (s *SessionDescription) WithPropertyAttribute(key string) *SessionDescription { s.Attributes = append(s.Attributes, NewPropertyAttribute(key)) return s } // WithValueAttribute adds a value attribute 'a=key:value' to the session description. func (s *SessionDescription) WithValueAttribute(key, value string) *SessionDescription { s.Attributes = append(s.Attributes, NewAttribute(key, value)) return s } // addOrUpdateICEOption adds or updates the ice-options attribute with the given value. func (s *SessionDescription) addOrUpdateICEOption(value string) *SessionDescription { for i := range s.Attributes { if s.Attributes[i].Key == AttrKeyICEOptions { prefix := " " if s.Attributes[i].Value == "" { prefix = "" } s.Attributes[i].Value += prefix + value return s } } return s.WithValueAttribute(AttrKeyICEOptions, value) } // WithICETrickleAdvertised advertises ICE trickle support in the session description. // See https://datatracker.ietf.org/doc/html/rfc9429#section-5.2.1 func (s *SessionDescription) WithICETrickleAdvertised() *SessionDescription { return s.addOrUpdateICEOption("trickle") } // WithICERenomination advertises ICE renomination support in the session description. // See https://datatracker.ietf.org/doc/html/draft-thatcher-ice-renomination-01#section-3 func (s *SessionDescription) WithICERenomination() *SessionDescription { return s.addOrUpdateICEOption("renomination") } // WithFingerprint adds a fingerprint to the session description. func (s *SessionDescription) WithFingerprint(algorithm, value string) *SessionDescription { return s.WithValueAttribute("fingerprint", algorithm+" "+value) } // WithMedia adds a media description to the session description. func (s *SessionDescription) WithMedia(md *MediaDescription) *SessionDescription { s.MediaDescriptions = append(s.MediaDescriptions, md) return s } // NewJSEPMediaDescription creates a new MediaName with // some settings that are required by the JSEP spec. func NewJSEPMediaDescription(codecType string, _ []string) *MediaDescription { return &MediaDescription{ MediaName: MediaName{ Media: codecType, Port: RangedPort{Value: 9}, Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, }, ConnectionInformation: &ConnectionInformation{ NetworkType: "IN", AddressType: "IP4", Address: &Address{ Address: "0.0.0.0", }, }, } } // WithPropertyAttribute adds a property attribute 'a=key' to the media description. func (d *MediaDescription) WithPropertyAttribute(key string) *MediaDescription { d.Attributes = append(d.Attributes, NewPropertyAttribute(key)) return d } // WithValueAttribute adds a value attribute 'a=key:value' to the media description. func (d *MediaDescription) WithValueAttribute(key, value string) *MediaDescription { d.Attributes = append(d.Attributes, NewAttribute(key, value)) return d } // WithFingerprint adds a fingerprint to the media description. func (d *MediaDescription) WithFingerprint(algorithm, value string) *MediaDescription { return d.WithValueAttribute("fingerprint", algorithm+" "+value) } // WithICECredentials adds ICE credentials to the media description. func (d *MediaDescription) WithICECredentials(username, password string) *MediaDescription { return d. WithValueAttribute("ice-ufrag", username). WithValueAttribute("ice-pwd", password) } // WithCodec adds codec information to the media description. func (d *MediaDescription) WithCodec( payloadType uint8, name string, clockrate uint32, channels uint16, fmtp string, ) *MediaDescription { d.MediaName.Formats = append(d.MediaName.Formats, strconv.Itoa(int(payloadType))) rtpmap := fmt.Sprintf("%d %s/%d", payloadType, name, clockrate) if channels > 0 { rtpmap += fmt.Sprintf("/%d", channels) } d.WithValueAttribute("rtpmap", rtpmap) if fmtp != "" { d.WithValueAttribute("fmtp", fmt.Sprintf("%d %s", payloadType, fmtp)) } return d } // WithMediaSource adds media source information to the media description. func (d *MediaDescription) WithMediaSource(ssrc uint32, cname, streamLabel, label string) *MediaDescription { return d. WithValueAttribute("ssrc", fmt.Sprintf("%d cname:%s", ssrc, cname)). // Deprecated but not phased out? WithValueAttribute("ssrc", fmt.Sprintf("%d msid:%s %s", ssrc, streamLabel, label)). WithValueAttribute("ssrc", fmt.Sprintf("%d mslabel:%s", ssrc, streamLabel)). // Deprecated but not phased out? WithValueAttribute("ssrc", fmt.Sprintf("%d label:%s", ssrc, label)) // Deprecated but not phased out? } // WithCandidate adds an ICE candidate to the media description. // Deprecated: use WithICECandidate instead. func (d *MediaDescription) WithCandidate(value string) *MediaDescription { return d.WithValueAttribute("candidate", value) } // WithExtMap adds an extmap to the media description. func (d *MediaDescription) WithExtMap(e ExtMap) *MediaDescription { return d.WithPropertyAttribute(e.Marshal()) } // WithTransportCCExtMap adds an extmap to the media description. func (d *MediaDescription) WithTransportCCExtMap() *MediaDescription { uri, _ := url.Parse(extMapURI()[ExtMapValueTransportCC]) e := ExtMap{ Value: ExtMapValueTransportCC, URI: uri, } return d.WithExtMap(e) } sdp-3.0.17/jsep_test.go000066400000000000000000000202371512144664500147330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "net/url" "testing" "github.com/stretchr/testify/assert" ) func TestNewJSEPSessionDescription(t *testing.T) { t.Run("Without Identity", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) assert.NotNil(t, sd) assert.Zero(t, sd.Version) assert.Equal(t, "-", sd.Origin.Username) assert.Equal(t, "IN", sd.Origin.NetworkType) assert.Equal(t, "IP4", sd.Origin.AddressType) assert.Equal(t, "0.0.0.0", sd.Origin.UnicastAddress) assert.Equal(t, SessionName("-"), sd.SessionName) assert.Len(t, sd.TimeDescriptions, 1) assert.Zero(t, sd.TimeDescriptions[0].Timing.StartTime) assert.Zero(t, sd.TimeDescriptions[0].Timing.StopTime) assert.Empty(t, sd.Attributes) }) t.Run("With Identity", func(t *testing.T) { sd, err := NewJSEPSessionDescription(true) assert.NoError(t, err) assert.NotNil(t, sd) assert.Len(t, sd.Attributes, 1) assert.Equal(t, AttrKeyIdentity, sd.Attributes[0].Key) }) } func TestSessionDescriptionAttributes(t *testing.T) { t.Run("WithPropertyAttribute", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) sd = sd.WithPropertyAttribute(AttrKeyRTCPMux) assert.Len(t, sd.Attributes, 1) assert.Equal(t, AttrKeyRTCPMux, sd.Attributes[0].Key) }) t.Run("WithValueAttribute", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) sd = sd.WithValueAttribute(AttrKeyMID, "video") assert.Len(t, sd.Attributes, 1) assert.Equal(t, AttrKeyMID, sd.Attributes[0].Key) assert.Equal(t, "video", sd.Attributes[0].Value) }) t.Run("WithICETrickleAdvertised", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) sd = sd.WithICETrickleAdvertised() assert.Len(t, sd.Attributes, 1) assert.Equal(t, AttrKeyICEOptions, sd.Attributes[0].Key) assert.Equal(t, "trickle", sd.Attributes[0].Value) }) t.Run("WithICERenomination", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) sd = sd.WithICETrickleAdvertised().WithICERenomination() assert.Len(t, sd.Attributes, 1) assert.Equal(t, AttrKeyICEOptions, sd.Attributes[0].Key) assert.Equal(t, "trickle renomination", sd.Attributes[0].Value) }) t.Run("WithFingerprint", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) sd = sd.WithFingerprint("sha-256", "test-fingerprint") assert.Len(t, sd.Attributes, 1) assert.Equal(t, "fingerprint", sd.Attributes[0].Key) assert.Equal(t, "sha-256 test-fingerprint", sd.Attributes[0].Value) }) } func TestSessionDescription_ICEOptions_Combined(t *testing.T) { t.Run("WithICETrickleAdvertised and WithICERenominationAdvertised", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) sd = sd.WithICETrickleAdvertised().WithICERenomination() iceOptionsCount := 0 var iceOptionsValue string for _, attr := range sd.Attributes { if attr.Key == AttrKeyICEOptions { iceOptionsCount++ iceOptionsValue = attr.Value } } assert.Equal(t, 1, iceOptionsCount, "Should have exactly one ice-options attribute") assert.Equal(t, "trickle renomination", iceOptionsValue, "Should combine both values with space") }) t.Run("WithICERenominationAdvertised and WithICETrickleAdvertised (reverse order)", func(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) sd = sd.WithICERenomination().WithICETrickleAdvertised() iceOptionsCount := 0 var iceOptionsValue string for _, attr := range sd.Attributes { if attr.Key == AttrKeyICEOptions { iceOptionsCount++ iceOptionsValue = attr.Value } } assert.Equal(t, 1, iceOptionsCount, "Should have exactly one ice-options attribute") assert.Equal(t, "renomination trickle", iceOptionsValue, "Should combine both values with space") }) } func TestNewJSEPMediaDescription(t *testing.T) { md := NewJSEPMediaDescription("video", []string{"96", "97"}) assert.NotNil(t, md) assert.Equal(t, "video", md.MediaName.Media) assert.Equal(t, int(9), md.MediaName.Port.Value) assert.Equal(t, []string{"UDP", "TLS", "RTP", "SAVPF"}, md.MediaName.Protos) assert.Equal(t, "IN", md.ConnectionInformation.NetworkType) assert.Equal(t, "IP4", md.ConnectionInformation.AddressType) assert.Equal(t, "0.0.0.0", md.ConnectionInformation.Address.Address) } func TestMediaDescriptionAttributes(t *testing.T) { md := NewJSEPMediaDescription("audio", nil) t.Run("WithPropertyAttribute", func(t *testing.T) { md = md.WithPropertyAttribute(AttrKeyRTCPMux) assert.Len(t, md.Attributes, 1) assert.Equal(t, AttrKeyRTCPMux, md.Attributes[0].Key) }) t.Run("WithValueAttribute", func(t *testing.T) { md = md.WithValueAttribute(AttrKeyMID, "audio") assert.Len(t, md.Attributes, 2) assert.Equal(t, AttrKeyMID, md.Attributes[1].Key) assert.Equal(t, "audio", md.Attributes[1].Value) }) t.Run("WithFingerprint", func(t *testing.T) { md = md.WithFingerprint("sha-256", "test-fingerprint") assert.Len(t, md.Attributes, 3) assert.Equal(t, "fingerprint", md.Attributes[2].Key) assert.Equal(t, "sha-256 test-fingerprint", md.Attributes[2].Value) }) t.Run("WithICECredentials", func(t *testing.T) { md = md.WithICECredentials("test-ufrag", "test-pwd") assert.Len(t, md.Attributes, 5) assert.Equal(t, "ice-ufrag", md.Attributes[3].Key) assert.Equal(t, "test-ufrag", md.Attributes[3].Value) assert.Equal(t, "ice-pwd", md.Attributes[4].Key) assert.Equal(t, "test-pwd", md.Attributes[4].Value) }) } func TestMediaDescriptionCodec(t *testing.T) { md := NewJSEPMediaDescription("audio", nil) t.Run("WithCodec", func(t *testing.T) { md = md.WithCodec(111, "opus", 48000, 2, "minptime=10;useinbandfec=1") assert.Len(t, md.MediaName.Formats, 1) assert.Equal(t, "111", md.MediaName.Formats[0]) assert.Len(t, md.Attributes, 2) assert.Equal(t, "rtpmap", md.Attributes[0].Key) assert.Equal(t, "111 opus/48000/2", md.Attributes[0].Value) assert.Equal(t, "fmtp", md.Attributes[1].Key) assert.Equal(t, "111 minptime=10;useinbandfec=1", md.Attributes[1].Value) }) t.Run("WithMediaSource", func(t *testing.T) { md = md.WithMediaSource(1234567890, "test-cname", "test-stream", "test-label") assert.Len(t, md.Attributes, 6) assert.Equal(t, "ssrc", md.Attributes[2].Key) assert.Equal(t, "1234567890 cname:test-cname", md.Attributes[2].Value) assert.Equal(t, "ssrc", md.Attributes[3].Key) assert.Equal(t, "1234567890 msid:test-stream test-label", md.Attributes[3].Value) assert.Equal(t, "ssrc", md.Attributes[4].Key) assert.Equal(t, "1234567890 mslabel:test-stream", md.Attributes[4].Value) assert.Equal(t, "ssrc", md.Attributes[5].Key) assert.Equal(t, "1234567890 label:test-label", md.Attributes[5].Value) }) } func Test_extMapURI_TransportCC(t *testing.T) { m := extMapURI() u, ok := m[ExtMapValueTransportCC] assert.True(t, ok) assert.Equal(t, "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", u) } func TestSessionDescription_WithMedia_Appends(t *testing.T) { sd, err := NewJSEPSessionDescription(false) assert.NoError(t, err) md := NewJSEPMediaDescription("audio", nil) prev := len(sd.MediaDescriptions) ret := sd.WithMedia(md) assert.Same(t, sd, ret) assert.Equal(t, prev+1, len(sd.MediaDescriptions)) assert.Equal(t, md, sd.MediaDescriptions[len(sd.MediaDescriptions)-1]) } func TestMediaDescription_WithExtMap_AddsPropertyAttribute(t *testing.T) { md := NewJSEPMediaDescription("audio", nil) u, _ := url.Parse(extMapURI()[ExtMapValueTransportCC]) em := ExtMap{Value: ExtMapValueTransportCC, URI: u} ret := md.WithExtMap(em) assert.Same(t, md, ret) if assert.Len(t, md.Attributes, 1) { assert.Equal(t, "extmap:3 "+u.String(), md.Attributes[0].Key) assert.Empty(t, md.Attributes[0].Value) } } func TestMediaDescription_WithTransportCCExtMap_AddsExpectedAttribute(t *testing.T) { md := NewJSEPMediaDescription("audio", nil) ret := md.WithTransportCCExtMap() assert.Same(t, md, ret) if assert.Len(t, md.Attributes, 1) { want := "extmap:3 " + extMapURI()[ExtMapValueTransportCC] assert.Equal(t, want, md.Attributes[0].Key) assert.Empty(t, md.Attributes[0].Value) } } sdp-3.0.17/marshal.go000066400000000000000000000133001512144664500143530ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp // Marshal takes a SDP struct to text // https://tools.ietf.org/html/rfc4566#section-5 // Session description // // v= (protocol version) // o= (originator and session identifier) // s= (session name) // i=* (session information) // u=* (URI of description) // e=* (email address) // p=* (phone number) // c=* (connection information -- not required if included in // all media) // b=* (zero or more bandwidth information lines) // One or more time descriptions ("t=" and "r=" lines; see below) // z=* (time zone adjustments) // k=* (encryption key) // a=* (zero or more session attribute lines) // Zero or more media descriptions // // Time description // // t= (time the session is active) // r=* (zero or more repeat times) // // Media description, if present // // m= (media name and transport address) // i=* (media title) // c=* (connection information -- optional if included at // session level) // b=* (zero or more bandwidth information lines) // k=* (encryption key) // a=* (zero or more media attribute lines) func (s *SessionDescription) Marshal() ([]byte, error) { //nolint:cyclop marsh := make(marshaller, 0, s.MarshalSize()) marsh.addKeyValue("v=", s.Version.marshalInto) marsh.addKeyValue("o=", s.Origin.marshalInto) marsh.addKeyValue("s=", s.SessionName.marshalInto) if s.SessionInformation != nil { marsh.addKeyValue("i=", s.SessionInformation.marshalInto) } if s.URI != nil { marsh = append(marsh, "u="...) marsh = append(marsh, s.URI.String()...) marsh = append(marsh, "\r\n"...) } if s.EmailAddress != nil { marsh.addKeyValue("e=", s.EmailAddress.marshalInto) } if s.PhoneNumber != nil { marsh.addKeyValue("p=", s.PhoneNumber.marshalInto) } if s.ConnectionInformation != nil { marsh.addKeyValue("c=", s.ConnectionInformation.marshalInto) } for _, b := range s.Bandwidth { marsh.addKeyValue("b=", b.marshalInto) } for _, td := range s.TimeDescriptions { marsh.addKeyValue("t=", td.Timing.marshalInto) for _, r := range td.RepeatTimes { marsh.addKeyValue("r=", r.marshalInto) } } if len(s.TimeZones) > 0 { marsh = append(marsh, "z="...) for i, z := range s.TimeZones { if i > 0 { marsh = append(marsh, ' ') } marsh = z.marshalInto(marsh) } marsh = append(marsh, "\r\n"...) } if s.EncryptionKey != nil { marsh.addKeyValue("k=", s.EncryptionKey.marshalInto) } for _, a := range s.Attributes { marsh.addKeyValue("a=", a.marshalInto) } for _, md := range s.MediaDescriptions { marsh.addKeyValue("m=", md.MediaName.marshalInto) if md.MediaTitle != nil { marsh.addKeyValue("i=", md.MediaTitle.marshalInto) } if md.ConnectionInformation != nil { marsh.addKeyValue("c=", md.ConnectionInformation.marshalInto) } for _, b := range md.Bandwidth { marsh.addKeyValue("b=", b.marshalInto) } if md.EncryptionKey != nil { marsh.addKeyValue("k=", md.EncryptionKey.marshalInto) } for _, a := range md.Attributes { marsh.addKeyValue("a=", a.marshalInto) } } return marsh, nil } // `$type=` and CRLF size. const lineBaseSize = 4 // MarshalSize returns the size of the SessionDescription once marshaled. func (s *SessionDescription) MarshalSize() (marshalSize int) { //nolint:cyclop marshalSize += lineBaseSize + s.Version.marshalSize() marshalSize += lineBaseSize + s.Origin.marshalSize() marshalSize += lineBaseSize + s.SessionName.marshalSize() if s.SessionInformation != nil { marshalSize += lineBaseSize + s.SessionInformation.marshalSize() } if s.URI != nil { marshalSize += lineBaseSize + len(s.URI.String()) } if s.EmailAddress != nil { marshalSize += lineBaseSize + s.EmailAddress.marshalSize() } if s.PhoneNumber != nil { marshalSize += lineBaseSize + s.PhoneNumber.marshalSize() } if s.ConnectionInformation != nil { marshalSize += lineBaseSize + s.ConnectionInformation.marshalSize() } for _, b := range s.Bandwidth { marshalSize += lineBaseSize + b.marshalSize() } for _, td := range s.TimeDescriptions { marshalSize += lineBaseSize + td.Timing.marshalSize() for _, r := range td.RepeatTimes { marshalSize += lineBaseSize + r.marshalSize() } } if len(s.TimeZones) > 0 { marshalSize += lineBaseSize for i, z := range s.TimeZones { if i > 0 { marshalSize++ } marshalSize += z.marshalSize() } } if s.EncryptionKey != nil { marshalSize += lineBaseSize + s.EncryptionKey.marshalSize() } for _, a := range s.Attributes { marshalSize += lineBaseSize + a.marshalSize() } for _, md := range s.MediaDescriptions { marshalSize += lineBaseSize + md.MediaName.marshalSize() if md.MediaTitle != nil { marshalSize += lineBaseSize + md.MediaTitle.marshalSize() } if md.ConnectionInformation != nil { marshalSize += lineBaseSize + md.ConnectionInformation.marshalSize() } for _, b := range md.Bandwidth { marshalSize += lineBaseSize + b.marshalSize() } if md.EncryptionKey != nil { marshalSize += lineBaseSize + md.EncryptionKey.marshalSize() } for _, a := range md.Attributes { marshalSize += lineBaseSize + a.marshalSize() } } return marshalSize } // marshaller contains state during marshaling. type marshaller []byte func (m *marshaller) addKeyValue(key string, value func([]byte) []byte) { *m = append(*m, key...) *m = value(*m) *m = append(*m, "\r\n"...) } func lenUint(i uint64) (count int) { if i == 0 { return 1 } for i != 0 { i /= 10 count++ } return } func lenInt(i int64) (count int) { if i < 0 { return lenUint(uint64(-i)) + 1 } return lenUint(uint64(i)) } func stringFromMarshal(marshalFunc func([]byte) []byte, sizeFunc func() int) string { return string(marshalFunc(make([]byte, 0, sizeFunc()))) } sdp-3.0.17/marshal_test.go000066400000000000000000000103161512144664500154160ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "net/url" "testing" "github.com/stretchr/testify/assert" ) //nolint:goconst const ( CanonicalMarshalSDP = "v=0\r\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + "s=SDP Seminar\r\n" + "i=A Seminar on the session description protocol\r\n" + "u=http://www.example.com/seminars/sdp.pdf\r\n" + "e=j.doe@example.com (Jane Doe)\r\n" + "p=+1 617 555-6011\r\n" + "c=IN IP4 224.2.17.12/127\r\n" + "b=X-YZ:128\r\n" + "b=AS:12345\r\n" + "t=2873397496 2873404696\r\n" + "t=3034423619 3042462419\r\n" + "r=604800 3600 0 90000\r\n" + "z=2882844526 -3600 2898848070 0\r\n" + "k=prompt\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + "a=recvonly\r\n" + "m=audio 49170 RTP/AVP 0\r\n" + "i=Vivamus a posuere nisl\r\n" + "c=IN IP4 203.0.113.1\r\n" + "b=X-YZ:128\r\n" + "k=prompt\r\n" + "a=sendrecv\r\n" + "m=video 51372 RTP/AVP 99\r\n" + "a=rtpmap:99 h263-1998/90000\r\n" ) //nolint:goconst func TestMarshalCanonical(t *testing.T) { sd := &SessionDescription{ Version: 0, Origin: Origin{ Username: "jdoe", SessionID: uint64(2890844526), SessionVersion: uint64(2890842807), NetworkType: "IN", AddressType: "IP4", UnicastAddress: "10.47.16.5", }, SessionName: "SDP Seminar", SessionInformation: &(&struct{ x Information }{"A Seminar on the session description protocol"}).x, URI: func() *url.URL { uri, err := url.Parse("http://www.example.com/seminars/sdp.pdf") if err != nil { return nil } return uri }(), EmailAddress: &(&struct{ x EmailAddress }{"j.doe@example.com (Jane Doe)"}).x, PhoneNumber: &(&struct{ x PhoneNumber }{"+1 617 555-6011"}).x, ConnectionInformation: &ConnectionInformation{ NetworkType: "IN", AddressType: "IP4", Address: &Address{ Address: "224.2.17.12", TTL: &(&struct{ x int }{127}).x, }, }, Bandwidth: []Bandwidth{ { Experimental: true, Type: "YZ", Bandwidth: 128, }, { Type: "AS", Bandwidth: 12345, }, }, TimeDescriptions: []TimeDescription{ { Timing: Timing{ StartTime: 2873397496, StopTime: 2873404696, }, RepeatTimes: nil, }, { Timing: Timing{ StartTime: 3034423619, StopTime: 3042462419, }, RepeatTimes: []RepeatTime{ { Interval: 604800, Duration: 3600, Offsets: []int64{0, 90000}, }, }, }, }, TimeZones: []TimeZone{ { AdjustmentTime: 2882844526, Offset: -3600, }, { AdjustmentTime: 2898848070, Offset: 0, }, }, EncryptionKey: &(&struct{ x EncryptionKey }{"prompt"}).x, Attributes: []Attribute{ NewAttribute("candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host", ""), NewAttribute("recvonly", ""), }, MediaDescriptions: []*MediaDescription{ { MediaName: MediaName{ Media: "audio", Port: RangedPort{ Value: 49170, }, Protos: []string{"RTP", "AVP"}, Formats: []string{"0"}, }, MediaTitle: &(&struct{ x Information }{"Vivamus a posuere nisl"}).x, ConnectionInformation: &ConnectionInformation{ NetworkType: "IN", AddressType: "IP4", Address: &Address{ Address: "203.0.113.1", }, }, Bandwidth: []Bandwidth{ { Experimental: true, Type: "YZ", Bandwidth: 128, }, }, EncryptionKey: &(&struct{ x EncryptionKey }{"prompt"}).x, Attributes: []Attribute{ NewAttribute("sendrecv", ""), }, }, { MediaName: MediaName{ Media: "video", Port: RangedPort{ Value: 51372, }, Protos: []string{"RTP", "AVP"}, Formats: []string{"99"}, }, Attributes: []Attribute{ NewAttribute("rtpmap:99 h263-1998/90000", ""), }, }, }, } actual, err := sd.Marshal() assert.NoError(t, err) assert.Equal(t, CanonicalMarshalSDP, string(actual)) } func BenchmarkMarshal(b *testing.B) { b.ReportAllocs() var sd SessionDescription err := sd.UnmarshalString(CanonicalUnmarshalSDP) assert.NoError(b, err) for i := 0; i < b.N; i++ { _, err = sd.Marshal() assert.NoError(b, err) } } sdp-3.0.17/media_description.go000066400000000000000000000056061512144664500164200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "strconv" ) // MediaDescription represents a media type. // https://tools.ietf.org/html/rfc4566#section-5.14 type MediaDescription struct { // m= / ... // https://tools.ietf.org/html/rfc4566#section-5.14 MediaName MediaName // i= // https://tools.ietf.org/html/rfc4566#section-5.4 MediaTitle *Information // c= // https://tools.ietf.org/html/rfc4566#section-5.7 ConnectionInformation *ConnectionInformation // b=: // https://tools.ietf.org/html/rfc4566#section-5.8 Bandwidth []Bandwidth // k= // k=: // https://tools.ietf.org/html/rfc4566#section-5.12 EncryptionKey *EncryptionKey // a= // a=: // https://tools.ietf.org/html/rfc4566#section-5.13 Attributes []Attribute } // Attribute returns the value of an attribute and if it exists. func (d *MediaDescription) Attribute(key string) (string, bool) { for _, a := range d.Attributes { if a.Key == key { return a.Value, true } } return "", false } // RangedPort supports special format for the media field "m=" port value. If // it may be necessary to specify multiple transport ports, the protocol allows // to write it as: / where number of ports is a an // offsetting range. type RangedPort struct { Value int Range *int } func (p *RangedPort) String() string { output := strconv.Itoa(p.Value) if p.Range != nil { output += "/" + strconv.Itoa(*p.Range) } return output } func (p RangedPort) marshalInto(b []byte) []byte { b = strconv.AppendInt(b, int64(p.Value), 10) if p.Range != nil { b = append(b, '/') b = strconv.AppendInt(b, int64(*p.Range), 10) } return b } func (p RangedPort) marshalSize() (size int) { size = lenInt(int64(p.Value)) if p.Range != nil { size += 1 + lenInt(int64(*p.Range)) } return } // MediaName describes the "m=" field storage structure. type MediaName struct { Media string Port RangedPort Protos []string Formats []string } func (m MediaName) String() string { return stringFromMarshal(m.marshalInto, m.marshalSize) } func (m MediaName) marshalInto(b []byte) []byte { appendList := func(list []string, sep byte) { for i, p := range list { if i != 0 && i != len(list) { b = append(b, sep) } b = append(b, p...) } } b = append(append(b, m.Media...), ' ') b = append(m.Port.marshalInto(b), ' ') appendList(m.Protos, '/') b = append(b, ' ') appendList(m.Formats, ' ') return b } func (m MediaName) marshalSize() (size int) { listSize := func(list []string) { for _, p := range list { size += 1 + len(p) } } size = len(m.Media) size += 1 + m.Port.marshalSize() listSize(m.Protos) listSize(m.Formats) return size } sdp-3.0.17/media_description_test.go000066400000000000000000000036341512144664500174560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "testing" "github.com/stretchr/testify/assert" ) func TestWithFingerprint(t *testing.T) { m := new(MediaDescription) assert.Equal(t, []Attribute(nil), m.Attributes) m = m.WithFingerprint("testalgorithm", "testfingerprint") assert.Equal(t, []Attribute{ {"fingerprint", "testalgorithm testfingerprint"}, }, m.Attributes) } func TestMediaDescription_Attribute(t *testing.T) { md := &MediaDescription{ Attributes: []Attribute{ {Key: "rtcp-mux"}, {Key: "mid", Value: "video"}, {Key: "setup", Value: "actpass"}, }, } t.Run("found", func(t *testing.T) { v, ok := md.Attribute("mid") assert.True(t, ok) assert.Equal(t, "video", v) }) t.Run("not found", func(t *testing.T) { v, ok := md.Attribute("nonexistent") assert.False(t, ok) assert.Equal(t, "", v) }) } func TestRangedPort_String(t *testing.T) { t.Run("no range", func(t *testing.T) { p := &RangedPort{Value: 5004} assert.Equal(t, "5004", p.String()) }) t.Run("with range", func(t *testing.T) { r := 2 p := &RangedPort{Value: 5004, Range: &r} assert.Equal(t, "5004/2", p.String()) }) } func TestRangedPort_marshalInto_RangeBranch(t *testing.T) { r := 3 p := RangedPort{Value: 49170, Range: &r} out := p.marshalInto(nil) assert.Equal(t, "49170/3", string(out)) } func TestRangedPort_marshalSize_RangeBranch(t *testing.T) { r := 12 p := RangedPort{Value: 65535, Range: &r} gotSize := p.marshalSize() wantLen := len((&RangedPort{Value: 65535, Range: &r}).String()) assert.Equal(t, wantLen, gotSize) } func TestMediaName_String(t *testing.T) { r := 2 m := MediaName{ Media: "audio", Port: RangedPort{Value: 5004, Range: &r}, Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, Formats: []string{ "111", "96", }, } assert.Equal(t, "audio 5004/2 UDP/TLS/RTP/SAVPF 111 96", m.String()) } sdp-3.0.17/renovate.json000066400000000000000000000001731512144664500151170ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>pion/renovate-config" ] } sdp-3.0.17/sdp.go000066400000000000000000000002571512144664500135210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package sdp implements Session Description Protocol (SDP) package sdp sdp-3.0.17/session_description.go000066400000000000000000000122611512144664500170170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "net/url" "strconv" ) // SessionDescription is a a well-defined format for conveying sufficient // information to discover and participate in a multimedia session. type SessionDescription struct { // v=0 // https://tools.ietf.org/html/rfc4566#section-5.1 Version Version // o= // https://tools.ietf.org/html/rfc4566#section-5.2 Origin Origin // s= // https://tools.ietf.org/html/rfc4566#section-5.3 SessionName SessionName // i= // https://tools.ietf.org/html/rfc4566#section-5.4 SessionInformation *Information // u= // https://tools.ietf.org/html/rfc4566#section-5.5 URI *url.URL // e= // https://tools.ietf.org/html/rfc4566#section-5.6 EmailAddress *EmailAddress // p= // https://tools.ietf.org/html/rfc4566#section-5.6 PhoneNumber *PhoneNumber // c= // https://tools.ietf.org/html/rfc4566#section-5.7 ConnectionInformation *ConnectionInformation // b=: // https://tools.ietf.org/html/rfc4566#section-5.8 Bandwidth []Bandwidth // https://tools.ietf.org/html/rfc4566#section-5.9 // https://tools.ietf.org/html/rfc4566#section-5.10 TimeDescriptions []TimeDescription // z= ... // https://tools.ietf.org/html/rfc4566#section-5.11 TimeZones []TimeZone // k= // k=: // https://tools.ietf.org/html/rfc4566#section-5.12 EncryptionKey *EncryptionKey // a= // a=: // https://tools.ietf.org/html/rfc4566#section-5.13 Attributes []Attribute // https://tools.ietf.org/html/rfc4566#section-5.14 MediaDescriptions []*MediaDescription } // Attribute returns the value of an attribute and if it exists. func (s *SessionDescription) Attribute(key string) (string, bool) { for _, a := range s.Attributes { if a.Key == key { return a.Value, true } } return "", false } // Version describes the value provided by the "v=" field which gives // the version of the Session Description Protocol. type Version int func (v Version) String() string { return stringFromMarshal(v.marshalInto, v.marshalSize) } func (v Version) marshalInto(b []byte) []byte { return strconv.AppendInt(b, int64(v), 10) } func (v Version) marshalSize() (size int) { return lenInt(int64(v)) } // Origin defines the structure for the "o=" field which provides the // originator of the session plus a session identifier and version number. type Origin struct { Username string SessionID uint64 SessionVersion uint64 NetworkType string AddressType string UnicastAddress string } func (o Origin) String() string { return stringFromMarshal(o.marshalInto, o.marshalSize) } func (o Origin) marshalInto(b []byte) []byte { b = append(append(b, o.Username...), ' ') b = append(strconv.AppendUint(b, o.SessionID, 10), ' ') b = append(strconv.AppendUint(b, o.SessionVersion, 10), ' ') b = append(append(b, o.NetworkType...), ' ') b = append(append(b, o.AddressType...), ' ') return append(b, o.UnicastAddress...) } func (o Origin) marshalSize() (size int) { return len(o.Username) + lenUint(o.SessionID) + lenUint(o.SessionVersion) + len(o.NetworkType) + len(o.AddressType) + len(o.UnicastAddress) + 5 } // SessionName describes a structured representations for the "s=" field // and is the textual session name. type SessionName string func (s SessionName) String() string { return stringFromMarshal(s.marshalInto, s.marshalSize) } func (s SessionName) marshalInto(b []byte) []byte { return append(b, s...) } func (s SessionName) marshalSize() (size int) { return len(s) } // EmailAddress describes a structured representations for the "e=" line // which specifies email contact information for the person responsible for // the conference. type EmailAddress string func (e EmailAddress) String() string { return stringFromMarshal(e.marshalInto, e.marshalSize) } func (e EmailAddress) marshalInto(b []byte) []byte { return append(b, e...) } func (e EmailAddress) marshalSize() (size int) { return len(e) } // PhoneNumber describes a structured representations for the "p=" line // specify phone contact information for the person responsible for the // conference. type PhoneNumber string func (p PhoneNumber) String() string { return stringFromMarshal(p.marshalInto, p.marshalSize) } func (p PhoneNumber) marshalInto(b []byte) []byte { return append(b, p...) } func (p PhoneNumber) marshalSize() (size int) { return len(p) } // TimeZone defines the structured object for "z=" line which describes // repeated sessions scheduling. type TimeZone struct { AdjustmentTime uint64 Offset int64 } func (z TimeZone) String() string { return stringFromMarshal(z.marshalInto, z.marshalSize) } func (z TimeZone) marshalInto(b []byte) []byte { b = strconv.AppendUint(b, z.AdjustmentTime, 10) b = append(b, ' ') return strconv.AppendInt(b, z.Offset, 10) } func (z TimeZone) marshalSize() (size int) { return lenUint(z.AdjustmentTime) + 1 + lenInt(z.Offset) } sdp-3.0.17/sessiondescription_test.go000066400000000000000000000043571512144664500177260ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "testing" "github.com/stretchr/testify/assert" ) const ( exampleAttrExtmap1 = "extmap:1 http://example.com/082005/ext.htm#ttime" exampleAttrExtmap1Line = exampleAttrExtmap1 exampleAttrExtmap2 = "extmap:2/sendrecv http://example.com/082005/ext.htm#xmeta short" exampleAttrExtmap2Line = exampleAttrExtmap2 failingAttrExtmap1 = "extmap:257/sendrecv http://example.com/082005/ext.htm#xmeta short" failingAttrExtmap1Line = attributeKey + failingAttrExtmap1 failingAttrExtmap2 = "extmap:2/blorg http://example.com/082005/ext.htm#xmeta short" failingAttrExtmap2Line = attributeKey + failingAttrExtmap2 ) func TestSessionDescription_Attribute(t *testing.T) { sd := &SessionDescription{ Attributes: []Attribute{ {Key: "ice-options", Value: "trickle"}, {Key: "mid", Value: "video"}, }, } t.Run("found", func(t *testing.T) { v, ok := sd.Attribute("mid") assert.True(t, ok) assert.Equal(t, "video", v) }) t.Run("not found", func(t *testing.T) { v, ok := sd.Attribute("does-not-exist") assert.False(t, ok) assert.Equal(t, "", v) }) } func TestVersion_String(t *testing.T) { var v Version = 0 assert.Equal(t, "0", v.String()) v = 2 assert.Equal(t, "2", v.String()) } func TestOrigin_String(t *testing.T) { o := Origin{ Username: "alice", SessionID: 12345, SessionVersion: 678, NetworkType: "IN", AddressType: "IP4", UnicastAddress: "111.1.111.1", } assert.Equal(t, "alice 12345 678 IN IP4 111.1.111.1", o.String()) } func TestSessionName_String(t *testing.T) { sn := SessionName("My Session") assert.Equal(t, "My Session", sn.String()) empty := SessionName("") assert.Equal(t, "", empty.String()) } func TestEmailAddress_String(t *testing.T) { e := EmailAddress("user@pion.com") assert.Equal(t, "user@pion.com", e.String()) } func TestPhoneNumber_String(t *testing.T) { p := PhoneNumber("+1 111 1111") assert.Equal(t, "+1 111 1111", p.String()) } func TestTimeZone_String(t *testing.T) { z := TimeZone{AdjustmentTime: 3600, Offset: -1800} assert.Equal(t, "3600 -1800", z.String()) z = TimeZone{AdjustmentTime: 0, Offset: 0} assert.Equal(t, "0 0", z.String()) } sdp-3.0.17/time_description.go000066400000000000000000000035511512144664500162740ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "strconv" ) // TimeDescription describes "t=", "r=" fields of the session description // which are used to specify the start and stop times for a session as well as // repeat intervals and durations for the scheduled session. type TimeDescription struct { // t= // https://tools.ietf.org/html/rfc4566#section-5.9 Timing Timing // r= // https://tools.ietf.org/html/rfc4566#section-5.10 RepeatTimes []RepeatTime } // Timing defines the "t=" field's structured representation for the start and // stop times. type Timing struct { StartTime uint64 StopTime uint64 } func (t Timing) String() string { return stringFromMarshal(t.marshalInto, t.marshalSize) } func (t Timing) marshalInto(b []byte) []byte { b = append(strconv.AppendUint(b, t.StartTime, 10), ' ') return strconv.AppendUint(b, t.StopTime, 10) } func (t Timing) marshalSize() (size int) { return lenUint(t.StartTime) + 1 + lenUint(t.StopTime) } // RepeatTime describes the "r=" fields of the session description which // represents the intervals and durations for repeated scheduled sessions. type RepeatTime struct { Interval int64 Duration int64 Offsets []int64 } func (r RepeatTime) String() string { return stringFromMarshal(r.marshalInto, r.marshalSize) } func (r RepeatTime) marshalInto(b []byte) []byte { b = strconv.AppendInt(b, r.Interval, 10) b = append(b, ' ') b = strconv.AppendInt(b, r.Duration, 10) for _, value := range r.Offsets { b = append(b, ' ') b = strconv.AppendInt(b, value, 10) } return b } func (r RepeatTime) marshalSize() (size int) { size = lenInt(r.Interval) size += 1 + lenInt(r.Duration) for _, o := range r.Offsets { size += 1 + lenInt(o) } return } sdp-3.0.17/time_description_test.go000066400000000000000000000011711512144664500173270ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "testing" "github.com/stretchr/testify/assert" ) func TestTiming_String(t *testing.T) { assert.Equal(t, "0 0", Timing{StartTime: 0, StopTime: 0}.String()) assert.Equal(t, "12345 67890", Timing{StartTime: 12345, StopTime: 67890}.String()) } func TestRepeatTime_String(t *testing.T) { assert.Equal(t, "3600 900", RepeatTime{Interval: 3600, Duration: 900}.String()) assert.Equal( t, "604800 3600 -60 0 60", RepeatTime{Interval: 604800, Duration: 3600, Offsets: []int64{-60, 0, 60}}.String(), ) } sdp-3.0.17/unmarshal.go000066400000000000000000000601041512144664500147220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "errors" "fmt" "net/url" "strconv" "strings" "sync" ) var ( errSDPInvalidSyntax = errors.New("sdp: invalid syntax") errSDPInvalidNumericValue = errors.New("sdp: invalid numeric value") errSDPInvalidValue = errors.New("sdp: invalid value") errSDPInvalidPortValue = errors.New("sdp: invalid port value") errSDPCacheInvalid = errors.New("sdp: invalid cache") //nolint: gochecknoglobals unmarshalCachePool = sync.Pool{ New: func() any { return &unmarshalCache{} }, } ) // UnmarshalString is the primary function that deserializes the session description // message and stores it inside of a structured SessionDescription object. // // The States Transition Table describes the computation flow between functions // (namely s1, s2, s3, ...) for a parsing procedure that complies with the // specifications laid out by the rfc4566#section-5 as well as by JavaScript // Session Establishment Protocol draft. Links: // // https://tools.ietf.org/html/rfc4566#section-5 // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24 // // https://tools.ietf.org/html/rfc4566#section-5 // Session description // // v= (protocol version) // o= (originator and session identifier) // s= (session name) // i=* (session information) // u=* (URI of description) // e=* (email address) // p=* (phone number) // c=* (connection information -- not required if included in // all media) // b=* (zero or more bandwidth information lines) // One or more time descriptions ("t=" and "r=" lines; see below) // z=* (time zone adjustments) // k=* (encryption key) // a=* (zero or more session attribute lines) // Zero or more media descriptions // // Time description // // t= (time the session is active) // r=* (zero or more repeat times) // // Media description, if present // // m= (media name and transport address) // i=* (media title) // c=* (connection information -- optional if included at // session level) // b=* (zero or more bandwidth information lines) // k=* (encryption key) // a=* (zero or more media attribute lines) // // In order to generate the following state table and draw subsequent // deterministic finite-state automota ("DFA") the following regex was used to // derive the DFA: // // vosi?u?e?p?c?b*(tr*)+z?k?a*(mi?c?b*k?a*)* // // possible place and state to exit: // // ** * * * ** * * * * // 99 1 1 1 11 1 1 1 1 // 3 1 1 26 5 5 4 4 // // Please pay close attention to the `k`, and `a` parsing states. In the table // below in order to distinguish between the states belonging to the media // description as opposed to the session description, the states are marked // with an asterisk ("a*", "k*"). // +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ // | STATES | a* | a*,k* | a | a,k | b | b,c | e | i | m | o | p | r,t | s | t | u | v | z | // +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ // | s1 | | | | | | | | | | | | | | | | 2 | | // | s2 | | | | | | | | | | 3 | | | | | | | | // | s3 | | | | | | | | | | | | | 4 | | | | | // | s4 | | | | | | 5 | 6 | 7 | | | 8 | | | 9 | 10 | | | // | s5 | | | | | 5 | | | | | | | | | 9 | | | | // | s6 | | | | | | 5 | | | | | 8 | | | 9 | | | | // | s7 | | | | | | 5 | 6 | | | | 8 | | | 9 | 10 | | | // | s8 | | | | | | 5 | | | | | | | | 9 | | | | // | s9 | | | | 11 | | | | | 12 | | | 9 | | | | | 13 | // | s10 | | | | | | 5 | 6 | | | | 8 | | | 9 | | | | // | s11 | | | 11 | | | | | | 12 | | | | | | | | | // | s12 | | 14 | | | | 15 | | 16 | 12 | | | | | | | | | // | s13 | | | | 11 | | | | | 12 | | | | | | | | | // | s14 | 14 | | | | | | | | 12 | | | | | | | | | // | s15 | | 14 | | | 15 | | | | 12 | | | | | | | | | // | s16 | | 14 | | | | 15 | | | 12 | | | | | | | | | // +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ . func (s *SessionDescription) UnmarshalString(value string) error { var ok bool lex := new(lexer) if lex.cache, ok = unmarshalCachePool.Get().(*unmarshalCache); !ok { return errSDPCacheInvalid } defer unmarshalCachePool.Put(lex.cache) lex.cache.reset() lex.desc = s lex.value = value for state := s1; state != nil; { var err error state, err = state(lex) if err != nil { return err } } s.Attributes = lex.cache.cloneSessionAttributes() populateMediaAttributes(lex.cache, lex.desc) return nil } // Unmarshal converts the value into a []byte and then calls UnmarshalString. // Callers should use the more performant UnmarshalString. func (s *SessionDescription) Unmarshal(value []byte) error { return s.UnmarshalString(string(value)) } func s1(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { if key == 'v' { return unmarshalProtocolVersion } return nil }) } func s2(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { if key == 'o' { return unmarshalOrigin } return nil }) } func s3(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { if key == 's' { return unmarshalSessionName } return nil }) } func s4(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'i': return unmarshalSessionInformation case 'u': return unmarshalURI case 'e': return unmarshalEmail case 'p': return unmarshalPhone case 'c': return unmarshalSessionConnectionInformation case 'b': return unmarshalSessionBandwidth case 't': return unmarshalTiming } return nil }) } func s5(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'b': return unmarshalSessionBandwidth case 't': return unmarshalTiming } return nil }) } func s6(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'p': return unmarshalPhone case 'c': return unmarshalSessionConnectionInformation case 'b': return unmarshalSessionBandwidth case 't': return unmarshalTiming } return nil }) } func s7(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'u': return unmarshalURI case 'e': return unmarshalEmail case 'p': return unmarshalPhone case 'c': return unmarshalSessionConnectionInformation case 'b': return unmarshalSessionBandwidth case 't': return unmarshalTiming } return nil }) } func s8(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'c': return unmarshalSessionConnectionInformation case 'b': return unmarshalSessionBandwidth case 't': return unmarshalTiming } return nil }) } func s9(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'z': return unmarshalTimeZones case 'k': return unmarshalSessionEncryptionKey case 'a': return unmarshalSessionAttribute case 'r': return unmarshalRepeatTimes case 't': return unmarshalTiming case 'm': return unmarshalMediaDescription } return nil }) } func s10(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'e': return unmarshalEmail case 'p': return unmarshalPhone case 'c': return unmarshalSessionConnectionInformation case 'b': return unmarshalSessionBandwidth case 't': return unmarshalTiming } return nil }) } func s11(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'a': return unmarshalSessionAttribute case 'm': return unmarshalMediaDescription } return nil }) } func s12(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'a': return unmarshalMediaAttribute case 'k': return unmarshalMediaEncryptionKey case 'b': return unmarshalMediaBandwidth case 'c': return unmarshalMediaConnectionInformation case 'i': return unmarshalMediaTitle case 'm': return unmarshalMediaDescription } return nil }) } func s13(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'a': return unmarshalSessionAttribute case 'k': return unmarshalSessionEncryptionKey case 'm': return unmarshalMediaDescription } return nil }) } func s14(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'a': return unmarshalMediaAttribute case 'k': // Non-spec ordering return unmarshalMediaEncryptionKey case 'b': // Non-spec ordering return unmarshalMediaBandwidth case 'c': // Non-spec ordering return unmarshalMediaConnectionInformation case 'i': // Non-spec ordering return unmarshalMediaTitle case 'm': return unmarshalMediaDescription } return nil }) } func s15(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'a': return unmarshalMediaAttribute case 'k': return unmarshalMediaEncryptionKey case 'b': return unmarshalMediaBandwidth case 'c': return unmarshalMediaConnectionInformation case 'i': // Non-spec ordering return unmarshalMediaTitle case 'm': return unmarshalMediaDescription } return nil }) } func s16(l *lexer) (stateFn, error) { return l.handleType(func(key byte) stateFn { switch key { case 'a': return unmarshalMediaAttribute case 'k': return unmarshalMediaEncryptionKey case 'c': return unmarshalMediaConnectionInformation case 'b': return unmarshalMediaBandwidth case 'i': // Non-spec ordering return unmarshalMediaTitle case 'm': return unmarshalMediaDescription } return nil }) } func unmarshalProtocolVersion(l *lexer) (stateFn, error) { version, err := l.readUint64Field() if err != nil { return nil, err } // As off the latest draft of the rfc this value is required to be 0. // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24#section-5.8.1 if version != 0 { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, version) } if err := l.nextLine(); err != nil { return nil, err } return s2, nil } func unmarshalOrigin(lex *lexer) (stateFn, error) { var err error lex.desc.Origin.Username, err = lex.readField() if err != nil { return nil, err } lex.desc.Origin.SessionID, err = lex.readUint64Field() if err != nil { return nil, err } lex.desc.Origin.SessionVersion, err = lex.readUint64Field() if err != nil { return nil, err } lex.desc.Origin.NetworkType, err = lex.readField() if err != nil { return nil, err } // Set according to currently registered with IANA // https://tools.ietf.org/html/rfc4566#section-8.2.6 if !anyOf(lex.desc.Origin.NetworkType, "IN") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, lex.desc.Origin.NetworkType) } // Handle potentially missing AddressType field err = handleAddressType(lex) if err != nil { return nil, err } // Set according to currently registered with IANA // https://tools.ietf.org/html/rfc4566#section-8.2.7 if !anyOf(lex.desc.Origin.AddressType, "IP4", "IP6") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, lex.desc.Origin.AddressType) } // Handle potentially missing UnicastAddress field err = handleUnicastAddress(lex) if err != nil { return nil, err } if err := lex.nextLine(); err != nil { return nil, err } return s3, nil } // handleAddressType processes AddressType field with graceful handling for missing fields. func handleAddressType(lex *lexer) error { addressType, err := lex.readRequiredField() if err != nil { if errors.Is(err, errFieldMissing) { // Field missing - use defaults for camera compatibility lex.desc.Origin.AddressType = "IP4" lex.desc.Origin.UnicastAddress = "0.0.0.0" return nil } return err } lex.desc.Origin.AddressType = addressType return nil } // handleUnicastAddress processes UnicastAddress field with graceful handling for missing fields. func handleUnicastAddress(lex *lexer) error { unicastAddress, err := lex.readRequiredField() if err != nil { if errors.Is(err, errFieldMissing) { // Use appropriate default based on address type if lex.desc.Origin.AddressType == "IP6" { lex.desc.Origin.UnicastAddress = "::" } else { lex.desc.Origin.UnicastAddress = "0.0.0.0" } return nil } return err } lex.desc.Origin.UnicastAddress = unicastAddress return nil } func unmarshalSessionName(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } l.desc.SessionName = SessionName(value) return s4, nil } func unmarshalSessionInformation(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } sessionInformation := Information(value) l.desc.SessionInformation = &sessionInformation return s7, nil } func unmarshalURI(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } l.desc.URI, err = url.Parse(value) if err != nil { return nil, err } return s10, nil } func unmarshalEmail(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } emailAddress := EmailAddress(value) l.desc.EmailAddress = &emailAddress return s6, nil } func unmarshalPhone(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } phoneNumber := PhoneNumber(value) l.desc.PhoneNumber = &phoneNumber return s8, nil } func unmarshalSessionConnectionInformation(l *lexer) (stateFn, error) { var err error l.desc.ConnectionInformation, err = l.unmarshalConnectionInformation() if err != nil { return nil, err } return s5, nil } func (l *lexer) unmarshalConnectionInformation() (*ConnectionInformation, error) { var err error var connInfo ConnectionInformation connInfo.NetworkType, err = l.readField() if err != nil { return nil, err } // Set according to currently registered with IANA // https://tools.ietf.org/html/rfc4566#section-8.2.6 if !anyOf(connInfo.NetworkType, "IN") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, connInfo.NetworkType) } connInfo.AddressType, err = l.readField() if err != nil { return nil, err } // Set according to currently registered with IANA // https://tools.ietf.org/html/rfc4566#section-8.2.7 if !anyOf(connInfo.AddressType, "IP4", "IP6") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, connInfo.AddressType) } address, err := l.readField() if err != nil { return nil, err } if address != "" { connInfo.Address = new(Address) connInfo.Address.Address = address } if err := l.nextLine(); err != nil { return nil, err } return &connInfo, nil } func unmarshalSessionBandwidth(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } bandwidth, err := unmarshalBandwidth(value) if err != nil { return nil, fmt.Errorf("%w `b=%v`", errSDPInvalidValue, value) } l.desc.Bandwidth = append(l.desc.Bandwidth, *bandwidth) return s5, nil } func unmarshalBandwidth(value string) (*Bandwidth, error) { parts := strings.Split(value, ":") if len(parts) != 2 { return nil, fmt.Errorf("%w `b=%v`", errSDPInvalidValue, parts) } experimental := strings.HasPrefix(parts[0], "X-") if experimental { parts[0] = strings.TrimPrefix(parts[0], "X-") } else if !anyOf(parts[0], "CT", "AS", "TIAS", "RS", "RR") { // Set according to currently registered with IANA // https://tools.ietf.org/html/rfc4566#section-5.8 // https://tools.ietf.org/html/rfc3890#section-6.2 // https://tools.ietf.org/html/rfc3556#section-2 return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, parts[0]) } bandwidth, err := strconv.ParseUint(parts[1], 10, 64) if err != nil { return nil, fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, parts[1]) } return &Bandwidth{ Experimental: experimental, Type: parts[0], Bandwidth: bandwidth, }, nil } func unmarshalTiming(lex *lexer) (stateFn, error) { var err error var td TimeDescription td.Timing.StartTime, err = lex.readUint64Field() if err != nil { return nil, err } td.Timing.StopTime, err = lex.readUint64Field() if err != nil { return nil, err } if err := lex.nextLine(); err != nil { return nil, err } lex.desc.TimeDescriptions = append(lex.desc.TimeDescriptions, td) return s9, nil } func unmarshalRepeatTimes(lex *lexer) (stateFn, error) { var err error var newRepeatTime RepeatTime latestTimeDesc := &lex.desc.TimeDescriptions[len(lex.desc.TimeDescriptions)-1] field, err := lex.readField() if err != nil { return nil, err } newRepeatTime.Interval, err = parseTimeUnits(field) if err != nil { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field) } field, err = lex.readField() if err != nil { return nil, err } newRepeatTime.Duration, err = parseTimeUnits(field) if err != nil { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field) } for { field, err := lex.readField() if err != nil { return nil, err } if field == "" { break } offset, err := parseTimeUnits(field) if err != nil { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field) } newRepeatTime.Offsets = append(newRepeatTime.Offsets, offset) } if err := lex.nextLine(); err != nil { return nil, err } latestTimeDesc.RepeatTimes = append(latestTimeDesc.RepeatTimes, newRepeatTime) return s9, nil } func unmarshalTimeZones(lex *lexer) (stateFn, error) { // These fields are transimitted in pairs // z= .... // so we are making sure that there are actually multiple of 2 total. for { var err error var timeZone TimeZone timeZone.AdjustmentTime, err = lex.readUint64Field() if err != nil { return nil, err } offset, err := lex.readField() if err != nil { return nil, err } if offset == "" { break } timeZone.Offset, err = parseTimeUnits(offset) if err != nil { return nil, err } lex.desc.TimeZones = append(lex.desc.TimeZones, timeZone) } if err := lex.nextLine(); err != nil { return nil, err } return s13, nil } func unmarshalSessionEncryptionKey(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } encryptionKey := EncryptionKey(value) l.desc.EncryptionKey = &encryptionKey return s11, nil } func unmarshalSessionAttribute(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } i := strings.IndexRune(value, ':') a := l.cache.getSessionAttribute() if i > 0 { a.Key = value[:i] a.Value = value[i+1:] } else { a.Key = value } return s11, nil } func unmarshalMediaDescription(lex *lexer) (stateFn, error) { //nolint:cyclop populateMediaAttributes(lex.cache, lex.desc) var newMediaDesc MediaDescription // field, err := lex.readField() if err != nil { return nil, err } // Set according to currently registered with IANA // https://tools.ietf.org/html/rfc4566#section-5.14 if !anyOf(field, "audio", "video", "text", "application", "message") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, field) } newMediaDesc.MediaName.Media = field // field, err = lex.readField() if err != nil { return nil, err } parts := strings.Split(field, "/") newMediaDesc.MediaName.Port.Value, err = parsePort(parts[0]) if err != nil { return nil, fmt.Errorf("%w `%v`", errSDPInvalidPortValue, parts[0]) } if len(parts) > 1 { var portRange int portRange, err = strconv.Atoi(parts[1]) if err != nil { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, parts) } newMediaDesc.MediaName.Port.Range = &portRange } // field, err = lex.readField() if err != nil { return nil, err } // Set according to currently registered with IANA // https://tools.ietf.org/html/rfc4566#section-5.14 // https://tools.ietf.org/html/rfc4975#section-8.1 for _, proto := range strings.Split(field, "/") { if !anyOf( proto, "UDP", "RTP", "AVP", "SAVP", "SAVPF", "TLS", "DTLS", "SCTP", "AVPF", "TCP", "MSRP", "BFCP", "UDT", "IX", "MRCPv2", ) { return nil, fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, field) } newMediaDesc.MediaName.Protos = append(newMediaDesc.MediaName.Protos, proto) } // ... for { field, err = lex.readField() if err != nil { return nil, err } if field == "" { break } newMediaDesc.MediaName.Formats = append(newMediaDesc.MediaName.Formats, field) } if err := lex.nextLine(); err != nil { return nil, err } lex.desc.MediaDescriptions = append(lex.desc.MediaDescriptions, &newMediaDesc) return s12, nil } func unmarshalMediaTitle(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] mediaTitle := Information(value) latestMediaDesc.MediaTitle = &mediaTitle return s16, nil } func unmarshalMediaConnectionInformation(l *lexer) (stateFn, error) { var err error latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] latestMediaDesc.ConnectionInformation, err = l.unmarshalConnectionInformation() if err != nil { return nil, err } return s15, nil } func unmarshalMediaBandwidth(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] bandwidth, err := unmarshalBandwidth(value) if err != nil { return nil, fmt.Errorf("%w `b=%v`", errSDPInvalidSyntax, value) } latestMediaDesc.Bandwidth = append(latestMediaDesc.Bandwidth, *bandwidth) return s15, nil } func unmarshalMediaEncryptionKey(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] encryptionKey := EncryptionKey(value) latestMediaDesc.EncryptionKey = &encryptionKey return s14, nil } func unmarshalMediaAttribute(l *lexer) (stateFn, error) { value, err := l.readLine() if err != nil { return nil, err } i := strings.IndexRune(value, ':') a := l.cache.getMediaAttribute() if i > 0 { a.Key = value[:i] a.Value = value[i+1:] } else { a.Key = value } return s14, nil } func parseTimeUnits(value string) (num int64, err error) { if len(value) == 0 { return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value) } k := timeShorthand(value[len(value)-1]) if k > 0 { num, err = strconv.ParseInt(value[:len(value)-1], 10, 64) } else { k = 1 num, err = strconv.ParseInt(value, 10, 64) } if err != nil { return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value) } return num * k, nil } func timeShorthand(b byte) int64 { // Some time offsets in the protocol can be provided with a shorthand // notation. This code ensures to convert it to NTP timestamp format. switch b { case 'd': // days return 86400 case 'h': // hours return 3600 case 'm': // minutes return 60 case 's': // seconds (allowed for completeness) return 1 default: return 0 } } func parsePort(value string) (int, error) { port, err := strconv.Atoi(value) if err != nil { return 0, fmt.Errorf("%w `%v`", errSDPInvalidPortValue, value) } if port < 0 || port > 65535 { return 0, fmt.Errorf("%w -- out of range `%v`", errSDPInvalidPortValue, port) } return port, nil } func populateMediaAttributes(c *unmarshalCache, s *SessionDescription) { if len(s.MediaDescriptions) != 0 { lastMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1] lastMediaDesc.Attributes = c.cloneMediaAttributes() } } sdp-3.0.17/unmarshal_cache.go000066400000000000000000000022561512144664500160510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp type unmarshalCache struct { sessionAttributes []Attribute mediaAttributes []Attribute } func (c *unmarshalCache) reset() { c.sessionAttributes = c.sessionAttributes[:0] c.mediaAttributes = c.mediaAttributes[:0] } func (c *unmarshalCache) getSessionAttribute() *Attribute { c.sessionAttributes = append(c.sessionAttributes, Attribute{}) return &c.sessionAttributes[len(c.sessionAttributes)-1] } func (c *unmarshalCache) cloneSessionAttributes() []Attribute { if len(c.sessionAttributes) == 0 { return nil } s := make([]Attribute, len(c.sessionAttributes)) copy(s, c.sessionAttributes) c.sessionAttributes = c.sessionAttributes[:0] return s } func (c *unmarshalCache) getMediaAttribute() *Attribute { c.mediaAttributes = append(c.mediaAttributes, Attribute{}) return &c.mediaAttributes[len(c.mediaAttributes)-1] } func (c *unmarshalCache) cloneMediaAttributes() []Attribute { if len(c.mediaAttributes) == 0 { return nil } s := make([]Attribute, len(c.mediaAttributes)) copy(s, c.mediaAttributes) c.mediaAttributes = c.mediaAttributes[:0] return s } sdp-3.0.17/unmarshal_test.go000066400000000000000000001276501512144664500157730ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "io" "testing" "github.com/stretchr/testify/assert" ) const ( BaseSDP = "v=0\r\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + "s=SDP Seminar\r\n" SessionInformationSDP = BaseSDP + "i=A Seminar on the session description protocol\r\n" + "t=3034423619 3042462419\r\n" // https://tools.ietf.org/html/rfc4566#section-5 // Parsers SHOULD be tolerant and also accept records terminated // with a single newline character. SessionInformationSDPLFOnly = "v=0\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\n" + "s=SDP Seminar\n" + "i=A Seminar on the session description protocol\n" + "t=3034423619 3042462419\n" // SessionInformationSDPCROnly = "v=0\r" + // "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r" + // "s=SDP Seminar\r" // "i=A Seminar on the session description protocol\r" + // "t=3034423619 3042462419\r" // Other SDP parsers (e.g. one in VLC media player) allow // empty lines. SessionInformationSDPExtraCRLF = "v=0\r\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + "\r\n" + "s=SDP Seminar\r\n" + "\r\n" + "i=A Seminar on the session description protocol\r\n" + "\r\n" + "t=3034423619 3042462419\r\n" + "\r\n" URISDP = BaseSDP + "u=http://www.example.com/seminars/sdp.pdf\r\n" + "t=3034423619 3042462419\r\n" EmailAddressSDP = BaseSDP + "e=j.doe@example.com (Jane Doe)\r\n" + "t=3034423619 3042462419\r\n" PhoneNumberSDP = BaseSDP + "p=+1 617 555-6011\r\n" + "t=3034423619 3042462419\r\n" SessionConnectionInformationSDP = BaseSDP + "c=IN IP4 224.2.17.12/127\r\n" + "t=3034423619 3042462419\r\n" SessionBandwidthSDP = BaseSDP + "b=X-YZ:128\r\n" + "b=AS:12345\r\n" + "t=3034423619 3042462419\r\n" TimingSDP = BaseSDP + "t=2873397496 2873404696\r\n" // Short hand time notation is converted into NTP timestamp format in // seconds. Because of that unittest comparisons will fail as the same time // will be expressed in different units. RepeatTimesSDP = TimingSDP + "r=604800 3600 0 90000\r\n" + "r=3d 2h 0 21h\r\n" RepeatTimesSDPExpected = TimingSDP + "r=604800 3600 0 90000\r\n" + "r=259200 7200 0 75600\r\n" RepeatTimesSDPExtraCRLF = RepeatTimesSDPExpected + "\r\n" // The expected value looks a bit different for the same reason as mentioned // above regarding RepeatTimes. TimeZonesSDP = TimingSDP + "r=2882844526 -1h 2898848070 0\r\n" TimeZonesSDPExpected = TimingSDP + "r=2882844526 -3600 2898848070 0\r\n" TimeZonesSDP2 = TimingSDP + "z=2882844526 -3600 2898848070 0\r\n" TimeZonesSDP2ExtraCRLF = TimeZonesSDP2 + "\r\n" SessionEncryptionKeySDP = TimingSDP + "k=prompt\r\n" SessionEncryptionKeySDPExtraCRLF = SessionEncryptionKeySDP + "\r\n" SessionAttributesSDP = TimingSDP + "a=rtpmap:96 opus/48000\r\n" MediaNameSDP = TimingSDP + "m=video 51372 RTP/AVP 99\r\n" + "m=audio 54400 RTP/SAVPF 0 96\r\n" + "m=message 5028 TCP/MSRP *\r\n" MediaNameSDPExtraCRLF = MediaNameSDP + "\r\n" MediaTitleSDP = MediaNameSDP + "i=Vivamus a posuere nisl\r\n" MediaConnectionInformationSDP = MediaNameSDP + "c=IN IP4 203.0.113.1\r\n" MediaConnectionInformationSDPExtraCRLF = MediaConnectionInformationSDP + "\r\n" MediaDescriptionOutOfOrderSDP = MediaNameSDP + "a=rtpmap:99 h263-1998/90000\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + "c=IN IP4 203.0.113.1\r\n" + "i=Vivamus a posuere nisl\r\n" MediaDescriptionOutOfOrderSDPActual = MediaNameSDP + "i=Vivamus a posuere nisl\r\n" + "c=IN IP4 203.0.113.1\r\n" + "a=rtpmap:99 h263-1998/90000\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" MediaBandwidthSDP = MediaNameSDP + "b=X-YZ:128\r\n" + "b=AS:12345\r\n" + "b=TIAS:12345\r\n" + "b=RS:12345\r\n" + "b=RR:12345\r\n" MediaEncryptionKeySDP = MediaNameSDP + "k=prompt\r\n" MediaEncryptionKeySDPExtraCRLF = MediaEncryptionKeySDP + "\r\n" MediaAttributesSDP = MediaNameSDP + "a=rtpmap:99 h263-1998/90000\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + "a=rtcp-fb:97 ccm fir\r\n" + "a=rtcp-fb:97 nack\r\n" + "a=rtcp-fb:97 nack pli\r\n" MediaBfcpSDP = TimingSDP + "m=application 3238 UDP/BFCP *\r\n" + "a=sendrecv\r\n" + "a=setup:actpass\r\n" + "a=connection:new\r\n" + "a=floorctrl:c-s\r\n" MediaCubeSDP = TimingSDP + "m=application 2455 UDP/UDT/IX *\r\n" + "a=ixmap:0 ping\r\n" + "a=ixmap:2 xccp\r\n" MediaTCPMRCPv2 = TimingSDP + "m=application 1544 TCP/MRCPv2 1\r\n" MediaTCPTLSMRCPv2 = TimingSDP + "m=application 1544 TCP/TLS/MRCPv2 1\r\n" CanonicalUnmarshalSDP = "v=0\r\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + "s=SDP Seminar\r\n" + "i=A Seminar on the session description protocol\r\n" + "u=http://www.example.com/seminars/sdp.pdf\r\n" + "e=j.doe@example.com (Jane Doe)\r\n" + "p=+1 617 555-6011\r\n" + "c=IN IP4 224.2.17.12/127\r\n" + "b=X-YZ:128\r\n" + "b=AS:12345\r\n" + "t=2873397496 2873404696\r\n" + "t=3034423619 3042462419\r\n" + "r=604800 3600 0 90000\r\n" + "z=2882844526 -3600 2898848070 0\r\n" + "k=prompt\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + "a=recvonly\r\n" + "m=audio 49170 RTP/AVP 0\r\n" + "i=Vivamus a posuere nisl\r\n" + "c=IN IP4 203.0.113.1\r\n" + "b=X-YZ:128\r\n" + "k=prompt\r\n" + "a=sendrecv\r\n" + "m=video 51372 RTP/AVP 99\r\n" + "a=rtpmap:99 h263-1998/90000\r\n" ) func TestRoundTrip(t *testing.T) { for _, test := range []struct { Name string SDP string Actual string }{ { Name: "SessionInformationSDPLFOnly", SDP: SessionInformationSDPLFOnly, Actual: SessionInformationSDP, }, // { // Name: "SessionInformationSDPCROnly", // SDP: SessionInformationSDPCROnly, // Actual: SessionInformationSDPBaseSDP, // }, { Name: "SessionInformationSDPExtraCRLF", SDP: SessionInformationSDPExtraCRLF, Actual: SessionInformationSDP, }, { Name: "SessionInformation", SDP: SessionInformationSDP, }, { Name: "URI", SDP: URISDP, }, { Name: "EmailAddress", SDP: EmailAddressSDP, }, { Name: "PhoneNumber", SDP: PhoneNumberSDP, }, { Name: "RepeatTimesSDPExtraCRLF", SDP: RepeatTimesSDPExtraCRLF, Actual: RepeatTimesSDPExpected, }, { Name: "SessionConnectionInformation", SDP: SessionConnectionInformationSDP, }, { Name: "SessionBandwidth", SDP: SessionBandwidthSDP, }, { Name: "SessionEncryptionKey", SDP: SessionEncryptionKeySDP, }, { Name: "SessionEncryptionKeyExtraCRLF", SDP: SessionEncryptionKeySDPExtraCRLF, Actual: SessionEncryptionKeySDP, }, { Name: "SessionAttributes", SDP: SessionAttributesSDP, }, { Name: "TimeZonesSDP2ExtraCRLF", SDP: TimeZonesSDP2ExtraCRLF, Actual: TimeZonesSDP2, }, { Name: "MediaName", SDP: MediaNameSDP, }, { Name: "MediaNameExtraCRLF", SDP: MediaNameSDPExtraCRLF, Actual: MediaNameSDP, }, { Name: "MediaTitle", SDP: MediaTitleSDP, }, { Name: "MediaConnectionInformation", SDP: MediaConnectionInformationSDP, }, { Name: "MediaConnectionInformationExtraCRLF", SDP: MediaConnectionInformationSDPExtraCRLF, Actual: MediaConnectionInformationSDP, }, { Name: "MediaDescriptionOutOfOrder", SDP: MediaDescriptionOutOfOrderSDP, Actual: MediaDescriptionOutOfOrderSDPActual, }, { Name: "MediaBandwidth", SDP: MediaBandwidthSDP, }, { Name: "MediaEncryptionKey", SDP: MediaEncryptionKeySDP, }, { Name: "MediaEncryptionKeyExtraCRLF", SDP: MediaEncryptionKeySDPExtraCRLF, Actual: MediaEncryptionKeySDP, }, { Name: "MediaAttributes", SDP: MediaAttributesSDP, }, { Name: "CanonicalUnmarshal", SDP: CanonicalUnmarshalSDP, }, { Name: "MediaBfcpSDP", SDP: MediaBfcpSDP, }, { Name: "MediaCubeSDP", SDP: MediaCubeSDP, }, { Name: "MediaTCPMRCPv2", SDP: MediaTCPMRCPv2, }, { Name: "MediaTCPTLSMRCPv2", SDP: MediaTCPTLSMRCPv2, }, } { test := test t.Run(test.Name, func(t *testing.T) { sd := &SessionDescription{} err := sd.UnmarshalString(test.SDP) assert.NoError(t, err) actual, err := sd.Marshal() assert.NoError(t, err) want := test.SDP if test.Actual != "" { want = test.Actual } assert.Equal(t, want, string(actual)) }) } } func TestUnmarshalRepeatTimes(t *testing.T) { sd := &SessionDescription{} assert.NoError(t, sd.UnmarshalString(RepeatTimesSDP)) actual, err := sd.Marshal() assert.NoError(t, err) assert.Equal(t, RepeatTimesSDPExpected, string(actual)) err = sd.UnmarshalString(TimingSDP + "r=\r\n") assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalTimeZones(t *testing.T) { sd := &SessionDescription{} assert.NoError(t, sd.UnmarshalString(TimeZonesSDP)) actual, err := sd.Marshal() assert.NoError(t, err) assert.Equal(t, TimeZonesSDPExpected, string(actual)) } func TestUnmarshalNonNilAddress(t *testing.T) { in := "v=0\r\no=0 0 0 IN IP4 0\r\ns=0\r\nc=IN IP4\r\nt=0 0\r\n" var sd SessionDescription err := sd.UnmarshalString(in) assert.NoError(t, err) out, err := sd.Marshal() assert.NoError(t, err) assert.Equal(t, in, string(out)) } func TestUnmarshalZeroValues(t *testing.T) { in := "v=0\r\no=0 0 0 IN IP4 0\r\ns=\r\nt=0 0\r\n" var sd SessionDescription assert.NoError(t, sd.UnmarshalString(in)) out, err := sd.Marshal() assert.NoError(t, err) assert.Equal(t, in, string(out)) } func TestUnmarshalPortRange(t *testing.T) { for _, test := range []struct { In string ExpectError error }{ { In: SessionAttributesSDP + "m=video -1 RTP/AVP 99\r\n", ExpectError: errSDPInvalidPortValue, }, { In: SessionAttributesSDP + "m=video 65536 RTP/AVP 99\r\n", ExpectError: errSDPInvalidPortValue, }, { In: SessionAttributesSDP + "m=video 0 RTP/AVP 99\r\n", ExpectError: nil, }, { In: SessionAttributesSDP + "m=video 65535 RTP/AVP 99\r\n", ExpectError: nil, }, { In: SessionAttributesSDP + "m=video --- RTP/AVP 99\r\n", ExpectError: errSDPInvalidPortValue, }, } { var sd SessionDescription err := sd.UnmarshalString(test.In) if test.ExpectError != nil { assert.ErrorIs(t, err, test.ExpectError) } else { assert.NoError(t, err) } } } func BenchmarkUnmarshal(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { var sd SessionDescription err := sd.UnmarshalString(CanonicalUnmarshalSDP) assert.NoError(b, err) } } func TestUnmarshalOriginIncomplete(t *testing.T) { tests := []struct { name string input string expected Origin }{ { name: "missing unicast address - Uniview camera case", input: "v=0\r\no=- 1001 1 IN IP4\r\ns=VCP IPC Realtime stream\r\nt=0 0\r\n", expected: Origin{ Username: "-", SessionID: 1001, SessionVersion: 1, NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0", }, }, { name: "missing address type and address", input: "v=0\r\no=- 1001 1 IN\r\ns=Test Stream\r\nt=0 0\r\n", expected: Origin{ Username: "-", SessionID: 1001, SessionVersion: 1, NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0", }, }, { name: "IPv6 missing address", input: "v=0\r\no=- 1001 1 IN IP6\r\ns=Test Stream\r\nt=0 0\r\n", expected: Origin{ Username: "-", SessionID: 1001, SessionVersion: 1, NetworkType: "IN", AddressType: "IP6", UnicastAddress: "::", }, }, { name: "complete origin line - should work as before", input: "v=0\r\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\ns=SDP Seminar\r\nt=3034423619 3042462419\r\n", expected: Origin{ Username: "jdoe", SessionID: 2890844526, SessionVersion: 2890842807, NetworkType: "IN", AddressType: "IP4", UnicastAddress: "10.47.16.5", }, }, { name: "empty address field", input: "v=0\r\no=- 1001 1 IN IP4 \r\ns=Test\r\nt=0 0\r\n", expected: Origin{ Username: "-", SessionID: 1001, SessionVersion: 1, NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var sd SessionDescription err := sd.UnmarshalString(test.input) assert.NoError(t, err) assert.Equal(t, test.expected, sd.Origin) }) } } func TestUnmarshalOriginInvalidFields(t *testing.T) { tests := []struct { name string input string }{ { name: "invalid network type", input: "v=0\r\no=- 1001 1 INVALID IP4 10.0.0.1\r\ns=Test\r\nt=0 0\r\n", }, { name: "invalid address type", input: "v=0\r\no=- 1001 1 IN INVALID 10.0.0.1\r\ns=Test\r\nt=0 0\r\n", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var sd SessionDescription err := sd.UnmarshalString(test.input) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid value") }) } } // Test edge cases for 100% coverage. func TestUnmarshalOriginEdgeCases(t *testing.T) { tests := []struct { name string input string expectError bool }{ { name: "missing mandatory username", input: "v=0\r\no=\r\ns=Test\r\nt=0 0\r\n", expectError: true, }, { name: "missing mandatory session ID", input: "v=0\r\no=user\r\ns=Test\r\nt=0 0\r\n", expectError: true, }, { name: "missing mandatory network type", input: "v=0\r\no=user 1001 1\r\ns=Test\r\nt=0 0\r\n", expectError: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var sd SessionDescription err := sd.UnmarshalString(test.input) if test.expectError { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestUnmarshalString_ErrSDPCacheInvalid(t *testing.T) { origNew := unmarshalCachePool.New t.Cleanup(func() { unmarshalCachePool.New = origNew }) // ensure there are no cached values. unmarshalCachePool.New = nil for v := unmarshalCachePool.Get(); v != nil; v = unmarshalCachePool.Get() { // discard } unmarshalCachePool.New = func() any { return 123 } var sd SessionDescription err := sd.UnmarshalString("") assert.ErrorIs(t, err, errSDPCacheInvalid) } func TestUnmarshal_DelegatesToUnmarshalString(t *testing.T) { in := []byte("v=0\r\no=0 0 0 IN IP4 0\r\ns=0\r\nt=0 0\r\n") var sd SessionDescription assert.NoError(t, sd.Unmarshal(in)) out, err := sd.Marshal() assert.NoError(t, err) assert.Equal(t, string(in), string(out)) } func TestS1_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} // not 'v' st, err := s1(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS2_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} // not 'o' st, err := s2(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS3_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} // not 's' st, err := s3(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS4_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} st, err := s4(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS5_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} st, err := s5(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS6_KeyC_UnmarshalSessionConnectionInformation(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "c=IN IP4 111.1.111.1\r\n"}, } st, err := s6(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.NotNil(t, lex.desc.ConnectionInformation) { ci := lex.desc.ConnectionInformation assert.Equal(t, "IN", ci.NetworkType) assert.Equal(t, "IP4", ci.AddressType) if assert.NotNil(t, ci.Address) { assert.Equal(t, "111.1.111.1", ci.Address.Address) } } } } func TestS6_KeyB_UnmarshalSessionBandwidth(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "b=AS:123\r\n"}, } st, err := s6(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.Len(t, lex.desc.Bandwidth, 1) { bw := lex.desc.Bandwidth[0] assert.False(t, bw.Experimental) assert.Equal(t, "AS", bw.Type) assert.Equal(t, uint64(123), bw.Bandwidth) } } } func TestS6_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} st, err := s6(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS7_KeyE_UnmarshalEmail(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "e=abc.Def@example.com (abc Def)\r\n"}, } st, err := s7(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.NotNil(t, lex.desc.EmailAddress) { assert.Equal(t, "abc.Def@example.com (abc Def)", string(*lex.desc.EmailAddress)) } } } func TestS7_KeyP_UnmarshalPhone(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "p=+1 111 111-1111\r\n"}, } st, err := s7(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.NotNil(t, lex.desc.PhoneNumber) { assert.Equal(t, "+1 111 111-1111", string(*lex.desc.PhoneNumber)) } } } func TestS7_KeyC_UnmarshalSessionConnectionInformation(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "c=IN IP4 111.1.111.1\r\n"}, } st, err := s7(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.NotNil(t, lex.desc.ConnectionInformation) { ci := lex.desc.ConnectionInformation assert.Equal(t, "IN", ci.NetworkType) assert.Equal(t, "IP4", ci.AddressType) if assert.NotNil(t, ci.Address) { assert.Equal(t, "111.1.111.1", ci.Address.Address) } } } } func TestS7_KeyB_UnmarshalSessionBandwidth(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "b=AS:123\r\n"}, } st, err := s7(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.Len(t, lex.desc.Bandwidth, 1) { bw := lex.desc.Bandwidth[0] assert.False(t, bw.Experimental) assert.Equal(t, "AS", bw.Type) assert.Equal(t, uint64(123), bw.Bandwidth) } } } func TestS7_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} st, err := s7(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS8_KeyB_UnmarshalSessionBandwidth(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "b=AS:123\r\n"}, } st, err := s8(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.Len(t, lex.desc.Bandwidth, 1) { bw := lex.desc.Bandwidth[0] assert.False(t, bw.Experimental) assert.Equal(t, "AS", bw.Type) assert.Equal(t, uint64(123), bw.Bandwidth) } } } func TestS8_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} st, err := s8(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS9_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "e="}} st, err := s9(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS10_KeyP_UnmarshalPhone(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "p=+1 111 111-1111\r\n"}, } st, err := s10(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.NotNil(t, lex.desc.PhoneNumber) { assert.Equal(t, "+1 111 111-1111", string(*lex.desc.PhoneNumber)) } } } func TestS10_KeyC_UnmarshalSessionConnectionInformation(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "c=IN IP4 111.1.111.1\r\n"}, } st, err := s10(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.NotNil(t, lex.desc.ConnectionInformation) { ci := lex.desc.ConnectionInformation assert.Equal(t, "IN", ci.NetworkType) assert.Equal(t, "IP4", ci.AddressType) if assert.NotNil(t, ci.Address) { assert.Equal(t, "111.1.111.1", ci.Address.Address) } } } } func TestS10_KeyB_UnmarshalSessionBandwidth(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "b=AS:123\r\n"}, } st, err := s10(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.Len(t, lex.desc.Bandwidth, 1) { bw := lex.desc.Bandwidth[0] assert.False(t, bw.Experimental) assert.Equal(t, "AS", bw.Type) assert.Equal(t, uint64(123), bw.Bandwidth) } } } func TestS10_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "a="}} st, err := s10(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS11_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "t="}} st, err := s11(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS12_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "u="}} st, err := s12(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS13_KeyA_UnmarshalSessionAttribute(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "a=recvonly\r\n"}, } st, err := s13(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) attrs := lex.cache.cloneSessionAttributes() if assert.Len(t, attrs, 1) { assert.Equal(t, "recvonly", attrs[0].Key) assert.Equal(t, "", attrs[0].Value) } } } func TestS13_KeyM_UnmarshalMediaDescription(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "m=audio 49170 RTP/AVP 0\r\n"}, } st, err := s13(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.Len(t, lex.desc.MediaDescriptions, 1) { md := lex.desc.MediaDescriptions[0] assert.Equal(t, "audio", md.MediaName.Media) assert.Equal(t, 49170, md.MediaName.Port.Value) assert.Equal(t, []string{"RTP", "AVP"}, md.MediaName.Protos) assert.Equal(t, []string{"0"}, md.MediaName.Formats) } } } func TestS13_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "t="}} st, err := s13(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS14_KeyK_UnmarshalMediaEncryptionKey(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "k=prompt\r\n"}, } st, err := s14(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) md := lex.desc.MediaDescriptions[len(lex.desc.MediaDescriptions)-1] if assert.NotNil(t, md.EncryptionKey) { assert.Equal(t, "prompt", string(*md.EncryptionKey)) } } } func TestS14_KeyB_UnmarshalMediaBandwidth(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "b=AS:123\r\n"}, } st, err := s14(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) md := lex.desc.MediaDescriptions[len(lex.desc.MediaDescriptions)-1] if assert.Len(t, md.Bandwidth, 1) { bw := md.Bandwidth[0] assert.False(t, bw.Experimental) assert.Equal(t, "AS", bw.Type) assert.Equal(t, uint64(123), bw.Bandwidth) } } } func TestS14_KeyI_UnmarshalMediaTitle(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "i=My Title\r\n"}, } st, err := s14(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) md := lex.desc.MediaDescriptions[len(lex.desc.MediaDescriptions)-1] if assert.NotNil(t, md.MediaTitle) { assert.Equal(t, "My Title", string(*md.MediaTitle)) } } } func TestS14_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "t="}} st, err := s14(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS15_KeyA_UnmarshalMediaAttribute(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, // need an existing media section }, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "a=rtpmap:96 opus/48000\r\n"}, } st, err := s15(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) // run unmarshalMediaAttribute assert.NoError(t, err) attrs := lex.cache.cloneMediaAttributes() if assert.Len(t, attrs, 1) { assert.Equal(t, "rtpmap", attrs[0].Key) assert.Equal(t, "96 opus/48000", attrs[0].Value) } } } func TestS15_KeyC_UnmarshalMediaConnectionInformation(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "c=IN IP4 203.0.113.1\r\n"}, } st, err := s15(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) md := lex.desc.MediaDescriptions[len(lex.desc.MediaDescriptions)-1] if assert.NotNil(t, md.ConnectionInformation) { ci := md.ConnectionInformation assert.Equal(t, "IN", ci.NetworkType) assert.Equal(t, "IP4", ci.AddressType) if assert.NotNil(t, ci.Address) { assert.Equal(t, "203.0.113.1", ci.Address.Address) } } } } func TestS15_KeyM_UnmarshalMediaDescription(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "m=audio 49170 RTP/AVP 0\r\n"}, } st, err := s15(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.Len(t, lex.desc.MediaDescriptions, 1) { md := lex.desc.MediaDescriptions[0] assert.Equal(t, "audio", md.MediaName.Media) assert.Equal(t, 49170, md.MediaName.Port.Value) assert.Equal(t, []string{"RTP", "AVP"}, md.MediaName.Protos) assert.Equal(t, []string{"0"}, md.MediaName.Formats) } } } func TestS15_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "t="}} st, err := s15(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestS16_KeyA_UnmarshalMediaAttribute(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "a=rtpmap:96 opus/48000\r\n"}, } st, err := s16(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) attrs := lex.cache.cloneMediaAttributes() if assert.Len(t, attrs, 1) { assert.Equal(t, "rtpmap", attrs[0].Key) assert.Equal(t, "96 opus/48000", attrs[0].Value) } } } func TestS16_KeyK_UnmarshalMediaEncryptionKey(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "k=prompt\r\n"}, } st, err := s16(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) md := lex.desc.MediaDescriptions[len(lex.desc.MediaDescriptions)-1] if assert.NotNil(t, md.EncryptionKey) { assert.Equal(t, "prompt", string(*md.EncryptionKey)) } } } func TestS16_KeyB_UnmarshalMediaBandwidth(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "b=AS:123\r\n"}, } st, err := s16(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) md := lex.desc.MediaDescriptions[len(lex.desc.MediaDescriptions)-1] if assert.Len(t, md.Bandwidth, 1) { bw := md.Bandwidth[0] assert.False(t, bw.Experimental) assert.Equal(t, "AS", bw.Type) assert.Equal(t, uint64(123), bw.Bandwidth) } } } func TestS16_KeyI_UnmarshalMediaTitle(t *testing.T) { lex := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "i=My Title\r\n"}, } st, err := s16(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) md := lex.desc.MediaDescriptions[len(lex.desc.MediaDescriptions)-1] if assert.NotNil(t, md.MediaTitle) { assert.Equal(t, "My Title", string(*md.MediaTitle)) } } } func TestS16_KeyM_UnmarshalMediaDescription(t *testing.T) { lex := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "m=audio 49170 RTP/AVP 0\r\n"}, } st, err := s16(lex) assert.NoError(t, err) if assert.NotNil(t, st) { _, err = st(lex) assert.NoError(t, err) if assert.Len(t, lex.desc.MediaDescriptions, 1) { md := lex.desc.MediaDescriptions[0] assert.Equal(t, "audio", md.MediaName.Media) assert.Equal(t, 49170, md.MediaName.Port.Value) assert.Equal(t, []string{"RTP", "AVP"}, md.MediaName.Protos) assert.Equal(t, []string{"0"}, md.MediaName.Formats) } } } func TestS16_SyntaxError(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "u="}} st, err := s16(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestUnmarshalProtocolVersion_Error_ReadUint64Field(t *testing.T) { // non-numeric l := &lexer{baseLexer: baseLexer{value: "x\r\n"}} st, err := unmarshalProtocolVersion(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestUnmarshalProtocolVersion_Error_InvalidNonZeroVersion(t *testing.T) { // version must be 0 l := &lexer{baseLexer: baseLexer{value: "1\r\n"}} st, err := unmarshalProtocolVersion(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalOrigin_Error_ReadUsernameField(t *testing.T) { l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalOrigin(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalOrigin_Error_ReadNetworkTypeField(t *testing.T) { // missing NetworkType l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "test 1 1"}, } st, err := unmarshalOrigin(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalOrigin_Error_ReadUint64_SessionID(t *testing.T) { // non-numeric sessionID l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "test NaN 1 IN IP4 11.1.1.1\r\n"}, } st, err := unmarshalOrigin(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestUnmarshalOrigin_Error_ReadUint64_SessionVersion(t *testing.T) { // non-numeric sessionVersion l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "test 1 NaN IN IP4 11.1.1.1\r\n"}, } st, err := unmarshalOrigin(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestUnmarshalOrigin_Error_InvalidNetworkType(t *testing.T) { // invalid network type l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "test 1 1 INVALID IP4 11.1.1.1\r\n"}, } st, err := unmarshalOrigin(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalOrigin_Error_HandleAddressType_Propagates(t *testing.T) { // missing AddressType l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "test 1 1 IN"}, } st, err := unmarshalOrigin(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalOrigin_Error_HandleUnicastAddress_Propagates(t *testing.T) { // missing UnicastAddress l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "test 1 1 IN IP4"}, } st, err := unmarshalOrigin(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestHandleAddressType_ReturnsUnderlyingError(t *testing.T) { l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } err := handleAddressType(l) assert.Error(t, err) assert.ErrorIs(t, err, io.EOF) } func TestHandleUnicastAddress_ReturnsUnderlyingError(t *testing.T) { l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } err := handleUnicastAddress(l) assert.Error(t, err) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalSessionName_Error_ReadLine(t *testing.T) { l := &lexer{desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}} st, err := unmarshalSessionName(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalSessionInformation_Error_ReadLine(t *testing.T) { l := &lexer{desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}} st, err := unmarshalSessionInformation(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalURI_Error_ReadLine(t *testing.T) { l := &lexer{desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}} st, err := unmarshalURI(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalURI_Error_Parse(t *testing.T) { l := &lexer{desc: &SessionDescription{}, baseLexer: baseLexer{value: "%zz\r\n"}} st, err := unmarshalURI(l) assert.Nil(t, st) assert.Error(t, err) } func TestUnmarshalEmail_Error_ReadLine(t *testing.T) { l := &lexer{desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}} st, err := unmarshalEmail(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalPhone_Error_ReadLine(t *testing.T) { l := &lexer{desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}} st, err := unmarshalPhone(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalSessionConnectionInformation_Error_FromInner(t *testing.T) { l := &lexer{desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}} st, err := unmarshalSessionConnectionInformation(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalConnectionInformation_ErrInvalidNetworkType(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "INVALID IP4 111.1.111.1\r\n"}} ci, err := l.unmarshalConnectionInformation() assert.Nil(t, ci) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalConnectionInformation_ErrReadAddressType(t *testing.T) { // missing AddressType token l := &lexer{baseLexer: baseLexer{value: "IN"}} ci, err := l.unmarshalConnectionInformation() assert.Nil(t, ci) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalConnectionInformation_ErrInvalidAddressType(t *testing.T) { l := &lexer{baseLexer: baseLexer{value: "IN INVALID 111.1.111.1\r\n"}} ci, err := l.unmarshalConnectionInformation() assert.Nil(t, ci) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalConnectionInformation_ErrReadAddress(t *testing.T) { // missing address token l := &lexer{baseLexer: baseLexer{value: "IN IP4"}} ci, err := l.unmarshalConnectionInformation() assert.Nil(t, ci) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalSessionBandwidth_Error_ReadLine(t *testing.T) { l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalSessionBandwidth(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalSessionBandwidth_Error_InvalidBandwidthValue(t *testing.T) { l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "bad\r\n"}, } st, err := unmarshalSessionBandwidth(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalBandwidth_InvalidType(t *testing.T) { bw, err := unmarshalBandwidth("ZZ:123") assert.Nil(t, bw) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalBandwidth_InvalidNumeric(t *testing.T) { bw, err := unmarshalBandwidth("AS:notanumber") assert.Nil(t, bw) assert.ErrorIs(t, err, errSDPInvalidNumericValue) } func TestUnmarshalTiming_Error_StartTime(t *testing.T) { // non-numeric start time l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "NaN 0\r\n"}, } st, err := unmarshalTiming(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestUnmarshalTiming_Error_StopTime(t *testing.T) { // non-numeric stop time l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "123 NaN\r\n"}, } st, err := unmarshalTiming(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestUnmarshalRepeatTimes_Error_FirstFieldRead(t *testing.T) { // no tokens l := &lexer{ desc: &SessionDescription{ TimeDescriptions: []TimeDescription{{}}, }, baseLexer: baseLexer{value: ""}, } st, err := unmarshalRepeatTimes(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalRepeatTimes_Error_SecondFieldRead(t *testing.T) { // missing duration l := &lexer{ desc: &SessionDescription{ TimeDescriptions: []TimeDescription{{}}, }, baseLexer: baseLexer{value: "604800"}, } st, err := unmarshalRepeatTimes(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalRepeatTimes_Error_DurationParse(t *testing.T) { // invalid duration token l := &lexer{ desc: &SessionDescription{ TimeDescriptions: []TimeDescription{{}}, }, baseLexer: baseLexer{value: "604800 bad\r\n"}, } st, err := unmarshalRepeatTimes(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalRepeatTimes_Error_OffsetParse(t *testing.T) { // invalid offset l := &lexer{ desc: &SessionDescription{ TimeDescriptions: []TimeDescription{{}}, }, baseLexer: baseLexer{value: "604800 3600 nope\r\n"}, } st, err := unmarshalRepeatTimes(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalRepeatTimes_Error_ReadFieldInsideLoop(t *testing.T) { l := &lexer{ desc: &SessionDescription{ TimeDescriptions: []TimeDescription{{}}, }, baseLexer: baseLexer{value: "604800 3600"}, } st, err := unmarshalRepeatTimes(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalTimeZones_Error_ReadUint64Field(t *testing.T) { // non-numeric starting token l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "NaN"}, } st, err := unmarshalTimeZones(l) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) } func TestUnmarshalTimeZones_Error_ReadField(t *testing.T) { // no space/offset token l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "123"}, } st, err := unmarshalTimeZones(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalTimeZones_Error_ParseOffset(t *testing.T) { // invalid offset token invalid l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "123 bad\r\n"}, } st, err := unmarshalTimeZones(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalSessionEncryptionKey_Error_ReadLine(t *testing.T) { l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalSessionEncryptionKey(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalSessionAttribute_Error_ReadLine(t *testing.T) { l := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalSessionAttribute(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaDescription_Error_ReadMediaField(t *testing.T) { // no tokens l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalMediaDescription(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaDescription_Error_InvalidMediaToken(t *testing.T) { // media token is not in allowed set l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "data 9 RTP/AVP 0\r\n"}, } st, err := unmarshalMediaDescription(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalMediaDescription_Error_ReadPortField(t *testing.T) { // no port token l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "audio"}, } st, err := unmarshalMediaDescription(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaDescription_Error_PortRangeInvalid(t *testing.T) { // has invalid range part in port token l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "audio 123/abc RTP/AVP 0\r\n"}, } st, err := unmarshalMediaDescription(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidValue) } func TestUnmarshalMediaDescription_Error_ReadProtoField(t *testing.T) { // but no proto token l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "audio 9"}, } st, err := unmarshalMediaDescription(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaDescription_Error_ReadFieldInFormatsLoop(t *testing.T) { // no newline or fmt tokens l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: "audio 9 RTP/AVP"}, } st, err := unmarshalMediaDescription(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaDescription_SetsPortRange(t *testing.T) { // valid port with a range lex := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "video 1234/7 RTP/AVP 99\r\n"}, } st, err := unmarshalMediaDescription(lex) assert.NoError(t, err) assert.NotNil(t, st) if assert.Len(t, lex.desc.MediaDescriptions, 1) { md := lex.desc.MediaDescriptions[0] if assert.NotNil(t, md.MediaName.Port.Range, "Range should be set when / is provided") { assert.Equal(t, 7, *md.MediaName.Port.Range) } } } func TestUnmarshalMediaDescription_Error_InvalidProto(t *testing.T) { // proto token not in the allowed list l := &lexer{ desc: &SessionDescription{}, cache: &unmarshalCache{}, baseLexer: baseLexer{value: "audio 9 WRONG/PROTO 0\r\n"}, } st, err := unmarshalMediaDescription(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidNumericValue) } func TestUnmarshalMediaTitle_Error_ReadLine(t *testing.T) { // empty input l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalMediaTitle(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaConnectionInformation_Error_FromInner(t *testing.T) { // empty input l := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: ""}, } st, err := unmarshalMediaConnectionInformation(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaBandwidth_Error_ReadLine(t *testing.T) { l := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: ""}, } st, err := unmarshalMediaBandwidth(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaBandwidth_Error_InvalidBandwidth(t *testing.T) { l := &lexer{ desc: &SessionDescription{ MediaDescriptions: []*MediaDescription{{}}, }, baseLexer: baseLexer{value: "bad\r\n"}, } st, err := unmarshalMediaBandwidth(l) assert.Nil(t, st) assert.ErrorIs(t, err, errSDPInvalidSyntax) } func TestUnmarshalMediaEncryptionKey_Error_ReadLine(t *testing.T) { // empty input l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalMediaEncryptionKey(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestUnmarshalMediaAttribute_Error_ReadLine(t *testing.T) { // empty input l := &lexer{ desc: &SessionDescription{}, baseLexer: baseLexer{value: ""}, } st, err := unmarshalMediaAttribute(l) assert.Nil(t, st) assert.ErrorIs(t, err, io.EOF) } func TestTimeShorthand_MinutesAndSeconds(t *testing.T) { t.Run("minutes (m)", func(t *testing.T) { assert.Equal(t, int64(60), timeShorthand('m')) }) t.Run("seconds (s)", func(t *testing.T) { assert.Equal(t, int64(1), timeShorthand('s')) }) } sdp-3.0.17/util.go000066400000000000000000000213601512144664500137060ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "errors" "fmt" "io" "slices" "sort" "strconv" "strings" "github.com/pion/randutil" ) const ( attributeKey = "a=" ) var ( errExtractCodecRtpmap = errors.New("could not extract codec from rtpmap") errExtractCodecFmtp = errors.New("could not extract codec from fmtp") errExtractCodecRtcpFb = errors.New("could not extract codec from rtcp-fb") errPayloadTypeNotFound = errors.New("payload type not found") errCodecNotFound = errors.New("codec not found") errSyntaxError = errors.New("SyntaxError") errFieldMissing = errors.New("field missing") ) // ConnectionRole indicates which of the end points should initiate the connection establishment. type ConnectionRole int const ( // ConnectionRoleActive indicates the endpoint will initiate an outgoing connection. ConnectionRoleActive ConnectionRole = iota + 1 // ConnectionRolePassive indicates the endpoint will accept an incoming connection. ConnectionRolePassive // ConnectionRoleActpass indicates the endpoint is willing to accept an incoming connection or // to initiate an outgoing connection. ConnectionRoleActpass // ConnectionRoleHoldconn indicates the endpoint does not want the connection to be established for the time being. ConnectionRoleHoldconn ) func (t ConnectionRole) String() string { switch t { case ConnectionRoleActive: return "active" case ConnectionRolePassive: return "passive" case ConnectionRoleActpass: return "actpass" case ConnectionRoleHoldconn: return "holdconn" default: return "Unknown" } } func newSessionID() (uint64, error) { // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-26#section-5.2.1 // Session ID is recommended to be constructed by generating a 64-bit // quantity with the highest bit set to zero and the remaining 63-bits // being cryptographically random. id, err := randutil.CryptoUint64() return id & (^(uint64(1) << 63)), err } // Codec represents a codec. type Codec struct { PayloadType uint8 Name string ClockRate uint32 EncodingParameters string Fmtp string RTCPFeedback []string } const ( unknown = iota ) func (c Codec) String() string { return fmt.Sprintf( "%d %s/%d/%s (%s) [%s]", c.PayloadType, c.Name, c.ClockRate, c.EncodingParameters, c.Fmtp, strings.Join(c.RTCPFeedback, ", "), ) } func (c *Codec) appendRTCPFeedback(rtcpFeedback string) { if slices.Contains(c.RTCPFeedback, rtcpFeedback) { return } c.RTCPFeedback = append(c.RTCPFeedback, rtcpFeedback) } func parseRtpmap(rtpmap string) (Codec, error) { var codec Codec parsingFailed := errExtractCodecRtpmap // a=rtpmap: /[/] split := strings.Split(rtpmap, " ") if len(split) != 2 { return codec, parsingFailed } ptSplit := strings.Split(split[0], ":") if len(ptSplit) != 2 { return codec, parsingFailed } ptInt, err := strconv.ParseUint(ptSplit[1], 10, 8) if err != nil { return codec, parsingFailed } codec.PayloadType = uint8(ptInt) split = strings.Split(split[1], "/") codec.Name = split[0] parts := len(split) if parts > 1 { rate, err := strconv.ParseUint(split[1], 10, 32) if err != nil { return codec, parsingFailed } codec.ClockRate = uint32(rate) } if parts > 2 { codec.EncodingParameters = split[2] } return codec, nil } func parseFmtp(fmtp string) (Codec, error) { var codec Codec parsingFailed := errExtractCodecFmtp // a=fmtp: split := strings.SplitN(fmtp, " ", 2) if len(split) != 2 { return codec, parsingFailed } formatParams := split[1] split = strings.Split(split[0], ":") if len(split) != 2 { return codec, parsingFailed } ptInt, err := strconv.ParseUint(split[1], 10, 8) if err != nil { return codec, parsingFailed } codec.PayloadType = uint8(ptInt) codec.Fmtp = formatParams return codec, nil } func parseRtcpFb(rtcpFb string) (codec Codec, isWildcard bool, err error) { var ptInt uint64 err = errExtractCodecRtcpFb // a=ftcp-fb: [] split := strings.SplitN(rtcpFb, " ", 2) if len(split) != 2 { return } ptSplit := strings.Split(split[0], ":") if len(ptSplit) != 2 { return } isWildcard = ptSplit[1] == "*" if !isWildcard { ptInt, err = strconv.ParseUint(ptSplit[1], 10, 8) if err != nil { return } codec.PayloadType = uint8(ptInt) } codec.RTCPFeedback = append(codec.RTCPFeedback, split[1]) return codec, isWildcard, nil } func mergeCodecs(codec Codec, codecs map[uint8]Codec) { savedCodec := codecs[codec.PayloadType] if savedCodec.PayloadType == 0 { savedCodec.PayloadType = codec.PayloadType } if savedCodec.Name == "" { savedCodec.Name = codec.Name } if savedCodec.ClockRate == 0 { savedCodec.ClockRate = codec.ClockRate } if savedCodec.EncodingParameters == "" { savedCodec.EncodingParameters = codec.EncodingParameters } if savedCodec.Fmtp == "" { savedCodec.Fmtp = codec.Fmtp } savedCodec.RTCPFeedback = append(savedCodec.RTCPFeedback, codec.RTCPFeedback...) codecs[savedCodec.PayloadType] = savedCodec } func (s *SessionDescription) buildCodecMap() map[uint8]Codec { //nolint:cyclop codecs := map[uint8]Codec{ // static codecs that do not require a rtpmap 0: { PayloadType: 0, Name: "PCMU", ClockRate: 8000, }, 8: { PayloadType: 8, Name: "PCMA", ClockRate: 8000, }, 9: { PayloadType: 9, Name: "G722", ClockRate: 8000, }, } wildcardRTCPFeedback := []string{} for _, m := range s.MediaDescriptions { for _, a := range m.Attributes { attr := a.String() switch { case strings.HasPrefix(attr, "rtpmap:"): codec, err := parseRtpmap(attr) if err == nil { mergeCodecs(codec, codecs) } case strings.HasPrefix(attr, "fmtp:"): codec, err := parseFmtp(attr) if err == nil { mergeCodecs(codec, codecs) } case strings.HasPrefix(attr, "rtcp-fb:"): codec, isWildcard, err := parseRtcpFb(attr) switch { case err != nil: case isWildcard: wildcardRTCPFeedback = append(wildcardRTCPFeedback, codec.RTCPFeedback...) default: mergeCodecs(codec, codecs) } } } } for i, codec := range codecs { for _, newRTCPFeedback := range wildcardRTCPFeedback { codec.appendRTCPFeedback(newRTCPFeedback) } codecs[i] = codec } return codecs } func equivalentFmtp(want, got string) bool { wantSplit := strings.Split(want, ";") gotSplit := strings.Split(got, ";") if len(wantSplit) != len(gotSplit) { return false } sort.Strings(wantSplit) sort.Strings(gotSplit) for i, wantPart := range wantSplit { wantPart = strings.TrimSpace(wantPart) gotPart := strings.TrimSpace(gotSplit[i]) if gotPart != wantPart { return false } } return true } func codecsMatch(wanted, got Codec) bool { if wanted.Name != "" && !strings.EqualFold(wanted.Name, got.Name) { return false } if wanted.ClockRate != 0 && wanted.ClockRate != got.ClockRate { return false } if wanted.EncodingParameters != "" && wanted.EncodingParameters != got.EncodingParameters { return false } if wanted.Fmtp != "" && !equivalentFmtp(wanted.Fmtp, got.Fmtp) { return false } return true } // GetCodecForPayloadType scans the SessionDescription for the given payload type and returns the codec. func (s *SessionDescription) GetCodecForPayloadType(payloadType uint8) (Codec, error) { codecs := s.buildCodecMap() codec, ok := codecs[payloadType] if ok { return codec, nil } return codec, errPayloadTypeNotFound } func (s *SessionDescription) GetCodecsForPayloadTypes(payloadTypes []uint8) ([]Codec, error) { codecs := s.buildCodecMap() result := make([]Codec, 0, len(payloadTypes)) for _, payloadType := range payloadTypes { codec, ok := codecs[payloadType] if ok { result = append(result, codec) } } return result, nil } // GetPayloadTypeForCodec scans the SessionDescription for a codec that matches the provided codec // as closely as possible and returns its payload type. func (s *SessionDescription) GetPayloadTypeForCodec(wanted Codec) (uint8, error) { codecs := s.buildCodecMap() for payloadType, codec := range codecs { if codecsMatch(wanted, codec) { return payloadType, nil } } return 0, errCodecNotFound } type stateFn func(*lexer) (stateFn, error) type lexer struct { desc *SessionDescription cache *unmarshalCache baseLexer } type keyToState func(key byte) stateFn func (l *lexer) handleType(fn keyToState) (stateFn, error) { key, err := l.readType() if errors.Is(err, io.EOF) && key == 0 { return nil, nil //nolint:nilnil } else if err != nil { return nil, err } if res := fn(key); res != nil { return res, nil } return nil, l.syntaxError() } sdp-3.0.17/util_test.go000066400000000000000000000316311512144664500147470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "testing" "github.com/stretchr/testify/assert" ) func getTestSessionDescription() SessionDescription { return SessionDescription{ MediaDescriptions: []*MediaDescription{ { MediaName: MediaName{ Media: "video", Port: RangedPort{ Value: 51372, }, Protos: []string{"RTP", "AVP"}, Formats: []string{"120", "121", "126", "97", "98"}, }, Attributes: []Attribute{ NewAttribute("fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1", ""), NewAttribute("fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1", ""), NewAttribute("fmtp:98 profile-level-id=42e01e; packetization-mode=1", ""), NewAttribute("fmtp:120 max-fs=12288;max-fr=60", ""), NewAttribute("fmtp:121 max-fs=12288;max-fr=60", ""), NewAttribute("rtpmap:120 VP8/90000", ""), NewAttribute("rtpmap:121 VP9/90000", ""), NewAttribute("rtpmap:126 H264/90000", ""), NewAttribute("rtpmap:97 H264/90000", ""), NewAttribute("rtpmap:98 H264/90000", ""), NewAttribute("rtcp-fb:97 ccm fir", ""), NewAttribute("rtcp-fb:97 nack", ""), NewAttribute("rtcp-fb:97 nack pli", ""), NewAttribute("rtcp-fb:* transport-cc", ""), NewAttribute("rtcp-fb:* nack", ""), }, }, }, } } func TestGetPayloadTypeForVP8(t *testing.T) { for _, test := range []struct { Codec Codec Expected uint8 }{ { Codec: Codec{ Name: "VP8", }, Expected: 120, }, { Codec: Codec{ Name: "VP9", }, Expected: 121, }, { Codec: Codec{ Name: "H264", Fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1", }, Expected: 97, }, { Codec: Codec{ Name: "H264", Fmtp: "level-asymmetry-allowed=1;profile-level-id=42e01f", }, Expected: 97, }, { Codec: Codec{ Name: "H264", Fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1", }, Expected: 126, }, } { sd := getTestSessionDescription() actual, err := sd.GetPayloadTypeForCodec(test.Codec) assert.NoError(t, err) assert.Equal(t, actual, test.Expected) } } func TestGetCodecForPayloadType(t *testing.T) { for _, test := range []struct { name string SD SessionDescription PayloadType uint8 Expected Codec }{ { "vp8", getTestSessionDescription(), 120, Codec{ PayloadType: 120, Name: "VP8", ClockRate: 90000, Fmtp: "max-fs=12288;max-fr=60", RTCPFeedback: []string{"transport-cc", "nack"}, }, }, { "vp9", getTestSessionDescription(), 121, Codec{ PayloadType: 121, Name: "VP9", ClockRate: 90000, Fmtp: "max-fs=12288;max-fr=60", RTCPFeedback: []string{"transport-cc", "nack"}, }, }, { "h264 126", getTestSessionDescription(), 126, Codec{ PayloadType: 126, Name: "H264", ClockRate: 90000, Fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1", RTCPFeedback: []string{"transport-cc", "nack"}, }, }, { "h264 97", getTestSessionDescription(), 97, Codec{ PayloadType: 97, Name: "H264", ClockRate: 90000, Fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1", RTCPFeedback: []string{"ccm fir", "nack", "nack pli", "transport-cc"}, }, }, { "h264 98", getTestSessionDescription(), 98, Codec{ PayloadType: 98, Name: "H264", ClockRate: 90000, Fmtp: "profile-level-id=42e01e; packetization-mode=1", RTCPFeedback: []string{"transport-cc", "nack"}, }, }, { "pcmu without rtpmap", SessionDescription{ MediaDescriptions: []*MediaDescription{ { MediaName: MediaName{ Media: "audio", Protos: []string{"RTP", "AVP"}, Formats: []string{"0", "8", "9"}, }, }, }, }, 0, Codec{ PayloadType: 0, Name: "PCMU", ClockRate: 8000, }, }, { "pcma without rtpmap", SessionDescription{ MediaDescriptions: []*MediaDescription{ { MediaName: MediaName{ Media: "audio", Protos: []string{"RTP", "AVP"}, Formats: []string{"0", "8", "9"}, }, }, }, }, 8, Codec{ PayloadType: 8, Name: "PCMA", ClockRate: 8000, }, }, { "g722 without rtpmap", SessionDescription{ MediaDescriptions: []*MediaDescription{ { MediaName: MediaName{ Media: "audio", Protos: []string{"RTP", "AVP"}, Formats: []string{"0", "8", "9"}, }, }, }, }, 9, Codec{ PayloadType: 9, Name: "G722", ClockRate: 8000, }, }, } { t.Run(test.name, func(t *testing.T) { actual, err := test.SD.GetCodecForPayloadType(test.PayloadType) assert.NoError(t, err) assert.Equal(t, actual, test.Expected) }) } } func TestGetCodecsForPayloadTypes(t *testing.T) { for _, test := range []struct { name string SD SessionDescription PayloadTypes []uint8 Expected []Codec }{ { "vp8-9", getTestSessionDescription(), []uint8{120, 121}, []Codec{ { PayloadType: 120, Name: "VP8", ClockRate: 90000, Fmtp: "max-fs=12288;max-fr=60", RTCPFeedback: []string{"transport-cc", "nack"}, }, { PayloadType: 121, Name: "VP9", ClockRate: 90000, Fmtp: "max-fs=12288;max-fr=60", RTCPFeedback: []string{"transport-cc", "nack"}, }, }, }, { "pcma without rtpmap", SessionDescription{ MediaDescriptions: []*MediaDescription{ { MediaName: MediaName{ Media: "audio", Protos: []string{"RTP", "AVP"}, Formats: []string{"0", "8"}, }, }, }, }, []uint8{0, 8}, []Codec{ { PayloadType: 0, Name: "PCMU", ClockRate: 8000, }, { PayloadType: 8, Name: "PCMA", ClockRate: 8000, }, }, }, } { t.Run(test.name, func(t *testing.T) { actual, err := test.SD.GetCodecsForPayloadTypes(test.PayloadTypes) assert.NoError(t, err) assert.Equal(t, actual, test.Expected) }) } } func TestNewSessionID(t *testing.T) { minVal := uint64(0x7FFFFFFFFFFFFFFF) maxVal := uint64(0) for i := 0; i < 10000; i++ { r, err := newSessionID() assert.NoError(t, err) assert.Lessf(t, r, uint64((1<<64)-1), "Session ID must be less than 2**64-1, got %d", r) if r < minVal { minVal = r } if r > maxVal { maxVal = r } } assert.Less(t, minVal, uint64(0x1000000000000000), "Value around upper boundary was not generated") assert.Greater(t, maxVal, uint64(0x7000000000000000), "Value around lower boundary was not generated") } func TestConnectionRole_String(t *testing.T) { assert.Equal(t, "active", ConnectionRoleActive.String()) assert.Equal(t, "passive", ConnectionRolePassive.String()) assert.Equal(t, "actpass", ConnectionRoleActpass.String()) assert.Equal(t, "holdconn", ConnectionRoleHoldconn.String()) var zero ConnectionRole assert.Equal(t, "Unknown", zero.String()) var bogus ConnectionRole = 99 assert.Equal(t, "Unknown", bogus.String()) } func TestCodec_String(t *testing.T) { c := Codec{ PayloadType: 111, Name: "opus", ClockRate: 48000, EncodingParameters: "2", Fmtp: "minptime=10;useinbandfec=1", RTCPFeedback: []string{"nack", "pli"}, } got := c.String() assert.Equal(t, "111 opus/48000/2 (minptime=10;useinbandfec=1) [nack, pli]", got) } func TestParseRtpmap_NoEncodingParams(t *testing.T) { codec, err := parseRtpmap("rtpmap:111 opus/48000") assert.NoError(t, err) assert.Equal(t, uint8(111), codec.PayloadType) assert.Equal(t, "opus", codec.Name) assert.Equal(t, uint32(48000), codec.ClockRate) assert.Equal(t, "", codec.EncodingParameters) } func TestParseRtpmap_WithEncodingParams(t *testing.T) { codec, err := parseRtpmap("rtpmap:96 MP4A-LATM/44100/2") assert.NoError(t, err) assert.Equal(t, uint8(96), codec.PayloadType) assert.Equal(t, "MP4A-LATM", codec.Name) assert.Equal(t, uint32(44100), codec.ClockRate) assert.Equal(t, "2", codec.EncodingParameters) } func TestParseRtpmap_Error_MissingSpace(t *testing.T) { // missing space _, err := parseRtpmap("rtpmap:111") assert.ErrorIs(t, err, errExtractCodecRtpmap) } func TestParseRtpmap_Error_MissingColon(t *testing.T) { // missing colon _, err := parseRtpmap("rtpmap111 opus/48000") assert.ErrorIs(t, err, errExtractCodecRtpmap) } func TestParseRtpmap_Error_NonNumericPayload(t *testing.T) { // non-numeric _, err := parseRtpmap("rtpmap:xx opus/48000") assert.ErrorIs(t, err, errExtractCodecRtpmap) } func TestParseRtpmap_Error_NonNumericClockRate(t *testing.T) { _, err := parseRtpmap("rtpmap:111 opus/notanumber") assert.ErrorIs(t, err, errExtractCodecRtpmap) } func TestParseFmtp_Error_MissingSpace(t *testing.T) { _, err := parseFmtp("fmtp:111") assert.ErrorIs(t, err, errExtractCodecFmtp) } func TestParseFmtp_Error_MissingColon(t *testing.T) { _, err := parseFmtp("fmtp111 a=b;c=d") assert.ErrorIs(t, err, errExtractCodecFmtp) } func TestParseFmtp_Error_NonNumericPayload(t *testing.T) { _, err := parseFmtp("fmtp:xx profile-level-id=42e01f;packetization-mode=1") assert.ErrorIs(t, err, errExtractCodecFmtp) } func TestEquivalentFmtp_MismatchAfterSortAndTrim(t *testing.T) { want := "profile-level-id=42e01f; packetization-mode=1" got := "packetization-mode=0; profile-level-id=42e01f" assert.False(t, equivalentFmtp(want, got)) } func TestParseRtcpFb_MissingSpace(t *testing.T) { c, wildcard, err := parseRtcpFb("rtcp-fb:97") assert.ErrorIs(t, err, errExtractCodecRtcpFb) assert.False(t, wildcard) assert.Equal(t, uint8(0), c.PayloadType) assert.Empty(t, c.RTCPFeedback) } func TestParseRtcpFb_MissingColon(t *testing.T) { c, wildcard, err := parseRtcpFb("rtcp-fb97 nack") assert.ErrorIs(t, err, errExtractCodecRtcpFb) assert.False(t, wildcard) assert.Equal(t, uint8(0), c.PayloadType) assert.Empty(t, c.RTCPFeedback) } func TestParseRtcpFb_NonNumeric(t *testing.T) { c, wildcard, err := parseRtcpFb("rtcp-fb:xx nack") assert.Error(t, err) assert.False(t, wildcard) assert.Equal(t, uint8(0), c.PayloadType) assert.Empty(t, c.RTCPFeedback) } func TestBuildCodecMap_RtcpFbError(t *testing.T) { sd := SessionDescription{ MediaDescriptions: []*MediaDescription{ { Attributes: []Attribute{ // non-numeric should return an error NewAttribute("rtcp-fb:xx nack", ""), }, }, }, } codecs := sd.buildCodecMap() // the three static codecs should be present, unchanged. if assert.Len(t, codecs, 3) { if c, ok := codecs[0]; assert.True(t, ok) { assert.Equal(t, uint8(0), c.PayloadType) assert.Equal(t, "PCMU", c.Name) assert.Equal(t, uint32(8000), c.ClockRate) assert.Empty(t, c.RTCPFeedback) } if c, ok := codecs[8]; assert.True(t, ok) { assert.Equal(t, uint8(8), c.PayloadType) assert.Equal(t, "PCMA", c.Name) assert.Equal(t, uint32(8000), c.ClockRate) assert.Empty(t, c.RTCPFeedback) } if c, ok := codecs[9]; assert.True(t, ok) { assert.Equal(t, uint8(9), c.PayloadType) assert.Equal(t, "G722", c.Name) assert.Equal(t, uint32(8000), c.ClockRate) assert.Empty(t, c.RTCPFeedback) } } } func TestCodecsMatch_MiddleFalse_ClockRateMismatch(t *testing.T) { expected := Codec{ClockRate: 44100} actual := Codec{Name: "opus", ClockRate: 48000} assert.False(t, codecsMatch(expected, actual)) } func TestCodecsMatch_MiddleFalse_EncodingParamsMismatch(t *testing.T) { expected := Codec{EncodingParameters: "1"} actual := Codec{EncodingParameters: "2"} assert.False(t, codecsMatch(expected, actual)) } func TestGetCodecForPayloadType_Error_NotFound(t *testing.T) { var sd SessionDescription _, err := sd.GetCodecForPayloadType(42) assert.ErrorIs(t, err, errPayloadTypeNotFound) } func TestGetPayloadTypeForCodec_Error_NotFound(t *testing.T) { var sd SessionDescription _, err := sd.GetPayloadTypeForCodec(Codec{Name: "doesnotexist"}) assert.ErrorIs(t, err, errCodecNotFound) } func TestLexer_HandleType_ElseIfErrorFromReadType(t *testing.T) { // should cause a syntaxError (key='a', err=syntaxError) because second byte != '=' l := &lexer{baseLexer: baseLexer{value: "a-"}} called := false fn := func(key byte) stateFn { called = true // should not be called because handleType returns early on err != nil return func(*lexer) (stateFn, error) { return nil, nil } } st, err := l.handleType(fn) assert.Nil(t, st) assert.False(t, called) var se syntaxError assert.ErrorAs(t, err, &se) } func TestLexer_HandleType_SyntaxErrorWhenFnReturnsNil(t *testing.T) { // valid type "a=" so readType returns nil error so fn returns nil l := &lexer{baseLexer: baseLexer{value: "a="}} fn := func(key byte) stateFn { assert.Equal(t, byte('a'), key) return nil // should trigger final syntaxError } st, err := l.handleType(fn) assert.Nil(t, st) var se syntaxError assert.ErrorAs(t, err, &se) }