pax_global_header00006660000000000000000000000064142746114640014523gustar00rootroot0000000000000052 comment=436dfcf7bf073ed545aa229eae52109fef1b3d21 sdp-3.0.6/000077500000000000000000000000001427461146400123175ustar00rootroot00000000000000sdp-3.0.6/.github/000077500000000000000000000000001427461146400136575ustar00rootroot00000000000000sdp-3.0.6/.github/.ci.conf000066400000000000000000000000571427461146400152010ustar00rootroot00000000000000EXCLUDED_CONTRIBUTORS=('Josh Bleecher Snyder') sdp-3.0.6/.github/generate-authors.sh000077500000000000000000000031401427461146400174710ustar00rootroot00000000000000#!/usr/bin/env bash # # 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. # set -e SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) AUTHORS_PATH="$GITHUB_WORKSPACE/AUTHORS.txt" if [ -f ${SCRIPT_PATH}/.ci.conf ] then . ${SCRIPT_PATH}/.ci.conf fi # # DO NOT EDIT THIS # EXCLUDED_CONTRIBUTORS+=('John R. Bradley' 'renovate[bot]' 'Renovate Bot' 'Pion Bot' 'pionbot') # If you want to exclude a name from all repositories, send a PR to # https://github.com/pion/.goassets instead of this repository. # If you want to exclude a name only from this repository, # add EXCLUDED_CONTRIBUTORS=('name') to .github/.ci.conf CONTRIBUTORS=() shouldBeIncluded () { for i in "${EXCLUDED_CONTRIBUTORS[@]}" do if [[ $1 =~ "$i" ]]; then return 1 fi done return 0 } IFS=$'\n' #Only split on newline for contributor in $(git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf) do if shouldBeIncluded $contributor; then CONTRIBUTORS+=("$contributor") fi done unset IFS if [ ${#CONTRIBUTORS[@]} -ne 0 ]; then cat >$AUTHORS_PATH <<-'EOH' # Thank you to everyone that made Pion possible. If you are interested in contributing # we would love to have you https://github.com/pion/webrtc/wiki/Contributing # # This file is auto generated, using git to list all individuals contributors. # see `.github/generate-authors.sh` for the scripting EOH for i in "${CONTRIBUTORS[@]}" do echo "$i" >> $AUTHORS_PATH done exit 0 fi sdp-3.0.6/.github/hooks/000077500000000000000000000000001427461146400150025ustar00rootroot00000000000000sdp-3.0.6/.github/hooks/commit-msg.sh000077500000000000000000000002671427461146400174220ustar00rootroot00000000000000#!/usr/bin/env bash # # DO NOT EDIT THIS FILE DIRECTLY # # It is automatically copied from https://github.com/pion/.goassets repository. # set -e .github/lint-commit-message.sh $1 sdp-3.0.6/.github/hooks/pre-commit.sh000077500000000000000000000004171427461146400174170ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE DIRECTLY # # It is automatically copied from https://github.com/pion/.goassets repository. # # Redirect output to stderr. exec 1>&2 .github/lint-disallowed-functions-in-library.sh .github/lint-no-trailing-newline-in-log-messages.sh sdp-3.0.6/.github/hooks/pre-push.sh000077500000000000000000000002571427461146400171100ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE DIRECTLY # # It is automatically copied from https://github.com/pion/.goassets repository. # set -e .github/generate-authors.sh exit 0 sdp-3.0.6/.github/install-hooks.sh000077500000000000000000000010361427461146400170050ustar00rootroot00000000000000#!/bin/bash # # 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. # SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) cp "$SCRIPT_PATH/hooks/commit-msg.sh" "$SCRIPT_PATH/../.git/hooks/commit-msg" cp "$SCRIPT_PATH/hooks/pre-commit.sh" "$SCRIPT_PATH/../.git/hooks/pre-commit" cp "$SCRIPT_PATH/hooks/pre-push.sh" "$SCRIPT_PATH/../.git/hooks/pre-push" sdp-3.0.6/.github/lint-commit-message.sh000077500000000000000000000035641427461146400201040ustar00rootroot00000000000000#!/usr/bin/env bash # # 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. # set -e display_commit_message_error() { cat << EndOfMessage $1 ------------------------------------------------- The preceding commit message is invalid it failed '$2' of the following checks * Separate subject from body with a blank line * Limit the subject line to 50 characters * Capitalize the subject line * Do not end the subject line with a period * Wrap the body at 72 characters EndOfMessage exit 1 } lint_commit_message() { if [[ "$(echo "$1" | awk 'NR == 2 {print $1;}' | wc -c)" -ne 1 ]]; then display_commit_message_error "$1" 'Separate subject from body with a blank line' fi if [[ "$(echo "$1" | head -n1 | awk '{print length}')" -gt 50 ]]; then display_commit_message_error "$1" 'Limit the subject line to 50 characters' fi if [[ ! $1 =~ ^[A-Z] ]]; then display_commit_message_error "$1" 'Capitalize the subject line' fi if [[ "$(echo "$1" | awk 'NR == 1 {print substr($0,length($0),1)}')" == "." ]]; then display_commit_message_error "$1" 'Do not end the subject line with a period' fi if [[ "$(echo "$1" | awk '{print length}' | sort -nr | head -1)" -gt 72 ]]; then display_commit_message_error "$1" 'Wrap the body at 72 characters' fi } if [ "$#" -eq 1 ]; then if [ ! -f "$1" ]; then echo "$0 was passed one argument, but was not a valid file" exit 1 fi lint_commit_message "$(sed -n '/# Please enter the commit message for your changes. Lines starting/q;p' "$1")" else for commit in $(git rev-list --no-merges origin/master..); do lint_commit_message "$(git log --format="%B" -n 1 $commit)" done fi sdp-3.0.6/.github/lint-disallowed-functions-in-library.sh000077500000000000000000000023571427461146400233740ustar00rootroot00000000000000#!/usr/bin/env bash # # 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. # set -e # Disallow usages of functions that cause the program to exit in the library code SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) if [ -f ${SCRIPT_PATH}/.ci.conf ] then . ${SCRIPT_PATH}/.ci.conf fi EXCLUDE_DIRECTORIES=${DISALLOWED_FUNCTIONS_EXCLUDED_DIRECTORIES:-"examples"} DISALLOWED_FUNCTIONS=('os.Exit(' 'panic(' 'Fatal(' 'Fatalf(' 'Fatalln(' 'fmt.Println(' 'fmt.Printf(' 'log.Print(' 'log.Println(' 'log.Printf(' 'print(' 'println(') files=$( find "$SCRIPT_PATH/.." -name "*.go" \ | grep -v -e '^.*_test.go$' \ | while read file do excluded=false for ex in $EXCLUDE_DIRECTORIES do if [[ $file == */$ex/* ]] then excluded=true break fi done $excluded || echo "$file" done ) for disallowedFunction in "${DISALLOWED_FUNCTIONS[@]}" do if grep -e "\s$disallowedFunction" $files | grep -v -e 'nolint'; then echo "$disallowedFunction may only be used in example code" exit 1 fi done sdp-3.0.6/.github/lint-filename.sh000077500000000000000000000012041427461146400167370ustar00rootroot00000000000000#!/usr/bin/env bash # # 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. # set -e SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) GO_REGEX="^[a-zA-Z][a-zA-Z0-9_]*\.go$" find "$SCRIPT_PATH/.." -name "*.go" | while read fullpath; do filename=$(basename -- "$fullpath") if ! [[ $filename =~ $GO_REGEX ]]; then echo "$filename is not a valid filename for Go code, only alpha, numbers and underscores are supported" exit 1 fi done sdp-3.0.6/.github/lint-no-trailing-newline-in-log-messages.sh000077500000000000000000000016561427461146400240440ustar00rootroot00000000000000#!/usr/bin/env bash # # 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. # set -e # Disallow usages of functions that cause the program to exit in the library code SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) if [ -f ${SCRIPT_PATH}/.ci.conf ] then . ${SCRIPT_PATH}/.ci.conf fi files=$( find "$SCRIPT_PATH/.." -name "*.go" \ | while read file do excluded=false for ex in $EXCLUDE_DIRECTORIES do if [[ $file == */$ex/* ]] then excluded=true break fi done $excluded || echo "$file" done ) if grep -E '\.(Trace|Debug|Info|Warn|Error)f?\("[^"]*\\n"\)?' $files | grep -v -e 'nolint'; then echo "Log format strings should have trailing new-line" exit 1 fisdp-3.0.6/.github/workflows/000077500000000000000000000000001427461146400157145ustar00rootroot00000000000000sdp-3.0.6/.github/workflows/codeql-analysis.yml000066400000000000000000000015411427461146400215300ustar00rootroot00000000000000name: "CodeQL" on: workflow_dispatch: schedule: - cron: '23 5 * * 0' pull_request: branches: - master paths: - '**.go' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - name: Checkout repo uses: actions/checkout@v3 # The code in examples/ might intentionally do things like log credentials # in order to show how the library is used, aid in debugging etc. We # should ignore those for CodeQL scanning, and only focus on the package # itself. - name: Remove example code run: | rm -rf examples/ - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: 'go' - name: CodeQL Analysis uses: github/codeql-action/analyze@v2 sdp-3.0.6/.github/workflows/generate-authors.yml000066400000000000000000000047721427461146400217260ustar00rootroot00000000000000# # 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. # name: generate-authors on: pull_request: jobs: checksecret: permissions: contents: none runs-on: ubuntu-latest outputs: is_PIONBOT_PRIVATE_KEY_set: ${{ steps.checksecret_job.outputs.is_PIONBOT_PRIVATE_KEY_set }} steps: - id: checksecret_job env: PIONBOT_PRIVATE_KEY: ${{ secrets.PIONBOT_PRIVATE_KEY }} run: | echo "is_PIONBOT_PRIVATE_KEY_set: ${{ env.PIONBOT_PRIVATE_KEY != '' }}" echo "::set-output name=is_PIONBOT_PRIVATE_KEY_set::${{ env.PIONBOT_PRIVATE_KEY != '' }}" generate-authors: permissions: contents: write needs: [checksecret] if: needs.checksecret.outputs.is_PIONBOT_PRIVATE_KEY_set == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: ref: ${{ github.head_ref }} fetch-depth: 0 token: ${{ secrets.PIONBOT_PRIVATE_KEY }} - name: Generate the authors file run: .github/generate-authors.sh - name: Add the authors file to git run: git add AUTHORS.txt - name: Get last commit message id: last-commit-message run: | COMMIT_MSG=$(git log -1 --pretty=%B) COMMIT_MSG="${COMMIT_MSG//'%'/'%25'}" COMMIT_MSG="${COMMIT_MSG//$'\n'/'%0A'}" COMMIT_MSG="${COMMIT_MSG//$'\r'/'%0D'}" echo "::set-output name=msg::$COMMIT_MSG" - name: Get last commit author id: last-commit-author run: | echo "::set-output name=msg::$(git log -1 --pretty='%aN <%ae>')" - name: Check if AUTHORS.txt file has changed id: git-status-output run: | echo "::set-output name=msg::$(git status -s | wc -l)" - name: Commit and push if: ${{ steps.git-status-output.outputs.msg != '0' }} run: | git config user.email $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\2/') git config user.name $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\1/') git add AUTHORS.txt git commit --amend --no-edit git push --force https://github.com/${GITHUB_REPOSITORY} $(git symbolic-ref -q --short HEAD) sdp-3.0.6/.github/workflows/lint.yaml000066400000000000000000000026251427461146400175530ustar00rootroot00000000000000# # 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. # name: Lint on: pull_request: types: - opened - edited - synchronize permissions: contents: read jobs: lint-commit-message: name: Metadata runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Commit Message run: .github/lint-commit-message.sh - name: File names run: .github/lint-filename.sh - name: Functions run: .github/lint-disallowed-functions-in-library.sh - name: Logging messages should not have trailing newlines run: .github/lint-no-trailing-newline-in-log-messages.sh lint-go: name: Go permissions: contents: read pull-requests: read runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: v1.45.2 args: $GOLANGCI_LINT_EXRA_ARGS sdp-3.0.6/.github/workflows/renovate-go-mod-fix.yaml000066400000000000000000000016071427461146400223730ustar00rootroot00000000000000# # 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. # name: go-mod-fix on: push: branches: - renovate/* permissions: contents: write jobs: go-mod-fix: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 with: fetch-depth: 2 - name: fix uses: at-wat/go-sum-fix-action@v0 with: git_user: Pion Bot git_email: 59523206+pionbot@users.noreply.github.com github_token: ${{ secrets.PIONBOT_PRIVATE_KEY }} commit_style: squash push: force sdp-3.0.6/.github/workflows/test.yaml000066400000000000000000000111011427461146400175510ustar00rootroot00000000000000# # 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. # name: Test on: push: branches: - master pull_request: branches: - master permissions: contents: read jobs: test: runs-on: ubuntu-latest strategy: matrix: go: ["1.17", "1.18"] fail-fast: false name: Go ${{ matrix.go }} steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: | ~/go/pkg/mod ~/go/bin ~/.cache key: ${{ runner.os }}-amd64-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-amd64-go- - name: Setup Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: Setup go-acc run: go install github.com/ory/go-acc@latest - name: Set up gotestfmt uses: haveyoudebuggedit/gotestfmt-action@v2 with: token: ${{ secrets.GITHUB_TOKEN }} # Avoid getting rate limited - name: Run test run: | TEST_BENCH_OPTION="-bench=." if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi set -euo pipefail go-acc -o cover.out ./... -- \ ${TEST_BENCH_OPTION} \ -json \ -v -race 2>&1 | grep -v '^go: downloading' | tee /tmp/gotest.log | gotestfmt - name: Upload test log uses: actions/upload-artifact@v2 if: always() with: name: test-log-${{ matrix.go }} path: /tmp/gotest.log if-no-files-found: error - name: Run TEST_HOOK run: | if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi if [ -n "${TEST_HOOK}" ]; then ${TEST_HOOK}; fi - uses: codecov/codecov-action@v2 with: name: codecov-umbrella fail_ci_if_error: true flags: go test-i386: runs-on: ubuntu-latest strategy: matrix: go: ["1.17", "1.18"] fail-fast: false name: Go i386 ${{ matrix.go }} steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: | ~/go/pkg/mod ~/.cache key: ${{ runner.os }}-i386-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-i386-go- - name: Run test run: | mkdir -p $HOME/go/pkg/mod $HOME/.cache docker run \ -u $(id -u):$(id -g) \ -e "GO111MODULE=on" \ -e "CGO_ENABLED=0" \ -v $GITHUB_WORKSPACE:/go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \ -v $HOME/go/pkg/mod:/go/pkg/mod \ -v $HOME/.cache:/.cache \ -w /go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \ i386/golang:${{matrix.go}}-alpine \ /usr/local/go/bin/go test \ ${TEST_EXTRA_ARGS:-} \ -v ./... test-wasm: runs-on: ubuntu-latest strategy: fail-fast: false name: WASM steps: - uses: actions/checkout@v3 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: '16.x' - uses: actions/cache@v3 with: path: | ~/go/pkg/mod ~/.cache key: ${{ runner.os }}-wasm-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-wasm-go- - name: Download Go run: curl -sSfL https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar -C ~ -xzf - env: GO_VERSION: 1.17 - name: Set Go Root run: echo "GOROOT=${HOME}/go" >> $GITHUB_ENV - name: Set Go Path run: echo "GOPATH=${HOME}/go" >> $GITHUB_ENV - name: Set Go Path run: echo "GO_JS_WASM_EXEC=${GOROOT}/misc/wasm/go_js_wasm_exec" >> $GITHUB_ENV - name: Insall NPM modules run: yarn install - name: Run Tests run: | if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi GOOS=js GOARCH=wasm $GOPATH/bin/go test \ -coverprofile=cover.out -covermode=atomic \ -exec="${GO_JS_WASM_EXEC}" \ -v ./... - uses: codecov/codecov-action@v2 with: name: codecov-umbrella fail_ci_if_error: true flags: wasm sdp-3.0.6/.github/workflows/tidy-check.yaml000066400000000000000000000015621427461146400206300ustar00rootroot00000000000000# # 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. # name: Go mod tidy on: pull_request: branches: - master push: branches: - master permissions: contents: read jobs: Check: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v3 - name: check run: | go mod download go mod tidy if ! git diff --exit-code then echo "Not go mod tidied" exit 1 fi sdp-3.0.6/.gitignore000066400000000000000000000004661427461146400143150ustar00rootroot00000000000000### 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.6/.golangci.yml000066400000000000000000000175411427461146400147130ustar00rootroot00000000000000linters-settings: govet: check-shadowing: true misspell: locale: US exhaustive: default-signifies-exhaustive: true gomodguard: blocked: modules: - github.com/pkg/errors: recommendations: - errors 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 - contextcheck # check the function whether use a non-inherited context - deadcode # Finds unused code - decorder # check declaration order and count of types, constants, variables and functions - depguard # Go linter that checks if package imports are in a list of acceptable packages - 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 - 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 - exportloopref # checks for pointers to enclosing loop variables - forcetypeassert # finds forced type assertions - gci # Gci control golang package import order and make it always deterministic. - gochecknoglobals # Checks that no globals are present in Go code - gochecknoinits # Checks that no init functions 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 - godox # Tool for detection of FIXME, TODO and other comment keywords - goerr113 # Golang linter to check the errors handling expressions - 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. - goheader # Checks is file header matches to pattern - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - 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. - goprintffuncname # Checks that printf-like functions are named with `f` at the end - gosec # Inspects source code for security problems - gosimple # Linter for Go source code that specializes in simplifying a code - 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 - misspell # Finds commonly misspelled English words in comments - nakedret # Finds naked returns in functions greater than a specified function length - 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. - 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 - structcheck # Finds unused struct fields - stylecheck # Stylecheck is a replacement for golint - tagliatelle # Checks the struct tags. - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code - unconvert # Remove unnecessary type conversions - unparam # Reports unused function parameters - unused # Checks Go code for unused constants, variables, functions and types - varcheck # Finds unused global variables and constants - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: - containedctx # containedctx is a linter that detects struct contained context.Context field - cyclop # checks function and package cyclomatic complexity - exhaustivestruct # Checks if all struct's fields are initialized - forbidigo # Forbids identifiers - funlen # Tool for detection of long functions - gocyclo # Computes and checks the cyclomatic complexity of functions - godot # Check if comments end in a period - gomnd # An analyzer to detect magic numbers. - ifshort # Checks that your code uses short syntax for if-statements whenever possible - ireturn # Accept Interfaces, Return Concrete Types - lll # Reports long lines - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - maligned # Tool to detect Go structs that would take less memory if their fields were sorted - nestif # Reports deeply nested if statements - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - 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 - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers - varnamelen # checks that the length of a variable's name matches its scope - wrapcheck # Checks that errors returned from external packages are wrapped - wsl # Whitespace Linter - Forces you to use empty lines! issues: exclude-use-default: false exclude-rules: # Allow complex tests, better to be self contained - path: _test\.go linters: - gocognit # Allow complex main function in examples - path: examples text: "of func `main` is high" linters: - gocognit run: skip-dirs-use-default: false sdp-3.0.6/AUTHORS.txt000066400000000000000000000024421427461146400142070ustar00rootroot00000000000000# Thank you to everyone that made Pion possible. If you are interested in contributing # we would love to have you https://github.com/pion/webrtc/wiki/Contributing # # This file is auto generated, using git to list all individuals contributors. # see `.github/generate-authors.sh` for the scripting adwpc Atsushi Watanabe backkem Brendan Abolivier chenkaiC4 cnderrauber Daniele Sluijters Graham King Guilherme Hugo Arregui Jason Jerko Steiner John Bradley Konstantin Itskov korymiller1489 Luke S Max Hawkins Maxim Oransky mchlrhw <4028654+mchlrhw@users.noreply.github.com> Michael MacDonald Mustafa Navruz Roman Romanenko Sean DuBois Sean DuBois tarrencev Woodrow Douglass ZHENK sdp-3.0.6/LICENSE000066400000000000000000000020411427461146400133210ustar00rootroot00000000000000MIT License Copyright (c) 2018 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.6/README.md000066400000000000000000000041751427461146400136050ustar00rootroot00000000000000


Pion SDP

A Go implementation of the SDP

Pion SDP Sourcegraph Widget Slack Widget
Build Status GoDoc 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 [Golang Slack](https://invite.slack.golangbridge.org/). Sign up and join the **#pion** channel for discussions and support. You can also use [Pion mailing list](https://groups.google.com/forum/#!forum/pion). 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.6/base_lexer.go000066400000000000000000000071241427461146400147630ustar00rootroot00000000000000package sdp import ( "errors" "fmt" "io" "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 []byte pos int } func (l baseLexer) syntaxError() error { return syntaxError{s: string(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 { 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) { 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 string(l.value[start:stop]), 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 string(l.value[start : l.pos-trim]), nil } } } func (l *baseLexer) readString(until byte) (string, error) { start := l.pos for { ch, err := l.readByte() if err != nil { return "", err } if ch == until { return string(l.value[start:l.pos]), nil } } } func (l *baseLexer) readType() (string, error) { for { b, err := l.readByte() if err != nil { return "", err } if isNewline(b) { continue } err = l.unreadByte() if err != nil { return "", err } key, err := l.readString('=') if err != nil { return key, err } if len(key) == 2 { return key, nil } return key, l.syntaxError() } } 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 { for _, v := range data { if element == v { return true } } return false } sdp-3.0.6/base_lexer_test.go000066400000000000000000000037321427461146400160230ustar00rootroot00000000000000package sdp import ( "fmt" "testing" ) 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: []byte(value)} field, err := l.readField() if err != nil { t.Fatal(err) } if field != "aaa" { t.Errorf("%s: aaa not parsed, got: '%v'", k, field) } } }) t.Run("syntax error", func(t *testing.T) { l := &baseLexer{value: []byte("12NaN")} _, err := l.readUint64Field() if err != nil { fmt.Println("error message:", err.Error()) } else { t.Fatal("no error") } }) t.Run("many fields", func(t *testing.T) { l := &baseLexer{value: []byte("aaa 123\nf1 f2\nlast")} t.Run("first line", func(t *testing.T) { field, err := l.readField() if err != nil { t.Fatal(err) } if field != "aaa" { t.Errorf("aaa not parsed, got: '%v'", field) } value, err := l.readUint64Field() if err != nil { t.Fatal(err) } if value != 123 { t.Errorf("aaa not parsed, got: '%v'", field) } if err := l.nextLine(); err != nil { t.Fatal(err) } }) t.Run("second line", func(t *testing.T) { field, err := l.readField() if err != nil { t.Fatal(err) } if field != "f1" { t.Errorf("value not parsed, got: '%v'", field) } field, err = l.readField() if err != nil { t.Fatal(err) } if field != "f2" { t.Errorf("value not parsed, got: '%v'", field) } field, err = l.readField() if err != nil { t.Fatal(err) } if field != "" { t.Errorf("value not parsed, got: '%v'", field) } if err := l.nextLine(); err != nil { t.Fatal(err) } }) t.Run("last line", func(t *testing.T) { field, err := l.readField() if err != nil { t.Fatal(err) } if field != "last" { t.Errorf("value not parsed, got: '%v'", field) } }) }) } sdp-3.0.6/codecov.yml000066400000000000000000000005521427461146400144660ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # 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.6/common_description.go000066400000000000000000000044321427461146400165440ustar00rootroot00000000000000package sdp import ( "strconv" "strings" ) // Information describes the "i=" field which provides textual information // about the session. type Information string func (i Information) String() string { return string(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 { parts := []string{c.NetworkType, c.AddressType} if c.Address != nil && c.Address.String() != "" { parts = append(parts, c.Address.String()) } return strings.Join(parts, " ") } // 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 { var parts []string parts = append(parts, c.Address) if c.TTL != nil { parts = append(parts, strconv.Itoa(*c.TTL)) } if c.Range != nil { parts = append(parts, strconv.Itoa(*c.Range)) } return strings.Join(parts, "/") } // 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 { var output string if b.Experimental { output += "X-" } output += b.Type + ":" + strconv.FormatUint(b.Bandwidth, 10) return output } // EncryptionKey describes the "k=" which conveys encryption key information. type EncryptionKey string func (s EncryptionKey) String() string { return string(s) } // 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 { output := a.Key if len(a.Value) > 0 { output += ":" + a.Value } return output } // IsICECandidate returns true if the attribute key equals "candidate". func (a Attribute) IsICECandidate() bool { return a.Key == "candidate" } sdp-3.0.6/direction.go000066400000000000000000000026431427461146400146330ustar00rootroot00000000000000package 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.6/direction_test.go000066400000000000000000000017711427461146400156730ustar00rootroot00000000000000package 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.Equal(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.Equal(t, u.expected, u.actual.String(), "%d: %+v", i, u) } } sdp-3.0.6/extmap.go000066400000000000000000000045611427461146400141520ustar00rootroot00000000000000package 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" 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.6/extmap_test.go000066400000000000000000000023051427461146400152030ustar00rootroot00000000000000package 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.Equal(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, } if e.Marshal() == "3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" { t.Error("TestTransportCC failed") } } sdp-3.0.6/fuzz.go000066400000000000000000000014571427461146400136530ustar00rootroot00000000000000// +build gofuzz package sdp // Fuzz implements a randomized fuzz test of the sdp // parser using go-fuzz. // // To run the fuzzer, first download go-fuzz: // `go get github.com/dvyukov/go-fuzz/...` // // Then build the testing package: // `go-fuzz-build` // // And run the fuzzer on the corpus: // `go-fuzz` func Fuzz(data []byte) int { // Check that unmarshalling any byte slice does not panic. var sd SessionDescription if err := sd.Unmarshal(data); err != nil { return 0 } // Check that we can marshal anything we unmarshalled. _, err := sd.Marshal() if err != nil { panic("failed to marshal") // nolint } // It'd be nice to check that if we round trip Marshal then Unmarshal, // we get the original back. Right now, though, we frequently don't, // and we'd need to fix that first. return 1 } sdp-3.0.6/go.mod000066400000000000000000000001711427461146400134240ustar00rootroot00000000000000module github.com/pion/sdp/v3 go 1.13 require ( github.com/pion/randutil v0.1.0 github.com/stretchr/testify v1.7.1 ) sdp-3.0.6/go.sum000066400000000000000000000022471427461146400134570ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sdp-3.0.6/jsep.go000066400000000000000000000151031427461146400136070ustar00rootroot00000000000000package 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" AttrKeyRTCPMux = "rtcp-mux" AttrKeyRTCPRsize = "rtcp-rsize" AttrKeyInactive = "inactive" AttrKeyRecvOnly = "recvonly" AttrKeySendOnly = "sendonly" AttrKeySendRecv = "sendrecv" AttrKeyExtMap = "extmap" AttrKeyExtMapAllowMixed = "extmap-allow-mixed" ) // Constants for semantic tokens used in JSEP const ( SemanticTokenLipSynchronization = "LS" SemanticTokenFlowIdentification = "FID" SemanticTokenForwardErrorCorrection = "FEC" 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 } d := &SessionDescription{ Version: 0, Origin: Origin{ Username: "-", SessionID: sid, SessionVersion: uint64(time.Now().Unix()), 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 { d.WithPropertyAttribute(AttrKeyIdentity) } return d, 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 } // 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, codecPrefs []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.6/marshal.go000066400000000000000000000061411427461146400142770ustar00rootroot00000000000000package sdp import ( "strings" ) // 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) { m := make(marshaller, 0, 1024) m.addKeyValue("v=", s.Version.String()) m.addKeyValue("o=", s.Origin.String()) m.addKeyValue("s=", s.SessionName.String()) if s.SessionInformation != nil { m.addKeyValue("i=", s.SessionInformation.String()) } if s.URI != nil { m.addKeyValue("u=", s.URI.String()) } if s.EmailAddress != nil { m.addKeyValue("e=", s.EmailAddress.String()) } if s.PhoneNumber != nil { m.addKeyValue("p=", s.PhoneNumber.String()) } if s.ConnectionInformation != nil { m.addKeyValue("c=", s.ConnectionInformation.String()) } for _, b := range s.Bandwidth { m.addKeyValue("b=", b.String()) } for _, td := range s.TimeDescriptions { m.addKeyValue("t=", td.Timing.String()) for _, r := range td.RepeatTimes { m.addKeyValue("r=", r.String()) } } if len(s.TimeZones) > 0 { var b strings.Builder for i, z := range s.TimeZones { if i > 0 { b.WriteString(" ") } b.WriteString(z.String()) } m.addKeyValue("z=", b.String()) } if s.EncryptionKey != nil { m.addKeyValue("k=", s.EncryptionKey.String()) } for _, a := range s.Attributes { m.addKeyValue("a=", a.String()) } for _, md := range s.MediaDescriptions { m.addKeyValue("m=", md.MediaName.String()) if md.MediaTitle != nil { m.addKeyValue("i=", md.MediaTitle.String()) } if md.ConnectionInformation != nil { m.addKeyValue("c=", md.ConnectionInformation.String()) } for _, b := range md.Bandwidth { m.addKeyValue("b=", b.String()) } if md.EncryptionKey != nil { m.addKeyValue("k=", md.EncryptionKey.String()) } for _, a := range md.Attributes { m.addKeyValue("a=", a.String()) } } return m.bytes(), nil } // marshaller contains state during marshaling. type marshaller []byte func (m *marshaller) addKeyValue(key, value string) { if value == "" { return } *m = append(*m, key...) *m = append(*m, value...) *m = append(*m, "\r\n"...) } func (m *marshaller) bytes() []byte { return *m } sdp-3.0.6/marshal_test.go000066400000000000000000000104351427461146400153370ustar00rootroot00000000000000package sdp import ( "errors" "net/url" "testing" ) 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" ) 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() if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Marshal(): err=%v, want %v", got, want) } if string(actual) != CanonicalMarshalSDP { t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", CanonicalMarshalSDP, string(actual)) } } // var sink []byte func BenchmarkMarshal(b *testing.B) { b.ReportAllocs() var sd SessionDescription err := sd.Unmarshal([]byte(CanonicalUnmarshalSDP)) if err != nil { b.Fatal(err) } for i := 0; i < b.N; i++ { // sink, err = sd.Marshal() _, err = sd.Marshal() if err != nil { b.Fatal(err) } } } sdp-3.0.6/media_description.go000066400000000000000000000036251427461146400163360ustar00rootroot00000000000000package sdp import ( "strconv" "strings" ) // 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 } // MediaName describes the "m=" field storage structure. type MediaName struct { Media string Port RangedPort Protos []string Formats []string } func (m MediaName) String() string { return strings.Join([]string{ m.Media, m.Port.String(), strings.Join(m.Protos, "/"), strings.Join(m.Formats, " "), }, " ") } sdp-3.0.6/media_description_test.go000066400000000000000000000005471427461146400173750ustar00rootroot00000000000000package 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) } sdp-3.0.6/renovate.json000066400000000000000000000011661427461146400150410ustar00rootroot00000000000000{ "extends": [ "config:base", ":disableDependencyDashboard" ], "postUpdateOptions": [ "gomodTidy" ], "commitBody": "Generated by renovateBot", "packageRules": [ { "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "automerge": true }, { "packagePatterns": ["^golang.org/x/"], "schedule": ["on the first day of the month"] } ], "ignorePaths": [ ".github/workflows/generate-authors.yml", ".github/workflows/lint.yaml", ".github/workflows/renovate-go-mod-fix.yaml", ".github/workflows/test.yaml", ".github/workflows/tidy-check.yaml" ] } sdp-3.0.6/sdp.go000066400000000000000000000001111427461146400134250ustar00rootroot00000000000000// Package sdp implements Session Description Protocol (SDP) package sdp sdp-3.0.6/session_description.go000066400000000000000000000072661427461146400167470ustar00rootroot00000000000000package sdp import ( "fmt" "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 strconv.Itoa(int(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 fmt.Sprintf( "%v %d %d %v %v %v", o.Username, o.SessionID, o.SessionVersion, o.NetworkType, o.AddressType, o.UnicastAddress, ) } // SessionName describes a structured representations for the "s=" field // and is the textual session name. type SessionName string func (s SessionName) String() string { return string(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 string(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 string(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 strconv.FormatUint(z.AdjustmentTime, 10) + " " + strconv.FormatInt(z.Offset, 10) } sdp-3.0.6/sessiondescription_test.go000066400000000000000000000011111427461146400176260ustar00rootroot00000000000000package sdp 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 ) sdp-3.0.6/time_description.go000066400000000000000000000025551427461146400162160ustar00rootroot00000000000000package sdp import ( "strconv" "strings" ) // 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 { output := strconv.FormatUint(t.StartTime, 10) output += " " + strconv.FormatUint(t.StopTime, 10) return output } // 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 { fields := make([]string, 0) fields = append(fields, strconv.FormatInt(r.Interval, 10)) fields = append(fields, strconv.FormatInt(r.Duration, 10)) for _, value := range r.Offsets { fields = append(fields, strconv.FormatInt(value, 10)) } return strings.Join(fields, " ") } sdp-3.0.6/unmarshal.go000066400000000000000000000537131427461146400146510ustar00rootroot00000000000000package sdp import ( "errors" "fmt" "net/url" "strconv" "strings" ) 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") ) // Unmarshal 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) Unmarshal(value []byte) error { l := new(lexer) l.desc = s l.value = value for state := s1; state != nil; { var err error state, err = state(l) if err != nil { return err } } return nil } func s1(l *lexer) (stateFn, error) { return l.handleType(func(key string) stateFn { if key == "v=" { return unmarshalProtocolVersion } return nil }) } func s2(l *lexer) (stateFn, error) { return l.handleType(func(key string) stateFn { if key == "o=" { return unmarshalOrigin } return nil }) } func s3(l *lexer) (stateFn, error) { return l.handleType(func(key string) stateFn { if key == "s=" { return unmarshalSessionName } return nil }) } func s4(l *lexer) (stateFn, error) { return l.handleType(func(key string) 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 string) stateFn { switch key { case "b=": return unmarshalSessionBandwidth case "t=": return unmarshalTiming } return nil }) } func s6(l *lexer) (stateFn, error) { return l.handleType(func(key string) 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 string) 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 string) 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 string) 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 string) 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 string) stateFn { switch key { case "a=": return unmarshalSessionAttribute case "m=": return unmarshalMediaDescription } return nil }) } func s12(l *lexer) (stateFn, error) { return l.handleType(func(key string) 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 string) 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 string) 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 string) 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 string) 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(l *lexer) (stateFn, error) { var err error l.desc.Origin.Username, err = l.readField() if err != nil { return nil, err } l.desc.Origin.SessionID, err = l.readUint64Field() if err != nil { return nil, err } l.desc.Origin.SessionVersion, err = l.readUint64Field() if err != nil { return nil, err } l.desc.Origin.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(l.desc.Origin.NetworkType, "IN") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, l.desc.Origin.NetworkType) } l.desc.Origin.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(l.desc.Origin.AddressType, "IP4", "IP6") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, l.desc.Origin.AddressType) } l.desc.Origin.UnicastAddress, err = l.readField() if err != nil { return nil, err } if err := l.nextLine(); err != nil { return nil, err } return s3, 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 c ConnectionInformation c.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(c.NetworkType, "IN") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, c.NetworkType) } c.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(c.AddressType, "IP4", "IP6") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, c.AddressType) } address, err := l.readField() if err != nil { return nil, err } if address != "" { c.Address = new(Address) c.Address.Address = address } if err := l.nextLine(); err != nil { return nil, err } return &c, 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(l *lexer) (stateFn, error) { var err error var td TimeDescription td.Timing.StartTime, err = l.readUint64Field() if err != nil { return nil, err } td.Timing.StopTime, err = l.readUint64Field() if err != nil { return nil, err } if err := l.nextLine(); err != nil { return nil, err } l.desc.TimeDescriptions = append(l.desc.TimeDescriptions, td) return s9, nil } func unmarshalRepeatTimes(l *lexer) (stateFn, error) { var err error var newRepeatTime RepeatTime latestTimeDesc := &l.desc.TimeDescriptions[len(l.desc.TimeDescriptions)-1] field, err := l.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 = l.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 := l.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 := l.nextLine(); err != nil { return nil, err } latestTimeDesc.RepeatTimes = append(latestTimeDesc.RepeatTimes, newRepeatTime) return s9, nil } func unmarshalTimeZones(l *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 = l.readUint64Field() if err != nil { return nil, err } offset, err := l.readField() if err != nil { return nil, err } if offset == "" { break } timeZone.Offset, err = parseTimeUnits(offset) if err != nil { return nil, err } l.desc.TimeZones = append(l.desc.TimeZones, timeZone) } if err := l.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, ':') var a Attribute if i > 0 { a = NewAttribute(value[:i], value[i+1:]) } else { a = NewPropertyAttribute(value) } l.desc.Attributes = append(l.desc.Attributes, a) return s11, nil } func unmarshalMediaDescription(l *lexer) (stateFn, error) { var newMediaDesc MediaDescription // field, err := l.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 = l.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 = l.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") { return nil, fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, field) } newMediaDesc.MediaName.Protos = append(newMediaDesc.MediaName.Protos, proto) } // ... for { field, err = l.readField() if err != nil { return nil, err } if field == "" { break } newMediaDesc.MediaName.Formats = append(newMediaDesc.MediaName.Formats, field) } if err := l.nextLine(); err != nil { return nil, err } l.desc.MediaDescriptions = append(l.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, ':') var a Attribute if i > 0 { a = NewAttribute(value[:i], value[i+1:]) } else { a = NewPropertyAttribute(value) } latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] latestMediaDesc.Attributes = append(latestMediaDesc.Attributes, a) return s14, nil } func parseTimeUnits(value string) (num int64, err error) { 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, port) } if port < 0 || port > 65536 { return 0, fmt.Errorf("%w -- out of range `%v`", errSDPInvalidPortValue, port) } return port, nil } sdp-3.0.6/unmarshal_test.go000066400000000000000000000225671427461146400157130ustar00rootroot00000000000000package sdp import ( "errors" "strconv" "testing" ) 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" 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, }, } { test := test t.Run(test.Name, func(t *testing.T) { sd := &SessionDescription{} err := sd.Unmarshal([]byte(test.SDP)) if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Unmarshal:\nerr=%v\nwant=%v", got, want) } actual, err := sd.Marshal() if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Marshal:\nerr=%v\nwant=%v", got, want) } want := test.SDP if test.Actual != "" { want = test.Actual } if got := string(actual); got != want { t.Fatalf("Marshal:\ngot=%s\nwant=%s", strconv.Quote(got), strconv.Quote(want), ) } }) } } func TestUnmarshalRepeatTimes(t *testing.T) { sd := &SessionDescription{} if err := sd.Unmarshal([]byte(RepeatTimesSDP)); err != nil { t.Errorf("error: %v", err) } actual, err := sd.Marshal() if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Marshal(): err=%v, want %v", got, want) } if string(actual) != RepeatTimesSDPExpected { t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", RepeatTimesSDPExpected, string(actual)) } } func TestUnmarshalTimeZones(t *testing.T) { sd := &SessionDescription{} if err := sd.Unmarshal([]byte(TimeZonesSDP)); err != nil { t.Errorf("error: %v", err) } actual, err := sd.Marshal() if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Marshal(): err=%v, want %v", got, want) } if string(actual) != TimeZonesSDPExpected { t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", 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.Unmarshal([]byte(in)) if err != nil { t.Fatalf("failed to unmarshal %q", in) } out, err := sd.Marshal() if err != nil { t.Errorf("failed to marshal unmarshalled %q", in) } if string(out) != in { t.Errorf("round trip = %q want %q", out, in) } } func BenchmarkUnmarshal(b *testing.B) { b.ReportAllocs() raw := []byte(CanonicalUnmarshalSDP) for i := 0; i < b.N; i++ { var sd SessionDescription err := sd.Unmarshal(raw) if err != nil { b.Fatal(err) } } } sdp-3.0.6/util.go000066400000000000000000000166131427461146400136320ustar00rootroot00000000000000package sdp import ( "errors" "fmt" "io" "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") ) // 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 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.Split(fmtp, " ") 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, error) { var codec Codec parsingFailed := errExtractCodecRtcpFb // a=ftcp-fb: [] split := strings.SplitN(rtcpFb, " ", 2) 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) codec.RTCPFeedback = append(codec.RTCPFeedback, split[1]) return codec, 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 { codecs := make(map[uint8]Codec) 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, err := parseRtcpFb(attr) if err == nil { mergeCodecs(codec, codecs) } } } } 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 } // 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 baseLexer } type keyToState func(key string) stateFn func (l *lexer) handleType(fn keyToState) (stateFn, error) { key, err := l.readType() if errors.Is(err, io.EOF) && key == "" { 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.6/util_test.go000066400000000000000000000074761427461146400147000ustar00rootroot00000000000000package sdp import ( "errors" "reflect" "testing" ) 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"}, }, 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: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("rtcp-fb:97 ccm fir", ""), NewAttribute("rtcp-fb:97 nack", ""), NewAttribute("rtcp-fb:97 nack pli", ""), }, }, }, } } 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) if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("GetPayloadTypeForCodec(): err=%v, want=%v", got, want) } if actual != test.Expected { t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", test.Expected, actual) } } } func TestGetCodecForPayloadType(t *testing.T) { for _, test := range []struct { PayloadType uint8 Expected Codec }{ { PayloadType: 120, Expected: Codec{ PayloadType: 120, Name: "VP8", ClockRate: 90000, Fmtp: "max-fs=12288;max-fr=60", }, }, { PayloadType: 121, Expected: Codec{ PayloadType: 121, Name: "VP9", ClockRate: 90000, Fmtp: "max-fs=12288;max-fr=60", }, }, { PayloadType: 126, Expected: Codec{ PayloadType: 126, Name: "H264", ClockRate: 90000, Fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1", }, }, { PayloadType: 97, Expected: Codec{ PayloadType: 97, Name: "H264", ClockRate: 90000, Fmtp: "profile-level-id=42e01f;level-asymmetry-allowed=1", RTCPFeedback: []string{"ccm fir", "nack", "nack pli"}, }, }, } { sd := getTestSessionDescription() actual, err := sd.GetCodecForPayloadType(test.PayloadType) if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("GetCodecForPayloadType(): err=%v, want=%v", got, want) } if !reflect.DeepEqual(actual, test.Expected) { t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", test.Expected, actual) } } } func TestNewSessionID(t *testing.T) { min := uint64(0x7FFFFFFFFFFFFFFF) max := uint64(0) for i := 0; i < 10000; i++ { r, err := newSessionID() if err != nil { t.Fatal(err) } if r > (1<<63)-1 { t.Fatalf("Session ID must be less than 2**64-1, got %d", r) } if r < min { min = r } if r > max { max = r } } if min > 0x1000000000000000 { t.Error("Value around lower boundary was not generated") } if max < 0x7000000000000000 { t.Error("Value around upper boundary was not generated") } }