pax_global_header00006660000000000000000000000064142657347550014534gustar00rootroot0000000000000052 comment=a82b843030a8e8a046cbda37ce33a100ddb3b5f1 interceptor-0.1.12/000077500000000000000000000000001426573475500141535ustar00rootroot00000000000000interceptor-0.1.12/.github/000077500000000000000000000000001426573475500155135ustar00rootroot00000000000000interceptor-0.1.12/.github/generate-authors.sh000077500000000000000000000031401426573475500213250ustar00rootroot00000000000000#!/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 interceptor-0.1.12/.github/hooks/000077500000000000000000000000001426573475500166365ustar00rootroot00000000000000interceptor-0.1.12/.github/hooks/commit-msg.sh000077500000000000000000000002671426573475500212560ustar00rootroot00000000000000#!/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 interceptor-0.1.12/.github/hooks/pre-commit.sh000077500000000000000000000004171426573475500212530ustar00rootroot00000000000000#!/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 interceptor-0.1.12/.github/hooks/pre-push.sh000077500000000000000000000002571426573475500207440ustar00rootroot00000000000000#!/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 interceptor-0.1.12/.github/install-hooks.sh000077500000000000000000000010361426573475500206410ustar00rootroot00000000000000#!/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" interceptor-0.1.12/.github/lint-commit-message.sh000077500000000000000000000035641426573475500217400ustar00rootroot00000000000000#!/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 interceptor-0.1.12/.github/lint-disallowed-functions-in-library.sh000077500000000000000000000023571426573475500252300ustar00rootroot00000000000000#!/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 interceptor-0.1.12/.github/lint-filename.sh000077500000000000000000000012041426573475500205730ustar00rootroot00000000000000#!/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 interceptor-0.1.12/.github/lint-no-trailing-newline-in-log-messages.sh000077500000000000000000000016561426573475500257000ustar00rootroot00000000000000#!/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 fiinterceptor-0.1.12/.github/workflows/000077500000000000000000000000001426573475500175505ustar00rootroot00000000000000interceptor-0.1.12/.github/workflows/codeql-analysis.yml000066400000000000000000000015411426573475500233640ustar00rootroot00000000000000name: "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 interceptor-0.1.12/.github/workflows/generate-authors.yml000066400000000000000000000047721426573475500235620ustar00rootroot00000000000000# # 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) interceptor-0.1.12/.github/workflows/lint.yaml000066400000000000000000000026251426573475500214070ustar00rootroot00000000000000# # 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 interceptor-0.1.12/.github/workflows/renovate-go-mod-fix.yaml000066400000000000000000000016071426573475500242270ustar00rootroot00000000000000# # 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 interceptor-0.1.12/.github/workflows/test.yaml000066400000000000000000000111011426573475500214050ustar00rootroot00000000000000# # 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 interceptor-0.1.12/.github/workflows/tidy-check.yaml000066400000000000000000000015621426573475500224640ustar00rootroot00000000000000# # 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 interceptor-0.1.12/.gitignore000066400000000000000000000004661426573475500161510ustar00rootroot00000000000000### 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 interceptor-0.1.12/.golangci.yml000066400000000000000000000175411426573475500165470ustar00rootroot00000000000000linters-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 interceptor-0.1.12/AUTHORS.txt000066400000000000000000000014401426573475500160400ustar00rootroot00000000000000# 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 Aaron Boushley Adam Kiss adamroach Aditya Kumar aler9 <46489434+aler9@users.noreply.github.com> Antoine Baché Atsushi Watanabe Bobby Peck boks1971 David Zhao Jonathan Müller Kevin Caffrey Mathis Engelbart Sean DuBois interceptor-0.1.12/LICENSE000066400000000000000000000020411426573475500151550ustar00rootroot00000000000000MIT 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. interceptor-0.1.12/README.md000066400000000000000000000115031426573475500154320ustar00rootroot00000000000000


Pion Interceptor

RTCP and RTCP processors for building real time communications

Pion Interceptor Slack Widget
GoDoc Coverage Status Go Report Card License: MIT


Interceptor is a framework for building RTP/RTCP communication software. This framework defines a interface that each interceptor must satisfy. These interceptors are then run sequentially. We also then provide common interceptors that will be useful for building RTC software. This package was built for [pion/webrtc](https://github.com/pion/webrtc), but we designed it to be consumable by anyone. With the following tenets in mind. * Useful defaults. Each interceptor will be configured to give you a good default experience. * Unblock unique use cases. New use cases are what is driving WebRTC, we want to empower them. * Encourage modification. Add your own interceptors without forking. Mixing with the ones we provide. * Empower learning. This code base should be useful to read and learn even if you aren't using Pion. #### Current Interceptors * [NACK Generator/Responder](https://github.com/pion/interceptor/tree/master/pkg/nack) * [Sender and Receiver Reports](https://github.com/pion/interceptor/tree/master/pkg/report) * [Transport Wide Congestion Control Feedback](https://github.com/pion/interceptor/tree/master/pkg/twcc) * [Packet Dump](https://github.com/pion/interceptor/tree/master/pkg/packetdump) * [Google Congestion Control](https://github.com/pion/interceptor/tree/master/pkg/gcc) #### Planned Interceptors * Bandwidth Estimation - [NADA](https://tools.ietf.org/html/rfc8698) * JitterBuffer, re-order packets and wait for arrival * [FlexFec](https://tools.ietf.org/html/draft-ietf-payload-flexible-fec-scheme-20) * [webrtc-stats](https://www.w3.org/TR/webrtc-stats/) compliant statistics generation * [RTCP Feedback for Congestion Control](https://datatracker.ietf.org/doc/html/rfc8888) the standardized alternative to TWCC. ### Interceptor Public API The public interface is defined in [interceptor.go](https://github.com/pion/interceptor/blob/master/interceptor.go). The methods you need to satisy are broken up into 4 groups. * `BindRTCPWriter` and `BindRTCPReader` allow you to inspect/modify RTCP traffic. * `BindLocalStream` and `BindRemoteStream` notify you of a new SSRC stream and allow you to inspect/modify. * `UnbindLocalStream` and `UnbindRemoteStream` notify you when a SSRC stream has been removed * `Close` called when the interceptor is closed. Interceptors also pass Attributes between each other. These are a collection of key/value pairs and are useful for storing metadata or caching. [noop.go](https://github.com/pion/interceptor/blob/master/noop.go) is an interceptor that satisfies this interface, but does nothing. You can embed this interceptor as a starting point so you only need to define exactly what you need. [chain.go]( https://github.com/pion/interceptor/blob/master/chain.go) is used to combine multiple interceptors into one. They are called sequentially as the packet moves through them. ### Examples The [examples](https://github.com/pion/interceptor/blob/master/examples) directory provides some basic examples. If you need more please file an issue! You should also look in [pion/webrtc](https://github.com/pion/webrtc) for real world examples. ### 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 interceptor-0.1.12/attributes.go000066400000000000000000000031741426573475500166750ustar00rootroot00000000000000package interceptor import ( "errors" "github.com/pion/rtcp" "github.com/pion/rtp" ) type unmarshaledDataKeyType int const ( rtpHeaderKey unmarshaledDataKeyType = iota rtcpPacketsKey ) var errInvalidType = errors.New("found value of invalid type in attributes map") // Attributes are a generic key/value store used by interceptors type Attributes map[interface{}]interface{} // Get returns the attribute associated with key. func (a Attributes) Get(key interface{}) interface{} { return a[key] } // Set sets the attribute associated with key to the given value. func (a Attributes) Set(key interface{}, val interface{}) { a[key] = val } // GetRTPHeader gets the RTP header if present. If it is not present, it will be // unmarshalled from the raw byte slice and stored in the attribtues. func (a Attributes) GetRTPHeader(raw []byte) (*rtp.Header, error) { if val, ok := a[rtpHeaderKey]; ok { if header, ok := val.(*rtp.Header); ok { return header, nil } return nil, errInvalidType } header := &rtp.Header{} if _, err := header.Unmarshal(raw); err != nil { return nil, err } a[rtpHeaderKey] = header return header, nil } // GetRTCPPackets gets the RTCP packets if present. If the packet slice is not // present, it will be unmarshaled from the raw byte slice and stored in the // attributes. func (a Attributes) GetRTCPPackets(raw []byte) ([]rtcp.Packet, error) { if val, ok := a[rtcpPacketsKey]; ok { if packets, ok := val.([]rtcp.Packet); ok { return packets, nil } return nil, errInvalidType } pkts, err := rtcp.Unmarshal(raw) if err != nil { return nil, err } a[rtcpPacketsKey] = pkts return pkts, nil } interceptor-0.1.12/attributes_test.go000066400000000000000000000062141426573475500177320ustar00rootroot00000000000000package interceptor import ( "testing" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestAttributesGetRTPHeader(t *testing.T) { t.Run("NilHeader", func(t *testing.T) { attributes := Attributes{} _, err := attributes.GetRTPHeader(nil) assert.Error(t, err) }) t.Run("Present", func(t *testing.T) { attributes := Attributes{ rtpHeaderKey: &rtp.Header{ Version: 0, Padding: false, Extension: false, Marker: false, PayloadType: 0, SequenceNumber: 0, Timestamp: 0, SSRC: 0, CSRC: []uint32{}, ExtensionProfile: 0, Extensions: nil, }, } header, err := attributes.GetRTPHeader(nil) assert.NoError(t, err) assert.Equal(t, attributes[rtpHeaderKey], header) }) t.Run("NotPresent", func(t *testing.T) { attributes := Attributes{} hdr := &rtp.Header{ Version: 0, Padding: false, Extension: false, Marker: false, PayloadType: 0, SequenceNumber: 0, Timestamp: 0, SSRC: 0, CSRC: []uint32{}, ExtensionProfile: 0, Extensions: nil, } buf, err := hdr.Marshal() assert.NoError(t, err) header, err := attributes.GetRTPHeader(buf) assert.NoError(t, err) assert.Equal(t, hdr, header) }) t.Run("NotPresentFromFullRTPPacket", func(t *testing.T) { attributes := Attributes{} pkt := &rtp.Packet{Header: rtp.Header{ Version: 0, Padding: false, Extension: false, Marker: false, PayloadType: 0, SequenceNumber: 0, Timestamp: 0, SSRC: 0, CSRC: []uint32{}, ExtensionProfile: 0, Extensions: nil, }, Payload: make([]byte, 1000)} buf, err := pkt.Marshal() assert.NoError(t, err) header, err := attributes.GetRTPHeader(buf) assert.NoError(t, err) assert.Equal(t, &pkt.Header, header) }) } func TestAttributesGetRTCPPackets(t *testing.T) { t.Run("NilPacket", func(t *testing.T) { attributes := Attributes{} _, err := attributes.GetRTCPPackets(nil) assert.Error(t, err) }) t.Run("Present", func(t *testing.T) { attributes := Attributes{ rtcpPacketsKey: []rtcp.Packet{ &rtcp.TransportLayerCC{ Header: rtcp.Header{Padding: false, Count: 0, Type: 0, Length: 0}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 0, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{}, RecvDeltas: []*rtcp.RecvDelta{}, }, }, } packets, err := attributes.GetRTCPPackets(nil) assert.NoError(t, err) assert.Equal(t, attributes[rtcpPacketsKey], packets) }) t.Run("NotPresent", func(t *testing.T) { attributes := Attributes{} sr := &rtcp.SenderReport{ SSRC: 0, NTPTime: 0, RTPTime: 0, PacketCount: 0, OctetCount: 0, } buf, err := sr.Marshal() assert.NoError(t, err) packets, err := attributes.GetRTCPPackets(buf) assert.NoError(t, err) assert.Equal(t, []rtcp.Packet{sr}, packets) }) } interceptor-0.1.12/chain.go000066400000000000000000000046621426573475500155740ustar00rootroot00000000000000package interceptor // Chain is an interceptor that runs all child interceptors in order. type Chain struct { interceptors []Interceptor } // NewChain returns a new Chain interceptor. func NewChain(interceptors []Interceptor) *Chain { return &Chain{interceptors: interceptors} } // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might // change in the future. The returned method will be called once per packet batch. func (i *Chain) BindRTCPReader(reader RTCPReader) RTCPReader { for _, interceptor := range i.interceptors { reader = interceptor.BindRTCPReader(reader) } return reader } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (i *Chain) BindRTCPWriter(writer RTCPWriter) RTCPWriter { for _, interceptor := range i.interceptors { writer = interceptor.BindRTCPWriter(writer) } return writer } // BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method // will be called once per rtp packet. func (i *Chain) BindLocalStream(ctx *StreamInfo, writer RTPWriter) RTPWriter { for _, interceptor := range i.interceptors { writer = interceptor.BindLocalStream(ctx, writer) } return writer } // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (i *Chain) UnbindLocalStream(ctx *StreamInfo) { for _, interceptor := range i.interceptors { interceptor.UnbindLocalStream(ctx) } } // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (i *Chain) BindRemoteStream(ctx *StreamInfo, reader RTPReader) RTPReader { for _, interceptor := range i.interceptors { reader = interceptor.BindRemoteStream(ctx, reader) } return reader } // UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (i *Chain) UnbindRemoteStream(ctx *StreamInfo) { for _, interceptor := range i.interceptors { interceptor.UnbindRemoteStream(ctx) } } // Close closes the Interceptor, cleaning up any data if necessary. func (i *Chain) Close() error { var errs []error for _, interceptor := range i.interceptors { errs = append(errs, interceptor.Close()) } return flattenErrs(errs) } interceptor-0.1.12/codecov.yml000066400000000000000000000005521426573475500163220ustar00rootroot00000000000000# # 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/**/*" interceptor-0.1.12/errors.go000066400000000000000000000014531426573475500160210ustar00rootroot00000000000000package interceptor import ( "errors" "strings" ) func flattenErrs(errs []error) error { errs2 := []error{} for _, e := range errs { if e != nil { errs2 = append(errs2, e) } } if len(errs2) == 0 { return nil } return multiError(errs2) } type multiError []error //nolint func (me multiError) Error() string { var errstrings []string for _, err := range me { if err != nil { errstrings = append(errstrings, err.Error()) } } if len(errstrings) == 0 { return "multiError must contain multiple error but is empty" } return strings.Join(errstrings, "\n") } func (me multiError) Is(err error) bool { for _, e := range me { if errors.Is(e, err) { return true } if me2, ok := e.(multiError); ok { //nolint if me2.Is(err) { return true } } } return false } interceptor-0.1.12/errors_test.go000066400000000000000000000014641426573475500170620ustar00rootroot00000000000000package interceptor import ( "errors" "testing" ) func TestMultiError(t *testing.T) { rawErrs := []error{ errors.New("err1"), //nolint errors.New("err2"), //nolint errors.New("err3"), //nolint errors.New("err4"), //nolint } errs := flattenErrs([]error{ rawErrs[0], nil, rawErrs[1], flattenErrs([]error{ rawErrs[2], }), }) str := "err1\nerr2\nerr3" if errs.Error() != str { t.Errorf("String representation doesn't match, expected: %s, got: %s", errs.Error(), str) } errIs, ok := errs.(multiError) //nolint if !ok { t.Fatal("FlattenErrs returns non-multiError") } for i := 0; i < 3; i++ { if !errIs.Is(rawErrs[i]) { t.Errorf("'%+v' should contains '%v'", errs, rawErrs[i]) } } if errIs.Is(rawErrs[3]) { t.Errorf("'%+v' should not contains '%v'", errs, rawErrs[3]) } } interceptor-0.1.12/examples/000077500000000000000000000000001426573475500157715ustar00rootroot00000000000000interceptor-0.1.12/examples/nack/000077500000000000000000000000001426573475500167055ustar00rootroot00000000000000interceptor-0.1.12/examples/nack/README.md000066400000000000000000000025361426573475500201720ustar00rootroot00000000000000# nack nack demonstrates how to send RTP packets over a connection that both generates and handles NACKs ## Instructions ### run main.go ``` go run main.go ``` You will then see output like ``` $ go run main.go 2020/12/16 00:27:54 Received RTP 2020/12/16 00:27:54 Received RTP 2020/12/16 00:27:54 Received RTP 2020/12/16 00:27:54 Received RTP 2020/12/16 00:27:54 Received RTP 2020/12/16 00:27:55 Received RTP 2020/12/16 00:27:55 Received NACK 2020/12/16 00:27:55 Received RTP 2020/12/16 00:27:55 Received RTP 2020/12/16 00:27:55 Received RTP 2020/12/16 00:27:55 Received RTP 2020/12/16 00:27:56 Received RTP 2020/12/16 00:27:56 Received NACK 2020/12/16 00:27:56 Received RTP 2020/12/16 00:27:56 Received NACK 2020/12/16 00:27:56 Received RTP 2020/12/16 00:27:56 Received RTP 2020/12/16 00:27:56 Received RTP 2020/12/16 00:27:57 Received RTP 2020/12/16 00:27:57 Received RTP 2020/12/16 00:27:57 Received RTP 2020/12/16 00:27:58 Received RTP 2020/12/16 00:27:58 Received NACK 2020/12/16 00:27:58 Received RTP 2020/12/16 00:27:58 Received RTP 2020/12/16 00:27:58 Received RTP 2020/12/16 00:27:58 Received NACK 2020/12/16 00:27:58 Received RTP 2020/12/16 00:27:58 Received RTP ``` ### Introduce loss You will not see much loss on loopback by default. To introduce 15% loss you can do ``` $ iptables -A INPUT -m statistic --mode random --probability 0.15 -p udp -j DROP ``` interceptor-0.1.12/examples/nack/main.go000066400000000000000000000077541426573475500201750ustar00rootroot00000000000000package main import ( "fmt" "log" "net" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/pkg/nack" "github.com/pion/rtcp" "github.com/pion/rtp" ) const ( listenPort = 6420 mtu = 1500 ssrc = 5000 ) func main() { go sendRoutine() receiveRoutine() } func receiveRoutine() { serverAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("127.0.0.1:%d", listenPort)) if err != nil { panic(err) } conn, err := net.ListenUDP("udp4", serverAddr) if err != nil { panic(err) } // Create NACK Generator generatorFactory, err := nack.NewGeneratorInterceptor() if err != nil { panic(err) } generator, err := generatorFactory.NewInterceptor("") if err != nil { panic(err) } // Create our interceptor chain with just a NACK Generator chain := interceptor.NewChain([]interceptor.Interceptor{generator}) // Create the writer just for a single SSRC stream // this is a callback that is fired everytime a RTP packet is ready to be sent streamReader := chain.BindRemoteStream(&interceptor.StreamInfo{ SSRC: ssrc, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack", Parameter: ""}}, }, interceptor.RTPReaderFunc(func(b []byte, _ interceptor.Attributes) (int, interceptor.Attributes, error) { return len(b), nil, nil })) for rtcpBound, buffer := false, make([]byte, mtu); ; { i, addr, err := conn.ReadFrom(buffer) if err != nil { panic(err) } log.Println("Received RTP") if _, _, err := streamReader.Read(buffer[:i], nil); err != nil { panic(err) } // Set the interceptor wide RTCP Writer // this is a callback that is fired everytime a RTCP packet is ready to be sent if !rtcpBound { chain.BindRTCPWriter(interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, _ interceptor.Attributes) (int, error) { buf, err := rtcp.Marshal(pkts) if err != nil { return 0, err } return conn.WriteTo(buf, addr) })) rtcpBound = true } } } func sendRoutine() { // Dial our UDP listener that we create in receiveRoutine serverAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("127.0.0.1:%d", listenPort)) if err != nil { panic(err) } conn, err := net.DialUDP("udp4", nil, serverAddr) if err != nil { panic(err) } // Create NACK Responder responderFactory, err := nack.NewResponderInterceptor() if err != nil { panic(err) } responder, err := responderFactory.NewInterceptor("") if err != nil { panic(err) } // Create our interceptor chain with just a NACK Responder. chain := interceptor.NewChain([]interceptor.Interceptor{responder}) // Set the interceptor wide RTCP Reader // this is a handle to send NACKs back into the interceptor. rtcpReader := chain.BindRTCPReader(interceptor.RTCPReaderFunc(func(in []byte, _ interceptor.Attributes) (int, interceptor.Attributes, error) { return len(in), nil, nil })) // Create the writer just for a single SSRC stream // this is a callback that is fired everytime a RTP packet is ready to be sent streamWriter := chain.BindLocalStream(&interceptor.StreamInfo{ SSRC: ssrc, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack", Parameter: ""}}, }, interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { headerBuf, err := header.Marshal() if err != nil { panic(err) } return conn.Write(append(headerBuf, payload...)) })) // Read RTCP packets sent by receiver and pass into Interceptor go func() { for rtcpBuf := make([]byte, mtu); ; { i, err := conn.Read(rtcpBuf) if err != nil { panic(err) } log.Println("Received NACK") if _, _, err = rtcpReader.Read(rtcpBuf[:i], nil); err != nil { panic(err) } } }() for sequenceNumber := uint16(0); ; sequenceNumber++ { // Send a RTP packet with a Payload of 0x0, 0x1, 0x2 if _, err := streamWriter.Write(&rtp.Header{ Version: 2, SSRC: ssrc, SequenceNumber: sequenceNumber, }, []byte{0x0, 0x1, 0x2}, nil); err != nil { fmt.Println(err) } time.Sleep(time.Millisecond * 200) } } interceptor-0.1.12/go.mod000066400000000000000000000002701426573475500152600ustar00rootroot00000000000000module github.com/pion/interceptor go 1.15 require ( github.com/pion/logging v0.2.2 github.com/pion/rtcp v1.2.10 github.com/pion/rtp v1.7.13 github.com/stretchr/testify v1.7.1 ) interceptor-0.1.12/go.sum000066400000000000000000000032141426573475500153060ustar00rootroot00000000000000github.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/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 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/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 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= interceptor-0.1.12/interceptor.go000066400000000000000000000070201426573475500170370ustar00rootroot00000000000000// Package interceptor contains the Interceptor interface, with some useful interceptors that should be safe to use // in most cases. package interceptor import ( "io" "github.com/pion/rtcp" "github.com/pion/rtp" ) // Factory provides an interface for constructing interceptors type Factory interface { NewInterceptor(id string) (Interceptor, error) } // Interceptor can be used to add functionality to you PeerConnections by modifying any incoming/outgoing rtp/rtcp // packets, or sending your own packets as needed. type Interceptor interface { // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might // change in the future. The returned method will be called once per packet batch. BindRTCPReader(reader RTCPReader) RTCPReader // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. BindRTCPWriter(writer RTCPWriter) RTCPWriter // BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method // will be called once per rtp packet. BindLocalStream(info *StreamInfo, writer RTPWriter) RTPWriter // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. UnbindLocalStream(info *StreamInfo) // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. BindRemoteStream(info *StreamInfo, reader RTPReader) RTPReader // UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. UnbindRemoteStream(info *StreamInfo) io.Closer } // RTPWriter is used by Interceptor.BindLocalStream. type RTPWriter interface { // Write a rtp packet Write(header *rtp.Header, payload []byte, attributes Attributes) (int, error) } // RTPReader is used by Interceptor.BindRemoteStream. type RTPReader interface { // Read a rtp packet Read([]byte, Attributes) (int, Attributes, error) } // RTCPWriter is used by Interceptor.BindRTCPWriter. type RTCPWriter interface { // Write a batch of rtcp packets Write(pkts []rtcp.Packet, attributes Attributes) (int, error) } // RTCPReader is used by Interceptor.BindRTCPReader. type RTCPReader interface { // Read a batch of rtcp packets Read([]byte, Attributes) (int, Attributes, error) } // RTPWriterFunc is an adapter for RTPWrite interface type RTPWriterFunc func(header *rtp.Header, payload []byte, attributes Attributes) (int, error) // RTPReaderFunc is an adapter for RTPReader interface type RTPReaderFunc func([]byte, Attributes) (int, Attributes, error) // RTCPWriterFunc is an adapter for RTCPWriter interface type RTCPWriterFunc func(pkts []rtcp.Packet, attributes Attributes) (int, error) // RTCPReaderFunc is an adapter for RTCPReader interface type RTCPReaderFunc func([]byte, Attributes) (int, Attributes, error) // Write a rtp packet func (f RTPWriterFunc) Write(header *rtp.Header, payload []byte, attributes Attributes) (int, error) { return f(header, payload, attributes) } // Read a rtp packet func (f RTPReaderFunc) Read(b []byte, a Attributes) (int, Attributes, error) { return f(b, a) } // Write a batch of rtcp packets func (f RTCPWriterFunc) Write(pkts []rtcp.Packet, attributes Attributes) (int, error) { return f(pkts, attributes) } // Read a batch of rtcp packets func (f RTCPReaderFunc) Read(b []byte, a Attributes) (int, Attributes, error) { return f(b, a) } interceptor-0.1.12/internal/000077500000000000000000000000001426573475500157675ustar00rootroot00000000000000interceptor-0.1.12/internal/cc/000077500000000000000000000000001426573475500163545ustar00rootroot00000000000000interceptor-0.1.12/internal/cc/acknowledgment.go000066400000000000000000000012741426573475500217110ustar00rootroot00000000000000package cc import ( "fmt" "time" "github.com/pion/rtcp" ) // Acknowledgment holds information about a packet and if/when it has been // sent/received. type Acknowledgment struct { SequenceNumber uint16 // Either RTP SequenceNumber or TWCC SSRC uint32 Size int Departure time.Time Arrival time.Time ECN rtcp.ECN } func (a Acknowledgment) String() string { s := "ACK:\n" s += fmt.Sprintf("\tTLCC:\t%v\n", a.SequenceNumber) s += fmt.Sprintf("\tSIZE:\t%v\n", a.Size) s += fmt.Sprintf("\tDEPARTURE:\t%v\n", int64(float64(a.Departure.UnixNano())/1e+6)) s += fmt.Sprintf("\tARRIVAL:\t%v\n", int64(float64(a.Arrival.UnixNano())/1e+6)) return s } interceptor-0.1.12/internal/cc/cc.go000066400000000000000000000001251426573475500172660ustar00rootroot00000000000000// Package cc implements common constructs used by Congestion Controllers package cc interceptor-0.1.12/internal/cc/feedback_adapter.go000066400000000000000000000163711426573475500221370ustar00rootroot00000000000000package cc import ( "container/list" "errors" "sync" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/ntp" "github.com/pion/rtcp" "github.com/pion/rtp" ) // TwccExtensionAttributesKey identifies the TWCC value in the attribute collection // so we don't need to reparse const TwccExtensionAttributesKey = iota var ( errMissingTWCCExtension = errors.New("missing transport layer cc header extension") errInvalidFeedback = errors.New("invalid feedback") ) // FeedbackAdapter converts incoming RTCP Packets (TWCC and RFC8888) into Acknowledgments. // Acknowledgments are the common format that Congestion Controllers in Pion understand. type FeedbackAdapter struct { lock sync.Mutex history *feedbackHistory } // NewFeedbackAdapter returns a new FeedbackAdapter func NewFeedbackAdapter() *FeedbackAdapter { return &FeedbackAdapter{history: newFeedbackHistory(250)} } func (f *FeedbackAdapter) onSentRFC8888(ts time.Time, header *rtp.Header, size int) error { f.lock.Lock() defer f.lock.Unlock() f.history.add(Acknowledgment{ SequenceNumber: header.SequenceNumber, SSRC: header.SSRC, Size: size, Departure: ts, Arrival: time.Time{}, ECN: 0, }) return nil } func (f *FeedbackAdapter) onSentTWCC(ts time.Time, extID uint8, header *rtp.Header, size int) error { sequenceNumber := header.GetExtension(extID) var tccExt rtp.TransportCCExtension err := tccExt.Unmarshal(sequenceNumber) if err != nil { return errMissingTWCCExtension } f.lock.Lock() defer f.lock.Unlock() f.history.add(Acknowledgment{ SequenceNumber: tccExt.TransportSequence, SSRC: 0, Size: header.MarshalSize() + size, Departure: ts, Arrival: time.Time{}, ECN: 0, }) return nil } // OnSent records that and when an outgoing packet was sent for later mapping to // acknowledgments func (f *FeedbackAdapter) OnSent(ts time.Time, header *rtp.Header, size int, attributes interceptor.Attributes) error { hdrExtensionID := attributes.Get(TwccExtensionAttributesKey) id, ok := hdrExtensionID.(uint8) if ok && hdrExtensionID != 0 { return f.onSentTWCC(ts, id, header, size) } return f.onSentRFC8888(ts, header, size) } func (f *FeedbackAdapter) unpackRunLengthChunk(start uint16, refTime time.Time, chunk *rtcp.RunLengthChunk, deltas []*rtcp.RecvDelta) (consumedDeltas int, nextRef time.Time, acks []Acknowledgment, err error) { result := make([]Acknowledgment, chunk.RunLength) deltaIndex := 0 end := start + chunk.RunLength resultIndex := 0 for i := start; i != end; i++ { key := feedbackHistoryKey{ ssrc: 0, sequenceNumber: i, } if ack, ok := f.history.get(key); ok { if chunk.PacketStatusSymbol != rtcp.TypeTCCPacketNotReceived { if len(deltas)-1 < deltaIndex { return deltaIndex, refTime, result, errInvalidFeedback } refTime = refTime.Add(time.Duration(deltas[deltaIndex].Delta) * time.Microsecond) ack.Arrival = refTime deltaIndex++ } result[resultIndex] = ack } resultIndex++ } return deltaIndex, refTime, result, nil } func (f *FeedbackAdapter) unpackStatusVectorChunk(start uint16, refTime time.Time, chunk *rtcp.StatusVectorChunk, deltas []*rtcp.RecvDelta) (consumedDeltas int, nextRef time.Time, acks []Acknowledgment, err error) { result := make([]Acknowledgment, len(chunk.SymbolList)) deltaIndex := 0 resultIndex := 0 for i, symbol := range chunk.SymbolList { key := feedbackHistoryKey{ ssrc: 0, sequenceNumber: start + uint16(i), } if ack, ok := f.history.get(key); ok { if symbol != rtcp.TypeTCCPacketNotReceived { if len(deltas)-1 < deltaIndex { return deltaIndex, refTime, result, errInvalidFeedback } refTime = refTime.Add(time.Duration(deltas[deltaIndex].Delta) * time.Microsecond) ack.Arrival = refTime deltaIndex++ } result[resultIndex] = ack } resultIndex++ } return deltaIndex, refTime, result, nil } // OnTransportCCFeedback converts incoming TWCC RTCP packet feedback to // Acknowledgments. func (f *FeedbackAdapter) OnTransportCCFeedback(ts time.Time, feedback *rtcp.TransportLayerCC) ([]Acknowledgment, error) { f.lock.Lock() defer f.lock.Unlock() result := []Acknowledgment{} index := feedback.BaseSequenceNumber refTime := time.Time{}.Add(time.Duration(feedback.ReferenceTime) * 64 * time.Millisecond) recvDeltas := feedback.RecvDeltas for _, chunk := range feedback.PacketChunks { switch chunk := chunk.(type) { case *rtcp.RunLengthChunk: n, nextRefTime, acks, err := f.unpackRunLengthChunk(index, refTime, chunk, recvDeltas) if err != nil { return nil, err } refTime = nextRefTime result = append(result, acks...) recvDeltas = recvDeltas[n:] index = uint16(int(index) + len(acks)) case *rtcp.StatusVectorChunk: n, nextRefTime, acks, err := f.unpackStatusVectorChunk(index, refTime, chunk, recvDeltas) if err != nil { return nil, err } refTime = nextRefTime result = append(result, acks...) recvDeltas = recvDeltas[n:] index = uint16(int(index) + len(acks)) default: return nil, errInvalidFeedback } } return result, nil } // OnRFC8888Feedback converts incoming Congestion Control Feedback RTCP packet // to Acknowledgments. func (f *FeedbackAdapter) OnRFC8888Feedback(ts time.Time, feedback *rtcp.CCFeedbackReport) []Acknowledgment { f.lock.Lock() defer f.lock.Unlock() result := []Acknowledgment{} referenceTime := ntp.ToTime(uint64(feedback.ReportTimestamp) << 16) for _, rb := range feedback.ReportBlocks { for i, mb := range rb.MetricBlocks { sequenceNumber := rb.BeginSequence + uint16(i) key := feedbackHistoryKey{ ssrc: rb.MediaSSRC, sequenceNumber: sequenceNumber, } if ack, ok := f.history.get(key); ok { if mb.Received { delta := time.Duration((float64(mb.ArrivalTimeOffset) / 1024.0) * float64(time.Second)) ack.Arrival = referenceTime.Add(-delta) ack.ECN = mb.ECN } result = append(result, ack) } } } return result } type feedbackHistoryKey struct { ssrc uint32 sequenceNumber uint16 } type feedbackHistory struct { size int evictList *list.List items map[feedbackHistoryKey]*list.Element } func newFeedbackHistory(size int) *feedbackHistory { return &feedbackHistory{ size: size, evictList: list.New(), items: make(map[feedbackHistoryKey]*list.Element), } } func (f *feedbackHistory) get(key feedbackHistoryKey) (Acknowledgment, bool) { ent, ok := f.items[key] if ok { if ack, ok := ent.Value.(Acknowledgment); ok { return ack, true } } return Acknowledgment{}, false } func (f *feedbackHistory) add(ack Acknowledgment) { key := feedbackHistoryKey{ ssrc: ack.SSRC, sequenceNumber: ack.SequenceNumber, } // Check for existing if ent, ok := f.items[key]; ok { f.evictList.MoveToFront(ent) ent.Value = ack return } // Add new ent := f.evictList.PushFront(ack) f.items[key] = ent // Evict if necessary if f.evictList.Len() > f.size { f.removeOldest() } } func (f *feedbackHistory) removeOldest() { if ent := f.evictList.Back(); ent != nil { f.evictList.Remove(ent) if ack, ok := ent.Value.(Acknowledgment); ok { key := feedbackHistoryKey{ ssrc: ack.SSRC, sequenceNumber: ack.SequenceNumber, } delete(f.items, key) } } } interceptor-0.1.12/internal/cc/feedback_adapter_test.go000066400000000000000000000643211426573475500231740ustar00rootroot00000000000000package cc import ( "fmt" "testing" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) const hdrExtID = uint8(1) func TestUnpackRunLengthChunk(t *testing.T) { attributes := make(interceptor.Attributes) attributes.Set(TwccExtensionAttributesKey, hdrExtID) cases := []struct { sentTLCC []uint16 chunk rtcp.RunLengthChunk deltas []*rtcp.RecvDelta start uint16 // expect: acks []Acknowledgment refTime time.Time n int }{ { sentTLCC: []uint16{}, chunk: rtcp.RunLengthChunk{}, deltas: []*rtcp.RecvDelta{}, start: 0, acks: []Acknowledgment{}, refTime: time.Time{}, n: 0, }, { sentTLCC: []uint16{0, 1, 2, 3, 4, 5}, chunk: rtcp.RunLengthChunk{ PacketStatusChunk: nil, Type: 0, PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 6, }, deltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, }, start: 0, //nolint:dupl acks: []Acknowledgment{ { SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 1, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 2, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 3, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 4, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 5, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, }, n: 6, refTime: time.Time{}, }, { sentTLCC: []uint16{65534, 65535, 0, 1, 2, 3}, chunk: rtcp.RunLengthChunk{ PacketStatusChunk: nil, Type: 0, PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 6, }, deltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, }, start: 65534, acks: []Acknowledgment{ { SequenceNumber: 65534, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(250 * time.Microsecond), }, { SequenceNumber: 65535, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(500 * time.Microsecond), }, { SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(750 * time.Microsecond), }, { SequenceNumber: 1, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(1000 * time.Microsecond), }, { SequenceNumber: 2, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(1250 * time.Microsecond), }, { SequenceNumber: 3, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(1500 * time.Microsecond), }, }, n: 6, refTime: time.Time{}.Add(1500 * time.Microsecond), }, { sentTLCC: []uint16{65534, 65535, 0, 1, 2, 3}, chunk: rtcp.RunLengthChunk{ PacketStatusChunk: nil, Type: 0, PacketStatusSymbol: rtcp.TypeTCCPacketNotReceived, RunLength: 6, }, deltas: []*rtcp.RecvDelta{}, start: 65534, //nolint:dupl acks: []Acknowledgment{ { SequenceNumber: 65534, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 65535, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 1, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 2, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 3, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, }, n: 0, refTime: time.Time{}, }, } //nolint:dupl for i, tc := range cases { i := i tc := tc t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { fa := NewFeedbackAdapter() headers := []*rtp.Header{} for i, nr := range tc.sentTLCC { headers = append(headers, &getPacketWithTransportCCExt(t, nr).Header) tc.acks[i].Size += headers[i].MarshalSize() } for _, h := range headers { assert.NoError(t, fa.OnSent(time.Time{}, h, 0, attributes)) } n, refTime, acks, err := fa.unpackRunLengthChunk(tc.start, time.Time{}, &tc.chunk, tc.deltas) assert.NoError(t, err) assert.Len(t, acks, len(tc.acks)) assert.Equal(t, tc.n, n) assert.Equal(t, tc.refTime, refTime) for i, a := range acks { assert.Equal(t, tc.sentTLCC[i], a.SequenceNumber) } assert.Equal(t, tc.acks, acks) }) } } func TestUnpackStatusVectorChunk(t *testing.T) { attributes := make(interceptor.Attributes) attributes.Set(TwccExtensionAttributesKey, hdrExtID) cases := []struct { sentTLCC []uint16 chunk rtcp.StatusVectorChunk deltas []*rtcp.RecvDelta start uint16 // expect: acks []Acknowledgment n int refTime time.Time }{ { sentTLCC: []uint16{}, chunk: rtcp.StatusVectorChunk{}, deltas: []*rtcp.RecvDelta{}, start: 0, acks: []Acknowledgment{}, n: 0, refTime: time.Time{}, }, { sentTLCC: []uint16{0, 1, 2, 3, 4, 5}, chunk: rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: 0, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, }, }, deltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, }, start: 0, //nolint:dupl acks: []Acknowledgment{ { SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 1, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 2, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 3, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 4, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 5, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, }, n: 6, refTime: time.Time{}, }, { sentTLCC: []uint16{65534, 65535, 0, 1, 2, 3}, chunk: rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: 0, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedSmallDelta, }, }, deltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 250}, }, start: 65534, acks: []Acknowledgment{ { SequenceNumber: 65534, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(250 * time.Microsecond), }, { SequenceNumber: 65535, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(500 * time.Microsecond), }, { SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(750 * time.Microsecond), }, { SequenceNumber: 1, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(1000 * time.Microsecond), }, { SequenceNumber: 2, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 3, Size: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(1250 * time.Microsecond), }, }, n: 5, refTime: time.Time{}.Add(1250 * time.Microsecond), }, } //nolint:dupl for i, tc := range cases { i := i tc := tc t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { fa := NewFeedbackAdapter() headers := []*rtp.Header{} for i, nr := range tc.sentTLCC { headers = append(headers, &getPacketWithTransportCCExt(t, nr).Header) tc.acks[i].Size += headers[i].MarshalSize() } for _, h := range headers { assert.NoError(t, fa.OnSent(time.Time{}, h, 0, attributes)) } n, refTime, acks, err := fa.unpackStatusVectorChunk(tc.start, time.Time{}, &tc.chunk, tc.deltas) assert.NoError(t, err) assert.Len(t, acks, len(tc.acks)) assert.Equal(t, tc.n, n) assert.Equal(t, tc.refTime, refTime) for i, a := range acks { assert.Equal(t, tc.sentTLCC[i], a.SequenceNumber) } assert.Equal(t, tc.acks, acks) }) } } func getPacketWithTransportCCExt(t *testing.T, sequenceNumber uint16) *rtp.Packet { pkt := rtp.Packet{ Header: rtp.Header{}, Payload: []byte{}, } ext := &rtp.TransportCCExtension{ TransportSequence: sequenceNumber, } b, err := ext.Marshal() assert.NoError(t, err) assert.NoError(t, pkt.SetExtension(hdrExtID, b)) return &pkt } func TestFeedbackAdapterTWCC(t *testing.T) { t.Run("empty", func(t *testing.T) { adapter := NewFeedbackAdapter() result, err := adapter.OnTransportCCFeedback(time.Time{}, &rtcp.TransportLayerCC{}) assert.NoError(t, err) assert.Empty(t, result) }) t.Run("setsCorrectReceiveTime", func(t *testing.T) { t0 := time.Time{} adapter := NewFeedbackAdapter() headers := []rtp.Header{} for i := uint16(0); i < 22; i++ { pkt := getPacketWithTransportCCExt(t, i) headers = append(headers, pkt.Header) assert.NoError(t, adapter.OnSent(t0, &pkt.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) } results, err := adapter.OnTransportCCFeedback(t0, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 22, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.TypeTCCStatusVectorChunk, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.TypeTCCStatusVectorChunk, SymbolSize: rtcp.TypeTCCSymbolSizeOneBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.RunLengthChunk{ Type: rtcp.TypeTCCRunLengthChunk, PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 1, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: 100, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 12, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, }, }) assert.NoError(t, err) assert.NotEmpty(t, results) assert.Len(t, results, 22) assert.Contains(t, results, Acknowledgment{ SequenceNumber: 0, Size: headers[0].MarshalSize() + 1200, Departure: t0, Arrival: t0.Add(4 * time.Microsecond), }) assert.Contains(t, results, Acknowledgment{ SequenceNumber: 1, Size: headers[1].MarshalSize() + 1200, Departure: t0, Arrival: t0.Add(104 * time.Microsecond), }) for i := uint16(2); i < 7; i++ { assert.Contains(t, results, Acknowledgment{ SequenceNumber: i, Size: headers[i].MarshalSize() + 1200, Departure: t0, Arrival: time.Time{}, }) } assert.Contains(t, results, Acknowledgment{ SequenceNumber: 7, Size: headers[7].MarshalSize() + 1200, Departure: t0, Arrival: t0.Add(116 * time.Microsecond), }) for i := uint16(8); i < 21; i++ { assert.Contains(t, results, Acknowledgment{ SequenceNumber: i, Size: headers[i].MarshalSize() + 1200, Departure: t0, Arrival: time.Time{}, }) } assert.Contains(t, results, Acknowledgment{ SequenceNumber: 21, Size: headers[21].MarshalSize() + 1200, Departure: t0, Arrival: t0.Add(120 * time.Microsecond), }) }) t.Run("doesNotCrashOnTooManyFeedbackReports", func(*testing.T) { adapter := NewFeedbackAdapter() assert.NotPanics(t, func() { _, err := adapter.OnTransportCCFeedback(time.Time{}, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 0, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.TypeTCCStatusVectorChunk, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, // 4*250us=1ms }, }, }) assert.NoError(t, err) }) }) t.Run("worksOnSequenceNumberWrapAround", func(t *testing.T) { t0 := time.Time{} adapter := NewFeedbackAdapter() pkt65535 := getPacketWithTransportCCExt(t, 65535) pkt0 := getPacketWithTransportCCExt(t, 0) assert.NoError(t, adapter.OnSent(t0, &pkt65535.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) assert.NoError(t, adapter.OnSent(t0, &pkt0.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) results, err := adapter.OnTransportCCFeedback(t0, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 65535, PacketStatusCount: 2, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.TypeTCCStatusVectorChunk, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, }, }) assert.NoError(t, err) assert.NotEmpty(t, results) assert.Len(t, results, 7) assert.Contains(t, results, Acknowledgment{ SequenceNumber: 65535, Size: pkt65535.Header.MarshalSize() + 1200, Departure: t0, Arrival: t0.Add(4 * time.Microsecond), }) assert.Contains(t, results, Acknowledgment{ SequenceNumber: 0, Size: pkt0.Header.MarshalSize() + 1200, Departure: t0, Arrival: t0.Add(8 * time.Microsecond), }) }) t.Run("ignoresPossiblyInFlightPackets", func(t *testing.T) { t0 := time.Time{} adapter := NewFeedbackAdapter() headers := []rtp.Header{} for i := uint16(0); i < 8; i++ { pkt := getPacketWithTransportCCExt(t, i) headers = append(headers, pkt.Header) assert.NoError(t, adapter.OnSent(t0, &pkt.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) } results, err := adapter.OnTransportCCFeedback(t0, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 3, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.TypeTCCStatusVectorChunk, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, // 4*250us=1ms }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, // 4*250us=1ms }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, // 4*250us=1ms }, }, }) assert.NoError(t, err) assert.Len(t, results, 7) for i := uint16(0); i < 3; i++ { assert.Contains(t, results, Acknowledgment{ SequenceNumber: i, Size: headers[i].MarshalSize() + 1200, Departure: t0, Arrival: t0.Add(time.Duration((i + 1)) * 4 * time.Microsecond), }) } for i := uint16(3); i < 7; i++ { assert.Contains(t, results, Acknowledgment{ SequenceNumber: i, Size: headers[i].MarshalSize() + 1200, Departure: t0, Arrival: time.Time{}, }) } }) t.Run("runLengthChunk", func(t *testing.T) { adapter := NewFeedbackAdapter() t0 := time.Time{} for i := uint16(0); i < 20; i++ { pkt := getPacketWithTransportCCExt(t, i) assert.NoError(t, adapter.OnSent(t0, &pkt.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) } packets, err := adapter.OnTransportCCFeedback(t0, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 3, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.RunLengthChunk{ PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 3, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, }, }) assert.NoError(t, err) assert.Len(t, packets, 3) }) t.Run("statusVectorChunk", func(t *testing.T) { adapter := NewFeedbackAdapter() t0 := time.Time{} for i := uint16(0); i < 20; i++ { pkt := getPacketWithTransportCCExt(t, i) assert.NoError(t, adapter.OnSent(t0, &pkt.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) } packets, err := adapter.OnTransportCCFeedback(t0, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 3, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeOneBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, }, }) assert.NoError(t, err) assert.Len(t, packets, 14) }) t.Run("mixedRunLengthAndStatusVector", func(t *testing.T) { adapter := NewFeedbackAdapter() t0 := time.Time{} for i := uint16(0); i < 20; i++ { pkt := getPacketWithTransportCCExt(t, i) assert.NoError(t, adapter.OnSent(t0, &pkt.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) } //nolint:dupl packets, err := adapter.OnTransportCCFeedback(t0, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 10, ReferenceTime: 0, FbPktCount: 0, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.RunLengthChunk{ PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 3, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 4, }, }, }) assert.NoError(t, err) assert.Len(t, packets, 10) }) t.Run("doesNotcrashOnInvalidTWCCPacket", func(t *testing.T) { adapter := NewFeedbackAdapter() t0 := time.Time{} for i := uint16(1008); i < 1030; i++ { pkt := getPacketWithTransportCCExt(t, i) assert.NoError(t, adapter.OnSent(t0, &pkt.Header, 1200, interceptor.Attributes{TwccExtensionAttributesKey: hdrExtID})) } //nolint:dupl assert.NotPanics(t, func() { packets, err := adapter.OnTransportCCFeedback(t0, &rtcp.TransportLayerCC{ Header: rtcp.Header{}, SenderSSRC: 0, MediaSSRC: 0, BaseSequenceNumber: 1008, PacketStatusCount: 8, ReferenceTime: 278, FbPktCount: 170, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.RunLengthChunk{ PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 5632, }, }, RecvDeltas: []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 25000, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 29500, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 16750, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 23500, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0, }, }, }) assert.Error(t, err) assert.Empty(t, packets) }) }) } interceptor-0.1.12/internal/ntp/000077500000000000000000000000001426573475500165705ustar00rootroot00000000000000interceptor-0.1.12/internal/ntp/ntp.go000066400000000000000000000016261426573475500177250ustar00rootroot00000000000000// Package ntp provides conversion methods between time.Time and NTP timestamps // stored in uint64 package ntp import ( "time" ) // ToNTP converts a time.Time oboject to an uint64 NTP timestamp func ToNTP(t time.Time) uint64 { // seconds since 1st January 1900 s := (float64(t.UnixNano()) / 1000000000) + 2208988800 // higher 32 bits are the integer part, lower 32 bits are the fractional part integerPart := uint32(s) fractionalPart := uint32((s - float64(integerPart)) * 0xFFFFFFFF) return uint64(integerPart)<<32 | uint64(fractionalPart) } // ToTime converts a uint64 NTP timestamps to a time.Time object func ToTime(t uint64) time.Time { seconds := (t & 0xFFFFFFFF00000000) >> 32 fractional := float64(t&0x00000000FFFFFFFF) / float64(0xFFFFFFFF) d := time.Duration(seconds)*time.Second + time.Duration(fractional*1e9)*time.Nanosecond return time.Unix(0, 0).Add(-2208988800 * time.Second).Add(d) } interceptor-0.1.12/internal/ntp/ntp_test.go000066400000000000000000000014011426573475500207530ustar00rootroot00000000000000package ntp import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) func TestNTPTimeConverstion(t *testing.T) { for i, cc := range []struct { ts time.Time }{ { ts: time.Now(), }, { ts: time.Unix(0, 0), }, } { t.Run(fmt.Sprintf("TimeToNTP/%v", i), func(t *testing.T) { assert.InDelta(t, cc.ts.UnixNano(), ToTime(ToNTP(cc.ts)).UnixNano(), float64(time.Millisecond.Nanoseconds())) }) } } func TestTimeToNTPConverstion(t *testing.T) { for i, cc := range []struct { ts uint64 }{ { ts: 0, }, { ts: 65535, }, { ts: 16606669245815957503, }, { ts: 9487534653230284800, }, } { t.Run(fmt.Sprintf("TimeToNTP/%v", i), func(t *testing.T) { assert.Equal(t, cc.ts, ToNTP(ToTime(cc.ts))) }) } } interceptor-0.1.12/internal/sequencenumber/000077500000000000000000000000001426573475500210105ustar00rootroot00000000000000interceptor-0.1.12/internal/sequencenumber/unwrapper.go000066400000000000000000000017341426573475500233670ustar00rootroot00000000000000// Package sequencenumber provides a sequence number unwrapper package sequencenumber const ( maxSequenceNumberPlusOne = int64(65536) breakpoint = 32768 // half of max uint16 ) // Unwrapper stores an unwrapped sequence number type Unwrapper struct { init bool lastUnwrapped int64 } func isNewer(value, previous uint16) bool { if value-previous == breakpoint { return value > previous } return value != previous && (value-previous) < breakpoint } // Unwrap unwraps the next sequencenumber func (u *Unwrapper) Unwrap(i uint16) int64 { if !u.init { u.init = true u.lastUnwrapped = int64(i) return u.lastUnwrapped } lastWrapped := uint16(u.lastUnwrapped) delta := int64(i - lastWrapped) if isNewer(i, lastWrapped) { if delta < 0 { delta += maxSequenceNumberPlusOne } } else if delta > 0 && u.lastUnwrapped+delta-maxSequenceNumberPlusOne >= 0 { delta -= maxSequenceNumberPlusOne } u.lastUnwrapped += delta return u.lastUnwrapped } interceptor-0.1.12/internal/sequencenumber/unwrapper_test.go000066400000000000000000000034251426573475500244250ustar00rootroot00000000000000package sequencenumber import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestIsNewer(t *testing.T) { cases := []struct { a, b uint16 expected bool }{ { a: 1, b: 0, expected: true, }, { a: 65534, b: 65535, expected: false, }, { a: 65535, b: 65535, expected: false, }, { a: 0, b: 65535, expected: true, }, { a: 0, b: 32767, expected: false, }, { a: 32770, b: 2, expected: true, }, { a: 3, b: 32770, expected: false, }, } for i, tc := range cases { t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { assert.Equalf(t, tc.expected, isNewer(tc.a, tc.b), "expected isNewer(%v, %v) to be %v", tc.a, tc.b, tc.expected) }) } } func TestUnwrapper(t *testing.T) { cases := []struct { input []uint16 expected []int64 }{ { input: []uint16{}, expected: []int64{}, }, { input: []uint16{0, 1, 2, 3, 4}, expected: []int64{0, 1, 2, 3, 4}, }, { input: []uint16{65534, 65535, 0, 1, 2}, expected: []int64{65534, 65535, 65536, 65537, 65538}, }, { input: []uint16{32769, 0}, expected: []int64{32769, 65536}, }, { input: []uint16{32767, 0}, expected: []int64{32767, 0}, }, { input: []uint16{ 0, 32767, 32768, 32769, 32770, 1, 2, 32765, 32770, 65535, }, expected: []int64{ 0, 32767, 32768, 32769, 32770, 65537, 65538, 98301, 98306, 131071, }, }, } for i, tc := range cases { t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { u := &Unwrapper{} result := []int64{} for _, i := range tc.input { result = append(result, u.Unwrap(i)) } assert.Equal(t, tc.expected, result) }) } } interceptor-0.1.12/internal/test/000077500000000000000000000000001426573475500167465ustar00rootroot00000000000000interceptor-0.1.12/internal/test/mock_stream.go000066400000000000000000000121121426573475500215760ustar00rootroot00000000000000// Package test provides helpers for testing interceptors package test import ( "errors" "io" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" ) // MockStream is a helper struct for testing interceptors. type MockStream struct { interceptor interceptor.Interceptor rtcpReader interceptor.RTCPReader rtcpWriter interceptor.RTCPWriter rtpReader interceptor.RTPReader rtpWriter interceptor.RTPWriter rtcpIn chan []rtcp.Packet rtpIn chan *rtp.Packet rtcpOutModified chan []rtcp.Packet rtpOutModified chan *rtp.Packet rtcpInModified chan RTCPWithError rtpInModified chan RTPWithError } // RTPWithError is used to send an rtp packet or an error on a channel type RTPWithError struct { Packet *rtp.Packet Err error } // RTCPWithError is used to send a batch of rtcp packets or an error on a channel type RTCPWithError struct { Packets []rtcp.Packet Err error } // NewMockStream creates a new MockStream func NewMockStream(info *interceptor.StreamInfo, i interceptor.Interceptor) *MockStream { //nolint s := &MockStream{ interceptor: i, rtcpIn: make(chan []rtcp.Packet, 1000), rtpIn: make(chan *rtp.Packet, 1000), rtcpOutModified: make(chan []rtcp.Packet, 1000), rtpOutModified: make(chan *rtp.Packet, 1000), rtcpInModified: make(chan RTCPWithError, 1000), rtpInModified: make(chan RTPWithError, 1000), } s.rtcpWriter = i.BindRTCPWriter(interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { select { case s.rtcpOutModified <- pkts: default: } return 0, nil })) s.rtcpReader = i.BindRTCPReader(interceptor.RTCPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { pkts, ok := <-s.rtcpIn if !ok { return 0, nil, io.EOF } marshaled, err := rtcp.Marshal(pkts) if err != nil { return 0, nil, io.EOF } else if len(marshaled) > len(b) { return 0, nil, io.ErrShortBuffer } copy(b, marshaled) return len(marshaled), a, err })) s.rtpWriter = i.BindLocalStream(info, interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { select { case s.rtpOutModified <- &rtp.Packet{Header: *header, Payload: payload}: default: } return 0, nil })) s.rtpReader = i.BindRemoteStream(info, interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { p, ok := <-s.rtpIn if !ok { return 0, nil, io.EOF } marshaled, err := p.Marshal() if err != nil { return 0, nil, io.EOF } else if len(marshaled) > len(b) { return 0, nil, io.ErrShortBuffer } copy(b, marshaled) return len(marshaled), a, err })) go func() { buf := make([]byte, 1500) for { i, _, err := s.rtcpReader.Read(buf, interceptor.Attributes{}) if err != nil { if !errors.Is(err, io.EOF) { s.rtcpInModified <- RTCPWithError{Err: err} } return } pkts, err := rtcp.Unmarshal(buf[:i]) if err != nil { s.rtcpInModified <- RTCPWithError{Err: err} return } s.rtcpInModified <- RTCPWithError{Packets: pkts} } }() go func() { buf := make([]byte, 1500) for { i, _, err := s.rtpReader.Read(buf, interceptor.Attributes{}) if err != nil { if errors.Is(err, io.EOF) { s.rtpInModified <- RTPWithError{Err: err} } return } p := &rtp.Packet{} if err := p.Unmarshal(buf[:i]); err != nil { s.rtpInModified <- RTPWithError{Err: err} return } s.rtpInModified <- RTPWithError{Packet: p} } }() return s } // WriteRTCP writes a batch of rtcp packet to the stream, using the interceptor func (s *MockStream) WriteRTCP(pkts []rtcp.Packet) error { _, err := s.rtcpWriter.Write(pkts, interceptor.Attributes{}) return err } // WriteRTP writes an rtp packet to the stream, using the interceptor func (s *MockStream) WriteRTP(p *rtp.Packet) error { _, err := s.rtpWriter.Write(&p.Header, p.Payload, interceptor.Attributes{}) return err } // ReceiveRTCP schedules a new rtcp batch, so it can be read be the stream func (s *MockStream) ReceiveRTCP(pkts []rtcp.Packet) { s.rtcpIn <- pkts } // ReceiveRTP schedules a rtp packet, so it can be read be the stream func (s *MockStream) ReceiveRTP(packet *rtp.Packet) { s.rtpIn <- packet } // WrittenRTCP returns a channel containing the rtcp batches written, modified by the interceptor func (s *MockStream) WrittenRTCP() chan []rtcp.Packet { return s.rtcpOutModified } // WrittenRTP returns a channel containing rtp packets written, modified by the interceptor func (s *MockStream) WrittenRTP() chan *rtp.Packet { return s.rtpOutModified } // ReadRTCP returns a channel containing the rtcp batched read, modified by the interceptor func (s *MockStream) ReadRTCP() chan RTCPWithError { return s.rtcpInModified } // ReadRTP returns a channel containing the rtp packets read, modified by the interceptor func (s *MockStream) ReadRTP() chan RTPWithError { return s.rtpInModified } // Close closes the stream and the underlying interceptor func (s *MockStream) Close() error { close(s.rtcpIn) close(s.rtpIn) return s.interceptor.Close() } interceptor-0.1.12/internal/test/mock_stream_test.go000066400000000000000000000033411426573475500226410ustar00rootroot00000000000000package test import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestMockStream(t *testing.T) { s := NewMockStream(&interceptor.StreamInfo{}, &interceptor.NoOp{}) assert.NoError(t, s.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{}})) select { case <-s.WrittenRTCP(): case <-time.After(10 * time.Millisecond): t.Error("rtcp packet written but not found") } select { case <-s.WrittenRTCP(): t.Error("single rtcp packet written, but multiple found") case <-time.After(10 * time.Millisecond): } assert.NoError(t, s.WriteRTP(&rtp.Packet{})) select { case <-s.WrittenRTP(): case <-time.After(10 * time.Millisecond): t.Error("rtp packet written but not found") } select { case <-s.WrittenRTP(): t.Error("single rtp packet written, but multiple found") case <-time.After(10 * time.Millisecond): } s.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{}}) select { case r := <-s.ReadRTCP(): if r.Err != nil { t.Errorf("read rtcp returned error: %v", r.Err) } case <-time.After(10 * time.Millisecond): t.Error("rtcp packet received but not read") } select { case r := <-s.ReadRTCP(): t.Errorf("single rtcp packet received, but multiple read: %v", r) case <-time.After(10 * time.Millisecond): } s.ReceiveRTP(&rtp.Packet{}) select { case r := <-s.ReadRTP(): if r.Err != nil { t.Errorf("read rtcp returned error: %v", r.Err) } case <-time.After(10 * time.Millisecond): t.Error("rtp packet received but not read") } select { case r := <-s.ReadRTP(): t.Errorf("single rtp packet received, but multiple read: %v", r) case <-time.After(10 * time.Millisecond): } assert.NoError(t, s.Close()) } interceptor-0.1.12/internal/test/mock_ticker.go000066400000000000000000000006161426573475500215720ustar00rootroot00000000000000package test import ( "time" ) // MockTicker is a helper to replace time.Ticker for testing purposes. type MockTicker struct { C chan time.Time } // Stop stops the MockTicker. func (t *MockTicker) Stop() { } // Ch returns the tickers channel func (t *MockTicker) Ch() <-chan time.Time { return t.C } // Tick sends now to the channel func (t *MockTicker) Tick(now time.Time) { t.C <- now } interceptor-0.1.12/internal/test/mock_time.go000066400000000000000000000006451426573475500212510ustar00rootroot00000000000000package test import ( "sync" "time" ) // MockTime is a helper to replace time.Now() for testing purposes. type MockTime struct { m sync.RWMutex curNow time.Time } // SetNow sets the current time. func (t *MockTime) SetNow(n time.Time) { t.m.Lock() defer t.m.Unlock() t.curNow = n } // Now returns the current time. func (t *MockTime) Now() time.Time { t.m.RLock() defer t.m.RUnlock() return t.curNow } interceptor-0.1.12/noop.go000066400000000000000000000032311426573475500154540ustar00rootroot00000000000000package interceptor // NoOp is an Interceptor that does not modify any packets. It can embedded in other interceptors, so it's // possible to implement only a subset of the methods. type NoOp struct{} // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might // change in the future. The returned method will be called once per packet batch. func (i *NoOp) BindRTCPReader(reader RTCPReader) RTCPReader { return reader } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (i *NoOp) BindRTCPWriter(writer RTCPWriter) RTCPWriter { return writer } // BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method // will be called once per rtp packet. func (i *NoOp) BindLocalStream(_ *StreamInfo, writer RTPWriter) RTPWriter { return writer } // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (i *NoOp) UnbindLocalStream(_ *StreamInfo) {} // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (i *NoOp) BindRemoteStream(_ *StreamInfo, reader RTPReader) RTPReader { return reader } // UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (i *NoOp) UnbindRemoteStream(_ *StreamInfo) {} // Close closes the Interceptor, cleaning up any data if necessary. func (i *NoOp) Close() error { return nil } interceptor-0.1.12/pkg/000077500000000000000000000000001426573475500147345ustar00rootroot00000000000000interceptor-0.1.12/pkg/cc/000077500000000000000000000000001426573475500153215ustar00rootroot00000000000000interceptor-0.1.12/pkg/cc/interceptor.go000066400000000000000000000075111426573475500202120ustar00rootroot00000000000000// Package cc implements an interceptor for bandwidth estimation that can be // used with different BandwidthEstimators. package cc import ( "github.com/pion/interceptor" "github.com/pion/interceptor/pkg/gcc" "github.com/pion/rtcp" ) // Option can be used to set initial options on CC interceptors type Option func(*Interceptor) error // BandwidthEstimatorFactory creates new BandwidthEstimators type BandwidthEstimatorFactory func() (BandwidthEstimator, error) // BandwidthEstimator is the interface that will be returned by a // NewPeerConnectionCallback and can be used to query current bandwidth // metrics and add feedback manually. type BandwidthEstimator interface { AddStream(*interceptor.StreamInfo, interceptor.RTPWriter) interceptor.RTPWriter WriteRTCP([]rtcp.Packet, interceptor.Attributes) error GetTargetBitrate() int OnTargetBitrateChange(f func(bitrate int)) GetStats() map[string]interface{} Close() error } // NewPeerConnectionCallback returns the BandwidthEstimator for the // PeerConnection with id type NewPeerConnectionCallback func(id string, estimator BandwidthEstimator) // InterceptorFactory is a factory for CC interceptors type InterceptorFactory struct { opts []Option bweFactory func() (BandwidthEstimator, error) addPeerConnection NewPeerConnectionCallback } // NewInterceptor returns a new CC interceptor factory func NewInterceptor(factory BandwidthEstimatorFactory, opts ...Option) (*InterceptorFactory, error) { if factory == nil { factory = func() (BandwidthEstimator, error) { return gcc.NewSendSideBWE() } } return &InterceptorFactory{ opts: opts, bweFactory: factory, addPeerConnection: nil, }, nil } // OnNewPeerConnection sets a callback that is called when a new CC interceptor // is created. func (f *InterceptorFactory) OnNewPeerConnection(cb NewPeerConnectionCallback) { f.addPeerConnection = cb } // NewInterceptor returns a new CC interceptor func (f *InterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { bwe, err := f.bweFactory() if err != nil { return nil, err } i := &Interceptor{ NoOp: interceptor.NoOp{}, estimator: bwe, feedback: make(chan []rtcp.Packet), close: make(chan struct{}), } for _, opt := range f.opts { if err := opt(i); err != nil { return nil, err } } if f.addPeerConnection != nil { f.addPeerConnection(id, i.estimator) } return i, nil } // Interceptor implements Google Congestion Control type Interceptor struct { interceptor.NoOp estimator BandwidthEstimator feedback chan []rtcp.Packet close chan struct{} } // BindRTCPReader lets you modify any incoming RTCP packets. It is called once // per sender/receiver, however this might change in the future. The returned // method will be called once per packet batch. func (c *Interceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { return interceptor.RTCPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(b, a) if err != nil { return 0, nil, err } buf := make([]byte, i) copy(buf, b[:i]) if attr == nil { attr = make(interceptor.Attributes) } pkts, err := attr.GetRTCPPackets(buf[:i]) if err != nil { return 0, nil, err } if err = c.estimator.WriteRTCP(pkts, attr); err != nil { return 0, nil, err } return i, attr, nil }) } // BindLocalStream lets you modify any outgoing RTP packets. It is called once // for per LocalStream. The returned method will be called once per rtp packet. func (c *Interceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { return c.estimator.AddStream(info, writer) } // Close closes the interceptor and the associated bandwidth estimator. func (c *Interceptor) Close() error { return c.estimator.Close() } interceptor-0.1.12/pkg/gcc/000077500000000000000000000000001426573475500154705ustar00rootroot00000000000000interceptor-0.1.12/pkg/gcc/adaptive_threshold.go000066400000000000000000000060601426573475500216720ustar00rootroot00000000000000package gcc import ( "math" "time" ) const ( maxDeltas = 60 ) type adaptiveThresholdOption func(*adaptiveThreshold) func setInitialThreshold(t time.Duration) adaptiveThresholdOption { return func(at *adaptiveThreshold) { at.thresh = t } } // adaptiveThreshold implements a threshold that continuously adapts depending on // the current measurements/estimates. This is necessary to avoid starving GCC // in the presence of concurrent TCP flows by allowing larger Queueing delays, // when measurements/estimates increase. overuseCoefficientU and // overuseCoefficientD define by how much the threshold adapts. We basically // want the threshold to increase fast, if the measurement is outside [-thresh, // thresh] and decrease slowly if it is within. // // See https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc-02#section-5.4 // or [Analysis and Design of the Google Congestion Control for Web Real-time // Communication (WebRTC)](https://c3lab.poliba.it/images/6/65/Gcc-analysis.pdf) // for a more detailed description type adaptiveThreshold struct { thresh time.Duration overuseCoefficientUp float64 overuseCoefficientDown float64 min time.Duration max time.Duration lastUpdate time.Time numDeltas int } // newAdaptiveThreshold initializes a new adaptiveThreshold with default // values taken from draft-ietf-rmcat-gcc-02 func newAdaptiveThreshold(opts ...adaptiveThresholdOption) *adaptiveThreshold { at := &adaptiveThreshold{ thresh: time.Duration(12500 * float64(time.Microsecond)), overuseCoefficientUp: 0.01, overuseCoefficientDown: 0.00018, min: 6 * time.Millisecond, max: 600 * time.Millisecond, lastUpdate: time.Time{}, numDeltas: 0, } for _, opt := range opts { opt(at) } return at } func (a *adaptiveThreshold) compare(estimate, dt time.Duration) (usage, time.Duration, time.Duration) { a.numDeltas++ if a.numDeltas < 2 { return usageNormal, estimate, a.max } t := time.Duration(minInt(a.numDeltas, maxDeltas)) * estimate use := usageNormal if t > a.thresh { use = usageOver } else if t < -a.thresh { use = usageUnder } thresh := a.thresh a.update(t) return use, t, thresh } func (a *adaptiveThreshold) update(estimate time.Duration) { now := time.Now() if a.lastUpdate.IsZero() { a.lastUpdate = now } absEstimate := time.Duration(math.Abs(float64(estimate.Microseconds()))) * time.Microsecond if absEstimate > a.thresh+15*time.Millisecond { a.lastUpdate = now return } k := a.overuseCoefficientUp if absEstimate < a.thresh { k = a.overuseCoefficientDown } maxTimeDelta := 100 * time.Millisecond timeDelta := time.Duration(minInt(int(now.Sub(a.lastUpdate).Milliseconds()), int(maxTimeDelta.Milliseconds()))) * time.Millisecond d := absEstimate - a.thresh add := k * float64(d.Milliseconds()) * float64(timeDelta.Milliseconds()) a.thresh += time.Duration(add) * 1000 * time.Microsecond a.thresh = clampDuration(a.thresh, a.min, a.max) a.lastUpdate = now } interceptor-0.1.12/pkg/gcc/adaptive_threshold_test.go000066400000000000000000000053711426573475500227350ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestAdaptiveThreshold(t *testing.T) { type input struct { estimate, delta time.Duration } cases := []struct { name string in []input expected []usage options []adaptiveThresholdOption }{ { name: "empty", in: []input{}, expected: []usage{}, options: []adaptiveThresholdOption{}, }, { name: "firstInputIsAlwaysNormal", in: []input{{ estimate: 1 * time.Second, delta: 0, }}, expected: []usage{usageNormal}, options: []adaptiveThresholdOption{}, }, { name: "singleOver", in: []input{ { estimate: 0, delta: 0, }, { estimate: 20 * time.Millisecond, delta: 0, }, }, expected: []usage{usageNormal, usageOver}, options: []adaptiveThresholdOption{ setInitialThreshold(10 * time.Millisecond), }, }, { name: "singleNormal", in: []input{ { estimate: 0, delta: 0, }, { estimate: 5 * time.Millisecond, delta: 0, }, }, expected: []usage{usageNormal, usageNormal}, options: []adaptiveThresholdOption{ setInitialThreshold(10 * time.Millisecond), }, }, { name: "singleUnder", in: []input{ { estimate: 0, delta: 0, }, { estimate: -20 * time.Millisecond, delta: 0, }, }, expected: []usage{usageNormal, usageUnder}, options: []adaptiveThresholdOption{ setInitialThreshold(10 * time.Millisecond), }, }, { name: "increaseThresholdOnOveruse", in: []input{ { estimate: 0, delta: 0, }, { estimate: 25 * time.Millisecond, delta: 30 * time.Millisecond, }, { estimate: 13 * time.Millisecond, delta: 30 * time.Millisecond, }, }, expected: []usage{usageNormal, usageOver, usageNormal}, options: []adaptiveThresholdOption{ setInitialThreshold(40 * time.Millisecond), }, }, { name: "overuseAfterOveruse", in: []input{ { estimate: 0, delta: 0, }, { estimate: 20 * time.Millisecond, delta: 30 * time.Millisecond, }, { estimate: 30 * time.Millisecond, delta: 30 * time.Millisecond, }, }, expected: []usage{usageNormal, usageOver, usageOver}, options: []adaptiveThresholdOption{ setInitialThreshold(10 * time.Millisecond), }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { threshold := newAdaptiveThreshold(tc.options...) usages := []usage{} for _, in := range tc.in { use, _, _ := threshold.compare(in.estimate, in.delta) usages = append(usages, use) } assert.Equal(t, tc.expected, usages, "%v != %v", tc.expected, usages) }) } } interceptor-0.1.12/pkg/gcc/arrival_group.go000066400000000000000000000011471426573475500206760ustar00rootroot00000000000000package gcc import ( "fmt" "time" "github.com/pion/interceptor/internal/cc" ) type arrivalGroup struct { packets []cc.Acknowledgment departure time.Time arrival time.Time } func (g *arrivalGroup) add(a cc.Acknowledgment) { g.packets = append(g.packets, a) g.arrival = a.Arrival g.departure = a.Departure } func (g arrivalGroup) String() string { s := "ARRIVALGROUP:\n" s += fmt.Sprintf("\tARRIVAL:\t%v\n", int64(float64(g.arrival.UnixNano())/1e+6)) s += fmt.Sprintf("\tDEPARTURE:\t%v\n", int64(float64(g.departure.UnixNano())/1e+6)) s += fmt.Sprintf("\tPACKETS:\n%v\n", g.packets) return s } interceptor-0.1.12/pkg/gcc/arrival_group_accumulator.go000066400000000000000000000033511426573475500232740ustar00rootroot00000000000000package gcc import ( "time" "github.com/pion/interceptor/internal/cc" ) type arrivalGroupAccumulator struct { interDepartureThreshold time.Duration interArrivalThreshold time.Duration interGroupDelayVariationTreshold time.Duration } func newArrivalGroupAccumulator() *arrivalGroupAccumulator { return &arrivalGroupAccumulator{ interDepartureThreshold: 5 * time.Millisecond, interArrivalThreshold: 5 * time.Millisecond, interGroupDelayVariationTreshold: 0, } } func (a *arrivalGroupAccumulator) run(in <-chan []cc.Acknowledgment, agWriter func(arrivalGroup)) { init := false group := arrivalGroup{} for acks := range in { for _, next := range acks { if !init { group.add(next) init = true continue } if next.Arrival.Before(group.arrival) { // ignore out of order arrivals continue } if next.Departure.After(group.departure) { if interDepartureTimePkt(group, next) <= a.interDepartureThreshold { group.add(next) continue } if interArrivalTimePkt(group, next) <= a.interArrivalThreshold && interGroupDelayVariationPkt(group, next) < a.interGroupDelayVariationTreshold { group.add(next) continue } agWriter(group) group = arrivalGroup{} group.add(next) } } } } func interArrivalTimePkt(a arrivalGroup, b cc.Acknowledgment) time.Duration { return b.Arrival.Sub(a.arrival) } func interDepartureTimePkt(a arrivalGroup, b cc.Acknowledgment) time.Duration { if len(a.packets) == 0 { return 0 } return b.Departure.Sub(a.packets[len(a.packets)-1].Departure) } func interGroupDelayVariationPkt(a arrivalGroup, b cc.Acknowledgment) time.Duration { return b.Arrival.Sub(a.arrival) - b.Departure.Sub(a.departure) } interceptor-0.1.12/pkg/gcc/arrival_group_accumulator_test.go000066400000000000000000000103671426573475500243400ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/pion/interceptor/internal/cc" "github.com/stretchr/testify/assert" ) func TestArrivalGroupAccumulator(t *testing.T) { triggerNewGroupElement := cc.Acknowledgment{ Departure: time.Time{}.Add(time.Second), Arrival: time.Time{}.Add(time.Second), } cases := []struct { name string log []cc.Acknowledgment exp []arrivalGroup }{ { name: "emptyCreatesNoGroups", log: []cc.Acknowledgment{}, exp: []arrivalGroup{}, }, { name: "createsSingleElementGroup", log: []cc.Acknowledgment{ { Departure: time.Time{}, Arrival: time.Time{}.Add(time.Millisecond), }, triggerNewGroupElement, }, exp: []arrivalGroup{ { packets: []cc.Acknowledgment{{ Departure: time.Time{}, Arrival: time.Time{}.Add(time.Millisecond), }}, arrival: time.Time{}.Add(time.Millisecond), departure: time.Time{}, }, }, }, { name: "createsTwoElementGroup", log: []cc.Acknowledgment{ { Arrival: time.Time{}.Add(15 * time.Millisecond), }, { Departure: time.Time{}.Add(3 * time.Millisecond), Arrival: time.Time{}.Add(20 * time.Millisecond), }, triggerNewGroupElement, }, exp: []arrivalGroup{{ packets: []cc.Acknowledgment{ { Departure: time.Time{}, Arrival: time.Time{}.Add(15 * time.Millisecond), }, { Departure: time.Time{}.Add(3 * time.Millisecond), Arrival: time.Time{}.Add(20 * time.Millisecond), }, }, arrival: time.Time{}.Add(20 * time.Millisecond), departure: time.Time{}.Add(3 * time.Millisecond), }}, }, { name: "createsTwoArrivalGroups", log: []cc.Acknowledgment{ { Departure: time.Time{}, Arrival: time.Time{}.Add(15 * time.Millisecond), }, { Departure: time.Time{}.Add(3 * time.Millisecond), Arrival: time.Time{}.Add(20 * time.Millisecond), }, { Departure: time.Time{}.Add(9 * time.Millisecond), Arrival: time.Time{}.Add(30 * time.Millisecond), }, triggerNewGroupElement, }, exp: []arrivalGroup{ { packets: []cc.Acknowledgment{ { Arrival: time.Time{}.Add(15 * time.Millisecond), }, { Departure: time.Time{}.Add(3 * time.Millisecond), Arrival: time.Time{}.Add(20 * time.Millisecond), }, }, arrival: time.Time{}.Add(20 * time.Millisecond), departure: time.Time{}.Add(3 * time.Millisecond), }, { packets: []cc.Acknowledgment{ { Departure: time.Time{}.Add(9 * time.Millisecond), Arrival: time.Time{}.Add(30 * time.Millisecond), }, }, arrival: time.Time{}.Add(30 * time.Millisecond), departure: time.Time{}.Add(9 * time.Millisecond), }, }, }, { name: "ignoresOutOfOrderPackets", log: []cc.Acknowledgment{ { Departure: time.Time{}, Arrival: time.Time{}.Add(15 * time.Millisecond), }, { Departure: time.Time{}.Add(6 * time.Millisecond), Arrival: time.Time{}.Add(34 * time.Millisecond), }, { Departure: time.Time{}.Add(8 * time.Millisecond), Arrival: time.Time{}.Add(30 * time.Millisecond), }, triggerNewGroupElement, }, exp: []arrivalGroup{ { packets: []cc.Acknowledgment{ { Departure: time.Time{}, Arrival: time.Time{}.Add(15 * time.Millisecond), }, }, arrival: time.Time{}.Add(15 * time.Millisecond), departure: time.Time{}, }, { packets: []cc.Acknowledgment{ { Departure: time.Time{}.Add(6 * time.Millisecond), Arrival: time.Time{}.Add(34 * time.Millisecond), }, }, arrival: time.Time{}.Add(34 * time.Millisecond), departure: time.Time{}.Add(6 * time.Millisecond), }, }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { aga := newArrivalGroupAccumulator() in := make(chan []cc.Acknowledgment) out := make(chan arrivalGroup) go func() { defer close(out) aga.run(in, func(ag arrivalGroup) { out <- ag }) }() go func() { in <- tc.log close(in) }() received := []arrivalGroup{} for g := range out { received = append(received, g) } assert.Equal(t, tc.exp, received) }) } } interceptor-0.1.12/pkg/gcc/arrival_group_test.go000066400000000000000000000035511426573475500217360ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/pion/interceptor/internal/cc" "github.com/stretchr/testify/assert" ) func TestArrivalGroup(t *testing.T) { cases := []struct { name string acks []cc.Acknowledgment expected arrivalGroup }{ { name: "createsEmptyArrivalGroup", acks: []cc.Acknowledgment{}, expected: arrivalGroup{ packets: nil, arrival: time.Time{}, departure: time.Time{}, }, }, { name: "createsArrivalGroupContainingSingleACK", acks: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }}, expected: arrivalGroup{ packets: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }}, arrival: time.Time{}, departure: time.Time{}, }, }, { name: "setsTimesToLastACK", acks: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 0, Size: 0, Departure: time.Time{}.Add(time.Second), Arrival: time.Time{}.Add(time.Second), }}, expected: arrivalGroup{ packets: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }, { SequenceNumber: 0, Size: 0, Departure: time.Time{}.Add(time.Second), Arrival: time.Time{}.Add(time.Second), }}, arrival: time.Time{}.Add(time.Second), departure: time.Time{}.Add(time.Second), }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { ag := arrivalGroup{} for _, ack := range tc.acks { ag.add(ack) } assert.Equal(t, tc.expected, ag) }) } } interceptor-0.1.12/pkg/gcc/delay_based_bwe.go000066400000000000000000000047461426573475500211230ustar00rootroot00000000000000package gcc import ( "sync" "time" "github.com/pion/interceptor/internal/cc" "github.com/pion/logging" ) // DelayStats contains some internal statistics of the delay based congestion // controller type DelayStats struct { Measurement time.Duration Estimate time.Duration Threshold time.Duration LastReceiveDelta time.Duration Usage usage State state TargetBitrate int } type now func() time.Time type delayController struct { ackPipe chan<- []cc.Acknowledgment ackRatePipe chan<- []cc.Acknowledgment *arrivalGroupAccumulator *rateController onUpdateCallback func(DelayStats) wg sync.WaitGroup log logging.LeveledLogger } type delayControllerConfig struct { nowFn now initialBitrate int minBitrate int maxBitrate int } func newDelayController(c delayControllerConfig) *delayController { ackPipe := make(chan []cc.Acknowledgment) ackRatePipe := make(chan []cc.Acknowledgment) delayController := &delayController{ ackPipe: ackPipe, ackRatePipe: ackRatePipe, arrivalGroupAccumulator: nil, rateController: nil, onUpdateCallback: nil, wg: sync.WaitGroup{}, log: logging.NewDefaultLoggerFactory().NewLogger("gcc_delay_controller"), } rateController := newRateController(c.nowFn, c.initialBitrate, c.minBitrate, c.maxBitrate, func(ds DelayStats) { delayController.log.Infof("delaystats: %v", ds) if delayController.onUpdateCallback != nil { delayController.onUpdateCallback(ds) } }) delayController.rateController = rateController overuseDetector := newOveruseDetector(newAdaptiveThreshold(), 10*time.Millisecond, rateController.onDelayStats) slopeEstimator := newSlopeEstimator(newKalman(), overuseDetector.onDelayStats) arrivalGroupAccumulator := newArrivalGroupAccumulator() rc := newRateCalculator(500 * time.Millisecond) delayController.wg.Add(2) go func() { defer delayController.wg.Done() arrivalGroupAccumulator.run(ackPipe, slopeEstimator.onArrivalGroup) }() go func() { defer delayController.wg.Done() rc.run(ackRatePipe, rateController.onReceivedRate) }() return delayController } func (d *delayController) onUpdate(f func(DelayStats)) { d.onUpdateCallback = f } func (d *delayController) updateDelayEstimate(acks []cc.Acknowledgment) { d.ackPipe <- acks d.ackRatePipe <- acks } func (d *delayController) Close() error { defer d.wg.Wait() close(d.ackPipe) close(d.ackRatePipe) return nil } interceptor-0.1.12/pkg/gcc/gcc.go000066400000000000000000000006671426573475500165640ustar00rootroot00000000000000// Package gcc implements Google Congestion Control for bandwidth estimation package gcc import "time" func minInt(a, b int) int { if a < b { return a } return b } func maxInt(a, b int) int { if a > b { return a } return b } func clampInt(b, min, max int) int { return maxInt(min, minInt(max, b)) } func clampDuration(d, min, max time.Duration) time.Duration { return time.Duration(clampInt(int(d), int(min), int(max))) } interceptor-0.1.12/pkg/gcc/gcc_test.go000066400000000000000000000035121426573475500176130ustar00rootroot00000000000000package gcc import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) func TestMinInt(t *testing.T) { tests := []struct { expected int a, b int }{ { expected: 0, a: 0, b: 100, }, { expected: 10, a: 10, b: 10, }, { expected: 1, a: 10, b: 1, }, } for i, tt := range tests { tt := tt t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { assert.Equal(t, tt.expected, minInt(tt.a, tt.b)) }) } } func TestMaxInt(t *testing.T) { tests := []struct { expected int a, b int }{ { expected: 100, a: 0, b: 100, }, { expected: 10, a: 10, b: 10, }, { expected: 10, a: 10, b: 1, }, } for i, tt := range tests { tt := tt t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { assert.Equal(t, tt.expected, maxInt(tt.a, tt.b)) }) } } func TestClamp(t *testing.T) { tests := []struct { expected int x int min int max int }{ { expected: 50, x: 50, min: 0, max: 100, }, { expected: 50, x: 50, min: 50, max: 100, }, { expected: 100, x: 100, min: 0, max: 100, }, { expected: 50, x: 3, min: 50, max: 100, }, { expected: 100, x: 150, min: 0, max: 100, }, } for i, tt := range tests { tt := tt t.Run(fmt.Sprintf("int/%v", i), func(t *testing.T) { assert.Equal(t, tt.expected, clampInt(tt.x, tt.min, tt.max)) }) t.Run(fmt.Sprintf("duration/%v", i), func(t *testing.T) { x := time.Duration(tt.x) min := time.Duration(tt.min) max := time.Duration(tt.max) expected := time.Duration(tt.expected) assert.Equal(t, expected, clampDuration(x, min, max)) }) } } interceptor-0.1.12/pkg/gcc/kalman.go000066400000000000000000000042121426573475500172610ustar00rootroot00000000000000package gcc import ( "math" "time" ) const ( chi = 0.001 ) type kalmanOption func(*kalman) type kalman struct { gain float64 estimate time.Duration processUncertainty float64 // Q_i estimateError float64 measurementUncertainty float64 disableMeasurementUncertaintyUpdates bool } func initEstimate(e time.Duration) kalmanOption { return func(k *kalman) { k.estimate = e } } func initProcessUncertainty(p float64) kalmanOption { return func(k *kalman) { k.processUncertainty = p } } func initEstimateError(e float64) kalmanOption { return func(k *kalman) { k.estimateError = e * e // Only need variance from now on } } func initMeasurementUncertainty(u float64) kalmanOption { return func(k *kalman) { k.measurementUncertainty = u } } func setDisableMeasurementUncertaintyUpdates(b bool) kalmanOption { return func(k *kalman) { k.disableMeasurementUncertaintyUpdates = b } } func newKalman(opts ...kalmanOption) *kalman { k := &kalman{ gain: 0, estimate: 0, processUncertainty: 1e-3, estimateError: 0.1, measurementUncertainty: 0, disableMeasurementUncertaintyUpdates: false, } for _, opt := range opts { opt(k) } return k } func (k *kalman) updateEstimate(measurement time.Duration) time.Duration { z := measurement - k.estimate zms := float64(z.Microseconds()) / 1000.0 if !k.disableMeasurementUncertaintyUpdates { alpha := math.Pow((1 - chi), 30.0/(1000.0*5*float64(time.Millisecond))) root := math.Sqrt(k.measurementUncertainty) root3 := 3 * root if zms > root3 { k.measurementUncertainty = math.Max(alpha*k.measurementUncertainty+(1-alpha)*root3*root3, 1) } k.measurementUncertainty = math.Max(alpha*k.measurementUncertainty+(1-alpha)*zms*zms, 1) } estimateUncertainty := k.estimateError + k.processUncertainty k.gain = estimateUncertainty / (estimateUncertainty + k.measurementUncertainty) k.estimate += time.Duration(k.gain * zms * float64(time.Millisecond)) k.estimateError = (1 - k.gain) * estimateUncertainty return k.estimate } interceptor-0.1.12/pkg/gcc/kalman_test.go000066400000000000000000000042311426573475500203210ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestKalman(t *testing.T) { cases := []struct { name string opts []kalmanOption measurements []time.Duration expected []time.Duration }{ { name: "empty", opts: []kalmanOption{}, measurements: []time.Duration{}, expected: []time.Duration{}, }, { name: "kalmanfilter.netExample", opts: []kalmanOption{ initEstimate(10 * time.Millisecond), initEstimateError(100), initProcessUncertainty(0.15), initMeasurementUncertainty(0.01), }, measurements: []time.Duration{ time.Duration(50.45 * float64(time.Millisecond)), time.Duration(50.967 * float64(time.Millisecond)), time.Duration(51.6 * float64(time.Millisecond)), time.Duration(52.106 * float64(time.Millisecond)), time.Duration(52.492 * float64(time.Millisecond)), time.Duration(52.819 * float64(time.Millisecond)), time.Duration(53.433 * float64(time.Millisecond)), time.Duration(54.007 * float64(time.Millisecond)), time.Duration(54.523 * float64(time.Millisecond)), time.Duration(54.99 * float64(time.Millisecond)), }, expected: []time.Duration{ time.Duration(50.449959 * float64(time.Millisecond)), time.Duration(50.936547 * float64(time.Millisecond)), time.Duration(51.560411 * float64(time.Millisecond)), time.Duration(52.07324 * float64(time.Millisecond)), time.Duration(52.466566 * float64(time.Millisecond)), time.Duration(52.797787 * float64(time.Millisecond)), time.Duration(53.395303 * float64(time.Millisecond)), time.Duration(53.970236 * float64(time.Millisecond)), time.Duration(54.489652 * float64(time.Millisecond)), time.Duration(54.960137 * float64(time.Millisecond)), }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { k := newKalman(append(tc.opts, setDisableMeasurementUncertaintyUpdates(true))...) estimates := []time.Duration{} for _, m := range tc.measurements { estimates = append(estimates, k.updateEstimate(m)) } assert.Equal(t, tc.expected, estimates, "%v != %v", tc.expected, estimates) }) } } interceptor-0.1.12/pkg/gcc/leaky_bucket_pacer.go000066400000000000000000000076041426573475500216420ustar00rootroot00000000000000package gcc import ( "container/list" "errors" "sync" "time" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtp" ) var errLeakyBucketPacerPoolCastFailed = errors.New("failed to access leaky bucket pacer pool, cast failed") type item struct { header *rtp.Header payload *[]byte size int attributes interceptor.Attributes } // LeakyBucketPacer implements a leaky bucket pacing algorithm type LeakyBucketPacer struct { log logging.LeveledLogger f float64 targetBitrate int targetBitrateLock sync.Mutex pacingInterval time.Duration qLock sync.RWMutex queue *list.List done chan struct{} ssrcToWriter map[uint32]interceptor.RTPWriter writerLock sync.RWMutex pool *sync.Pool } // NewLeakyBucketPacer initializes a new LeakyBucketPacer func NewLeakyBucketPacer(initialBitrate int) *LeakyBucketPacer { p := &LeakyBucketPacer{ log: logging.NewDefaultLoggerFactory().NewLogger("pacer"), f: 1.5, targetBitrate: initialBitrate, pacingInterval: 5 * time.Millisecond, qLock: sync.RWMutex{}, queue: list.New(), done: make(chan struct{}), ssrcToWriter: map[uint32]interceptor.RTPWriter{}, pool: &sync.Pool{}, } p.pool = &sync.Pool{ New: func() interface{} { b := make([]byte, 1460) return &b }, } go p.Run() return p } // AddStream adds a new stream and its corresponding writer to the pacer func (p *LeakyBucketPacer) AddStream(ssrc uint32, writer interceptor.RTPWriter) { p.writerLock.Lock() defer p.writerLock.Unlock() p.ssrcToWriter[ssrc] = writer } // SetTargetBitrate updates the target bitrate at which the pacer is allowed to // send packets. The pacer may exceed this limit by p.f func (p *LeakyBucketPacer) SetTargetBitrate(rate int) { p.targetBitrateLock.Lock() defer p.targetBitrateLock.Unlock() p.targetBitrate = int(p.f * float64(rate)) } func (p *LeakyBucketPacer) getTargetBitrate() int { p.targetBitrateLock.Lock() defer p.targetBitrateLock.Unlock() return p.targetBitrate } // Write sends a packet with header and payload the a previously registered // stream. func (p *LeakyBucketPacer) Write(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { buf, ok := p.pool.Get().(*[]byte) if !ok { return 0, errLeakyBucketPacerPoolCastFailed } copy(*buf, payload) hdr := header.Clone() p.qLock.Lock() p.queue.PushBack(&item{ header: &hdr, payload: buf, size: len(payload), attributes: attributes, }) p.qLock.Unlock() return header.MarshalSize() + len(payload), nil } // Run starts the LeakyBucketPacer func (p *LeakyBucketPacer) Run() { ticker := time.NewTicker(p.pacingInterval) defer ticker.Stop() lastSent := time.Now() for { select { case <-p.done: return case now := <-ticker.C: budget := int(float64(now.Sub(lastSent).Milliseconds()) * float64(p.getTargetBitrate()) / 8000.0) p.qLock.Lock() for p.queue.Len() != 0 && budget > 0 { p.log.Infof("budget=%v, len(queue)=%v, targetBitrate=%v", budget, p.queue.Len(), p.getTargetBitrate()) next, ok := p.queue.Remove(p.queue.Front()).(*item) p.qLock.Unlock() if !ok { p.log.Warnf("failed to access leaky bucket pacer queue, cast failed") continue } p.writerLock.RLock() writer, ok := p.ssrcToWriter[next.header.SSRC] p.writerLock.RUnlock() if !ok { p.log.Warnf("no writer found for ssrc: %v", next.header.SSRC) p.pool.Put(next.payload) p.qLock.Lock() continue } n, err := writer.Write(next.header, (*next.payload)[:next.size], next.attributes) if err != nil { p.log.Errorf("failed to write packet: %v", err) } lastSent = now budget -= n p.pool.Put(next.payload) p.qLock.Lock() } p.qLock.Unlock() } } } // Close closes the LeakyBucketPacer func (p *LeakyBucketPacer) Close() error { close(p.done) return nil } interceptor-0.1.12/pkg/gcc/loss_based_bwe.go000066400000000000000000000060111426573475500207700ustar00rootroot00000000000000package gcc import ( "math" "sync" "time" "github.com/pion/interceptor/internal/cc" "github.com/pion/logging" ) const ( // constants from // https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc-02#section-6 increaseLossThreshold = 0.02 increaseTimeThreshold = 200 * time.Millisecond increaseFactor = 1.05 decreaseLossThreshold = 0.1 decreaseTimeThreshold = 200 * time.Millisecond ) // LossStats contains internal statistics of the loss based controller type LossStats struct { TargetBitrate int AverageLoss float64 } type lossBasedBandwidthEstimator struct { lock sync.Mutex maxBitrate int minBitrate int bitrate int averageLoss float64 lastLossUpdate time.Time lastIncrease time.Time lastDecrease time.Time log logging.LeveledLogger } func newLossBasedBWE(initialBitrate int) *lossBasedBandwidthEstimator { return &lossBasedBandwidthEstimator{ lock: sync.Mutex{}, maxBitrate: 100_000_000, // 100 mbit minBitrate: 100_000, // 100 kbit bitrate: initialBitrate, averageLoss: 0, lastLossUpdate: time.Time{}, lastIncrease: time.Time{}, lastDecrease: time.Time{}, log: logging.NewDefaultLoggerFactory().NewLogger("gcc_loss_controller"), } } func (e *lossBasedBandwidthEstimator) getEstimate(wantedRate int) LossStats { e.lock.Lock() defer e.lock.Unlock() if e.bitrate <= 0 { e.bitrate = clampInt(wantedRate, e.minBitrate, e.maxBitrate) } e.bitrate = minInt(wantedRate, e.bitrate) return LossStats{ TargetBitrate: e.bitrate, AverageLoss: e.averageLoss, } } func (e *lossBasedBandwidthEstimator) updateLossEstimate(results []cc.Acknowledgment) { if len(results) == 0 { return } packetsLost := 0 for _, p := range results { if p.Arrival.IsZero() { packetsLost++ } } e.lock.Lock() defer e.lock.Unlock() lossRatio := float64(packetsLost) / float64(len(results)) e.averageLoss = e.average(time.Since(e.lastLossUpdate), e.averageLoss, lossRatio) e.lastLossUpdate = time.Now() increaseLoss := math.Max(e.averageLoss, lossRatio) decreaseLoss := math.Min(e.averageLoss, lossRatio) if increaseLoss < increaseLossThreshold && time.Since(e.lastIncrease) > increaseTimeThreshold { e.log.Infof("loss controller increasing; averageLoss: %v, decreaseLoss: %v, increaseLoss: %v", e.averageLoss, decreaseLoss, increaseLoss) e.lastIncrease = time.Now() e.bitrate = clampInt(int(increaseFactor*float64(e.bitrate)), e.minBitrate, e.maxBitrate) } else if decreaseLoss > decreaseLossThreshold && time.Since(e.lastDecrease) > decreaseTimeThreshold { e.log.Infof("loss controller decreasing; averageLoss: %v, decreaseLoss: %v, increaseLoss: %v", e.averageLoss, decreaseLoss, increaseLoss) e.lastDecrease = time.Now() e.bitrate = clampInt(int(float64(e.bitrate)*(1-0.5*decreaseLoss)), e.minBitrate, e.maxBitrate) } } func (e *lossBasedBandwidthEstimator) average(delta time.Duration, prev, sample float64) float64 { return sample + math.Exp(-float64(delta.Milliseconds())/200.0)*(prev-sample) } interceptor-0.1.12/pkg/gcc/noop_pacer.go000066400000000000000000000026431426573475500201510ustar00rootroot00000000000000package gcc import ( "errors" "fmt" "sync" "github.com/pion/interceptor" "github.com/pion/rtp" ) // ErrUnknownStream is returned when trying to send a packet with a SSRC that // was never registered with any stream var ErrUnknownStream = errors.New("unknown ssrc") // NoOpPacer implements a pacer that always immediately sends incoming packets type NoOpPacer struct { lock sync.Mutex ssrcToWriter map[uint32]interceptor.RTPWriter } // NewNoOpPacer initializes a new NoOpPacer func NewNoOpPacer() *NoOpPacer { return &NoOpPacer{ lock: sync.Mutex{}, ssrcToWriter: map[uint32]interceptor.RTPWriter{}, } } // SetTargetBitrate sets the bitrate at which the pacer sends data. NoOp for // NoOp pacer. func (p *NoOpPacer) SetTargetBitrate(int) { } // AddStream adds a stream and corresponding writer to the p func (p *NoOpPacer) AddStream(ssrc uint32, writer interceptor.RTPWriter) { p.lock.Lock() defer p.lock.Unlock() p.ssrcToWriter[ssrc] = writer } // Write sends a packet with header and payload to a previously added stream func (p *NoOpPacer) Write(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { p.lock.Lock() defer p.lock.Unlock() if w, ok := p.ssrcToWriter[header.SSRC]; ok { return w.Write(header, payload, attributes) } return 0, fmt.Errorf("%w: %v", ErrUnknownStream, header.SSRC) } // Close closes p func (p *NoOpPacer) Close() error { return nil } interceptor-0.1.12/pkg/gcc/overuse_detector.go000066400000000000000000000033671426573475500214110ustar00rootroot00000000000000package gcc import ( "time" ) type threshold interface { compare(estimate time.Duration, delta time.Duration) (usage, time.Duration, time.Duration) } type overuseDetector struct { threshold threshold overuseTime time.Duration dsWriter func(DelayStats) lastEstimate time.Duration lastUpdate time.Time increasingDuration time.Duration increasingCounter int } func newOveruseDetector(thresh threshold, overuseTime time.Duration, dsw func(DelayStats)) *overuseDetector { return &overuseDetector{ threshold: thresh, overuseTime: overuseTime, dsWriter: dsw, lastEstimate: 0, lastUpdate: time.Now(), increasingDuration: 0, increasingCounter: 0, } } func (d *overuseDetector) onDelayStats(ds DelayStats) { now := time.Now() delta := now.Sub(d.lastUpdate) d.lastUpdate = now thresholdUse, estimate, currentThreshold := d.threshold.compare(ds.Estimate, ds.LastReceiveDelta) use := usageNormal if thresholdUse == usageOver { if d.increasingDuration == 0 { d.increasingDuration = delta / 2 } else { d.increasingDuration += delta } d.increasingCounter++ if d.increasingDuration > d.overuseTime && d.increasingCounter > 1 { if estimate > d.lastEstimate { use = usageOver } } } if thresholdUse == usageUnder { d.increasingCounter = 0 d.increasingDuration = 0 use = usageUnder } if thresholdUse == usageNormal { d.increasingDuration = 0 d.increasingCounter = 0 use = usageNormal } d.lastEstimate = estimate d.dsWriter(DelayStats{ Measurement: ds.Measurement, Estimate: estimate, Threshold: currentThreshold, LastReceiveDelta: ds.LastReceiveDelta, Usage: use, State: 0, TargetBitrate: 0, }) } interceptor-0.1.12/pkg/gcc/overuse_detector_test.go000066400000000000000000000051351426573475500224430ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/stretchr/testify/assert" ) type staticThreshold time.Duration func (t staticThreshold) compare(estimate, _ time.Duration) (usage, time.Duration, time.Duration) { if estimate > time.Duration(t) { return usageOver, estimate, time.Duration(t) } if estimate < -time.Duration(t) { return usageUnder, estimate, time.Duration(t) } return usageNormal, estimate, time.Duration(t) } func TestOveruseDetectorWithoutDelay(t *testing.T) { cases := []struct { name string estimates []DelayStats expected []usage thresh threshold delay time.Duration }{ { name: "noEstimateNoUsage", estimates: []DelayStats{}, expected: []usage{}, thresh: staticThreshold(time.Millisecond), delay: 0, }, { name: "overuse", estimates: []DelayStats{ {}, {Estimate: 2 * time.Millisecond}, {Estimate: 3 * time.Millisecond}, }, expected: []usage{usageNormal, usageNormal, usageOver}, thresh: staticThreshold(time.Millisecond), delay: 13 * time.Millisecond, }, { name: "normaluse", estimates: []DelayStats{{Estimate: 0}}, expected: []usage{usageNormal}, thresh: staticThreshold(time.Millisecond), delay: 0, }, { name: "underuse", estimates: []DelayStats{{Estimate: -2 * time.Millisecond}}, expected: []usage{usageUnder}, thresh: staticThreshold(time.Millisecond), delay: 0, }, { name: "noOverUseBeforeDelay", estimates: []DelayStats{ {}, {Estimate: 3 * time.Millisecond}, {Estimate: 5 * time.Millisecond}, }, expected: []usage{usageNormal, usageNormal, usageOver}, thresh: staticThreshold(1 * time.Millisecond), delay: 10 * time.Millisecond, }, { name: "noOverUseIfEstimateDecreased", estimates: []DelayStats{ {}, {Estimate: 4 * time.Millisecond}, {Estimate: 5 * time.Millisecond}, {Estimate: 3 * time.Millisecond}, }, expected: []usage{usageNormal, usageNormal, usageOver, usageNormal}, thresh: staticThreshold(1 * time.Millisecond), delay: 0, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { out := make(chan DelayStats) dsw := func(ds DelayStats) { out <- ds } od := newOveruseDetector(tc.thresh, tc.delay, dsw) go func() { defer close(out) for _, e := range tc.estimates { od.onDelayStats(e) time.Sleep(tc.delay) } }() received := []usage{} for s := range out { received = append(received, s.Usage) } assert.Equal(t, tc.expected, received, "%v != %v", tc.expected, received) }) } } interceptor-0.1.12/pkg/gcc/rate_calculator.go000066400000000000000000000023221426573475500211620ustar00rootroot00000000000000package gcc import ( "time" "github.com/pion/interceptor/internal/cc" ) type rateCalculator struct { window time.Duration } func newRateCalculator(window time.Duration) *rateCalculator { return &rateCalculator{ window: window, } } func (c *rateCalculator) run(in <-chan []cc.Acknowledgment, onRateUpdate func(int)) { var history []cc.Acknowledgment init := false sum := 0 for acks := range in { for _, next := range acks { if next.Arrival.IsZero() { // Ignore packet if it didn't arrive continue } history = append(history, next) sum += next.Size if !init { init = true // Don't know any timeframe here, only arrival of last packet, // which is by definition in the window that ends with the last // arrival time onRateUpdate(next.Size * 8) continue } del := 0 for _, ack := range history { deadline := next.Arrival.Add(-c.window) if !ack.Arrival.Before(deadline) { break } del++ sum -= ack.Size } history = history[del:] if len(history) == 0 { onRateUpdate(0) continue } dt := next.Arrival.Sub(history[0].Arrival) bits := 8 * sum rate := int(float64(bits) / dt.Seconds()) onRateUpdate(rate) } } } interceptor-0.1.12/pkg/gcc/rate_calculator_test.go000066400000000000000000000043011426573475500222200ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/pion/interceptor/internal/cc" "github.com/stretchr/testify/assert" ) func TestRateCalculator(t *testing.T) { t0 := time.Now() cases := []struct { name string acks []cc.Acknowledgment expected []int }{ { name: "emptyCreatesNoRate", acks: []cc.Acknowledgment{}, expected: []int{}, }, { name: "ignoresZeroArrivalTimes", acks: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 0, Departure: time.Time{}, Arrival: time.Time{}, }}, expected: []int{}, }, { name: "singleAckCreatesRate", acks: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 1000, Departure: time.Time{}, Arrival: t0, }}, expected: []int{8000}, }, { name: "twoAcksCalculateCorrectRates", acks: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 125, Departure: time.Time{}, Arrival: t0, }, { SequenceNumber: 0, Size: 125, Departure: time.Time{}, Arrival: t0.Add(100 * time.Millisecond), }}, expected: []int{1000, 20_000}, }, { name: "steadyACKsCalculateCorrectRates", acks: getACKStream(10, 1200, 100*time.Millisecond), expected: []int{ 9_600, 192_000, 144_000, 128_000, 120_000, 115_200, 115_200, 115_200, 115_200, 115_200, }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { rc := newRateCalculator(500 * time.Millisecond) in := make(chan []cc.Acknowledgment) out := make(chan int) onRateUpdate := func(rate int) { out <- rate } go func() { defer close(out) rc.run(in, onRateUpdate) }() go func() { in <- tc.acks close(in) }() received := []int{} for r := range out { received = append(received, r) } assert.Equal(t, tc.expected, received) }) } } func getACKStream(length int, size int, interval time.Duration) []cc.Acknowledgment { res := []cc.Acknowledgment{} t0 := time.Now() for i := 0; i < length; i++ { res = append(res, cc.Acknowledgment{ Size: size, Arrival: t0, }) t0 = t0.Add(interval) } return res } interceptor-0.1.12/pkg/gcc/rate_controller.go000066400000000000000000000106451426573475500212230ustar00rootroot00000000000000package gcc import ( "math" "sync" "time" ) const ( decreaseEMAAlpha = 0.95 beta = 0.85 ) type rateController struct { now now initialTargetBitrate int minBitrate int maxBitrate int dsWriter func(DelayStats) lock sync.Mutex init bool delayStats DelayStats target int lastUpdate time.Time lastState state latestRTT time.Duration latestReceivedRate int latestDecreaseRate *exponentialMovingAverage } type exponentialMovingAverage struct { average float64 variance float64 stdDeviation float64 } func (a *exponentialMovingAverage) update(value float64) { if a.average == 0.0 { a.average = value } else { x := value - a.average a.average += decreaseEMAAlpha * x a.variance = (1 - decreaseEMAAlpha) * (a.variance + decreaseEMAAlpha*x*x) a.stdDeviation = math.Sqrt(a.variance) } } func newRateController(now now, initialTargetBitrate, minBitrate, maxBitrate int, dsw func(DelayStats)) *rateController { return &rateController{ now: now, initialTargetBitrate: initialTargetBitrate, minBitrate: minBitrate, maxBitrate: maxBitrate, dsWriter: dsw, init: false, delayStats: DelayStats{}, target: initialTargetBitrate, lastUpdate: time.Time{}, lastState: stateIncrease, latestRTT: 0, latestReceivedRate: 0, latestDecreaseRate: &exponentialMovingAverage{}, } } func (c *rateController) onReceivedRate(rate int) { c.lock.Lock() defer c.lock.Unlock() c.latestReceivedRate = rate } func (c *rateController) updateRTT(rtt time.Duration) { c.lock.Lock() defer c.lock.Unlock() c.latestRTT = rtt } func (c *rateController) onDelayStats(ds DelayStats) { now := time.Now() if !c.init { c.delayStats = ds c.delayStats.State = stateIncrease c.init = true return } c.delayStats = ds c.delayStats.State = c.delayStats.State.transition(ds.Usage) if c.delayStats.State == stateHold { return } var next DelayStats c.lock.Lock() switch c.delayStats.State { case stateHold: // should never occur due to check above, but makes the linter happy case stateIncrease: c.target = clampInt(c.increase(now), c.minBitrate, c.maxBitrate) next = DelayStats{ Measurement: c.delayStats.Measurement, Estimate: c.delayStats.Estimate, Threshold: c.delayStats.Threshold, LastReceiveDelta: c.delayStats.LastReceiveDelta, Usage: c.delayStats.Usage, State: c.delayStats.State, TargetBitrate: c.target, } case stateDecrease: c.target = clampInt(c.decrease(), c.minBitrate, c.maxBitrate) next = DelayStats{ Measurement: c.delayStats.Measurement, Estimate: c.delayStats.Estimate, Threshold: c.delayStats.Threshold, LastReceiveDelta: c.delayStats.LastReceiveDelta, Usage: c.delayStats.Usage, State: c.delayStats.State, TargetBitrate: c.target, } } c.lock.Unlock() c.dsWriter(next) } func (c *rateController) increase(now time.Time) int { if c.latestDecreaseRate.average > 0 && float64(c.latestReceivedRate) > c.latestDecreaseRate.average-3*c.latestDecreaseRate.stdDeviation && float64(c.latestReceivedRate) < c.latestDecreaseRate.average+3*c.latestDecreaseRate.stdDeviation { bitsPerFrame := float64(c.target) / 30.0 packetsPerFrame := math.Ceil(bitsPerFrame / (1200 * 8)) expectedPacketSizeBits := bitsPerFrame / packetsPerFrame responseTime := 100*time.Millisecond + c.latestRTT alpha := 0.5 * math.Min(float64(now.Sub(c.lastUpdate).Milliseconds())/float64(responseTime.Milliseconds()), 1.0) increase := int(math.Max(1000.0, alpha*expectedPacketSizeBits)) c.lastUpdate = now return int(math.Min(float64(c.target+increase), 1.5*float64(c.latestReceivedRate))) } eta := math.Pow(1.08, math.Min(float64(now.Sub(c.lastUpdate).Milliseconds())/1000, 1.0)) c.lastUpdate = now rate := int(eta * float64(c.target)) // maximum increase to 1.5 * received rate received := int(1.5 * float64(c.latestReceivedRate)) if rate > received && received > c.target { return received } if rate < c.target { return c.target } return rate } func (c *rateController) decrease() int { target := int(beta * float64(c.latestReceivedRate)) c.latestDecreaseRate.update(float64(c.latestReceivedRate)) c.lastUpdate = c.now() return target } interceptor-0.1.12/pkg/gcc/rate_controller_test.go000066400000000000000000000031021426573475500222500ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestRateControllerRun(t *testing.T) { cases := []struct { name string initialBitrate int usage []usage expected []DelayStats }{ { name: "empty", initialBitrate: 100_000, usage: []usage{}, expected: []DelayStats{}, }, { name: "increasesMultiplicativelyBy8000", initialBitrate: 100_000, usage: []usage{usageNormal, usageNormal}, expected: []DelayStats{{ Usage: usageNormal, State: stateIncrease, TargetBitrate: 108_000, Estimate: 0, Threshold: 0, }}, }, } t0 := time.Time{} mockNoFn := func() time.Time { t0 = t0.Add(100 * time.Millisecond) return t0 } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { out := make(chan DelayStats) dc := newRateController(mockNoFn, 100_000, 1_000, 50_000_000, func(ds DelayStats) { out <- ds }) in := make(chan DelayStats) dc.onReceivedRate(100_000) dc.updateRTT(300 * time.Millisecond) go func() { defer close(out) for _, state := range tc.usage { dc.onDelayStats(DelayStats{ Measurement: 0, Estimate: 0, Threshold: 0, Usage: state, State: 0, TargetBitrate: 0, }) } close(in) }() received := []DelayStats{} for ds := range out { received = append(received, ds) } if len(tc.expected) > 0 { assert.Equal(t, tc.expected[0], received[0]) } }) } } interceptor-0.1.12/pkg/gcc/send_side_bwe.go000066400000000000000000000165571426573475500206270ustar00rootroot00000000000000package gcc import ( "errors" "math" "sync" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/cc" "github.com/pion/interceptor/internal/ntp" "github.com/pion/rtcp" "github.com/pion/rtp" ) const ( transportCCURI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" latestBitrate = 10_000 minBitrate = 5_000 maxBitrate = 50_000_000 ) // ErrSendSideBWEClosed is raised when SendSideBWE.WriteRTCP is called after SendSideBWE.Close var ErrSendSideBWEClosed = errors.New("SendSideBwe closed") // Pacer is the interface implemented by packet pacers type Pacer interface { interceptor.RTPWriter AddStream(ssrc uint32, writer interceptor.RTPWriter) SetTargetBitrate(int) Close() error } // Stats contains internal statistics of the bandwidth estimator type Stats struct { LossStats DelayStats } // SendSideBWE implements a combination of loss and delay based GCC type SendSideBWE struct { pacer Pacer lossController *lossBasedBandwidthEstimator delayController *delayController feedbackAdapter *cc.FeedbackAdapter onTargetBitrateChange func(bitrate int) lock sync.Mutex latestStats Stats latestBitrate int minBitrate int maxBitrate int close chan struct{} closeLock sync.RWMutex } // Option configures a bandwidth estimator type Option func(*SendSideBWE) error // SendSideBWEInitialBitrate sets the initial bitrate of new GCC interceptors func SendSideBWEInitialBitrate(rate int) Option { return func(e *SendSideBWE) error { e.latestBitrate = rate return nil } } // SendSideBWEMaxBitrate sets the initial bitrate of new GCC interceptors func SendSideBWEMaxBitrate(rate int) Option { return func(e *SendSideBWE) error { e.maxBitrate = rate return nil } } // SendSideBWEMinBitrate sets the initial bitrate of new GCC interceptors func SendSideBWEMinBitrate(rate int) Option { return func(e *SendSideBWE) error { e.minBitrate = rate return nil } } // SendSideBWEPacer sets the pacing algorithm to use. func SendSideBWEPacer(p Pacer) Option { return func(e *SendSideBWE) error { e.pacer = p return nil } } // NewSendSideBWE creates a new sender side bandwidth estimator func NewSendSideBWE(opts ...Option) (*SendSideBWE, error) { e := &SendSideBWE{ pacer: nil, lossController: nil, delayController: nil, feedbackAdapter: cc.NewFeedbackAdapter(), onTargetBitrateChange: nil, lock: sync.Mutex{}, latestStats: Stats{}, latestBitrate: latestBitrate, minBitrate: minBitrate, maxBitrate: maxBitrate, close: make(chan struct{}), } for _, opt := range opts { if err := opt(e); err != nil { return nil, err } } if e.pacer == nil { e.pacer = NewLeakyBucketPacer(e.latestBitrate) } e.lossController = newLossBasedBWE(e.latestBitrate) e.delayController = newDelayController(delayControllerConfig{ nowFn: time.Now, initialBitrate: e.latestBitrate, minBitrate: e.minBitrate, maxBitrate: e.maxBitrate, }) e.delayController.onUpdate(e.onDelayUpdate) return e, nil } // AddStream adds a new stream to the bandwidth estimator func (e *SendSideBWE) AddStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { var hdrExtID uint8 for _, e := range info.RTPHeaderExtensions { if e.URI == transportCCURI { hdrExtID = uint8(e.ID) break } } e.pacer.AddStream(info.SSRC, interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { if hdrExtID != 0 { if attributes == nil { attributes = make(interceptor.Attributes) } attributes.Set(cc.TwccExtensionAttributesKey, hdrExtID) } if err := e.feedbackAdapter.OnSent(time.Now(), header, len(payload), attributes); err != nil { return 0, err } return writer.Write(header, payload, attributes) })) return e.pacer } // WriteRTCP adds some RTCP feedback to the bandwidth estimator func (e *SendSideBWE) WriteRTCP(pkts []rtcp.Packet, attributes interceptor.Attributes) error { now := time.Now() e.closeLock.RLock() defer e.closeLock.RUnlock() if e.isClosed() { return ErrSendSideBWEClosed } for _, pkt := range pkts { var acks []cc.Acknowledgment var err error var feedbackSentTime time.Time switch fb := pkt.(type) { case *rtcp.TransportLayerCC: acks, err = e.feedbackAdapter.OnTransportCCFeedback(now, fb) if err != nil { return err } for i, ack := range acks { if i == 0 { feedbackSentTime = ack.Arrival continue } if ack.Arrival.After(feedbackSentTime) { feedbackSentTime = ack.Arrival } } case *rtcp.CCFeedbackReport: acks = e.feedbackAdapter.OnRFC8888Feedback(now, fb) feedbackSentTime = ntp.ToTime(uint64(fb.ReportTimestamp) << 16) default: continue } feedbackMinRTT := time.Duration(math.MaxInt) for _, ack := range acks { if ack.Arrival.IsZero() { continue } pendingTime := feedbackSentTime.Sub(ack.Arrival) rtt := now.Sub(ack.Departure) - pendingTime feedbackMinRTT = time.Duration(minInt(int(rtt), int(feedbackMinRTT))) } if feedbackMinRTT < math.MaxInt { e.delayController.updateRTT(feedbackMinRTT) } e.lossController.updateLossEstimate(acks) e.delayController.updateDelayEstimate(acks) } return nil } // GetTargetBitrate returns the current target bitrate in bits per second func (e *SendSideBWE) GetTargetBitrate() int { e.lock.Lock() defer e.lock.Unlock() return e.latestBitrate } // GetStats returns some internal statistics of the bandwidth estimator func (e *SendSideBWE) GetStats() map[string]interface{} { e.lock.Lock() defer e.lock.Unlock() return map[string]interface{}{ "lossTargetBitrate": e.latestStats.LossStats.TargetBitrate, "averageLoss": e.latestStats.AverageLoss, "delayTargetBitrate": e.latestStats.DelayStats.TargetBitrate, "delayMeasurement": float64(e.latestStats.Measurement.Microseconds()) / 1000.0, "delayEstimate": float64(e.latestStats.Estimate.Microseconds()) / 1000.0, "delayThreshold": float64(e.latestStats.Threshold.Microseconds()) / 1000.0, "usage": e.latestStats.Usage.String(), "state": e.latestStats.State.String(), } } // OnTargetBitrateChange sets the callback that is called when the target // bitrate in bits per second changes func (e *SendSideBWE) OnTargetBitrateChange(f func(bitrate int)) { e.onTargetBitrateChange = f } // isClosed returns true if SendSideBWE is closed func (e *SendSideBWE) isClosed() bool { select { case <-e.close: return true default: return false } } // Close stops and closes the bandwidth estimator func (e *SendSideBWE) Close() error { e.closeLock.Lock() defer e.closeLock.Unlock() if err := e.delayController.Close(); err != nil { return err } close(e.close) return e.pacer.Close() } func (e *SendSideBWE) onDelayUpdate(delayStats DelayStats) { e.lock.Lock() defer e.lock.Unlock() lossStats := e.lossController.getEstimate(delayStats.TargetBitrate) bitrateChanged := false bitrate := minInt(delayStats.TargetBitrate, lossStats.TargetBitrate) if bitrate != e.latestBitrate { bitrateChanged = true e.latestBitrate = bitrate e.pacer.SetTargetBitrate(e.latestBitrate) } if bitrateChanged && e.onTargetBitrateChange != nil { go e.onTargetBitrateChange(bitrate) } e.latestStats = Stats{ LossStats: lossStats, DelayStats: delayStats, } } interceptor-0.1.12/pkg/gcc/send_side_bwe_test.go000066400000000000000000000075631426573475500216630ustar00rootroot00000000000000package gcc import ( "fmt" "math/rand" "testing" "github.com/pion/interceptor" "github.com/pion/interceptor/pkg/twcc" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/require" ) // mockTWCCResponder is a RTPWriter that writes // TWCC feedback to a embedded SendSideBWE instantly type mockTWCCResponder struct { bwe *SendSideBWE rtpChan chan []byte } func (m *mockTWCCResponder) Read(out []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { pkt := <-m.rtpChan copy(out, pkt) return len(pkt), nil, nil } func (m *mockTWCCResponder) Write(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { return 0, m.bwe.WriteRTCP(pkts, attributes) } // mockGCCWriteStream receives RTP packets that have been paced by // the congestion controller type mockGCCWriteStream struct { twccResponder *mockTWCCResponder } func (m *mockGCCWriteStream) Write(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { pkt, err := (&rtp.Packet{Header: *header, Payload: payload}).Marshal() if err != nil { panic(err) } m.twccResponder.rtpChan <- pkt return 0, err } func TestSendSideBWE(t *testing.T) { buffer := make([]byte, 1500) rtpPayload := make([]byte, 1460) streamInfo := &interceptor.StreamInfo{ SSRC: 1, RTPHeaderExtensions: []interceptor.RTPHeaderExtension{{URI: transportCCURI, ID: 1}}, } bwe, err := NewSendSideBWE() require.NoError(t, err) require.NotNil(t, bwe) m := &mockGCCWriteStream{ &mockTWCCResponder{ bwe, make(chan []byte, 500), }, } twccSender, err := (&twcc.SenderInterceptorFactory{}).NewInterceptor("") require.NoError(t, err) require.NotNil(t, twccSender) twccInboundRTP := twccSender.BindRemoteStream(streamInfo, m.twccResponder) twccSender.BindRTCPWriter(m.twccResponder) require.Equal(t, latestBitrate, bwe.GetTargetBitrate()) require.NotEqual(t, 0, len(bwe.GetStats())) rtpWriter := bwe.AddStream(streamInfo, m) require.NotNil(t, rtpWriter) twccWriter := twcc.HeaderExtensionInterceptor{} rtpWriter = twccWriter.BindLocalStream(streamInfo, rtpWriter) for i := 0; i <= 100; i++ { if _, err = rtpWriter.Write(&rtp.Header{SSRC: 1, Extensions: []rtp.Extension{}}, rtpPayload, nil); err != nil { panic(err) } if _, _, err = twccInboundRTP.Read(buffer, nil); err != nil { panic(err) } } // Sending a stream with zero loss and no RTT should increase estimate require.Less(t, latestBitrate, bwe.GetTargetBitrate()) } func TestSendSideBWE_ErrorOnWriteRTCPAtClosedState(t *testing.T) { bwe, err := NewSendSideBWE() require.NoError(t, err) require.NotNil(t, bwe) pkts := []rtcp.Packet{&rtcp.TransportLayerCC{}} require.NoError(t, bwe.WriteRTCP(pkts, nil)) require.Equal(t, bwe.isClosed(), false) require.NoError(t, bwe.Close()) require.ErrorIs(t, bwe.WriteRTCP(pkts, nil), ErrSendSideBWEClosed) require.Equal(t, bwe.isClosed(), true) } func BenchmarkSendSideBWE_WriteRTCP(b *testing.B) { numSequencesPerTwccReport := []int{10, 100, 500, 1000} for _, count := range numSequencesPerTwccReport { b.Run(fmt.Sprintf("num_sequences=%d", count), func(b *testing.B) { bwe, err := NewSendSideBWE(SendSideBWEPacer(NewNoOpPacer())) require.NoError(b, err) require.NotNil(b, bwe) r := twcc.NewRecorder(5000) seq := uint16(0) arrivalTime := int64(0) for i := 0; i < b.N; i++ { // nolint:gosec seqs := rand.Intn(count/2) + count // [count, count * 1.5) for j := 0; j < seqs; j++ { seq++ if rand.Intn(5) == 0 { //nolint:gosec,staticcheck // skip this packet } arrivalTime += int64(rtcp.TypeTCCDeltaScaleFactor * (rand.Intn(128) + 1)) //nolint:gosec r.Record(5000, seq, arrivalTime) } rtcpPackets := r.BuildFeedbackPacket() require.Equal(b, 1, len(rtcpPackets)) require.NoError(b, bwe.WriteRTCP(rtcpPackets, nil)) } require.NoError(b, bwe.Close()) }) } } interceptor-0.1.12/pkg/gcc/slope_estimator.go000066400000000000000000000021731426573475500212330ustar00rootroot00000000000000package gcc import ( "time" ) type estimator interface { updateEstimate(measurement time.Duration) time.Duration } type estimatorFunc func(time.Duration) time.Duration func (f estimatorFunc) updateEstimate(d time.Duration) time.Duration { return f(d) } type slopeEstimator struct { estimator init bool group arrivalGroup delayStatsWriter func(DelayStats) } func newSlopeEstimator(e estimator, dsw func(DelayStats)) *slopeEstimator { return &slopeEstimator{ estimator: e, delayStatsWriter: dsw, } } func (e *slopeEstimator) onArrivalGroup(ag arrivalGroup) { if !e.init { e.group = ag e.init = true return } measurement := interGroupDelayVariation(e.group, ag) delta := ag.arrival.Sub(e.group.arrival) e.group = ag e.delayStatsWriter(DelayStats{ Measurement: measurement, Estimate: e.updateEstimate(measurement), Threshold: 0, LastReceiveDelta: delta, Usage: 0, State: 0, TargetBitrate: 0, }) } func interGroupDelayVariation(a, b arrivalGroup) time.Duration { return b.arrival.Sub(a.arrival) - b.departure.Sub(a.departure) } interceptor-0.1.12/pkg/gcc/slope_estimator_test.go000066400000000000000000000045441426573475500222760ustar00rootroot00000000000000package gcc import ( "testing" "time" "github.com/stretchr/testify/assert" ) func identity(d time.Duration) time.Duration { return d } func TestSlopeEstimator(t *testing.T) { cases := []struct { name string ags []arrivalGroup expected []DelayStats }{ { name: "emptyReturnsEmpty", ags: []arrivalGroup{}, expected: []DelayStats{}, }, { name: "simpleDeltaTest", ags: []arrivalGroup{ { arrival: time.Time{}.Add(5 * time.Millisecond), departure: time.Time{}.Add(15 * time.Millisecond), }, { arrival: time.Time{}.Add(10 * time.Millisecond), departure: time.Time{}.Add(20 * time.Millisecond), }, }, expected: []DelayStats{ { Measurement: 0, Estimate: 0, Threshold: 0, LastReceiveDelta: 5 * time.Millisecond, Usage: 0, State: 0, TargetBitrate: 0, }, }, }, { name: "twoMeasurements", ags: []arrivalGroup{ { arrival: time.Time{}.Add(5 * time.Millisecond), departure: time.Time{}.Add(15 * time.Millisecond), }, { arrival: time.Time{}.Add(10 * time.Millisecond), departure: time.Time{}.Add(20 * time.Millisecond), }, { arrival: time.Time{}.Add(15 * time.Millisecond), departure: time.Time{}.Add(30 * time.Millisecond), }, }, expected: []DelayStats{ { Measurement: 0, Estimate: 0, Threshold: 0, LastReceiveDelta: 5 * time.Millisecond, Usage: 0, State: 0, TargetBitrate: 0, }, { Measurement: -5 * time.Millisecond, Estimate: -5 * time.Millisecond, Threshold: 0, LastReceiveDelta: 5 * time.Millisecond, Usage: 0, State: 0, TargetBitrate: 0, }, }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { out := make(chan DelayStats) se := newSlopeEstimator(estimatorFunc(identity), func(ds DelayStats) { out <- ds }) input := []time.Duration{} go func() { defer close(out) for _, ag := range tc.ags { se.onArrivalGroup(ag) } }() received := []DelayStats{} for d := range out { received = append(received, d) } assert.Equal(t, tc.expected, received, "%v != %v", input, received) }) } } interceptor-0.1.12/pkg/gcc/state.go000066400000000000000000000015761426573475500171500ustar00rootroot00000000000000package gcc import "fmt" type state int const ( stateIncrease state = iota stateDecrease stateHold ) func (s state) transition(u usage) state { switch s { case stateHold: switch u { case usageOver: return stateDecrease case usageNormal: return stateIncrease case usageUnder: return stateHold } case stateIncrease: switch u { case usageOver: return stateDecrease case usageNormal: return stateIncrease case usageUnder: return stateHold } case stateDecrease: switch u { case usageOver: return stateDecrease case usageNormal: return stateHold case usageUnder: return stateHold } } return stateIncrease } func (s state) String() string { switch s { case stateIncrease: return "increase" case stateDecrease: return "decrease" case stateHold: return "hold" default: return fmt.Sprintf("invalid state: %d", s) } } interceptor-0.1.12/pkg/gcc/usage.go000066400000000000000000000004771426573475500171330ustar00rootroot00000000000000package gcc import "fmt" type usage int const ( usageOver usage = iota usageUnder usageNormal ) func (u usage) String() string { switch u { case usageOver: return "overuse" case usageUnder: return "underuse" case usageNormal: return "normal" default: return fmt.Sprintf("invalid usage: %d", u) } } interceptor-0.1.12/pkg/mock/000077500000000000000000000000001426573475500156655ustar00rootroot00000000000000interceptor-0.1.12/pkg/mock/factory.go000066400000000000000000000005231426573475500176630ustar00rootroot00000000000000package mock import "github.com/pion/interceptor" // Factory is a mock Factory for testing. type Factory struct { NewInterceptorFn func(id string) (interceptor.Interceptor, error) } // NewInterceptor implements Interceptor func (f *Factory) NewInterceptor(id string) (interceptor.Interceptor, error) { return f.NewInterceptorFn(id) } interceptor-0.1.12/pkg/mock/interceptor.go000066400000000000000000000066631426573475500205650ustar00rootroot00000000000000// Package mock provides mock Interceptor for testing. package mock import ( "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" ) // Interceptor is an mock Interceptor fot testing. type Interceptor struct { BindRTCPReaderFn func(reader interceptor.RTCPReader) interceptor.RTCPReader BindRTCPWriterFn func(writer interceptor.RTCPWriter) interceptor.RTCPWriter BindLocalStreamFn func(i *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter UnbindLocalStreamFn func(i *interceptor.StreamInfo) BindRemoteStreamFn func(i *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader UnbindRemoteStreamFn func(i *interceptor.StreamInfo) CloseFn func() error } // BindRTCPReader implements Interceptor. func (i *Interceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { if i.BindRTCPReaderFn != nil { return i.BindRTCPReaderFn(reader) } return reader } // BindRTCPWriter implements Interceptor. func (i *Interceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { if i.BindRTCPWriterFn != nil { return i.BindRTCPWriterFn(writer) } return writer } // BindLocalStream implements Interceptor. func (i *Interceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { if i.BindLocalStreamFn != nil { return i.BindLocalStreamFn(info, writer) } return writer } // UnbindLocalStream implements Interceptor. func (i *Interceptor) UnbindLocalStream(info *interceptor.StreamInfo) { if i.UnbindLocalStreamFn != nil { i.UnbindLocalStreamFn(info) } } // BindRemoteStream implements Interceptor. func (i *Interceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { if i.BindRemoteStreamFn != nil { return i.BindRemoteStreamFn(info, reader) } return reader } // UnbindRemoteStream implements Interceptor. func (i *Interceptor) UnbindRemoteStream(info *interceptor.StreamInfo) { if i.UnbindRemoteStreamFn != nil { i.UnbindRemoteStreamFn(info) } } // Close implements Interceptor. func (i *Interceptor) Close() error { if i.CloseFn != nil { return i.CloseFn() } return nil } // RTPWriter is a mock RTPWriter. type RTPWriter struct { WriteFn func(*rtp.Header, []byte, interceptor.Attributes) (int, error) } // Write implements RTPWriter. func (w *RTPWriter) Write(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { return w.WriteFn(header, payload, attributes) } // RTPReader is a mock RTPReader. type RTPReader struct { ReadFn func([]byte, interceptor.Attributes) (int, interceptor.Attributes, error) } // Read implements RTPReader. func (r *RTPReader) Read(b []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { return r.ReadFn(b, attributes) } // RTCPWriter is a mock RTCPWriter. type RTCPWriter struct { WriteFn func([]rtcp.Packet, interceptor.Attributes) (int, error) } // Write implements RTCPWriter. func (w *RTCPWriter) Write(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { return w.WriteFn(pkts, attributes) } // RTCPReader is a mock RTCPReader. type RTCPReader struct { ReadFn func([]byte, interceptor.Attributes) (int, interceptor.Attributes, error) } // Read implements RTCPReader. func (r *RTCPReader) Read(b []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { return r.ReadFn(b, attributes) } interceptor-0.1.12/pkg/mock/interceptor_test.go000066400000000000000000000100211426573475500216030ustar00rootroot00000000000000package mock import ( "sync/atomic" "testing" "github.com/pion/interceptor" ) func TestInterceptor(t *testing.T) { dummyRTPWriter := &RTPWriter{} dummyRTPReader := &RTPReader{} dummyRTCPWriter := &RTCPWriter{} dummyRTCPReader := &RTCPReader{} dummyStreamInfo := &interceptor.StreamInfo{} t.Run("Default", func(t *testing.T) { i := &Interceptor{} if i.BindRTCPWriter(dummyRTCPWriter) != dummyRTCPWriter { t.Error("Default BindRTCPWriter should return given writer") } if i.BindRTCPReader(dummyRTCPReader) != dummyRTCPReader { t.Error("Default BindRTCPReader should return given reader") } if i.BindLocalStream(dummyStreamInfo, dummyRTPWriter) != dummyRTPWriter { t.Error("Default BindLocalStream should return given writer") } i.UnbindLocalStream(dummyStreamInfo) if i.BindRemoteStream(dummyStreamInfo, dummyRTPReader) != dummyRTPReader { t.Error("Default BindRemoteStream should return given reader") } i.UnbindRemoteStream(dummyStreamInfo) if i.Close() != nil { t.Error("Default Close should return nil") } }) t.Run("Custom", func(t *testing.T) { var ( cntBindRTCPReader uint32 cntBindRTCPWriter uint32 cntBindLocalStream uint32 cntUnbindLocalStream uint32 cntBindRemoteStream uint32 cntUnbindRemoteStream uint32 cntClose uint32 ) i := &Interceptor{ BindRTCPReaderFn: func(reader interceptor.RTCPReader) interceptor.RTCPReader { atomic.AddUint32(&cntBindRTCPReader, 1) return reader }, BindRTCPWriterFn: func(writer interceptor.RTCPWriter) interceptor.RTCPWriter { atomic.AddUint32(&cntBindRTCPWriter, 1) return writer }, BindLocalStreamFn: func(i *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { atomic.AddUint32(&cntBindLocalStream, 1) return writer }, UnbindLocalStreamFn: func(i *interceptor.StreamInfo) { atomic.AddUint32(&cntUnbindLocalStream, 1) }, BindRemoteStreamFn: func(i *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { atomic.AddUint32(&cntBindRemoteStream, 1) return reader }, UnbindRemoteStreamFn: func(i *interceptor.StreamInfo) { atomic.AddUint32(&cntUnbindRemoteStream, 1) }, CloseFn: func() error { atomic.AddUint32(&cntClose, 1) return nil }, } if i.BindRTCPWriter(dummyRTCPWriter) != dummyRTCPWriter { t.Error("Mocked BindRTCPWriter should return given writer") } if i.BindRTCPReader(dummyRTCPReader) != dummyRTCPReader { t.Error("Mocked BindRTCPReader should return given reader") } if i.BindLocalStream(dummyStreamInfo, dummyRTPWriter) != dummyRTPWriter { t.Error("Mocked BindLocalStream should return given writer") } i.UnbindLocalStream(dummyStreamInfo) if i.BindRemoteStream(dummyStreamInfo, dummyRTPReader) != dummyRTPReader { t.Error("Mocked BindRemoteStream should return given reader") } i.UnbindRemoteStream(dummyStreamInfo) if i.Close() != nil { t.Error("Mocked Close should return nil") } if cnt := atomic.LoadUint32(&cntBindRTCPWriter); cnt != 1 { t.Errorf("BindRTCPWriterFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntBindRTCPReader); cnt != 1 { t.Errorf("BindRTCPReaderFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntBindLocalStream); cnt != 1 { t.Errorf("BindLocalStreamFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntUnbindLocalStream); cnt != 1 { t.Errorf("UnbindLocalStreamFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntBindRemoteStream); cnt != 1 { t.Errorf("BindRemoteStreamFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntUnbindRemoteStream); cnt != 1 { t.Errorf("UnbindRemoteStreamFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntClose); cnt != 1 { t.Errorf("CloseFn is expected to be called once, but called %d times", cnt) } }) } interceptor-0.1.12/pkg/nack/000077500000000000000000000000001426573475500156505ustar00rootroot00000000000000interceptor-0.1.12/pkg/nack/errors.go000066400000000000000000000007131426573475500175140ustar00rootroot00000000000000package nack import "errors" // ErrInvalidSize is returned by newReceiveLog/newSendBuffer, when an incorrect buffer size is supplied. var ErrInvalidSize = errors.New("invalid buffer size") var ( errPacketReleased = errors.New("could not retain packet, already released") errFailedToCastHeaderPool = errors.New("could not access header pool, failed cast") errFailedToCastPayloadPool = errors.New("could not access payload pool, failed cast") ) interceptor-0.1.12/pkg/nack/generator_interceptor.go000066400000000000000000000101661426573475500226070ustar00rootroot00000000000000package nack import ( "math/rand" "sync" "time" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" ) // GeneratorInterceptorFactory is a interceptor.Factory for a GeneratorInterceptor type GeneratorInterceptorFactory struct { opts []GeneratorOption } // NewInterceptor constructs a new ReceiverInterceptor func (g *GeneratorInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { i := &GeneratorInterceptor{ size: 512, skipLastN: 0, interval: time.Millisecond * 100, receiveLogs: map[uint32]*receiveLog{}, close: make(chan struct{}), log: logging.NewDefaultLoggerFactory().NewLogger("nack_generator"), } for _, opt := range g.opts { if err := opt(i); err != nil { return nil, err } } if _, err := newReceiveLog(i.size); err != nil { return nil, err } return i, nil } // GeneratorInterceptor interceptor generates nack feedback messages. type GeneratorInterceptor struct { interceptor.NoOp size uint16 skipLastN uint16 interval time.Duration m sync.Mutex wg sync.WaitGroup close chan struct{} log logging.LeveledLogger receiveLogs map[uint32]*receiveLog receiveLogsMu sync.Mutex } // NewGeneratorInterceptor returns a new GeneratorInterceptorFactory func NewGeneratorInterceptor(opts ...GeneratorOption) (*GeneratorInterceptorFactory, error) { return &GeneratorInterceptorFactory{opts}, nil } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (n *GeneratorInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { n.m.Lock() defer n.m.Unlock() if n.isClosed() { return writer } n.wg.Add(1) go n.loop(writer) return writer } // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (n *GeneratorInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { if !streamSupportNack(info) { return reader } // error is already checked in NewGeneratorInterceptor receiveLog, _ := newReceiveLog(n.size) n.receiveLogsMu.Lock() n.receiveLogs[info.SSRC] = receiveLog n.receiveLogsMu.Unlock() return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(b, a) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } header, err := attr.GetRTPHeader(b[:i]) if err != nil { return 0, nil, err } receiveLog.add(header.SequenceNumber) return i, attr, nil }) } // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (n *GeneratorInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { n.receiveLogsMu.Lock() delete(n.receiveLogs, info.SSRC) n.receiveLogsMu.Unlock() } // Close closes the interceptor func (n *GeneratorInterceptor) Close() error { defer n.wg.Wait() n.m.Lock() defer n.m.Unlock() if !n.isClosed() { close(n.close) } return nil } func (n *GeneratorInterceptor) loop(rtcpWriter interceptor.RTCPWriter) { defer n.wg.Done() senderSSRC := rand.Uint32() // #nosec ticker := time.NewTicker(n.interval) defer ticker.Stop() for { select { case <-ticker.C: func() { n.receiveLogsMu.Lock() defer n.receiveLogsMu.Unlock() for ssrc, receiveLog := range n.receiveLogs { missing := receiveLog.missingSeqNumbers(n.skipLastN) if len(missing) == 0 { continue } nack := &rtcp.TransportLayerNack{ SenderSSRC: senderSSRC, MediaSSRC: ssrc, Nacks: rtcp.NackPairsFromSequenceNumbers(missing), } if _, err := rtcpWriter.Write([]rtcp.Packet{nack}, interceptor.Attributes{}); err != nil { n.log.Warnf("failed sending nack: %+v", err) } } }() case <-n.close: return } } } func (n *GeneratorInterceptor) isClosed() bool { select { case <-n.close: return true default: return false } } interceptor-0.1.12/pkg/nack/generator_interceptor_test.go000066400000000000000000000040301426573475500236370ustar00rootroot00000000000000package nack import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestGeneratorInterceptor(t *testing.T) { const interval = time.Millisecond * 10 f, err := NewGeneratorInterceptor( GeneratorSize(64), GeneratorSkipLastN(2), GeneratorInterval(interval), GeneratorLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 1, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, i) defer func() { assert.NoError(t, stream.Close()) }() for _, seqNum := range []uint16{10, 11, 12, 14, 16, 18} { stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum}}) select { case r := <-stream.ReadRTP(): assert.NoError(t, r.Err) assert.Equal(t, seqNum, r.Packet.SequenceNumber) case <-time.After(10 * time.Millisecond): t.Fatal("receiver rtp packet not found") } } time.Sleep(interval * 2) // wait for at least 2 nack packets select { case <-stream.WrittenRTCP(): // ignore the first nack, it might only contain the sequence id 13 as missing default: } select { case pkts := <-stream.WrittenRTCP(): assert.Equal(t, 1, len(pkts), "single packet RTCP Compound Packet expected") p, ok := pkts[0].(*rtcp.TransportLayerNack) assert.True(t, ok, "TransportLayerNack rtcp packet expected, found: %T", pkts[0]) assert.Equal(t, uint16(13), p.Nacks[0].PacketID) assert.Equal(t, rtcp.PacketBitmap(0b10), p.Nacks[0].LostPackets) // we want packets: 13, 15 (not packet 17, because skipLastN is setReceived to 2) case <-time.After(10 * time.Millisecond): t.Fatal("written rtcp packet not found") } } func TestGeneratorInterceptor_InvalidSize(t *testing.T) { f, _ := NewGeneratorInterceptor(GeneratorSize(5)) _, err := f.NewInterceptor("") assert.Error(t, err, ErrInvalidSize) } interceptor-0.1.12/pkg/nack/generator_option.go000066400000000000000000000022001426573475500215470ustar00rootroot00000000000000package nack import ( "time" "github.com/pion/logging" ) // GeneratorOption can be used to configure GeneratorInterceptor type GeneratorOption func(r *GeneratorInterceptor) error // GeneratorSize sets the size of the interceptor. // Size must be one of: 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 func GeneratorSize(size uint16) GeneratorOption { return func(r *GeneratorInterceptor) error { r.size = size return nil } } // GeneratorSkipLastN sets the number of packets (n-1 packets before the last received packets) to ignore when generating // nack requests. func GeneratorSkipLastN(skipLastN uint16) GeneratorOption { return func(r *GeneratorInterceptor) error { r.skipLastN = skipLastN return nil } } // GeneratorLog sets a logger for the interceptor func GeneratorLog(log logging.LeveledLogger) GeneratorOption { return func(r *GeneratorInterceptor) error { r.log = log return nil } } // GeneratorInterval sets the nack send interval for the interceptor func GeneratorInterval(interval time.Duration) GeneratorOption { return func(r *GeneratorInterceptor) error { r.interval = interval return nil } } interceptor-0.1.12/pkg/nack/nack.go000066400000000000000000000005211426573475500171110ustar00rootroot00000000000000// Package nack provides interceptors to implement sending and receiving negative acknowledgements package nack import "github.com/pion/interceptor" func streamSupportNack(info *interceptor.StreamInfo) bool { for _, fb := range info.RTCPFeedback { if fb.Type == "nack" && fb.Parameter == "" { return true } } return false } interceptor-0.1.12/pkg/nack/receive_log.go000066400000000000000000000054761426573475500204760ustar00rootroot00000000000000package nack import ( "fmt" "sync" ) type receiveLog struct { packets []uint64 size uint16 end uint16 started bool lastConsecutive uint16 m sync.RWMutex } func newReceiveLog(size uint16) (*receiveLog, error) { allowedSizes := make([]uint16, 0) correctSize := false for i := 6; i < 16; i++ { if size == 1< end (with counting for rollovers) for i := s.end + 1; i != seq; i++ { // clear packets between end and seq (these may contain packets from a "size" ago) s.delReceived(i) } s.end = seq if s.lastConsecutive+1 == seq { s.lastConsecutive = seq } else if seq-s.lastConsecutive > s.size { s.lastConsecutive = seq - s.size s.fixLastConsecutive() // there might be valid packets at the beginning of the buffer now } case s.lastConsecutive+1 == seq: // negative diff, seq < end (with counting for rollovers) s.lastConsecutive = seq s.fixLastConsecutive() // there might be other valid packets after seq } s.setReceived(seq) } func (s *receiveLog) get(seq uint16) bool { s.m.RLock() defer s.m.RUnlock() diff := s.end - seq if diff >= uint16SizeHalf { return false } if diff >= s.size { return false } return s.getReceived(seq) } func (s *receiveLog) missingSeqNumbers(skipLastN uint16) []uint16 { s.m.RLock() defer s.m.RUnlock() until := s.end - skipLastN if until-s.lastConsecutive >= uint16SizeHalf { // until < s.lastConsecutive (counting for rollover) return nil } missingPacketSeqNums := make([]uint16, 0) for i := s.lastConsecutive + 1; i != until+1; i++ { if !s.getReceived(i) { missingPacketSeqNums = append(missingPacketSeqNums, i) } } return missingPacketSeqNums } func (s *receiveLog) setReceived(seq uint16) { pos := seq % s.size s.packets[pos/64] |= 1 << (pos % 64) } func (s *receiveLog) delReceived(seq uint16) { pos := seq % s.size s.packets[pos/64] &^= 1 << (pos % 64) } func (s *receiveLog) getReceived(seq uint16) bool { pos := seq % s.size return (s.packets[pos/64] & (1 << (pos % 64))) != 0 } func (s *receiveLog) fixLastConsecutive() { i := s.lastConsecutive + 1 for ; i != s.end+1 && s.getReceived(i); i++ { // find all consecutive packets } s.lastConsecutive = i - 1 } interceptor-0.1.12/pkg/nack/receive_log_test.go000066400000000000000000000066331426573475500215310ustar00rootroot00000000000000package nack import ( "fmt" "reflect" "testing" ) func TestReceivedBuffer(t *testing.T) { for _, start := range []uint16{0, 1, 127, 128, 129, 511, 512, 513, 32767, 32768, 32769, 65407, 65408, 65409, 65534, 65535} { start := start t.Run(fmt.Sprintf("StartFrom%d", start), func(t *testing.T) { rl, err := newReceiveLog(128) if err != nil { t.Fatalf("%+v", err) } all := func(min uint16, max uint16) []uint16 { result := make([]uint16, 0) for i := min; i != max+1; i++ { result = append(result, i) } return result } join := func(parts ...[]uint16) []uint16 { result := make([]uint16, 0) for _, p := range parts { result = append(result, p...) } return result } add := func(nums ...uint16) { for _, n := range nums { seq := start + n rl.add(seq) } } assertGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n if !rl.get(seq) { t.Errorf("not found: %d", seq) } } } assertNOTGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n if rl.get(seq) { t.Errorf("packet found for %d", seq) } } } assertMissing := func(skipLastN uint16, nums []uint16) { t.Helper() missing := rl.missingSeqNumbers(skipLastN) if missing == nil { missing = []uint16{} } want := make([]uint16, 0, len(nums)) for _, n := range nums { want = append(want, start+n) } if !reflect.DeepEqual(want, missing) { t.Errorf("missing want/got %v / %v", want, missing) } } assertLastConsecutive := func(lastConsecutive uint16) { want := lastConsecutive + start if rl.lastConsecutive != want { t.Errorf("invalid lastConsecutive want %d got %d", want, rl.lastConsecutive) } } add(0) assertGet(0) assertMissing(0, []uint16{}) assertLastConsecutive(0) // first element added add(all(1, 127)...) assertGet(all(1, 127)...) assertMissing(0, []uint16{}) assertLastConsecutive(127) add(128) assertGet(128) assertNOTGet(0) assertMissing(0, []uint16{}) assertLastConsecutive(128) add(130) assertGet(130) assertNOTGet(1, 2, 129) assertMissing(0, []uint16{129}) assertLastConsecutive(128) add(333) assertGet(333) assertNOTGet(all(0, 332)...) assertMissing(0, all(206, 332)) // all 127 elements missing before 333 assertMissing(10, all(206, 323)) // skip last 10 packets (324-333) from check assertLastConsecutive(205) // lastConsecutive is still out of the buffer add(329) assertGet(329) assertMissing(0, join(all(206, 328), all(330, 332))) assertMissing(5, join(all(206, 328))) // skip last 5 packets (329-333) from check assertLastConsecutive(205) add(all(207, 320)...) assertGet(all(207, 320)...) assertMissing(0, join([]uint16{206}, all(321, 328), all(330, 332))) assertLastConsecutive(205) add(334) assertGet(334) assertNOTGet(206) assertMissing(0, join(all(321, 328), all(330, 332))) assertLastConsecutive(320) // head of buffer is full of consecutive packages add(all(322, 328)...) assertGet(all(322, 328)...) assertMissing(0, join([]uint16{321}, all(330, 332))) assertLastConsecutive(320) add(321) assertGet(321) assertMissing(0, all(330, 332)) assertLastConsecutive(329) // after adding a single missing packet, lastConsecutive should jump forward }) } } interceptor-0.1.12/pkg/nack/responder_interceptor.go000066400000000000000000000076671426573475500226360ustar00rootroot00000000000000package nack import ( "sync" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" ) // ResponderInterceptorFactory is a interceptor.Factory for a ResponderInterceptor type ResponderInterceptorFactory struct { opts []ResponderOption } type packetFactory interface { NewPacket(header *rtp.Header, payload []byte) (*retainablePacket, error) } // NewInterceptor constructs a new ResponderInterceptor func (r *ResponderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { i := &ResponderInterceptor{ size: 1024, log: logging.NewDefaultLoggerFactory().NewLogger("nack_responder"), streams: map[uint32]*localStream{}, } for _, opt := range r.opts { if err := opt(i); err != nil { return nil, err } } if i.packetFactory == nil { i.packetFactory = newPacketManager() } if _, err := newSendBuffer(i.size); err != nil { return nil, err } return i, nil } // ResponderInterceptor responds to nack feedback messages type ResponderInterceptor struct { interceptor.NoOp size uint16 log logging.LeveledLogger packetFactory packetFactory streams map[uint32]*localStream streamsMu sync.Mutex } type localStream struct { sendBuffer *sendBuffer rtpWriter interceptor.RTPWriter } // NewResponderInterceptor returns a new ResponderInterceptorFactor func NewResponderInterceptor(opts ...ResponderOption) (*ResponderInterceptorFactory, error) { return &ResponderInterceptorFactory{opts}, nil } // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might // change in the future. The returned method will be called once per packet batch. func (n *ResponderInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { return interceptor.RTCPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(b, a) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } pkts, err := attr.GetRTCPPackets(b[:i]) if err != nil { return 0, nil, err } for _, rtcpPacket := range pkts { nack, ok := rtcpPacket.(*rtcp.TransportLayerNack) if !ok { continue } go n.resendPackets(nack) } return i, attr, err }) } // BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method // will be called once per rtp packet. func (n *ResponderInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { if !streamSupportNack(info) { return writer } // error is already checked in NewGeneratorInterceptor sendBuffer, _ := newSendBuffer(n.size) n.streamsMu.Lock() n.streams[info.SSRC] = &localStream{sendBuffer: sendBuffer, rtpWriter: writer} n.streamsMu.Unlock() return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { pkt, err := n.packetFactory.NewPacket(header, payload) if err != nil { return 0, err } sendBuffer.add(pkt) return writer.Write(header, payload, attributes) }) } // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (n *ResponderInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { n.streamsMu.Lock() delete(n.streams, info.SSRC) n.streamsMu.Unlock() } func (n *ResponderInterceptor) resendPackets(nack *rtcp.TransportLayerNack) { n.streamsMu.Lock() stream, ok := n.streams[nack.MediaSSRC] n.streamsMu.Unlock() if !ok { return } for i := range nack.Nacks { nack.Nacks[i].Range(func(seq uint16) bool { if p := stream.sendBuffer.get(seq); p != nil { if _, err := stream.rtpWriter.Write(p.Header(), p.Payload(), interceptor.Attributes{}); err != nil { n.log.Warnf("failed resending nacked packet: %+v", err) } p.Release() } return true }) } } interceptor-0.1.12/pkg/nack/responder_interceptor_test.go000066400000000000000000000073121426573475500236600ustar00rootroot00000000000000package nack import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/require" ) func TestResponderInterceptor(t *testing.T) { tests := []struct { name string opts []ResponderOption }{ { name: "with copy", opts: []ResponderOption{ ResponderSize(8), ResponderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), }, }, { name: "without copy", opts: []ResponderOption{ ResponderSize(8), ResponderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), DisableCopy(), }, }, } for _, item := range tests { item := item t.Run(item.name, func(t *testing.T) { f, err := NewResponderInterceptor(item.opts...) require.NoError(t, err) i, err := f.NewInterceptor("") require.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 1, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, i) defer func() { require.NoError(t, stream.Close()) }() for _, seqNum := range []uint16{10, 11, 12, 14, 15} { require.NoError(t, stream.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum}})) select { case p := <-stream.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) case <-time.After(10 * time.Millisecond): t.Fatal("written rtp packet not found") } } stream.ReceiveRTCP([]rtcp.Packet{ &rtcp.TransportLayerNack{ MediaSSRC: 1, SenderSSRC: 2, Nacks: []rtcp.NackPair{ {PacketID: 11, LostPackets: 0b1011}, // sequence numbers: 11, 12, 13, 15 }, }, }) // seq number 13 was never sent, so it can't be resent for _, seqNum := range []uint16{11, 12, 15} { select { case p := <-stream.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) case <-time.After(10 * time.Millisecond): t.Fatal("written rtp packet not found") } } select { case p := <-stream.WrittenRTP(): t.Errorf("no more rtp packets expected, found sequence number: %v", p.SequenceNumber) case <-time.After(10 * time.Millisecond): } }) } } func TestResponderInterceptor_InvalidSize(t *testing.T) { f, _ := NewResponderInterceptor(ResponderSize(5)) _, err := f.NewInterceptor("") require.Error(t, err, ErrInvalidSize) } func TestResponderInterceptor_DisableCopy(t *testing.T) { f, err := NewResponderInterceptor( ResponderSize(8), ResponderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), DisableCopy(), ) require.NoError(t, err) i, err := f.NewInterceptor("id") require.NoError(t, err) _, ok := i.(*ResponderInterceptor).packetFactory.(*noOpPacketFactory) require.True(t, ok) } // this test is only useful when being run with the race detector, it won't fail otherwise: // // go test -race ./pkg/nack/ func TestResponderInterceptor_Race(t *testing.T) { f, err := NewResponderInterceptor( ResponderSize(32768), ResponderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ) require.NoError(t, err) i, err := f.NewInterceptor("") require.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 1, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, i) for seqNum := uint16(0); seqNum < 500; seqNum++ { require.NoError(t, stream.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum}})) // 25% packet loss if seqNum%4 == 0 { time.Sleep(time.Duration(seqNum%23) * time.Millisecond) stream.ReceiveRTCP([]rtcp.Packet{ &rtcp.TransportLayerNack{ MediaSSRC: 1, SenderSSRC: 2, Nacks: []rtcp.NackPair{ {PacketID: seqNum, LostPackets: 0}, }, }, }) } } } interceptor-0.1.12/pkg/nack/responder_option.go000066400000000000000000000016531426573475500215750ustar00rootroot00000000000000package nack import "github.com/pion/logging" // ResponderOption can be used to configure ResponderInterceptor type ResponderOption func(s *ResponderInterceptor) error // ResponderSize sets the size of the interceptor. // Size must be one of: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 func ResponderSize(size uint16) ResponderOption { return func(r *ResponderInterceptor) error { r.size = size return nil } } // ResponderLog sets a logger for the interceptor func ResponderLog(log logging.LeveledLogger) ResponderOption { return func(r *ResponderInterceptor) error { r.log = log return nil } } // DisableCopy bypasses copy of underlying packets. It should be used when // you are not re-using underlying buffers of packets that have been written func DisableCopy() ResponderOption { return func(s *ResponderInterceptor) error { s.packetFactory = &noOpPacketFactory{} return nil } } interceptor-0.1.12/pkg/nack/retainable_packet.go000066400000000000000000000044271426573475500216430ustar00rootroot00000000000000package nack import ( "io" "sync" "github.com/pion/rtp" ) const maxPayloadLen = 1460 type packetManager struct { headerPool *sync.Pool payloadPool *sync.Pool } func newPacketManager() *packetManager { return &packetManager{ headerPool: &sync.Pool{ New: func() interface{} { return &rtp.Header{} }, }, payloadPool: &sync.Pool{ New: func() interface{} { buf := make([]byte, maxPayloadLen) return &buf }, }, } } func (m *packetManager) NewPacket(header *rtp.Header, payload []byte) (*retainablePacket, error) { if len(payload) > maxPayloadLen { return nil, io.ErrShortBuffer } p := &retainablePacket{ onRelease: m.releasePacket, // new packets have retain count of 1 count: 1, } var ok bool p.header, ok = m.headerPool.Get().(*rtp.Header) if !ok { return nil, errFailedToCastHeaderPool } *p.header = header.Clone() if payload != nil { p.buffer, ok = m.payloadPool.Get().(*[]byte) if !ok { return nil, errFailedToCastPayloadPool } size := copy(*p.buffer, payload) p.payload = (*p.buffer)[:size] } return p, nil } func (m *packetManager) releasePacket(header *rtp.Header, payload *[]byte) { m.headerPool.Put(header) if payload != nil { m.payloadPool.Put(payload) } } type noOpPacketFactory struct{} func (f *noOpPacketFactory) NewPacket(header *rtp.Header, payload []byte) (*retainablePacket, error) { return &retainablePacket{ onRelease: f.releasePacket, count: 1, header: header, payload: payload, }, nil } func (f *noOpPacketFactory) releasePacket(header *rtp.Header, payload *[]byte) { // no-op } type retainablePacket struct { onRelease func(*rtp.Header, *[]byte) countMu sync.Mutex count int header *rtp.Header buffer *[]byte payload []byte } func (p *retainablePacket) Header() *rtp.Header { return p.header } func (p *retainablePacket) Payload() []byte { return p.payload } func (p *retainablePacket) Retain() error { p.countMu.Lock() defer p.countMu.Unlock() if p.count == 0 { // already released return errPacketReleased } p.count++ return nil } func (p *retainablePacket) Release() { p.countMu.Lock() defer p.countMu.Unlock() p.count-- if p.count == 0 { // release back to pool p.onRelease(p.header, p.buffer) p.header = nil p.buffer = nil p.payload = nil } } interceptor-0.1.12/pkg/nack/send_buffer.go000066400000000000000000000032771426573475500204720ustar00rootroot00000000000000package nack import ( "fmt" "sync" ) const ( uint16SizeHalf = 1 << 15 ) type sendBuffer struct { packets []*retainablePacket size uint16 lastAdded uint16 started bool m sync.RWMutex } func newSendBuffer(size uint16) (*sendBuffer, error) { allowedSizes := make([]uint16, 0) correctSize := false for i := 0; i < 16; i++ { if size == 1<= uint16SizeHalf { return nil } if diff >= s.size { return nil } pkt := s.packets[seq%s.size] if pkt != nil { if pkt.Header().SequenceNumber != seq { return nil } // already released if err := pkt.Retain(); err != nil { return nil } } return pkt } interceptor-0.1.12/pkg/nack/send_buffer_test.go000066400000000000000000000061521426573475500215240ustar00rootroot00000000000000package nack import ( "testing" "github.com/pion/rtp" "github.com/stretchr/testify/require" ) func TestSendBuffer(t *testing.T) { pm := newPacketManager() for _, start := range []uint16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 511, 512, 513, 32767, 32768, 32769, 65527, 65528, 65529, 65530, 65531, 65532, 65533, 65534, 65535} { start := start sb, err := newSendBuffer(8) require.NoError(t, err) add := func(nums ...uint16) { for _, n := range nums { seq := start + n pkt, err := pm.NewPacket(&rtp.Header{SequenceNumber: seq}, nil) require.NoError(t, err) sb.add(pkt) } } assertGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n packet := sb.get(seq) if packet == nil { t.Errorf("packet not found: %d", seq) continue } if packet.Header().SequenceNumber != seq { t.Errorf("packet for %d returned with incorrect SequenceNumber: %d", seq, packet.Header().SequenceNumber) } packet.Release() } } assertNOTGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n packet := sb.get(seq) if packet != nil { t.Errorf("packet found for %d: %d", seq, packet.Header().SequenceNumber) } } } add(0, 1, 2, 3, 4, 5, 6, 7) assertGet(0, 1, 2, 3, 4, 5, 6, 7) add(8) assertGet(8) assertNOTGet(0) add(10) assertGet(10) assertNOTGet(1, 2, 9) add(22) assertGet(22) assertNOTGet(3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21) } } func TestSendBuffer_Overridden(t *testing.T) { // override original packet content and get pm := newPacketManager() sb, err := newSendBuffer(1) require.NoError(t, err) require.Equal(t, uint16(1), sb.size) originalBytes := []byte("originalContent") pkt, err := pm.NewPacket(&rtp.Header{SequenceNumber: 1}, originalBytes) require.NoError(t, err) sb.add(pkt) // change payload copy(originalBytes, "altered") retrieved := sb.get(1) require.NotNil(t, retrieved) require.Equal(t, "originalContent", string(retrieved.Payload())) retrieved.Release() require.Equal(t, 1, retrieved.count) // ensure original packet is released pkt, err = pm.NewPacket(&rtp.Header{SequenceNumber: 2}, originalBytes) require.NoError(t, err) sb.add(pkt) require.Equal(t, 0, retrieved.count) require.Nil(t, sb.get(1)) } // this test is only useful when being run with the race detector, it won't fail otherwise: // // go test -race ./pkg/nack/ func TestSendBuffer_Race(t *testing.T) { pm := newPacketManager() for _, start := range []uint16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 511, 512, 513, 32767, 32768, 32769, 65527, 65528, 65529, 65530, 65531, 65532, 65533, 65534, 65535} { start := start sb, err := newSendBuffer(8) require.NoError(t, err) add := func(nums ...uint16) { for _, n := range nums { seq := start + n pkt, err := pm.NewPacket(&rtp.Header{SequenceNumber: seq}, nil) require.NoError(t, err) sb.add(pkt) } } get := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n sb.get(seq) } } go add(0, 1, 2, 3, 4, 5, 6, 7) go get(0, 1, 2, 3, 4, 5, 6, 7) } } interceptor-0.1.12/pkg/packetdump/000077500000000000000000000000001426573475500170715ustar00rootroot00000000000000interceptor-0.1.12/pkg/packetdump/filter.go000066400000000000000000000007171426573475500207120ustar00rootroot00000000000000package packetdump import ( "github.com/pion/rtcp" "github.com/pion/rtp" ) // RTPFilterCallback can be used to filter RTP packets to dump. // The callback returns whether or not to print dump the packet's content. type RTPFilterCallback func(pkt *rtp.Packet) bool // RTCPFilterCallback can be used to filter RTCP packets to dump. // The callback returns whether or not to print dump the packet's content. type RTCPFilterCallback func(pkt []rtcp.Packet) bool interceptor-0.1.12/pkg/packetdump/format.go000066400000000000000000000017671426573475500207230ustar00rootroot00000000000000package packetdump import ( "fmt" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" ) // RTPFormatCallback can be used to apply custom formatting to each dumped RTP // packet. If new lines should be added after each packet, they must be included // in the returned format. type RTPFormatCallback func(*rtp.Packet, interceptor.Attributes) string // RTCPFormatCallback can be used to apply custom formatting to each dumped RTCP // packet. If new lines should be added after each packet, they must be included // in the returned format. type RTCPFormatCallback func([]rtcp.Packet, interceptor.Attributes) string // DefaultRTPFormatter returns the default log format for RTP packets func DefaultRTPFormatter(pkt *rtp.Packet, _ interceptor.Attributes) string { return fmt.Sprintf("%s\n", pkt) } // DefaultRTCPFormatter returns the default log format for RTCP packets func DefaultRTCPFormatter(pkts []rtcp.Packet, _ interceptor.Attributes) string { return fmt.Sprintf("%s\n", pkts) } interceptor-0.1.12/pkg/packetdump/option.go000066400000000000000000000026561426573475500207410ustar00rootroot00000000000000package packetdump import ( "io" "github.com/pion/logging" ) // PacketDumperOption can be used to configure SenderInterceptor type PacketDumperOption func(d *PacketDumper) error // Log sets a logger for the interceptor func Log(log logging.LeveledLogger) PacketDumperOption { return func(d *PacketDumper) error { d.log = log return nil } } // RTPWriter sets the io.Writer on which RTP packets will be dumped. func RTPWriter(w io.Writer) PacketDumperOption { return func(d *PacketDumper) error { d.rtpStream = w return nil } } // RTCPWriter sets the io.Writer on which RTCP packets will be dumped. func RTCPWriter(w io.Writer) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpStream = w return nil } } // RTPFormatter sets the RTP format func RTPFormatter(f RTPFormatCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtpFormat = f return nil } } // RTCPFormatter sets the RTCP format func RTCPFormatter(f RTCPFormatCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpFormat = f return nil } } // RTPFilter sets the RTP filter. func RTPFilter(callback RTPFilterCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtpFilter = callback return nil } } // RTCPFilter sets the RTCP filter. func RTCPFilter(callback RTCPFilterCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpFilter = callback return nil } } interceptor-0.1.12/pkg/packetdump/packet_dump.go000066400000000000000000000005241426573475500217150ustar00rootroot00000000000000// Package packetdump implements RTP & RTCP packet dumpers. package packetdump import ( "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" ) type rtpDump struct { attributes interceptor.Attributes packet *rtp.Packet } type rtcpDump struct { attributes interceptor.Attributes packets []rtcp.Packet } interceptor-0.1.12/pkg/packetdump/packet_dumper.go000066400000000000000000000050151426573475500222440ustar00rootroot00000000000000package packetdump import ( "fmt" "io" "os" "sync" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" ) // PacketDumper dumps packet to a io.Writer type PacketDumper struct { log logging.LeveledLogger wg sync.WaitGroup close chan struct{} rtpChan chan *rtpDump rtcpChan chan *rtcpDump rtpStream io.Writer rtcpStream io.Writer rtpFormat RTPFormatCallback rtcpFormat RTCPFormatCallback rtpFilter RTPFilterCallback rtcpFilter RTCPFilterCallback } // NewPacketDumper creates a new PacketDumper func NewPacketDumper(opts ...PacketDumperOption) (*PacketDumper, error) { d := &PacketDumper{ log: logging.NewDefaultLoggerFactory().NewLogger("packet_dumper"), wg: sync.WaitGroup{}, close: make(chan struct{}), rtpChan: make(chan *rtpDump), rtcpChan: make(chan *rtcpDump), rtpStream: os.Stdout, rtcpStream: os.Stdout, rtpFormat: DefaultRTPFormatter, rtcpFormat: DefaultRTCPFormatter, rtpFilter: func(pkt *rtp.Packet) bool { return true }, rtcpFilter: func(pkt []rtcp.Packet) bool { return true }, } for _, opt := range opts { if err := opt(d); err != nil { return nil, err } } d.wg.Add(1) go d.loop() return d, nil } func (d *PacketDumper) logRTPPacket(header *rtp.Header, payload []byte, attributes interceptor.Attributes) { select { case d.rtpChan <- &rtpDump{ attributes: attributes, packet: &rtp.Packet{ Header: *header, Payload: payload, }, }: case <-d.close: } } func (d *PacketDumper) logRTCPPackets(pkts []rtcp.Packet, attributes interceptor.Attributes) { select { case d.rtcpChan <- &rtcpDump{ attributes: attributes, packets: pkts, }: case <-d.close: } } // Close closes the PacketDumper func (d *PacketDumper) Close() error { defer d.wg.Wait() if !d.isClosed() { close(d.close) } return nil } func (d *PacketDumper) isClosed() bool { select { case <-d.close: return true default: return false } } func (d *PacketDumper) loop() { defer d.wg.Done() for { select { case <-d.close: return case dump := <-d.rtpChan: if d.rtpFilter(dump.packet) { if _, err := fmt.Fprint(d.rtpStream, d.rtpFormat(dump.packet, dump.attributes)); err != nil { d.log.Errorf("could not dump RTP packet %v", err) } } case dump := <-d.rtcpChan: if d.rtcpFilter(dump.packets) { if _, err := fmt.Fprint(d.rtcpStream, d.rtcpFormat(dump.packets, dump.attributes)); err != nil { d.log.Errorf("could not dump RTCP packet %v", err) } } } } } interceptor-0.1.12/pkg/packetdump/receiver_interceptor.go000066400000000000000000000047311426573475500236470ustar00rootroot00000000000000package packetdump import ( "github.com/pion/interceptor" ) // ReceiverInterceptorFactory is a interceptor.Factory for a ReceiverInterceptor type ReceiverInterceptorFactory struct { opts []PacketDumperOption } // NewReceiverInterceptor returns a new ReceiverInterceptor func NewReceiverInterceptor(opts ...PacketDumperOption) (*ReceiverInterceptorFactory, error) { return &ReceiverInterceptorFactory{ opts: opts, }, nil } // NewInterceptor returns a new ReceiverInterceptor interceptor. func (r *ReceiverInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { dumper, err := NewPacketDumper(r.opts...) if err != nil { return nil, err } i := &ReceiverInterceptor{ NoOp: interceptor.NoOp{}, PacketDumper: dumper, } return i, nil } // ReceiverInterceptor interceptor dumps outgoing RTP packets. type ReceiverInterceptor struct { interceptor.NoOp *PacketDumper } // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (r *ReceiverInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { return interceptor.RTPReaderFunc(func(bytes []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(bytes, attributes) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } header, err := attr.GetRTPHeader(bytes) if err != nil { return 0, nil, err } r.logRTPPacket(header, bytes[header.MarshalSize():i], attr) return i, attr, nil }) } // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might // change in the future. The returned method will be called once per packet batch. func (r *ReceiverInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { return interceptor.RTCPReaderFunc(func(bytes []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(bytes, attributes) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } pkts, err := attr.GetRTCPPackets(bytes[:i]) if err != nil { return 0, nil, err } r.logRTCPPackets(pkts, attr) return i, attr, err }) } // Close closes the interceptor func (r *ReceiverInterceptor) Close() error { return r.PacketDumper.Close() } interceptor-0.1.12/pkg/packetdump/receiver_interceptor_test.go000066400000000000000000000044051426573475500247040ustar00rootroot00000000000000package packetdump import ( "bytes" "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestReceiverFilterEverythingOut(t *testing.T) { buf := bytes.Buffer{} factory, err := NewReceiverInterceptor( RTPWriter(&buf), RTCPWriter(&buf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), RTPFilter(func(pkt *rtp.Packet) bool { return false }), RTCPFilter(func(pkt []rtcp.Packet) bool { return false }), ) assert.NoError(t, err) i, err := factory.NewInterceptor("") assert.NoError(t, err) assert.Zero(t, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 456, }}) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(0), }}) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = i.Close() assert.NoError(t, err) // Every packet should have been filtered out – nothing should be written. assert.Zero(t, buf.Len()) } func TestReceiverFilterNothing(t *testing.T) { buf := bytes.Buffer{} factory, err := NewReceiverInterceptor( RTPWriter(&buf), RTCPWriter(&buf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), RTPFilter(func(pkt *rtp.Packet) bool { return true }), RTCPFilter(func(pkt []rtcp.Packet) bool { return true }), ) assert.NoError(t, err) i, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 456, }}) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(0), }}) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = i.Close() assert.NoError(t, err) assert.NotZero(t, buf.Len()) } interceptor-0.1.12/pkg/packetdump/sender_interceptor.go000066400000000000000000000036761426573475500233320ustar00rootroot00000000000000package packetdump import ( "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" ) // SenderInterceptorFactory is a interceptor.Factory for a SenderInterceptor type SenderInterceptorFactory struct { opts []PacketDumperOption } // NewSenderInterceptor returns a new SenderInterceptorFactory func NewSenderInterceptor(opts ...PacketDumperOption) (*SenderInterceptorFactory, error) { return &SenderInterceptorFactory{ opts: opts, }, nil } // NewInterceptor returns a new SenderInterceptor interceptor func (s *SenderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { dumper, err := NewPacketDumper(s.opts...) if err != nil { return nil, err } i := &SenderInterceptor{ PacketDumper: dumper, } return i, nil } // SenderInterceptor responds to nack feedback messages type SenderInterceptor struct { interceptor.NoOp *PacketDumper } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (s *SenderInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { return interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { s.logRTCPPackets(pkts, attributes) return writer.Write(pkts, attributes) }) } // BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method // will be called once per rtp packet. func (s *SenderInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { s.logRTPPacket(header, payload, attributes) return writer.Write(header, payload, attributes) }) } // Close closes the interceptor func (s *SenderInterceptor) Close() error { return s.PacketDumper.Close() } interceptor-0.1.12/pkg/packetdump/sender_interceptor_test.go000066400000000000000000000045571426573475500243700ustar00rootroot00000000000000package packetdump import ( "bytes" "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestSenderFilterEverythingOut(t *testing.T) { buf := bytes.Buffer{} factory, err := NewSenderInterceptor( RTPWriter(&buf), RTCPWriter(&buf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), RTPFilter(func(pkt *rtp.Packet) bool { return false }), RTCPFilter(func(pkt []rtcp.Packet) bool { return false }), ) assert.NoError(t, err) i, err := factory.NewInterceptor("") assert.NoError(t, err) assert.Zero(t, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() err = stream.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 456, }}) assert.NoError(t, err) err = stream.WriteRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(0), }}) assert.NoError(t, err) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = i.Close() assert.NoError(t, err) // Every packet should have been filtered out – nothing should be written. assert.Zero(t, buf.Len()) } func TestSenderFilterNothing(t *testing.T) { buf := bytes.Buffer{} factory, err := NewSenderInterceptor( RTPWriter(&buf), RTCPWriter(&buf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), RTPFilter(func(pkt *rtp.Packet) bool { return true }), RTCPFilter(func(pkt []rtcp.Packet) bool { return true }), ) assert.NoError(t, err) i, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() err = stream.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 456, }}) assert.NoError(t, err) err = stream.WriteRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(0), }}) assert.NoError(t, err) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = i.Close() assert.NoError(t, err) assert.NotZero(t, buf.Len()) } interceptor-0.1.12/pkg/report/000077500000000000000000000000001426573475500162475ustar00rootroot00000000000000interceptor-0.1.12/pkg/report/receiver_interceptor.go000066400000000000000000000107421426573475500230240ustar00rootroot00000000000000package report import ( "sync" "time" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" ) // ReceiverInterceptorFactory is a interceptor.Factory for a ReceiverInterceptor type ReceiverInterceptorFactory struct { opts []ReceiverOption } // NewInterceptor constructs a new ReceiverInterceptor func (r *ReceiverInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { i := &ReceiverInterceptor{ interval: 1 * time.Second, now: time.Now, log: logging.NewDefaultLoggerFactory().NewLogger("receiver_interceptor"), close: make(chan struct{}), } for _, opt := range r.opts { if err := opt(i); err != nil { return nil, err } } return i, nil } // NewReceiverInterceptor returns a new ReceiverInterceptorFactory func NewReceiverInterceptor(opts ...ReceiverOption) (*ReceiverInterceptorFactory, error) { return &ReceiverInterceptorFactory{opts}, nil } // ReceiverInterceptor interceptor generates receiver reports. type ReceiverInterceptor struct { interceptor.NoOp interval time.Duration now func() time.Time streams sync.Map log logging.LeveledLogger m sync.Mutex wg sync.WaitGroup close chan struct{} } func (r *ReceiverInterceptor) isClosed() bool { select { case <-r.close: return true default: return false } } // Close closes the interceptor. func (r *ReceiverInterceptor) Close() error { defer r.wg.Wait() r.m.Lock() defer r.m.Unlock() if !r.isClosed() { close(r.close) } return nil } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (r *ReceiverInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { r.m.Lock() defer r.m.Unlock() if r.isClosed() { return writer } r.wg.Add(1) go r.loop(writer) return writer } func (r *ReceiverInterceptor) loop(rtcpWriter interceptor.RTCPWriter) { defer r.wg.Done() ticker := time.NewTicker(r.interval) defer ticker.Stop() for { select { case <-ticker.C: now := r.now() r.streams.Range(func(key, value interface{}) bool { if stream, ok := value.(*receiverStream); !ok { r.log.Warnf("failed to cast ReceiverInterceptor stream") } else if _, err := rtcpWriter.Write([]rtcp.Packet{stream.generateReport(now)}, interceptor.Attributes{}); err != nil { r.log.Warnf("failed sending: %+v", err) } return true }) case <-r.close: return } } } // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (r *ReceiverInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { stream := newReceiverStream(info.SSRC, info.ClockRate) r.streams.Store(info.SSRC, stream) return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(b, a) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } header, err := attr.GetRTPHeader(b[:i]) if err != nil { return 0, nil, err } stream.processRTP(r.now(), header) return i, attr, nil }) } // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (r *ReceiverInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { r.streams.Delete(info.SSRC) } // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might // change in the future. The returned method will be called once per packet batch. func (r *ReceiverInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { return interceptor.RTCPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(b, a) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } pkts, err := attr.GetRTCPPackets(b[:i]) if err != nil { return 0, nil, err } for _, pkt := range pkts { if sr, ok := (pkt).(*rtcp.SenderReport); ok { value, ok := r.streams.Load(sr.SSRC) if !ok { continue } if stream, ok := value.(*receiverStream); !ok { r.log.Warnf("failed to cast ReceiverInterceptor stream") } else { stream.processSenderReport(r.now(), sr) } } } return i, attr, nil }) } interceptor-0.1.12/pkg/report/receiver_interceptor_test.go000066400000000000000000000265411426573475500240670ustar00rootroot00000000000000package report import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/ntp" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestReceiverInterceptor(t *testing.T) { t.Run("before any packet", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 0, LastSenderReport: 0, FractionLost: 0, TotalLost: 0, Delay: 0, Jitter: 0, }, rr.Reports[0]) }) rtpTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) t.Run("after RTP packets", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() for i := 0; i < 10; i++ { stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(i), }}) } pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 9, LastSenderReport: 0, FractionLost: 0, TotalLost: 0, Delay: 0, Jitter: 0, }, rr.Reports[0]) }) t.Run("after RTP and RTCP packets", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() for i := 0; i < 10; i++ { stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(i), }}) } now := time.Date(2009, time.November, 10, 23, 0, 1, 0, time.UTC) stream.ReceiveRTCP([]rtcp.Packet{ &rtcp.SenderReport{ SSRC: 123456, NTPTime: ntp.ToNTP(now), RTPTime: 987654321 + uint32(now.Sub(rtpTime).Seconds()*90000), PacketCount: 10, OctetCount: 0, }, }) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 9, LastSenderReport: 1861287936, FractionLost: 0, TotalLost: 0, Delay: rr.Reports[0].Delay, Jitter: 0, }, rr.Reports[0]) }) t.Run("overflow", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0xffff, }}) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0x00, }}) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 1<<16 | 0x0000, LastSenderReport: 0, FractionLost: 0, TotalLost: 0, Delay: 0, Jitter: 0, }, rr.Reports[0]) }) t.Run("packet loss", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0x01, }}) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0x03, }}) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 0x03, LastSenderReport: 0, FractionLost: 256 * 1 / 3, TotalLost: 1, Delay: 0, Jitter: 0, }, rr.Reports[0]) now := time.Date(2009, time.November, 10, 23, 0, 1, 0, time.UTC) stream.ReceiveRTCP([]rtcp.Packet{ &rtcp.SenderReport{ SSRC: 123456, NTPTime: ntp.ToNTP(now), RTPTime: 987654321 + uint32(now.Sub(rtpTime).Seconds()*90000), PacketCount: 10, OctetCount: 0, }, }) pkts = <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok = pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 0x03, LastSenderReport: 1861287936, FractionLost: 0, TotalLost: 1, Delay: rr.Reports[0].Delay, Jitter: 0, }, rr.Reports[0]) }) t.Run("overflow and packet loss", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0xffff, }}) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0x01, }}) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 1<<16 | 0x01, LastSenderReport: 0, FractionLost: 256 * 1 / 3, TotalLost: 1, Delay: 0, Jitter: 0, }, rr.Reports[0]) }) t.Run("reordered packets", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() for _, seqNum := range []uint16{0x01, 0x03, 0x02, 0x04} { stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: seqNum, }}) } pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 0x04, LastSenderReport: 0, FractionLost: 0, TotalLost: 0, Delay: 0, Jitter: 0, }, rr.Reports[0]) }) t.Run("jitter", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() mt.SetNow(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0x01, Timestamp: 42378934, }}) <-stream.ReadRTP() mt.SetNow(time.Date(2009, time.November, 10, 23, 0, 1, 0, time.UTC)) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0x02, Timestamp: 42378934 + 60000, }}) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 0x02, LastSenderReport: 0, FractionLost: 0, TotalLost: 0, Delay: 0, Jitter: 30000 / 16, }, rr.Reports[0]) }) t.Run("delay", func(t *testing.T) { mt := test.MockTime{} f, err := NewReceiverInterceptor( ReceiverInterval(time.Millisecond*50), ReceiverLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ReceiverNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() mt.SetNow(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)) stream.ReceiveRTCP([]rtcp.Packet{ &rtcp.SenderReport{ SSRC: 123456, NTPTime: ntp.ToNTP(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)), RTPTime: 987654321, PacketCount: 0, OctetCount: 0, }, }) <-stream.ReadRTCP() mt.SetNow(time.Date(2009, time.November, 10, 23, 0, 1, 0, time.UTC)) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) rr, ok := pkts[0].(*rtcp.ReceiverReport) assert.True(t, ok) assert.Equal(t, 1, len(rr.Reports)) assert.Equal(t, rtcp.ReceptionReport{ SSRC: uint32(123456), LastSequenceNumber: 0, LastSenderReport: 1861222400, FractionLost: 0, TotalLost: 0, Delay: 65536, Jitter: 0, }, rr.Reports[0]) }) } interceptor-0.1.12/pkg/report/receiver_option.go000066400000000000000000000014071426573475500217740ustar00rootroot00000000000000package report import ( "time" "github.com/pion/logging" ) // ReceiverOption can be used to configure ReceiverInterceptor. type ReceiverOption func(r *ReceiverInterceptor) error // ReceiverLog sets a logger for the interceptor. func ReceiverLog(log logging.LeveledLogger) ReceiverOption { return func(r *ReceiverInterceptor) error { r.log = log return nil } } // ReceiverInterval sets send interval for the interceptor. func ReceiverInterval(interval time.Duration) ReceiverOption { return func(r *ReceiverInterceptor) error { r.interval = interval return nil } } // ReceiverNow sets an alternative for the time.Now function. func ReceiverNow(f func() time.Time) ReceiverOption { return func(r *ReceiverInterceptor) error { r.now = f return nil } } interceptor-0.1.12/pkg/report/receiver_stream.go000066400000000000000000000077641426573475500217730ustar00rootroot00000000000000package report import ( "math/rand" "sync" "time" "github.com/pion/rtcp" "github.com/pion/rtp" ) type receiverStream struct { ssrc uint32 receiverSSRC uint32 clockRate float64 m sync.Mutex size uint16 packets []uint64 started bool seqnumCycles uint16 lastSeqnum uint16 lastReportSeqnum uint16 lastRTPTimeRTP uint32 lastRTPTimeTime time.Time jitter float64 lastSenderReport uint32 lastSenderReportTime time.Time totalLost uint32 } func newReceiverStream(ssrc uint32, clockRate uint32) *receiverStream { receiverSSRC := rand.Uint32() // #nosec return &receiverStream{ ssrc: ssrc, receiverSSRC: receiverSSRC, clockRate: float64(clockRate), size: 128, packets: make([]uint64, 128), } } func (stream *receiverStream) processRTP(now time.Time, pktHeader *rtp.Header) { stream.m.Lock() defer stream.m.Unlock() if !stream.started { // first frame stream.started = true stream.setReceived(pktHeader.SequenceNumber) stream.lastSeqnum = pktHeader.SequenceNumber stream.lastReportSeqnum = pktHeader.SequenceNumber - 1 stream.lastRTPTimeRTP = pktHeader.Timestamp stream.lastRTPTimeTime = now } else { // following frames stream.setReceived(pktHeader.SequenceNumber) diff := int32(pktHeader.SequenceNumber) - int32(stream.lastSeqnum) if diff > 0 || diff < -0x0FFF { // overflow if diff < -0x0FFF { stream.seqnumCycles++ } // set missing packets as missing for i := stream.lastSeqnum + 1; i != pktHeader.SequenceNumber; i++ { stream.delReceived(i) } stream.lastSeqnum = pktHeader.SequenceNumber } // compute jitter // https://tools.ietf.org/html/rfc3550#page-39 D := now.Sub(stream.lastRTPTimeTime).Seconds()*stream.clockRate - (float64(pktHeader.Timestamp) - float64(stream.lastRTPTimeRTP)) if D < 0 { D = -D } stream.jitter += (D - stream.jitter) / 16 stream.lastRTPTimeRTP = pktHeader.Timestamp stream.lastRTPTimeTime = now } } func (stream *receiverStream) setReceived(seq uint16) { pos := seq % stream.size stream.packets[pos/64] |= 1 << (pos % 64) } func (stream *receiverStream) delReceived(seq uint16) { pos := seq % stream.size stream.packets[pos/64] &^= 1 << (pos % 64) } func (stream *receiverStream) getReceived(seq uint16) bool { pos := seq % stream.size return (stream.packets[pos/64] & (1 << (pos % 64))) != 0 } func (stream *receiverStream) processSenderReport(now time.Time, sr *rtcp.SenderReport) { stream.m.Lock() defer stream.m.Unlock() stream.lastSenderReport = uint32(sr.NTPTime >> 16) stream.lastSenderReportTime = now } func (stream *receiverStream) generateReport(now time.Time) *rtcp.ReceiverReport { stream.m.Lock() defer stream.m.Unlock() totalSinceReport := stream.lastSeqnum - stream.lastReportSeqnum totalLostSinceReport := func() uint32 { if stream.lastSeqnum == stream.lastReportSeqnum { return 0 } ret := uint32(0) for i := stream.lastReportSeqnum + 1; i != stream.lastSeqnum; i++ { if !stream.getReceived(i) { ret++ } } return ret }() stream.totalLost += totalLostSinceReport // allow up to 24 bits if totalLostSinceReport > 0xFFFFFF { totalLostSinceReport = 0xFFFFFF } if stream.totalLost > 0xFFFFFF { stream.totalLost = 0xFFFFFF } r := &rtcp.ReceiverReport{ SSRC: stream.receiverSSRC, Reports: []rtcp.ReceptionReport{ { SSRC: stream.ssrc, LastSequenceNumber: uint32(stream.seqnumCycles)<<16 | uint32(stream.lastSeqnum), LastSenderReport: stream.lastSenderReport, FractionLost: uint8(float64(totalLostSinceReport*256) / float64(totalSinceReport)), TotalLost: stream.totalLost, Delay: func() uint32 { if stream.lastSenderReportTime.IsZero() { return 0 } return uint32(now.Sub(stream.lastSenderReportTime).Seconds() * 65536) }(), Jitter: uint32(stream.jitter), }, }, } stream.lastReportSeqnum = stream.lastSeqnum return r } interceptor-0.1.12/pkg/report/report.go000066400000000000000000000001511426573475500201060ustar00rootroot00000000000000// Package report provides interceptors to implement sending sender and receiver reports. package report interceptor-0.1.12/pkg/report/sender_interceptor.go000066400000000000000000000060351426573475500225000ustar00rootroot00000000000000package report import ( "sync" "time" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" ) // SenderInterceptorFactory is a interceptor.Factory for a SenderInterceptor type SenderInterceptorFactory struct { opts []SenderOption } // NewInterceptor constructs a new SenderInterceptor func (s *SenderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { i := &SenderInterceptor{ interval: 1 * time.Second, now: time.Now, log: logging.NewDefaultLoggerFactory().NewLogger("sender_interceptor"), close: make(chan struct{}), } for _, opt := range s.opts { if err := opt(i); err != nil { return nil, err } } return i, nil } // NewSenderInterceptor returns a new SenderInterceptorFactory func NewSenderInterceptor(opts ...SenderOption) (*SenderInterceptorFactory, error) { return &SenderInterceptorFactory{opts}, nil } // SenderInterceptor interceptor generates sender reports. type SenderInterceptor struct { interceptor.NoOp interval time.Duration now func() time.Time streams sync.Map log logging.LeveledLogger m sync.Mutex wg sync.WaitGroup close chan struct{} } func (s *SenderInterceptor) isClosed() bool { select { case <-s.close: return true default: return false } } // Close closes the interceptor. func (s *SenderInterceptor) Close() error { defer s.wg.Wait() s.m.Lock() defer s.m.Unlock() if !s.isClosed() { close(s.close) } return nil } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (s *SenderInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { s.m.Lock() defer s.m.Unlock() if s.isClosed() { return writer } s.wg.Add(1) go s.loop(writer) return writer } func (s *SenderInterceptor) loop(rtcpWriter interceptor.RTCPWriter) { defer s.wg.Done() ticker := time.NewTicker(s.interval) defer ticker.Stop() for { select { case <-ticker.C: now := s.now() s.streams.Range(func(key, value interface{}) bool { if stream, ok := value.(*senderStream); !ok { s.log.Warnf("failed to cast SenderInterceptor stream") } else if _, err := rtcpWriter.Write([]rtcp.Packet{stream.generateReport(now)}, interceptor.Attributes{}); err != nil { s.log.Warnf("failed sending: %+v", err) } return true }) case <-s.close: return } } } // BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method // will be called once per rtp packet. func (s *SenderInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { stream := newSenderStream(info.SSRC, info.ClockRate) s.streams.Store(info.SSRC, stream) return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, a interceptor.Attributes) (int, error) { stream.processRTP(s.now(), header, payload) return writer.Write(header, payload, a) }) } interceptor-0.1.12/pkg/report/sender_interceptor_test.go000066400000000000000000000043031426573475500235330ustar00rootroot00000000000000package report import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/ntp" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestSenderInterceptor(t *testing.T) { t.Run("before any packet", func(t *testing.T) { mt := &test.MockTime{} f, err := NewSenderInterceptor( SenderInterval(time.Millisecond*50), SenderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), SenderNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() mt.SetNow(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) sr, ok := pkts[0].(*rtcp.SenderReport) assert.True(t, ok) assert.Equal(t, &rtcp.SenderReport{ SSRC: 123456, NTPTime: ntp.ToNTP(mt.Now()), RTPTime: 2269117121, PacketCount: 0, OctetCount: 0, }, sr) }) t.Run("after RTP packets", func(t *testing.T) { mt := &test.MockTime{} f, err := NewSenderInterceptor( SenderInterval(time.Millisecond*50), SenderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), SenderNow(mt.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, i) defer func() { assert.NoError(t, stream.Close()) }() for i := 0; i < 10; i++ { assert.NoError(t, stream.WriteRTP(&rtp.Packet{ Header: rtp.Header{SequenceNumber: uint16(i)}, Payload: []byte("\x00\x00"), })) } mt.SetNow(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) sr, ok := pkts[0].(*rtcp.SenderReport) assert.True(t, ok) assert.Equal(t, &rtcp.SenderReport{ SSRC: 123456, NTPTime: ntp.ToNTP(mt.Now()), RTPTime: 2269117121, PacketCount: 10, OctetCount: 20, }, sr) }) } interceptor-0.1.12/pkg/report/sender_option.go000066400000000000000000000013471426573475500214530ustar00rootroot00000000000000package report import ( "time" "github.com/pion/logging" ) // SenderOption can be used to configure SenderInterceptor. type SenderOption func(r *SenderInterceptor) error // SenderLog sets a logger for the interceptor. func SenderLog(log logging.LeveledLogger) SenderOption { return func(r *SenderInterceptor) error { r.log = log return nil } } // SenderInterval sets send interval for the interceptor. func SenderInterval(interval time.Duration) SenderOption { return func(r *SenderInterceptor) error { r.interval = interval return nil } } // SenderNow sets an alternative for the time.Now function. func SenderNow(f func() time.Time) SenderOption { return func(r *SenderInterceptor) error { r.now = f return nil } } interceptor-0.1.12/pkg/report/sender_stream.go000066400000000000000000000022701426573475500214320ustar00rootroot00000000000000package report import ( "sync" "time" "github.com/pion/interceptor/internal/ntp" "github.com/pion/rtcp" "github.com/pion/rtp" ) type senderStream struct { ssrc uint32 clockRate float64 m sync.Mutex // data from rtp packets lastRTPTimeRTP uint32 lastRTPTimeTime time.Time packetCount uint32 octetCount uint32 } func newSenderStream(ssrc uint32, clockRate uint32) *senderStream { return &senderStream{ ssrc: ssrc, clockRate: float64(clockRate), } } func (stream *senderStream) processRTP(now time.Time, header *rtp.Header, payload []byte) { stream.m.Lock() defer stream.m.Unlock() // always update time to minimize errors stream.lastRTPTimeRTP = header.Timestamp stream.lastRTPTimeTime = now stream.packetCount++ stream.octetCount += uint32(len(payload)) } func (stream *senderStream) generateReport(now time.Time) *rtcp.SenderReport { stream.m.Lock() defer stream.m.Unlock() return &rtcp.SenderReport{ SSRC: stream.ssrc, NTPTime: ntp.ToNTP(now), RTPTime: stream.lastRTPTimeRTP + uint32(now.Sub(stream.lastRTPTimeTime).Seconds()*stream.clockRate), PacketCount: stream.packetCount, OctetCount: stream.octetCount, } } interceptor-0.1.12/pkg/rfc8888/000077500000000000000000000000001426573475500160465ustar00rootroot00000000000000interceptor-0.1.12/pkg/rfc8888/interceptor.go000066400000000000000000000105761426573475500207440ustar00rootroot00000000000000// Package rfc8888 provides an interceptor that generates congestion control // feedback reports as defined by RFC 8888. package rfc8888 import ( "sync" "time" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" ) // TickerFactory is a factory to create new tickers type TickerFactory func(d time.Duration) ticker // SenderInterceptorFactory is a interceptor.Factory for a SenderInterceptor type SenderInterceptorFactory struct { opts []Option } // NewInterceptor constructs a new SenderInterceptor func (s *SenderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { i := &SenderInterceptor{ NoOp: interceptor.NoOp{}, log: logging.NewDefaultLoggerFactory().NewLogger("rfc8888_interceptor"), lock: sync.Mutex{}, wg: sync.WaitGroup{}, recorder: NewRecorder(), interval: 100 * time.Millisecond, maxReportSize: 1200, packetChan: make(chan packet), newTicker: func(d time.Duration) ticker { return &timeTicker{time.NewTicker(d)} }, now: time.Now, close: make(chan struct{}), } for _, opt := range s.opts { err := opt(i) if err != nil { return nil, err } } return i, nil } // NewSenderInterceptor returns a new SenderInterceptorFactory configured with the given options. func NewSenderInterceptor(opts ...Option) (*SenderInterceptorFactory, error) { return &SenderInterceptorFactory{opts: opts}, nil } // SenderInterceptor sends congestion control feedback as specified in RFC 8888. type SenderInterceptor struct { interceptor.NoOp log logging.LeveledLogger lock sync.Mutex wg sync.WaitGroup recorder *Recorder interval time.Duration maxReportSize int64 packetChan chan packet newTicker TickerFactory now func() time.Time close chan struct{} } type packet struct { arrival time.Time ssrc uint32 sequenceNumber uint16 ecn uint8 } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (s *SenderInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { s.lock.Lock() defer s.lock.Unlock() if s.isClosed() { return writer } s.wg.Add(1) go s.loop(writer) return writer } // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (s *SenderInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(b, a) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } header, err := attr.GetRTPHeader(b[:i]) if err != nil { return 0, nil, err } p := packet{ arrival: s.now(), ssrc: header.SSRC, sequenceNumber: header.SequenceNumber, ecn: 0, // ECN is not supported (yet). } s.packetChan <- p return i, attr, nil }) } // Close closes the interceptor. func (s *SenderInterceptor) Close() error { s.log.Trace("close") defer s.wg.Wait() if !s.isClosed() { close(s.close) } return nil } func (s *SenderInterceptor) isClosed() bool { select { case <-s.close: return true default: return false } } func (s *SenderInterceptor) loop(writer interceptor.RTCPWriter) { defer s.wg.Done() select { case <-s.close: return case pkt := <-s.packetChan: s.log.Tracef("got first packet: %v", pkt) s.recorder.AddPacket(pkt.arrival, pkt.ssrc, pkt.sequenceNumber, pkt.ecn) } s.log.Trace("start loop") t := s.newTicker(s.interval) for { select { case <-s.close: t.Stop() return case pkt := <-s.packetChan: s.log.Tracef("got packet: %v", pkt) s.recorder.AddPacket(pkt.arrival, pkt.ssrc, pkt.sequenceNumber, pkt.ecn) case now := <-t.Ch(): s.log.Tracef("report triggered at %v", now) if writer == nil { s.log.Trace("no writer added, continue") continue } pkts := s.recorder.BuildReport(now, int(s.maxReportSize)) if pkts == nil { continue } s.log.Tracef("got report: %v", pkts) if _, err := writer.Write([]rtcp.Packet{pkts}, nil); err != nil { s.log.Error(err.Error()) } } } } interceptor-0.1.12/pkg/rfc8888/interceptor_test.go000066400000000000000000000146411426573475500220000ustar00rootroot00000000000000package rfc8888 import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestInterceptor(t *testing.T) { t.Run("before any packet", func(t *testing.T) { f, err := NewSenderInterceptor() assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, }, i) defer func() { assert.NoError(t, stream.Close()) }() var pkts []rtcp.Packet select { case pkts = <-stream.WrittenRTCP(): case <-time.After(300 * time.Millisecond): } assert.Equal(t, len(pkts), 0) }) t.Run("after RTP packets", func(t *testing.T) { f, err := NewSenderInterceptor() assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, }, i) defer func() { assert.NoError(t, stream.Close()) }() for i := 0; i < 10; i++ { stream.ReceiveRTP(&rtp.Packet{ Header: rtp.Header{ Version: 0, Padding: false, Extension: false, Marker: false, PayloadType: 0, SequenceNumber: uint16(i), Timestamp: 0, SSRC: 123456, CSRC: []uint32{}, ExtensionProfile: 0, Extensions: []rtp.Extension{}, }, Payload: []byte{}, PaddingSize: 0, }) } pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) fb, ok := pkts[0].(*rtcp.CCFeedbackReport) assert.True(t, ok) assert.Equal(t, 1, len(fb.ReportBlocks)) assert.Equal(t, uint32(123456), fb.ReportBlocks[0].MediaSSRC) assert.Equal(t, 10, len(fb.ReportBlocks[0].MetricBlocks)) }) t.Run("different delays between RTP packets", func(t *testing.T) { mNow := &test.MockTime{} mTick := &test.MockTicker{ C: make(chan time.Time), } f, err := NewSenderInterceptor( SenderTicker(func(d time.Duration) ticker { return mTick }), SenderNow(mNow.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, }, i) defer func() { assert.NoError(t, stream.Close()) }() zero := time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC) delays := []time.Duration{ 0, 250 * time.Millisecond, 500 * time.Millisecond, time.Second, } for i, d := range delays { mNow.SetNow(zero.Add(d)) stream.ReceiveRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), SSRC: 123456, }, }) select { case r := <-stream.ReadRTP(): assert.NoError(t, r.Err) case <-time.After(10 * time.Millisecond): t.Fatal("receiver rtp packet not found") } } mTick.Tick(zero.Add(time.Second)) pkts := <-stream.WrittenRTCP() assert.Equal(t, 1, len(pkts)) ccfb, ok := pkts[0].(*rtcp.CCFeedbackReport) assert.True(t, ok) assert.Equal(t, uint32(1<<16), ccfb.ReportTimestamp) assert.Equal(t, 1, len(ccfb.ReportBlocks)) assert.Equal(t, uint32(123456), ccfb.ReportBlocks[0].MediaSSRC) assert.Equal(t, 4, len(ccfb.ReportBlocks[0].MetricBlocks)) assert.Equal(t, uint16(0), ccfb.ReportBlocks[0].BeginSequence) assert.Equal(t, []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: true, ECN: 0, ArrivalTimeOffset: 512 + 256, }, { Received: true, ECN: 0, ArrivalTimeOffset: 512, }, { Received: true, ECN: 0, ArrivalTimeOffset: 0, }, }, ccfb.ReportBlocks[0].MetricBlocks) }) t.Run("packet loss", func(t *testing.T) { mNow := &test.MockTime{} mTick := &test.MockTicker{ C: make(chan time.Time), } f, err := NewSenderInterceptor( SenderTicker(func(d time.Duration) ticker { return mTick }), SenderNow(mNow.Now), ) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, }, i) defer func() { assert.NoError(t, stream.Close()) }() zero := time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC) sequenceNumberToDelay := map[int]int{ 0: 0, 1: 125, 4: 250, 8: 500, 9: 750, 10: 1000, } for i := 0; i <= 10; i++ { if _, ok := sequenceNumberToDelay[i]; !ok { continue } mNow.SetNow(zero.Add(time.Duration(sequenceNumberToDelay[i]) * time.Millisecond)) stream.ReceiveRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), SSRC: 123456, }, }) select { case r := <-stream.ReadRTP(): assert.NoError(t, r.Err) case <-time.After(10 * time.Millisecond): t.Fatal("receiver rtp packet not found") } } mTick.Tick(zero.Add(time.Second)) pkts := <-stream.WrittenRTCP() assert.Equal(t, 1, len(pkts)) ccfb, ok := pkts[0].(*rtcp.CCFeedbackReport) assert.True(t, ok) assert.Equal(t, uint32(1<<16), ccfb.ReportTimestamp) assert.Equal(t, 1, len(ccfb.ReportBlocks)) assert.Equal(t, uint32(123456), ccfb.ReportBlocks[0].MediaSSRC) assert.Equal(t, 11, len(ccfb.ReportBlocks[0].MetricBlocks)) assert.Equal(t, uint16(0), ccfb.ReportBlocks[0].BeginSequence) assert.Equal(t, []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 128, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 256, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: true, ECN: 0, ArrivalTimeOffset: 512, }, { Received: true, ECN: 0, ArrivalTimeOffset: 256, }, { Received: true, ECN: 0, ArrivalTimeOffset: 0, }, }, ccfb.ReportBlocks[0].MetricBlocks) }) } interceptor-0.1.12/pkg/rfc8888/option.go000066400000000000000000000013101426573475500177000ustar00rootroot00000000000000package rfc8888 import "time" // An Option is a function that can be used to configure a SenderInterceptor type Option func(*SenderInterceptor) error // SenderTicker sets an alternative for time.Ticker. func SenderTicker(f TickerFactory) Option { return func(i *SenderInterceptor) error { i.newTicker = f return nil } } // SenderNow sets an alternative for the time.Now function. func SenderNow(f func() time.Time) Option { return func(i *SenderInterceptor) error { i.now = f return nil } } // SendInterval sets the feedback send interval for the interceptor func SendInterval(interval time.Duration) Option { return func(s *SenderInterceptor) error { s.interval = interval return nil } } interceptor-0.1.12/pkg/rfc8888/recorder.go000066400000000000000000000040041426573475500202000ustar00rootroot00000000000000package rfc8888 import ( "time" "github.com/pion/rtcp" ) type packetReport struct { arrivalTime time.Time ecn uint8 } // Recorder records incoming RTP packets and their arrival times. Recorder can // be used to create feedback reports as defined by RFC 8888. type Recorder struct { ssrc uint32 streams map[uint32]*streamLog } // NewRecorder creates a new Recorder func NewRecorder() *Recorder { return &Recorder{ streams: map[uint32]*streamLog{}, } } // AddPacket writes a packet to the underlying stream. func (r *Recorder) AddPacket(ts time.Time, ssrc uint32, seq uint16, ecn uint8) { stream, ok := r.streams[ssrc] if !ok { stream = newStreamLog(ssrc) r.streams[ssrc] = stream } stream.add(ts, seq, ecn) } // BuildReport creates a new rtcp.CCFeedbackReport containing all packets that // were added by AddPacket and missing packets. func (r *Recorder) BuildReport(now time.Time, maxSize int) *rtcp.CCFeedbackReport { report := &rtcp.CCFeedbackReport{ SenderSSRC: r.ssrc, ReportBlocks: []rtcp.CCFeedbackReportBlock{}, ReportTimestamp: ntpTime32(now), } maxReportBlocks := (maxSize - 12 - (8 * len(r.streams))) / 2 var maxReportBlocksPerStream int if len(r.streams) > 1 { maxReportBlocksPerStream = maxReportBlocks / (len(r.streams) - 1) } else { maxReportBlocksPerStream = maxReportBlocks } for i, log := range r.streams { if len(r.streams) > 1 && int(i) == len(r.streams)-1 { maxReportBlocksPerStream = maxReportBlocks % len(r.streams) } block := log.metricsAfter(now, int64(maxReportBlocksPerStream)) report.ReportBlocks = append(report.ReportBlocks, block) } return report } func ntpTime32(t time.Time) uint32 { // seconds since 1st January 1900 s := (float64(t.UnixNano()) / 1000000000.0) + 2208988800 integerPart := uint32(s) fractionalPart := uint32((s - float64(integerPart)) * 0xFFFFFFFF) // higher 32 bits are the integer part, lower 32 bits are the fractional part return uint32(((uint64(integerPart)<<32 | uint64(fractionalPart)) >> 16) & 0xFFFFFFFF) } interceptor-0.1.12/pkg/rfc8888/recorder_test.go000066400000000000000000000111661426573475500212460ustar00rootroot00000000000000package rfc8888 import ( "fmt" "testing" "time" "github.com/pion/rtcp" "github.com/stretchr/testify/assert" ) func TestGetArrivalTimeOffset(t *testing.T) { for _, test := range []struct { base time.Time arrival time.Time want uint16 }{ { base: time.Time{}.Add(time.Second), arrival: time.Time{}, want: 1024, }, { base: time.Time{}.Add(500 * time.Millisecond), arrival: time.Time{}, want: 512, }, { base: time.Time{}.Add(8 * time.Second), arrival: time.Time{}, want: 0x1FFE, }, { base: time.Time{}, arrival: time.Time{}.Add(time.Second), want: 0x1FFF, }, } { assert.Equal(t, test.want, getArrivalTimeOffset(test.base, test.arrival)) } } func TestRecorder(t *testing.T) { t.Run("normal", func(t *testing.T) { recorder := NewRecorder() now := time.Time{} recorder.AddPacket(now, 123456, 0, 0) recorder.AddPacket(now.Add(125*time.Millisecond), 123456, 1, 0) recorder.AddPacket(now.Add(250*time.Millisecond), 123456, 2, 0) recorder.AddPacket(now.Add(500*time.Millisecond), 123456, 3, 0) recorder.AddPacket(now.Add(625*time.Millisecond), 123456, 4, 0) recorder.AddPacket(now.Add(750*time.Millisecond), 123456, 5, 0) report := recorder.BuildReport(now.Add(time.Second), 1500) assert.Equal(t, 1, len(report.ReportBlocks)) assert.Equal(t, rtcp.CCFeedbackReportBlock{ MediaSSRC: 123456, BeginSequence: 0, MetricBlocks: []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 128, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 256, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 512, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 640, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 768, }, }, }, report.ReportBlocks[0]) }) t.Run("packet loss", func(t *testing.T) { recorder := NewRecorder() now := time.Time{} recorder.AddPacket(now, 123456, 0, 0) recorder.AddPacket(now.Add(250*time.Millisecond), 123456, 2, 0) recorder.AddPacket(now.Add(625*time.Millisecond), 123456, 4, 0) recorder.AddPacket(now.Add(750*time.Millisecond), 123456, 5, 0) report := recorder.BuildReport(now.Add(time.Second), 1500) assert.Equal(t, 1, len(report.ReportBlocks)) assert.Equal(t, 6, len(report.ReportBlocks[0].MetricBlocks)) assert.Equal(t, rtcp.CCFeedbackReportBlock{ MediaSSRC: 123456, BeginSequence: 0, MetricBlocks: []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 256, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 640, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1024 - 768, }, }, }, report.ReportBlocks[0]) }) t.Run("MaxreportsPerStream", func(t *testing.T) { recorder := NewRecorder() now := time.Time{} // Add 1000 packets on 10 different streams for i := 0; i < 10; i++ { for j := 0; j < 100; j++ { recorder.AddPacket(now, uint32(i), uint16(j), 0) } } reports := recorder.BuildReport(time.Time{}, 1380) for i := 0; i < 10; i++ { assert.Greater(t, 72, len(reports.ReportBlocks[i].MetricBlocks)) assert.Less(t, 3, len(reports.ReportBlocks[i].MetricBlocks)) } }) } func TestNTPTime32(t *testing.T) { zero := time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC) notSoLongAgo := time.Date(2022, time.May, 5, 14, 48, 20, 0, time.UTC) for i, cc := range []struct { input time.Time expected uint32 }{ { input: zero, expected: 0, }, { input: zero.Add(time.Second), expected: 1 << 16, }, { input: notSoLongAgo, expected: uint32(int(notSoLongAgo.Sub(zero).Seconds())&0xffff) << 16, }, { input: zero.Add(400 * time.Millisecond), expected: 26214, }, { input: zero.Add(1400 * time.Millisecond), expected: 1<<16 + 26214, }, } { t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { res := ntpTime32(cc.input) assert.Equalf(t, cc.expected, res, "%b != %b", cc.expected, res) }) } } interceptor-0.1.12/pkg/rfc8888/stream_log.go000066400000000000000000000056221426573475500205360ustar00rootroot00000000000000package rfc8888 import ( "time" "github.com/pion/rtcp" ) const maxReportsPerReportBlock = 16384 type streamLog struct { ssrc uint32 sequence unwrapper init bool nextSequenceNumberToReport int64 // next to report lastSequenceNumberReceived int64 // highest received log map[int64]*packetReport } func newStreamLog(ssrc uint32) *streamLog { return &streamLog{ ssrc: ssrc, sequence: unwrapper{}, init: false, nextSequenceNumberToReport: 0, lastSequenceNumberReceived: 0, log: map[int64]*packetReport{}, } } func (l *streamLog) add(ts time.Time, sequenceNumber uint16, ecn uint8) { unwrappedSequenceNumber := l.sequence.unwrap(sequenceNumber) if !l.init { l.init = true l.nextSequenceNumberToReport = unwrappedSequenceNumber } l.log[unwrappedSequenceNumber] = &packetReport{ arrivalTime: ts, ecn: ecn, } if l.lastSequenceNumberReceived < unwrappedSequenceNumber { l.lastSequenceNumberReceived = unwrappedSequenceNumber } } // metricsAfter iterates over all packets order of their sequence number. // Packets are removed until the first loss is detected. func (l *streamLog) metricsAfter(reference time.Time, maxReportBlocks int64) rtcp.CCFeedbackReportBlock { if len(l.log) == 0 { return rtcp.CCFeedbackReportBlock{ MediaSSRC: l.ssrc, BeginSequence: uint16(l.nextSequenceNumberToReport), MetricBlocks: []rtcp.CCFeedbackMetricBlock{}, } } numReports := l.lastSequenceNumberReceived - l.nextSequenceNumberToReport + 1 if numReports > maxReportBlocks { numReports = maxReportBlocks l.nextSequenceNumberToReport = l.lastSequenceNumberReceived - maxReportBlocks + 1 } metricBlocks := make([]rtcp.CCFeedbackMetricBlock, numReports) offset := l.nextSequenceNumberToReport lastReceived := l.nextSequenceNumberToReport gapDetected := false for i := offset; i <= l.lastSequenceNumberReceived; i++ { received := false ecn := uint8(0) ato := uint16(0) if report, ok := l.log[i]; ok { received = true ecn = report.ecn ato = getArrivalTimeOffset(reference, report.arrivalTime) } metricBlocks[i-offset] = rtcp.CCFeedbackMetricBlock{ Received: received, ECN: rtcp.ECN(ecn), ArrivalTimeOffset: ato, } if !gapDetected { if received && i == l.nextSequenceNumberToReport { delete(l.log, i) l.nextSequenceNumberToReport++ lastReceived = i } if i > lastReceived+1 { gapDetected = true } } } return rtcp.CCFeedbackReportBlock{ MediaSSRC: l.ssrc, BeginSequence: uint16(offset), MetricBlocks: metricBlocks, } } func getArrivalTimeOffset(base time.Time, arrival time.Time) uint16 { if base.Before(arrival) { return 0x1FFF } ato := uint16(base.Sub(arrival).Seconds() * 1024.0) if ato > 0x1FFD { return 0x1FFE } return ato } interceptor-0.1.12/pkg/rfc8888/stream_log_test.go000066400000000000000000000276721426573475500216060ustar00rootroot00000000000000package rfc8888 import ( "testing" "time" "github.com/pion/rtcp" "github.com/stretchr/testify/assert" ) type input struct { ts time.Time nr uint16 ecn uint8 } func TestStreamLogAdd(t *testing.T) { tests := []struct { name string inputs []input expectedNext int64 expectedLast int64 expectedLog map[int64]*packetReport }{ { name: "emptyLog", inputs: []input{}, expectedNext: 0, expectedLast: 0, expectedLog: map[int64]*packetReport{}, }, //nolint { name: "addInOrderSequence", inputs: []input{ { ts: time.Time{}, nr: 0, ecn: 0, }, { ts: time.Time{}.Add(10 * time.Millisecond), nr: 1, ecn: 0, }, { ts: time.Time{}.Add(20 * time.Millisecond), nr: 2, ecn: 0, }, { ts: time.Time{}.Add(30 * time.Millisecond), nr: 3, ecn: 0, }, }, expectedNext: 0, expectedLast: 3, expectedLog: map[int64]*packetReport{ 0: { arrivalTime: time.Time{}, ecn: 0, }, 1: { arrivalTime: time.Time{}.Add(10 * time.Millisecond), ecn: 0, }, 2: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 3: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, }, }, //nolint { name: "reorderedSequence", inputs: []input{ { ts: time.Time{}, nr: 0, ecn: 0, }, { ts: time.Time{}.Add(10 * time.Millisecond), nr: 2, ecn: 0, }, { ts: time.Time{}.Add(20 * time.Millisecond), nr: 1, ecn: 0, }, { ts: time.Time{}.Add(30 * time.Millisecond), nr: 3, ecn: 0, }, }, expectedNext: 0, expectedLast: 3, expectedLog: map[int64]*packetReport{ 0: { arrivalTime: time.Time{}, ecn: 0, }, 1: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 2: { arrivalTime: time.Time{}.Add(10 * time.Millisecond), ecn: 0, }, 3: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, }, }, { name: "reorderedWrappingSequence", inputs: []input{ { ts: time.Time{}, nr: 65534, ecn: 0, }, { ts: time.Time{}.Add(10 * time.Millisecond), nr: 0, ecn: 0, }, { ts: time.Time{}.Add(20 * time.Millisecond), nr: 65535, ecn: 0, }, { ts: time.Time{}.Add(30 * time.Millisecond), nr: 2, ecn: 0, }, { ts: time.Time{}.Add(40 * time.Millisecond), nr: 1, ecn: 0, }, { ts: time.Time{}.Add(50 * time.Millisecond), nr: 3, ecn: 0, }, }, expectedNext: 65534, expectedLast: 65539, expectedLog: map[int64]*packetReport{ 65534: { arrivalTime: time.Time{}, ecn: 0, }, 65535: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 65536: { arrivalTime: time.Time{}.Add(10 * time.Millisecond), ecn: 0, }, 65537: { arrivalTime: time.Time{}.Add(40 * time.Millisecond), ecn: 0, }, 65538: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, 65539: { arrivalTime: time.Time{}.Add(50 * time.Millisecond), ecn: 0, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { sl := newStreamLog(0) for _, input := range test.inputs { sl.add(input.ts, input.nr, input.ecn) } assert.Equal(t, test.expectedNext, sl.nextSequenceNumberToReport) assert.Equal(t, test.expectedLast, sl.lastSequenceNumberReceived) assert.Equal(t, test.expectedLog, sl.log) }) } } func TestStreamLogMetricsAfter(t *testing.T) { tests := []struct { name string inputs []input expectedLast int64 expectedNextBefore int64 expectedLogBefore map[int64]*packetReport expectedNextAfter int64 expectedLogAfter map[int64]*packetReport expectedMetrics rtcp.CCFeedbackReportBlock }{ { name: "emptyLog", inputs: []input{}, expectedNextBefore: 0, expectedLast: 0, expectedLogBefore: map[int64]*packetReport{}, expectedNextAfter: 0, expectedLogAfter: map[int64]*packetReport{}, expectedMetrics: rtcp.CCFeedbackReportBlock{ MediaSSRC: 0, BeginSequence: 0, MetricBlocks: []rtcp.CCFeedbackMetricBlock{}, }, }, //nolint { name: "addInOrderSequence", inputs: []input{ { ts: time.Time{}, nr: 0, ecn: 0, }, { ts: time.Time{}.Add(10 * time.Millisecond), nr: 1, ecn: 0, }, { ts: time.Time{}.Add(20 * time.Millisecond), nr: 2, ecn: 0, }, { ts: time.Time{}.Add(30 * time.Millisecond), nr: 3, ecn: 0, }, }, expectedNextBefore: 0, expectedLast: 3, expectedLogBefore: map[int64]*packetReport{ 0: { arrivalTime: time.Time{}, ecn: 0, }, 1: { arrivalTime: time.Time{}.Add(10 * time.Millisecond), ecn: 0, }, 2: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 3: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, }, expectedNextAfter: 4, expectedLogAfter: map[int64]*packetReport{}, expectedMetrics: rtcp.CCFeedbackReportBlock{ MetricBlocks: []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1013, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1003, }, { Received: true, ECN: 0, ArrivalTimeOffset: 993, }, }, }, }, //nolint { name: "reorderedSequence", inputs: []input{ { ts: time.Time{}, nr: 0, ecn: 0, }, { ts: time.Time{}.Add(10 * time.Millisecond), nr: 2, ecn: 0, }, { ts: time.Time{}.Add(20 * time.Millisecond), nr: 1, ecn: 0, }, { ts: time.Time{}.Add(30 * time.Millisecond), nr: 3, ecn: 0, }, }, expectedNextBefore: 0, expectedLast: 3, expectedLogBefore: map[int64]*packetReport{ 0: { arrivalTime: time.Time{}, ecn: 0, }, 1: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 2: { arrivalTime: time.Time{}.Add(10 * time.Millisecond), ecn: 0, }, 3: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, }, expectedNextAfter: 4, expectedLogAfter: map[int64]*packetReport{}, expectedMetrics: rtcp.CCFeedbackReportBlock{ MetricBlocks: []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1003, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1013, }, { Received: true, ECN: 0, ArrivalTimeOffset: 993, }, }, }, }, { name: "reorderedWrappingSequence", inputs: []input{ { ts: time.Time{}, nr: 65534, ecn: 0, }, { ts: time.Time{}.Add(10 * time.Millisecond), nr: 0, ecn: 0, }, { ts: time.Time{}.Add(20 * time.Millisecond), nr: 65535, ecn: 0, }, { ts: time.Time{}.Add(30 * time.Millisecond), nr: 2, ecn: 0, }, { ts: time.Time{}.Add(40 * time.Millisecond), nr: 1, ecn: 0, }, { ts: time.Time{}.Add(50 * time.Millisecond), nr: 3, ecn: 0, }, }, expectedNextBefore: 65534, expectedLast: 65539, expectedLogBefore: map[int64]*packetReport{ 65534: { arrivalTime: time.Time{}, ecn: 0, }, 65535: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 65536: { arrivalTime: time.Time{}.Add(10 * time.Millisecond), ecn: 0, }, 65537: { arrivalTime: time.Time{}.Add(40 * time.Millisecond), ecn: 0, }, 65538: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, 65539: { arrivalTime: time.Time{}.Add(50 * time.Millisecond), ecn: 0, }, }, expectedNextAfter: 65540, expectedLogAfter: map[int64]*packetReport{}, expectedMetrics: rtcp.CCFeedbackReportBlock{ BeginSequence: 65534, MetricBlocks: []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1003, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1013, }, { Received: true, ECN: 0, ArrivalTimeOffset: 983, }, { Received: true, ECN: 0, ArrivalTimeOffset: 993, }, { Received: true, ECN: 0, ArrivalTimeOffset: 972, }, }, }, }, { name: "addMissingPacketSequence", inputs: []input{ { ts: time.Time{}, nr: 0, ecn: 0, }, { ts: time.Time{}.Add(20 * time.Millisecond), nr: 2, ecn: 0, }, { ts: time.Time{}.Add(30 * time.Millisecond), nr: 3, ecn: 0, }, }, expectedNextBefore: 0, expectedLast: 3, expectedLogBefore: map[int64]*packetReport{ 0: { arrivalTime: time.Time{}, ecn: 0, }, 2: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 3: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, }, expectedNextAfter: 1, expectedLogAfter: map[int64]*packetReport{ 2: { arrivalTime: time.Time{}.Add(20 * time.Millisecond), ecn: 0, }, 3: { arrivalTime: time.Time{}.Add(30 * time.Millisecond), ecn: 0, }, }, expectedMetrics: rtcp.CCFeedbackReportBlock{ MetricBlocks: []rtcp.CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 1024, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: true, ECN: 0, ArrivalTimeOffset: 1003, }, { Received: true, ECN: 0, ArrivalTimeOffset: 993, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { sl := newStreamLog(0) for _, input := range test.inputs { sl.add(input.ts, input.nr, input.ecn) } assert.Equal(t, test.expectedNextBefore, sl.nextSequenceNumberToReport) assert.Equal(t, test.expectedLast, sl.lastSequenceNumberReceived) assert.Equal(t, test.expectedLogBefore, sl.log) metrics := sl.metricsAfter(time.Time{}.Add(time.Second), 500) assert.Equal(t, test.expectedNextAfter, sl.nextSequenceNumberToReport) assert.Equal(t, test.expectedLast, sl.lastSequenceNumberReceived) assert.Equal(t, test.expectedLogAfter, sl.log) assert.Equal(t, test.expectedMetrics, metrics) }) } } func TestRemoveOldestPackets(t *testing.T) { sl := newStreamLog(0) sl.add(time.Time{}.Add(time.Second), 1, 0) now := time.Now().Add(10 * time.Second) for i := 2; i < 16386; i++ { now = now.Add(10 * time.Millisecond) sl.add(now, uint16(i), 0) } metrics := sl.metricsAfter(now, maxReportsPerReportBlock) assert.Equal(t, uint16(2), metrics.BeginSequence) assert.Lenf(t, metrics.MetricBlocks, 16384, "%v != %v", len(metrics.MetricBlocks), 16384) } interceptor-0.1.12/pkg/rfc8888/ticker.go000066400000000000000000000002771426573475500176640ustar00rootroot00000000000000package rfc8888 import "time" type ticker interface { Ch() <-chan time.Time Stop() } type timeTicker struct { *time.Ticker } func (t *timeTicker) Ch() <-chan time.Time { return t.C } interceptor-0.1.12/pkg/rfc8888/unwrapper.go000066400000000000000000000014731426573475500204250ustar00rootroot00000000000000package rfc8888 const ( maxSequenceNumberPlusOne = int64(65536) breakpoint = 32768 // half of max uint16 ) type unwrapper struct { init bool lastUnwrapped int64 } func isNewer(value, previous uint16) bool { if value-previous == breakpoint { return value > previous } return value != previous && (value-previous) < breakpoint } func (u *unwrapper) unwrap(i uint16) int64 { if !u.init { u.init = true u.lastUnwrapped = int64(i) return u.lastUnwrapped } lastWrapped := uint16(u.lastUnwrapped) delta := int64(i - lastWrapped) if isNewer(i, lastWrapped) { if delta < 0 { delta += maxSequenceNumberPlusOne } } else if delta > 0 && u.lastUnwrapped+delta-maxSequenceNumberPlusOne >= 0 { delta -= maxSequenceNumberPlusOne } u.lastUnwrapped += delta return u.lastUnwrapped } interceptor-0.1.12/pkg/rfc8888/unwrapper_test.go000066400000000000000000000020651426573475500214620ustar00rootroot00000000000000package rfc8888 import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestUnwrapper(t *testing.T) { cases := []struct { input []uint16 expected []int64 }{ { input: []uint16{}, expected: []int64{}, }, { input: []uint16{0, 1, 2, 3, 4}, expected: []int64{0, 1, 2, 3, 4}, }, { input: []uint16{65534, 65535, 0, 1, 2}, expected: []int64{65534, 65535, 65536, 65537, 65538}, }, { input: []uint16{32769, 0}, expected: []int64{32769, 65536}, }, { input: []uint16{32767, 0}, expected: []int64{32767, 0}, }, { input: []uint16{0, 1, 4, 3, 2, 5}, expected: []int64{0, 1, 4, 3, 2, 5}, }, { input: []uint16{65534, 0, 1, 65535, 4, 3, 2, 5}, expected: []int64{65534, 65536, 65537, 65535, 65540, 65539, 65538, 65541}, }, } for i, tc := range cases { t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { u := &unwrapper{} result := []int64{} for _, i := range tc.input { result = append(result, u.unwrap(i)) } assert.Equal(t, tc.expected, result) }) } } interceptor-0.1.12/pkg/stats/000077500000000000000000000000001426573475500160725ustar00rootroot00000000000000interceptor-0.1.12/pkg/stats/interceptor.go000066400000000000000000000143141426573475500207620ustar00rootroot00000000000000// Package stats provides an interceptor that records RTP/RTCP stream statistics package stats import ( "sync" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" ) // Option can be used to configure the stats interceptor type Option func(*Interceptor) error // SetRecorderFactory sets the factory that is used to create new stats // recorders for new streams func SetRecorderFactory(f RecorderFactory) Option { return func(i *Interceptor) error { i.RecorderFactory = f return nil } } // SetNowFunc sets the function the interceptor uses to get a current timestamp. // This is mostly useful for testing. func SetNowFunc(now func() time.Time) Option { return func(i *Interceptor) error { i.now = now return nil } } // Getter returns the most recent stats of a stream type Getter interface { Get(ssrc uint32) *Stats } // NewPeerConnectionCallback receives a new StatsGetter for a newly created // PeerConnection type NewPeerConnectionCallback func(string, Getter) // InterceptorFactory is a interceptor.Factory for a stats Interceptor type InterceptorFactory struct { opts []Option addPeerConnection NewPeerConnectionCallback } // NewInterceptor creates a new InterceptorFactory func NewInterceptor(opts ...Option) (*InterceptorFactory, error) { return &InterceptorFactory{ opts: opts, addPeerConnection: nil, }, nil } // OnNewPeerConnection sets the callback that is called when a new // PeerConnection is created. func (r *InterceptorFactory) OnNewPeerConnection(cb NewPeerConnectionCallback) { r.addPeerConnection = cb } // NewInterceptor creates a new Interceptor func (r *InterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { i := &Interceptor{ NoOp: interceptor.NoOp{}, now: time.Now, lock: sync.Mutex{}, RecorderFactory: func(ssrc uint32, clockRate float64) Recorder { return newRecorder(ssrc, clockRate) }, recorders: map[uint32]Recorder{}, wg: sync.WaitGroup{}, } for _, opt := range r.opts { if err := opt(i); err != nil { return nil, err } } if r.addPeerConnection != nil { r.addPeerConnection(id, i) } return i, nil } // Recorder is the interface of a statistics recorder type Recorder interface { QueueIncomingRTP(ts time.Time, buf []byte, attr interceptor.Attributes) QueueIncomingRTCP(ts time.Time, buf []byte, attr interceptor.Attributes) QueueOutgoingRTP(ts time.Time, header *rtp.Header, payload []byte, attr interceptor.Attributes) QueueOutgoingRTCP(ts time.Time, pkts []rtcp.Packet, attr interceptor.Attributes) GetStats() Stats Stop() Start() } // RecorderFactory creates new Recorders to be used by the interceptor type RecorderFactory func(ssrc uint32, clockRate float64) Recorder // Interceptor is the interceptor that collects stream stats type Interceptor struct { interceptor.NoOp now func() time.Time lock sync.Mutex RecorderFactory RecorderFactory recorders map[uint32]Recorder wg sync.WaitGroup } // Get returns the statistics for the stream with ssrc func (r *Interceptor) Get(ssrc uint32) *Stats { r.lock.Lock() defer r.lock.Unlock() if rec, ok := r.recorders[ssrc]; ok { stats := rec.GetStats() return &stats } return nil } func (r *Interceptor) getRecorder(ssrc uint32, clockRate float64) Recorder { r.lock.Lock() defer r.lock.Unlock() if rec, ok := r.recorders[ssrc]; ok { return rec } rec := r.RecorderFactory(ssrc, clockRate) r.wg.Add(1) go func() { defer r.wg.Done() rec.Start() }() r.recorders[ssrc] = rec return rec } // Close closes the interceptor and associated stats recorders func (r *Interceptor) Close() error { defer r.wg.Wait() r.lock.Lock() defer r.lock.Unlock() for _, r := range r.recorders { r.Stop() } return nil } // BindRTCPReader lets you modify any incoming RTCP packets. It is called once per sender/receiver, however this might // change in the future. The returned method will be called once per packet batch. func (r *Interceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { return interceptor.RTCPReaderFunc(func(bytes []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { n, attattributes, err := reader.Read(bytes, attributes) if err != nil { return 0, attattributes, err } r.lock.Lock() for _, recorder := range r.recorders { recorder.QueueIncomingRTCP(r.now(), bytes[:n], attributes) } r.lock.Unlock() return n, attattributes, err }) } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (r *Interceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { return interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) { r.lock.Lock() for _, recorder := range r.recorders { recorder.QueueOutgoingRTCP(r.now(), pkts, attributes) } r.lock.Unlock() return writer.Write(pkts, attributes) }) } // BindLocalStream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method // will be called once per rtp packet. func (r *Interceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { recorder := r.getRecorder(info.SSRC, float64(info.ClockRate)) return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { recorder.QueueOutgoingRTP(r.now(), header, payload, attributes) return writer.Write(header, payload, attributes) }) } // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (r *Interceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { recorder := r.getRecorder(info.SSRC, float64(info.ClockRate)) return interceptor.RTPReaderFunc(func(bytes []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { n, attributes, err := reader.Read(bytes, attributes) if err != nil { return 0, nil, err } recorder.QueueIncomingRTP(r.now(), bytes[:n], attributes) return n, attributes, nil }) } interceptor-0.1.12/pkg/stats/interceptor_test.go000066400000000000000000000123671426573475500220270ustar00rootroot00000000000000package stats import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestInterceptor(t *testing.T) { t.Run("before any packets", func(t *testing.T) { f, err := NewInterceptor() assert.NoError(t, err) statsCh := make(chan Getter) f.OnNewPeerConnection(func(_ string, g Getter) { go func() { statsCh <- g }() }) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{SSRC: 0}, i) defer func() { assert.NoError(t, stream.Close()) }() var statsGetter Getter select { case statsGetter = <-statsCh: case <-time.After(time.Second): assert.FailNow(t, "expected to receive statsgetter") } assert.Equal(t, statsGetter.Get(0), &Stats{}) }) t.Run("records packets", func(t *testing.T) { mockRecorder := newMockRecorder() now := time.Now() f, err := NewInterceptor( SetRecorderFactory(func(ssrc uint32, clockRate float64) Recorder { return mockRecorder }), SetNowFunc(func() time.Time { return now }), ) assert.NoError(t, err) statsCh := make(chan Getter) f.OnNewPeerConnection(func(_ string, g Getter) { go func() { statsCh <- g }() }) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{SSRC: 0}, i) defer func() { assert.NoError(t, stream.Close()) }() incomingRTP := &rtp.Packet{} incomingRTCP := []rtcp.Packet{&rtcp.RawPacket{}} outgoingRTP := &rtp.Packet{} outgoingRTCP := []rtcp.Packet{&rtcp.RawPacket{}} stream.ReceiveRTP(incomingRTP) stream.ReceiveRTCP(incomingRTCP) assert.NoError(t, stream.WriteRTP(outgoingRTP)) assert.NoError(t, stream.WriteRTCP(outgoingRTCP)) var statsGetter Getter select { case statsGetter = <-statsCh: case <-time.After(time.Second): assert.FailNow(t, "expected to receive statsgetter") } var riRTP recordedIncomingRTP select { case riRTP = <-mockRecorder.incomingRTPQueue: case <-time.After(time.Second): assert.FailNow(t, "expected to record RTP packet") } var riRTCP recordedIncomingRTCP select { case riRTCP = <-mockRecorder.incomingRTCPQueue: case <-time.After(time.Second): } var roRTP recordedOutgoingRTP select { case roRTP = <-mockRecorder.outgoingRTPQueue: case <-time.After(time.Second): } var roRTCP recordedOutgoingRTCP select { case roRTCP = <-mockRecorder.outgoingRTCPQueue: case <-time.After(time.Second): } assert.Equal(t, &Stats{}, statsGetter.Get(0)) buf, err := incomingRTP.Marshal() assert.NoError(t, err) expectedIncomingRTP := recordedIncomingRTP{ ts: now, buf: buf, attr: map[interface{}]interface{}{}, } assert.Equal(t, expectedIncomingRTP, riRTP) buf, err = rtcp.Marshal(incomingRTCP) assert.NoError(t, err) expectedIncomingRTCP := recordedIncomingRTCP{ ts: now, buf: buf, attr: map[interface{}]interface{}{}, } assert.Equal(t, expectedIncomingRTCP, riRTCP) expectedOutgoingRTP := recordedOutgoingRTP{ ts: now, header: &rtp.Header{}, payload: outgoingRTP.Payload, attr: map[interface{}]interface{}{}, } assert.Equal(t, expectedOutgoingRTP, roRTP) expectedOutgoingRTCP := recordedOutgoingRTCP{ ts: now, pkts: outgoingRTCP, attr: map[interface{}]interface{}{}, } assert.Equal(t, expectedOutgoingRTCP, roRTCP) }) } type recordedOutgoingRTP struct { ts time.Time header *rtp.Header payload []byte attr interceptor.Attributes } type recordedOutgoingRTCP struct { ts time.Time pkts []rtcp.Packet attr interceptor.Attributes } type recordedIncomingRTP struct { ts time.Time buf []byte attr interceptor.Attributes } type recordedIncomingRTCP struct { ts time.Time buf []byte attr interceptor.Attributes } type mockRecorder struct { incomingRTPQueue chan recordedIncomingRTP incomingRTCPQueue chan recordedIncomingRTCP outgoingRTPQueue chan recordedOutgoingRTP outgoingRTCPQueue chan recordedOutgoingRTCP } func newMockRecorder() *mockRecorder { return &mockRecorder{ incomingRTPQueue: make(chan recordedIncomingRTP, 1), incomingRTCPQueue: make(chan recordedIncomingRTCP, 1), outgoingRTPQueue: make(chan recordedOutgoingRTP, 1), outgoingRTCPQueue: make(chan recordedOutgoingRTCP, 1), } } func (r *mockRecorder) QueueIncomingRTP(ts time.Time, buf []byte, attr interceptor.Attributes) { r.incomingRTPQueue <- recordedIncomingRTP{ ts: ts, buf: buf, attr: attr, } } func (r *mockRecorder) QueueIncomingRTCP(ts time.Time, buf []byte, attr interceptor.Attributes) { r.incomingRTCPQueue <- recordedIncomingRTCP{ ts: ts, buf: buf, attr: attr, } } func (r *mockRecorder) QueueOutgoingRTP(ts time.Time, header *rtp.Header, payload []byte, attr interceptor.Attributes) { r.outgoingRTPQueue <- recordedOutgoingRTP{ ts: ts, header: header, payload: payload, attr: attr, } } func (r *mockRecorder) QueueOutgoingRTCP(ts time.Time, pkts []rtcp.Packet, attr interceptor.Attributes) { r.outgoingRTCPQueue <- recordedOutgoingRTCP{ ts: ts, pkts: pkts, attr: attr, } } func (r *mockRecorder) GetStats() Stats { return Stats{} } func (r *mockRecorder) Start() {} func (r *mockRecorder) Stop() {} interceptor-0.1.12/pkg/stats/received_stats.go000066400000000000000000000043561426573475500214350ustar00rootroot00000000000000package stats import ( "fmt" "time" ) // ReceivedRTPStreamStats contains common receiver stats of RTP streams type ReceivedRTPStreamStats struct { PacketsReceived uint64 PacketsLost int64 Jitter float64 } // String returns a string representation of ReceivedRTPStreamStats func (s ReceivedRTPStreamStats) String() string { out := fmt.Sprintf("\tPacketsReceived: %v\n", s.PacketsReceived) out += fmt.Sprintf("\tPacketsLost: %v\n", s.PacketsLost) out += fmt.Sprintf("\tJitter: %v\n", s.Jitter) return out } // InboundRTPStreamStats contains stats of inbound RTP streams type InboundRTPStreamStats struct { ReceivedRTPStreamStats LastPacketReceivedTimestamp time.Time HeaderBytesReceived uint64 BytesReceived uint64 FIRCount uint32 PLICount uint32 NACKCount uint32 } // String returns a string representation of InboundRTPStreamStats func (s InboundRTPStreamStats) String() string { out := "InboundRTPStreamStats:\n" out += s.ReceivedRTPStreamStats.String() out += fmt.Sprintf("\tLastPacketReceivedTimestamp: %v\n", s.LastPacketReceivedTimestamp) out += fmt.Sprintf("\tHeaderBytesReceived: %v\n", s.HeaderBytesReceived) out += fmt.Sprintf("\tBytesReceived: %v\n", s.BytesReceived) out += fmt.Sprintf("\tFIRCount: %v\n", s.FIRCount) out += fmt.Sprintf("\tPLICount: %v\n", s.PLICount) out += fmt.Sprintf("\tNACKCount: %v\n", s.NACKCount) return out } // RemoteInboundRTPStreamStats contains stats of inbound RTP streams of the // remote peer type RemoteInboundRTPStreamStats struct { ReceivedRTPStreamStats RoundTripTime time.Duration TotalRoundTripTime time.Duration FractionLost float64 RoundTripTimeMeasurements uint64 } // String returns a string representation of RemoteInboundRTPStreamStats func (s RemoteInboundRTPStreamStats) String() string { out := "RemoteInboundRTPStreamStats:\n" out += s.ReceivedRTPStreamStats.String() out += fmt.Sprintf("\tRoundTripTime: %v\n", s.RoundTripTime) out += fmt.Sprintf("\tTotalRoundTripTime: %v\n", s.TotalRoundTripTime) out += fmt.Sprintf("\tFractionLost: %v\n", s.FractionLost) out += fmt.Sprintf("\tRoundTripTimeMeasurements: %v\n", s.RoundTripTimeMeasurements) return out } interceptor-0.1.12/pkg/stats/sent_stats.go000066400000000000000000000037041426573475500206140ustar00rootroot00000000000000package stats import ( "fmt" "time" ) // SentRTPStreamStats contains common sender stats of RTP streams type SentRTPStreamStats struct { PacketsSent uint64 BytesSent uint64 } // String returns a string representation of SentRTPStreamStats func (s SentRTPStreamStats) String() string { out := fmt.Sprintf("\tPacketsSent: %v\n", s.PacketsSent) out += fmt.Sprintf("\tBytesSent: %v\n", s.BytesSent) return out } // OutboundRTPStreamStats contains stats of outbound RTP streams type OutboundRTPStreamStats struct { SentRTPStreamStats HeaderBytesSent uint64 NACKCount uint32 FIRCount uint32 PLICount uint32 } // String returns a string representation of OutboundRTPStreamStats func (s OutboundRTPStreamStats) String() string { out := "OutboundRTPStreamStats\n" out += s.SentRTPStreamStats.String() out += fmt.Sprintf("\tHeaderBytesSent: %v\n", s.HeaderBytesSent) out += fmt.Sprintf("\tNACKCount: %v\n", s.NACKCount) out += fmt.Sprintf("\tFIRCount: %v\n", s.FIRCount) out += fmt.Sprintf("\tPLICount: %v\n", s.PLICount) return out } // RemoteOutboundRTPStreamStats contains stats of outbound RTP streams of the // remote peer type RemoteOutboundRTPStreamStats struct { SentRTPStreamStats RemoteTimeStamp time.Time ReportsSent uint64 RoundTripTime time.Duration TotalRoundTripTime time.Duration RoundTripTimeMeasurements uint64 } // String returns a string representation of RemoteOutboundRTPStreamStats func (s RemoteOutboundRTPStreamStats) String() string { out := "RemoteOutboundRTPStreamStats:\n" out += s.SentRTPStreamStats.String() out += fmt.Sprintf("\tRemoteTimeStamp: %v\n", s.RemoteTimeStamp) out += fmt.Sprintf("\tReportsSent: %v\n", s.ReportsSent) out += fmt.Sprintf("\tRoundTripTime: %v\n", s.RoundTripTime) out += fmt.Sprintf("\tTotalRoundTripTime: %v\n", s.TotalRoundTripTime) out += fmt.Sprintf("\tRoundTripTimeMeasurements: %v\n", s.RoundTripTimeMeasurements) return out } interceptor-0.1.12/pkg/stats/stats_recorder.go000066400000000000000000000265571426573475500214630ustar00rootroot00000000000000package stats import ( "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/ntp" "github.com/pion/interceptor/internal/sequencenumber" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" ) // Stats contains all the available statistics of RTP streams type Stats struct { InboundRTPStreamStats OutboundRTPStreamStats RemoteInboundRTPStreamStats RemoteOutboundRTPStreamStats } type internalStats struct { inboundSequencerNumber sequencenumber.Unwrapper inboundSequenceNumberInitialized bool inboundFirstSequenceNumber int64 inboundHighestSequenceNumber int64 inboundLastArrivalInitialized bool inboundLastArrival time.Time inboundLastTransit int remoteInboundFirstSequenceNumberInitialized bool remoteInboundFirstSequenceNumber int64 lastSenderReports []uint64 lastReceiverReferenceTimes []uint64 InboundRTPStreamStats OutboundRTPStreamStats RemoteInboundRTPStreamStats RemoteOutboundRTPStreamStats } type incomingRTP struct { ts time.Time header rtp.Header payloadLen int attr interceptor.Attributes } type incomingRTCP struct { ts time.Time pkts []rtcp.Packet attr interceptor.Attributes } type outgoingRTP struct { ts time.Time header rtp.Header payloadLen int attr interceptor.Attributes } type outgoingRTCP struct { ts time.Time pkts []rtcp.Packet attr interceptor.Attributes } type recorder struct { logger logging.LeveledLogger ssrc uint32 clockRate float64 maxLastSenderReports int maxLastReceiverReferenceTimes int incomingRTPChan chan *incomingRTP incomingRTCPChan chan *incomingRTCP outgoingRTPChan chan *outgoingRTP outgoingRTCPChan chan *outgoingRTCP getStatsChan chan Stats done chan struct{} } func newRecorder(ssrc uint32, clockRate float64) *recorder { return &recorder{ logger: logging.NewDefaultLoggerFactory().NewLogger("stats_recorder"), ssrc: ssrc, clockRate: clockRate, maxLastSenderReports: 5, maxLastReceiverReferenceTimes: 5, incomingRTPChan: make(chan *incomingRTP), incomingRTCPChan: make(chan *incomingRTCP), outgoingRTPChan: make(chan *outgoingRTP), outgoingRTCPChan: make(chan *outgoingRTCP), getStatsChan: make(chan Stats), done: make(chan struct{}), } } func (r *recorder) Stop() { close(r.done) } func (r *recorder) GetStats() Stats { return <-r.getStatsChan } func (r *recorder) recordIncomingRTP(latestStats internalStats, v *incomingRTP) internalStats { sequenceNumber := latestStats.inboundSequencerNumber.Unwrap(v.header.SequenceNumber) if !latestStats.inboundSequenceNumberInitialized { latestStats.inboundFirstSequenceNumber = sequenceNumber latestStats.inboundSequenceNumberInitialized = true } if sequenceNumber > latestStats.inboundHighestSequenceNumber { latestStats.inboundHighestSequenceNumber = sequenceNumber } latestStats.InboundRTPStreamStats.PacketsReceived++ expectedPackets := latestStats.inboundHighestSequenceNumber - latestStats.inboundFirstSequenceNumber + 1 latestStats.InboundRTPStreamStats.PacketsLost = expectedPackets - int64(latestStats.InboundRTPStreamStats.PacketsReceived) if !latestStats.inboundLastArrivalInitialized { latestStats.inboundLastArrival = v.ts latestStats.inboundLastArrivalInitialized = true } else { arrival := int(v.ts.Sub(latestStats.inboundLastArrival).Seconds() * r.clockRate) transit := arrival - int(v.header.Timestamp) d := transit - latestStats.inboundLastTransit latestStats.inboundLastTransit = transit if d < 0 { d = -d } latestStats.InboundRTPStreamStats.Jitter += (1.0 / 16.0) * (float64(d) - latestStats.InboundRTPStreamStats.Jitter) latestStats.inboundLastArrival = v.ts } latestStats.LastPacketReceivedTimestamp = v.ts latestStats.HeaderBytesReceived += uint64(v.header.MarshalSize()) latestStats.BytesReceived += uint64(v.header.MarshalSize() + v.payloadLen) return latestStats } func (r *recorder) recordOutgoingRTCP(latestStats internalStats, v *outgoingRTCP) internalStats { for _, pkt := range v.pkts { switch rtcpPkt := pkt.(type) { case *rtcp.FullIntraRequest: latestStats.InboundRTPStreamStats.FIRCount++ case *rtcp.PictureLossIndication: latestStats.InboundRTPStreamStats.PLICount++ case *rtcp.TransportLayerNack: latestStats.InboundRTPStreamStats.NACKCount++ case *rtcp.SenderReport: latestStats.lastSenderReports = append(latestStats.lastSenderReports, rtcpPkt.NTPTime) if len(latestStats.lastSenderReports) > r.maxLastSenderReports { latestStats.lastSenderReports = latestStats.lastSenderReports[len(latestStats.lastSenderReports)-r.maxLastSenderReports:] } case *rtcp.ExtendedReport: for _, block := range rtcpPkt.Reports { if xr, ok := block.(*rtcp.ReceiverReferenceTimeReportBlock); ok { latestStats.lastReceiverReferenceTimes = append(latestStats.lastReceiverReferenceTimes, xr.NTPTimestamp) if len(latestStats.lastReceiverReferenceTimes) > r.maxLastReceiverReferenceTimes { latestStats.lastReceiverReferenceTimes = latestStats.lastReceiverReferenceTimes[len(latestStats.lastReceiverReferenceTimes)-r.maxLastReceiverReferenceTimes:] } } } } } return latestStats } func (r *recorder) recordOutgoingRTP(latestStats internalStats, v *outgoingRTP) internalStats { headerSize := v.header.MarshalSize() latestStats.OutboundRTPStreamStats.PacketsSent++ latestStats.OutboundRTPStreamStats.BytesSent += uint64(headerSize + v.payloadLen) latestStats.HeaderBytesSent += uint64(headerSize) if !latestStats.remoteInboundFirstSequenceNumberInitialized { latestStats.remoteInboundFirstSequenceNumber = int64(v.header.SequenceNumber) latestStats.remoteInboundFirstSequenceNumberInitialized = true } return latestStats } func (r *recorder) recordIncomingRR(latestStats internalStats, pkt *rtcp.ReceiverReport, ts time.Time) internalStats { for _, report := range pkt.Reports { if report.SSRC == r.ssrc { if latestStats.remoteInboundFirstSequenceNumberInitialized { cycles := uint64(report.LastSequenceNumber & 0xFFFF0000) nr := uint64(report.LastSequenceNumber & 0x0000FFFF) highest := cycles*0xFFFF + nr latestStats.RemoteInboundRTPStreamStats.PacketsReceived = highest - uint64(report.TotalLost) - uint64(latestStats.remoteInboundFirstSequenceNumber) + 1 } latestStats.RemoteInboundRTPStreamStats.PacketsLost = int64(report.TotalLost) latestStats.RemoteInboundRTPStreamStats.Jitter = float64(report.Jitter) / r.clockRate if report.Delay != 0 && report.LastSenderReport != 0 { for i := min(r.maxLastSenderReports, len(latestStats.lastSenderReports)) - 1; i >= 0; i-- { lastReport := latestStats.lastSenderReports[i] if (lastReport&0x0000FFFFFFFF0000)>>16 == uint64(report.LastSenderReport) { dlsr := time.Duration(float64(report.Delay) / 65536.0 * float64(time.Second)) latestStats.RemoteInboundRTPStreamStats.RoundTripTime = (ts.Add(-dlsr)).Sub(ntp.ToTime(lastReport)) latestStats.RemoteInboundRTPStreamStats.TotalRoundTripTime += latestStats.RemoteInboundRTPStreamStats.RoundTripTime latestStats.RemoteInboundRTPStreamStats.RoundTripTimeMeasurements++ break } } } latestStats.FractionLost = float64(report.FractionLost) / 256.0 } } return latestStats } func (r *recorder) recordIncomingXR(latestStats internalStats, pkt *rtcp.ExtendedReport, ts time.Time) internalStats { for _, report := range pkt.Reports { if xr, ok := report.(*rtcp.DLRRReportBlock); ok { for _, xrReport := range xr.Reports { if xrReport.LastRR != 0 && xrReport.DLRR != 0 { for i := min(r.maxLastReceiverReferenceTimes, len(latestStats.lastReceiverReferenceTimes)) - 1; i >= 0; i-- { lastRR := latestStats.lastReceiverReferenceTimes[i] if (lastRR&0x0000FFFFFFFF0000)>>16 == uint64(xrReport.LastRR) { dlrr := time.Duration(xrReport.DLRR/65536.0) * time.Second latestStats.RemoteOutboundRTPStreamStats.RoundTripTime = (ts.Add(-dlrr)).Sub(ntp.ToTime(lastRR)) latestStats.RemoteOutboundRTPStreamStats.TotalRoundTripTime += latestStats.RemoteOutboundRTPStreamStats.RoundTripTime latestStats.RemoteOutboundRTPStreamStats.RoundTripTimeMeasurements++ } } } } } } return latestStats } func (r *recorder) recordIncomingRTCP(latestStats internalStats, v *incomingRTCP) internalStats { for _, pkt := range v.pkts { switch pkt := pkt.(type) { case *rtcp.TransportLayerNack: latestStats.OutboundRTPStreamStats.NACKCount++ case *rtcp.FullIntraRequest: latestStats.OutboundRTPStreamStats.FIRCount++ case *rtcp.PictureLossIndication: latestStats.OutboundRTPStreamStats.PLICount++ case *rtcp.ReceiverReport: return r.recordIncomingRR(latestStats, pkt, v.ts) case *rtcp.SenderReport: latestStats.RemoteOutboundRTPStreamStats.PacketsSent = uint64(pkt.PacketCount) latestStats.RemoteOutboundRTPStreamStats.BytesSent = uint64(pkt.OctetCount) latestStats.RemoteTimeStamp = ntp.ToTime(pkt.NTPTime) latestStats.ReportsSent++ case *rtcp.ExtendedReport: return r.recordIncomingXR(latestStats, pkt, v.ts) } } return latestStats } func (r *recorder) Start() { latestStats := &internalStats{} for { select { case <-r.done: return case v := <-r.incomingRTPChan: s := r.recordIncomingRTP(*latestStats, v) latestStats = &s case v := <-r.outgoingRTCPChan: s := r.recordOutgoingRTCP(*latestStats, v) latestStats = &s case v := <-r.outgoingRTPChan: s := r.recordOutgoingRTP(*latestStats, v) latestStats = &s case v := <-r.incomingRTCPChan: s := r.recordIncomingRTCP(*latestStats, v) latestStats = &s case r.getStatsChan <- Stats{ InboundRTPStreamStats: latestStats.InboundRTPStreamStats, OutboundRTPStreamStats: latestStats.OutboundRTPStreamStats, RemoteInboundRTPStreamStats: latestStats.RemoteInboundRTPStreamStats, RemoteOutboundRTPStreamStats: latestStats.RemoteOutboundRTPStreamStats, }: } } } func (r *recorder) QueueIncomingRTP(ts time.Time, buf []byte, attr interceptor.Attributes) { if attr == nil { attr = make(interceptor.Attributes) } header, err := attr.GetRTPHeader(buf) if err != nil { r.logger.Warnf("failed to get RTP Header, skipping incoming RTP packet in stats calculation: %v", err) return } hdr := header.Clone() r.incomingRTPChan <- &incomingRTP{ ts: ts, header: hdr, payloadLen: len(buf) - hdr.MarshalSize(), attr: attr, } } func (r *recorder) QueueIncomingRTCP(ts time.Time, buf []byte, attr interceptor.Attributes) { if attr == nil { attr = make(interceptor.Attributes) } pkts, err := attr.GetRTCPPackets(buf) if err != nil { r.logger.Warnf("failed to get RTCP packets, skipping incoming RTCP packet in stats calculation: %v", err) return } r.incomingRTCPChan <- &incomingRTCP{ ts: ts, pkts: pkts, attr: attr, } } func (r *recorder) QueueOutgoingRTP(ts time.Time, header *rtp.Header, payload []byte, attr interceptor.Attributes) { hdr := header.Clone() r.outgoingRTPChan <- &outgoingRTP{ ts: ts, header: hdr, payloadLen: len(payload), attr: attr, } } func (r *recorder) QueueOutgoingRTCP(ts time.Time, pkts []rtcp.Packet, attr interceptor.Attributes) { r.outgoingRTCPChan <- &outgoingRTCP{ ts: ts, pkts: pkts, attr: attr, } } func min(a, b int) int { if a < b { return a } return b } interceptor-0.1.12/pkg/stats/stats_recorder_test.go000066400000000000000000000152201426573475500225030ustar00rootroot00000000000000package stats import ( "fmt" "testing" "time" "github.com/pion/interceptor/internal/ntp" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func mustMarshalRTP(t *testing.T, pkt rtp.Packet) []byte { buf, err := pkt.Marshal() assert.NoError(t, err) return buf } func mustMarshalRTCPs(t *testing.T, pkt rtcp.Packet) []byte { buf, err := pkt.Marshal() assert.NoError(t, err) return buf } func TestStatsRecorder(t *testing.T) { cname := &rtcp.SourceDescription{ Chunks: []rtcp.SourceDescriptionChunk{{ Source: 1234, Items: []rtcp.SourceDescriptionItem{{ Type: rtcp.SDESCNAME, Text: "cname", }}, }}, } type record struct { ts time.Time content interface{} } type input struct { name string records []record expectedInboundRTPStreamStats InboundRTPStreamStats expectedOutboundRTPStreamStats OutboundRTPStreamStats expectedRemoteInboundRTPStreamStats RemoteInboundRTPStreamStats expectedRemoteOutboundRTPStreamStats RemoteOutboundRTPStreamStats } now := time.Date(2022, time.July, 18, 0, 0, 0, 0, time.Local) for i, cc := range []input{ { name: "basicIncomingRTP", records: []record{ { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 7, Timestamp: 0, }, }, }, { ts: now.Add(1 * time.Second), content: incomingRTP{ header: rtp.Header{ SequenceNumber: 10, Timestamp: 90000, }, }, }, { ts: now.Add(2 * time.Second), content: incomingRTP{ header: rtp.Header{ SequenceNumber: 11, Timestamp: 2 * 90000, }, }, }, }, expectedInboundRTPStreamStats: InboundRTPStreamStats{ ReceivedRTPStreamStats: ReceivedRTPStreamStats{ PacketsReceived: 3, PacketsLost: 2, Jitter: 90000 / 16, }, LastPacketReceivedTimestamp: now.Add(2 * time.Second), HeaderBytesReceived: 36, BytesReceived: 36, }, }, { name: "basicOutgoingRTP", records: []record{ { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 1, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 3, }, }, }, { ts: now, content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{ SSRC: 0, Reports: []rtcp.ReceptionReport{ { SSRC: 0, FractionLost: 85, TotalLost: 1, LastSequenceNumber: 3, Jitter: 45000, }, }, }, cname, }, }, }, }, expectedOutboundRTPStreamStats: OutboundRTPStreamStats{ SentRTPStreamStats: SentRTPStreamStats{ PacketsSent: 2, BytesSent: 24, }, HeaderBytesSent: 24, }, expectedRemoteInboundRTPStreamStats: RemoteInboundRTPStreamStats{ ReceivedRTPStreamStats: ReceivedRTPStreamStats{ PacketsReceived: 2, PacketsLost: 1, Jitter: 0.5, }, FractionLost: 0.33203125, }, }, { name: "basicOutgoingRTCP", records: []record{ { ts: now, content: outgoingRTCP{ ts: now, pkts: []rtcp.Packet{&rtcp.SenderReport{ NTPTime: ntp.ToNTP(now), }}, }, }, { ts: now.Add(2 * time.Second), content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{ SSRC: 0, Reports: []rtcp.ReceptionReport{{ SSRC: 0, LastSenderReport: uint32((ntp.ToNTP(now) & 0x0000FFFFFFFF0000) >> 16), Delay: 1 * 65536.0, }}, }, cname, }, }, }, }, expectedRemoteInboundRTPStreamStats: RemoteInboundRTPStreamStats{ RoundTripTime: time.Second, TotalRoundTripTime: time.Second, RoundTripTimeMeasurements: 1, }, }, { name: "basicIncomingRTCP", records: []record{ { ts: now, content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.SenderReport{ NTPTime: ntp.ToNTP(now), }, cname, }, }, }, }, expectedRemoteOutboundRTPStreamStats: RemoteOutboundRTPStreamStats{ ReportsSent: 1, RemoteTimeStamp: ntp.ToTime(ntp.ToNTP(now)), }, }, { name: "remoteOutboundRTT", records: []record{ { ts: now, content: outgoingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{}, &rtcp.ExtendedReport{ SenderSSRC: 0, Reports: []rtcp.ReportBlock{ &rtcp.ReceiverReferenceTimeReportBlock{ NTPTimestamp: ntp.ToNTP(now), }, }, }, }, }, }, { ts: now.Add(2 * time.Second), content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.SenderReport{ NTPTime: ntp.ToNTP(now.Add(time.Second)), }, cname, &rtcp.ExtendedReport{ SenderSSRC: 0, Reports: []rtcp.ReportBlock{ &rtcp.DLRRReportBlock{ Reports: []rtcp.DLRRReport{ { SSRC: 0, LastRR: uint32((ntp.ToNTP(now) >> 16) & 0xFFFFFFFF), DLRR: 1 * 65536.0, }, }, }, }, }, }, }, }, }, expectedRemoteOutboundRTPStreamStats: RemoteOutboundRTPStreamStats{ RemoteTimeStamp: now.Add(time.Second), ReportsSent: 1, RoundTripTime: time.Second, TotalRoundTripTime: time.Second, RoundTripTimeMeasurements: 1, }, }, } { t.Run(fmt.Sprintf("%v:%v", i, cc.name), func(t *testing.T) { r := newRecorder(0, 90_000) go r.Start() defer r.Stop() for _, record := range cc.records { switch v := record.content.(type) { case incomingRTP: r.QueueIncomingRTP(record.ts, mustMarshalRTP(t, rtp.Packet{Header: v.header}), v.attr) case incomingRTCP: pkts := make(rtcp.CompoundPacket, len(v.pkts)) copy(pkts, v.pkts) r.QueueIncomingRTCP(record.ts, mustMarshalRTCPs(t, &pkts), v.attr) case outgoingRTP: r.QueueOutgoingRTP(record.ts, &v.header, []byte{}, v.attr) case outgoingRTCP: r.QueueOutgoingRTCP(record.ts, v.pkts, v.attr) default: assert.FailNow(t, "invalid test case") } } s := r.GetStats() assert.Equal(t, cc.expectedInboundRTPStreamStats, s.InboundRTPStreamStats) assert.Equal(t, cc.expectedOutboundRTPStreamStats, s.OutboundRTPStreamStats) assert.Equal(t, cc.expectedRemoteInboundRTPStreamStats, s.RemoteInboundRTPStreamStats) assert.Equal(t, cc.expectedRemoteOutboundRTPStreamStats, s.RemoteOutboundRTPStreamStats) }) } } interceptor-0.1.12/pkg/twcc/000077500000000000000000000000001426573475500156745ustar00rootroot00000000000000interceptor-0.1.12/pkg/twcc/header_extension_interceptor.go000066400000000000000000000036421426573475500241720ustar00rootroot00000000000000package twcc import ( "sync/atomic" "github.com/pion/interceptor" "github.com/pion/rtp" ) // HeaderExtensionInterceptorFactory is a interceptor.Factory for a HeaderExtensionInterceptor type HeaderExtensionInterceptorFactory struct{} // NewInterceptor constructs a new HeaderExtensionInterceptor func (h *HeaderExtensionInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { return &HeaderExtensionInterceptor{}, nil } // NewHeaderExtensionInterceptor returns a HeaderExtensionInterceptorFactory func NewHeaderExtensionInterceptor() (*HeaderExtensionInterceptorFactory, error) { return &HeaderExtensionInterceptorFactory{}, nil } // HeaderExtensionInterceptor adds transport wide sequence numbers as header extension to each RTP packet type HeaderExtensionInterceptor struct { interceptor.NoOp nextSequenceNr uint32 } const transportCCURI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" // BindLocalStream returns a writer that adds a rtp.TransportCCExtension // header with increasing sequence numbers to each outgoing packet. func (h *HeaderExtensionInterceptor) BindLocalStream(info *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { var hdrExtID uint8 for _, e := range info.RTPHeaderExtensions { if e.URI == transportCCURI { hdrExtID = uint8(e.ID) break } } if hdrExtID == 0 { // Don't add header extension if ID is 0, because 0 is an invalid extension ID return writer } return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { sequenceNumber := atomic.AddUint32(&h.nextSequenceNr, 1) - 1 tcc, err := (&rtp.TransportCCExtension{TransportSequence: uint16(sequenceNumber)}).Marshal() if err != nil { return 0, err } err = header.SetExtension(hdrExtID, tcc) if err != nil { return 0, err } return writer.Write(header, payload, attributes) }) } interceptor-0.1.12/pkg/twcc/header_extension_interceptor_test.go000066400000000000000000000035071426573475500252310ustar00rootroot00000000000000package twcc import ( "sync" "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestHeaderExtensionInterceptor(t *testing.T) { t.Run("add transport wide cc to each packet", func(t *testing.T) { factory, err := NewHeaderExtensionInterceptor() assert.NoError(t, err) inter, err := factory.NewInterceptor("") assert.NoError(t, err) pChan := make(chan *rtp.Packet, 10*5) go func() { // start some parallel streams using the same interceptor to test for race conditions var wg sync.WaitGroup num := 10 wg.Add(num) for i := 0; i < num; i++ { go func(ch chan *rtp.Packet, id uint16) { stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, inter) defer func() { wg.Done() assert.NoError(t, stream.Close()) }() for _, seqNum := range []uint16{id * 1, id * 2, id * 3, id * 4, id * 5} { assert.NoError(t, stream.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum}})) select { case p := <-stream.WrittenRTP(): assert.Equal(t, seqNum, p.SequenceNumber) ch <- p case <-time.After(10 * time.Millisecond): panic("written rtp packet not found") } } }(pChan, uint16(i+1)) } wg.Wait() close(pChan) }() for p := range pChan { // Can't check for increasing transport cc sequence number, since we can't ensure ordering between the streams // on pChan is same as in the interceptor, but at least make sure each packet has a seq nr. extensionHeader := p.GetExtension(1) twcc := &rtp.TransportCCExtension{} err = twcc.Unmarshal(extensionHeader) assert.NoError(t, err) } }) } interceptor-0.1.12/pkg/twcc/sender_interceptor.go000066400000000000000000000107431426573475500221260ustar00rootroot00000000000000package twcc import ( "math/rand" "sync" "time" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtp" ) // SenderInterceptorFactory is a interceptor.Factory for a SenderInterceptor type SenderInterceptorFactory struct { opts []Option } // NewInterceptor constructs a new SenderInterceptor func (s *SenderInterceptorFactory) NewInterceptor(id string) (interceptor.Interceptor, error) { i := &SenderInterceptor{ log: logging.NewDefaultLoggerFactory().NewLogger("twcc_sender_interceptor"), packetChan: make(chan packet), close: make(chan struct{}), interval: 100 * time.Millisecond, startTime: time.Now(), } for _, opt := range s.opts { err := opt(i) if err != nil { return nil, err } } return i, nil } // NewSenderInterceptor returns a new SenderInterceptorFactory configured with the given options. func NewSenderInterceptor(opts ...Option) (*SenderInterceptorFactory, error) { return &SenderInterceptorFactory{opts: opts}, nil } // SenderInterceptor sends transport wide congestion control reports as specified in: // https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01 type SenderInterceptor struct { interceptor.NoOp log logging.LeveledLogger m sync.Mutex wg sync.WaitGroup close chan struct{} interval time.Duration startTime time.Time recorder *Recorder packetChan chan packet } // An Option is a function that can be used to configure a SenderInterceptor type Option func(*SenderInterceptor) error // SendInterval sets the interval at which the interceptor // will send new feedback reports. func SendInterval(interval time.Duration) Option { return func(s *SenderInterceptor) error { s.interval = interval return nil } } // BindRTCPWriter lets you modify any outgoing RTCP packets. It is called once per PeerConnection. The returned method // will be called once per packet batch. func (s *SenderInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter { s.m.Lock() defer s.m.Unlock() s.recorder = NewRecorder(rand.Uint32()) // #nosec if s.isClosed() { return writer } s.wg.Add(1) go s.loop(writer) return writer } type packet struct { hdr *rtp.Header sequenceNumber uint16 arrivalTime int64 ssrc uint32 } // BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method // will be called once per rtp packet. func (s *SenderInterceptor) BindRemoteStream(info *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { var hdrExtID uint8 for _, e := range info.RTPHeaderExtensions { if e.URI == transportCCURI { hdrExtID = uint8(e.ID) break } } if hdrExtID == 0 { // Don't try to read header extension if ID is 0, because 0 is an invalid extension ID return reader } return interceptor.RTPReaderFunc(func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) { i, attr, err := reader.Read(buf, attributes) if err != nil { return 0, nil, err } if attr == nil { attr = make(interceptor.Attributes) } header, err := attr.GetRTPHeader(buf[:i]) if err != nil { return 0, nil, err } var tccExt rtp.TransportCCExtension if ext := header.GetExtension(hdrExtID); ext != nil { err = tccExt.Unmarshal(ext) if err != nil { return 0, nil, err } s.packetChan <- packet{ hdr: header, sequenceNumber: tccExt.TransportSequence, arrivalTime: time.Since(s.startTime).Microseconds(), ssrc: info.SSRC, } } return i, attr, nil }) } // Close closes the interceptor. func (s *SenderInterceptor) Close() error { defer s.wg.Wait() s.m.Lock() defer s.m.Unlock() if !s.isClosed() { close(s.close) } return nil } func (s *SenderInterceptor) isClosed() bool { select { case <-s.close: return true default: return false } } func (s *SenderInterceptor) loop(w interceptor.RTCPWriter) { defer s.wg.Done() select { case <-s.close: return case p := <-s.packetChan: s.recorder.Record(p.ssrc, p.sequenceNumber, p.arrivalTime) } ticker := time.NewTicker(s.interval) for { select { case <-s.close: ticker.Stop() return case p := <-s.packetChan: s.recorder.Record(p.ssrc, p.sequenceNumber, p.arrivalTime) case <-ticker.C: // build and send twcc pkts := s.recorder.BuildFeedbackPacket() if pkts == nil { continue } if _, err := w.Write(pkts, nil); err != nil { s.log.Error(err.Error()) } } } } interceptor-0.1.12/pkg/twcc/sender_interceptor_test.go000066400000000000000000000162571426573475500231730ustar00rootroot00000000000000package twcc import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestSenderInterceptor(t *testing.T) { t.Run("before any packets", func(t *testing.T) { f, err := NewSenderInterceptor() assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{SSRC: 1, RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, i) defer func() { assert.NoError(t, stream.Close()) }() var pkts []rtcp.Packet select { case pkts = <-stream.WrittenRTCP(): case <-time.After(300 * time.Millisecond): // wait longer than default interval } assert.Equal(t, 0, len(pkts)) }) t.Run("after RTP packets", func(t *testing.T) { f, err := NewSenderInterceptor() assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{SSRC: 1, RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, i) defer func() { assert.NoError(t, stream.Close()) }() for i := 0; i < 10; i++ { hdr := rtp.Header{} tcc, err := (&rtp.TransportCCExtension{TransportSequence: uint16(i)}).Marshal() assert.NoError(t, err) err = hdr.SetExtension(1, tcc) assert.NoError(t, err) stream.ReceiveRTP(&rtp.Packet{Header: hdr}) } pkts := <-stream.WrittenRTCP() assert.Equal(t, 1, len(pkts)) cc, ok := pkts[0].(*rtcp.TransportLayerCC) assert.True(t, ok) assert.Equal(t, uint32(1), cc.MediaSSRC) assert.Equal(t, uint16(0), cc.BaseSequenceNumber) assert.Equal(t, []rtcp.PacketStatusChunk{ &rtcp.RunLengthChunk{ PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 10, }, }, cc.PacketChunks) }) t.Run("different delays between RTP packets", func(t *testing.T) { f, err := NewSenderInterceptor(SendInterval(500 * time.Millisecond)) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, i) defer func() { assert.NoError(t, stream.Close()) }() delays := []int{0, 10, 100, 200} for i, d := range delays { time.Sleep(time.Duration(d) * time.Millisecond) hdr := rtp.Header{} tcc, err := (&rtp.TransportCCExtension{TransportSequence: uint16(i)}).Marshal() assert.NoError(t, err) err = hdr.SetExtension(1, tcc) assert.NoError(t, err) stream.ReceiveRTP(&rtp.Packet{Header: hdr}) } pkts := <-stream.WrittenRTCP() assert.Equal(t, 1, len(pkts)) cc, ok := pkts[0].(*rtcp.TransportLayerCC) assert.True(t, ok) assert.Equal(t, uint16(0), cc.BaseSequenceNumber) assert.Equal(t, []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketReceivedLargeDelta, }, }, }, cc.PacketChunks) }) t.Run("packet loss", func(t *testing.T) { f, err := NewSenderInterceptor(SendInterval(2 * time.Second)) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, i) defer func() { assert.NoError(t, stream.Close()) }() sequenceNumberToDelay := map[int]int{ 0: 0, 1: 10, 4: 100, 8: 200, 9: 20, 10: 20, 30: 300, } for _, i := range []int{0, 1, 4, 8, 9, 10, 30} { d := sequenceNumberToDelay[i] time.Sleep(time.Duration(d) * time.Millisecond) hdr := rtp.Header{} tcc, err := (&rtp.TransportCCExtension{TransportSequence: uint16(i)}).Marshal() assert.NoError(t, err) err = hdr.SetExtension(1, tcc) assert.NoError(t, err) stream.ReceiveRTP(&rtp.Packet{Header: hdr}) } pkts := <-stream.WrittenRTCP() assert.Equal(t, 1, len(pkts)) cc, ok := pkts[0].(*rtcp.TransportLayerCC) assert.True(t, ok) assert.Equal(t, uint16(0), cc.BaseSequenceNumber) assert.Equal(t, []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.RunLengthChunk{ PacketStatusSymbol: rtcp.TypeTCCPacketNotReceived, RunLength: 16, }, &rtcp.RunLengthChunk{ PacketStatusSymbol: rtcp.TypeTCCPacketReceivedLargeDelta, RunLength: 1, }, }, cc.PacketChunks) }) t.Run("overflow", func(t *testing.T) { f, err := NewSenderInterceptor(SendInterval(2 * time.Second)) assert.NoError(t, err) i, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, i) defer func() { assert.NoError(t, stream.Close()) }() for _, i := range []int{65530, 65534, 65535, 1, 2, 10} { hdr := rtp.Header{} tcc, err := (&rtp.TransportCCExtension{TransportSequence: uint16(i)}).Marshal() assert.NoError(t, err) err = hdr.SetExtension(1, tcc) assert.NoError(t, err) stream.ReceiveRTP(&rtp.Packet{Header: hdr}) } pkts := <-stream.WrittenRTCP() assert.Equal(t, 1, len(pkts)) cc, ok := pkts[0].(*rtcp.TransportLayerCC) assert.True(t, ok) assert.Equal(t, uint16(65530), cc.BaseSequenceNumber) assert.Equal(t, []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeOneBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedSmallDelta, }, }, }, cc.PacketChunks) }) } interceptor-0.1.12/pkg/twcc/twcc.go000066400000000000000000000161351426573475500171710ustar00rootroot00000000000000// Package twcc provides interceptors to implement transport wide congestion control. package twcc import ( "math" "github.com/pion/rtcp" ) type pktInfo struct { sequenceNumber uint32 arrivalTime int64 } // Recorder records incoming RTP packets and their delays and creates // transport wide congestion control feedback reports as specified in // https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01 type Recorder struct { receivedPackets []pktInfo cycles uint32 lastSequenceNumber uint16 senderSSRC uint32 mediaSSRC uint32 fbPktCnt uint8 } // NewRecorder creates a new Recorder which uses the given senderSSRC in the created // feedback packets. func NewRecorder(senderSSRC uint32) *Recorder { return &Recorder{ receivedPackets: []pktInfo{}, senderSSRC: senderSSRC, } } // Record marks a packet with mediaSSRC and a transport wide sequence number sequenceNumber as received at arrivalTime. func (r *Recorder) Record(mediaSSRC uint32, sequenceNumber uint16, arrivalTime int64) { r.mediaSSRC = mediaSSRC if sequenceNumber < 0x0fff && (r.lastSequenceNumber&0xffff) > 0xf000 { r.cycles += 1 << 16 } r.receivedPackets = insertSorted(r.receivedPackets, pktInfo{ sequenceNumber: r.cycles | uint32(sequenceNumber), arrivalTime: arrivalTime, }) r.lastSequenceNumber = sequenceNumber } func insertSorted(list []pktInfo, element pktInfo) []pktInfo { if len(list) == 0 { return append(list, element) } for i := len(list) - 1; i >= 0; i-- { if list[i].sequenceNumber < element.sequenceNumber { list = append(list, pktInfo{}) copy(list[i+2:], list[i+1:]) list[i+1] = element return list } if list[i].sequenceNumber == element.sequenceNumber { list[i] = element return list } } // element.sequenceNumber is between 0 and first ever received sequenceNumber return append([]pktInfo{element}, list...) } // BuildFeedbackPacket creates a new RTCP packet containing a TWCC feedback report. func (r *Recorder) BuildFeedbackPacket() []rtcp.Packet { feedback := newFeedback(r.senderSSRC, r.mediaSSRC, r.fbPktCnt) r.fbPktCnt++ if len(r.receivedPackets) < 2 { r.receivedPackets = []pktInfo{} return []rtcp.Packet{feedback.getRTCP()} } feedback.setBase(uint16(r.receivedPackets[0].sequenceNumber&0xffff), r.receivedPackets[0].arrivalTime) var pkts []rtcp.Packet for _, pkt := range r.receivedPackets { ok := feedback.addReceived(uint16(pkt.sequenceNumber&0xffff), pkt.arrivalTime) if !ok { pkts = append(pkts, feedback.getRTCP()) feedback = newFeedback(r.senderSSRC, r.mediaSSRC, r.fbPktCnt) r.fbPktCnt++ feedback.addReceived(uint16(pkt.sequenceNumber&0xffff), pkt.arrivalTime) } } r.receivedPackets = []pktInfo{} pkts = append(pkts, feedback.getRTCP()) return pkts } type feedback struct { rtcp *rtcp.TransportLayerCC baseSequenceNumber uint16 refTimestamp64MS int64 lastTimestampUS int64 nextSequenceNumber uint16 sequenceNumberCount uint16 len int lastChunk chunk chunks []rtcp.PacketStatusChunk deltas []*rtcp.RecvDelta } func newFeedback(senderSSRC, mediaSSRC uint32, count uint8) *feedback { return &feedback{ rtcp: &rtcp.TransportLayerCC{ SenderSSRC: senderSSRC, MediaSSRC: mediaSSRC, FbPktCount: count, }, } } func (f *feedback) setBase(sequenceNumber uint16, timeUS int64) { f.baseSequenceNumber = sequenceNumber f.nextSequenceNumber = f.baseSequenceNumber f.refTimestamp64MS = timeUS / 64e3 f.lastTimestampUS = f.refTimestamp64MS * 64e3 } func (f *feedback) getRTCP() *rtcp.TransportLayerCC { f.rtcp.PacketStatusCount = f.sequenceNumberCount f.rtcp.ReferenceTime = uint32(f.refTimestamp64MS) f.rtcp.BaseSequenceNumber = f.baseSequenceNumber for len(f.lastChunk.deltas) > 0 { f.chunks = append(f.chunks, f.lastChunk.encode()) } f.rtcp.PacketChunks = append(f.rtcp.PacketChunks, f.chunks...) f.rtcp.RecvDeltas = f.deltas padLen := 20 + len(f.rtcp.PacketChunks)*2 + f.len // 4 bytes header + 16 bytes twcc header + 2 bytes for each chunk + length of deltas padding := padLen%4 != 0 for padLen%4 != 0 { padLen++ } f.rtcp.Header = rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Padding: padding, Length: uint16((padLen / 4) - 1), } return f.rtcp } func (f *feedback) addReceived(sequenceNumber uint16, timestampUS int64) bool { deltaUS := timestampUS - f.lastTimestampUS delta250US := deltaUS / 250 if delta250US < math.MinInt16 || delta250US > math.MaxInt16 { // delta doesn't fit into 16 bit, need to create new packet return false } for ; f.nextSequenceNumber != sequenceNumber; f.nextSequenceNumber++ { if !f.lastChunk.canAdd(rtcp.TypeTCCPacketNotReceived) { f.chunks = append(f.chunks, f.lastChunk.encode()) } f.lastChunk.add(rtcp.TypeTCCPacketNotReceived) f.sequenceNumberCount++ } var recvDelta uint16 switch { case delta250US >= 0 && delta250US <= 0xff: f.len++ recvDelta = rtcp.TypeTCCPacketReceivedSmallDelta default: f.len += 2 recvDelta = rtcp.TypeTCCPacketReceivedLargeDelta } if !f.lastChunk.canAdd(recvDelta) { f.chunks = append(f.chunks, f.lastChunk.encode()) } f.lastChunk.add(recvDelta) f.deltas = append(f.deltas, &rtcp.RecvDelta{ Type: recvDelta, Delta: deltaUS, }) f.lastTimestampUS = timestampUS f.sequenceNumberCount++ f.nextSequenceNumber++ return true } const ( maxRunLengthCap = 0x1fff // 13 bits maxOneBitCap = 14 // bits maxTwoBitCap = 7 // bits ) type chunk struct { hasLargeDelta bool hasDifferentTypes bool deltas []uint16 } func (c *chunk) canAdd(delta uint16) bool { if len(c.deltas) < maxTwoBitCap { return true } if len(c.deltas) < maxOneBitCap && !c.hasLargeDelta && delta != rtcp.TypeTCCPacketReceivedLargeDelta { return true } if len(c.deltas) < maxRunLengthCap && !c.hasDifferentTypes && delta == c.deltas[0] { return true } return false } func (c *chunk) add(delta uint16) { c.deltas = append(c.deltas, delta) c.hasLargeDelta = c.hasLargeDelta || delta == rtcp.TypeTCCPacketReceivedLargeDelta c.hasDifferentTypes = c.hasDifferentTypes || delta != c.deltas[0] } func (c *chunk) encode() rtcp.PacketStatusChunk { if !c.hasDifferentTypes { defer c.reset() return &rtcp.RunLengthChunk{ PacketStatusSymbol: c.deltas[0], RunLength: uint16(len(c.deltas)), } } if len(c.deltas) == maxOneBitCap { defer c.reset() return &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeOneBit, SymbolList: c.deltas, } } minCap := min(maxTwoBitCap, len(c.deltas)) svc := &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: c.deltas[:minCap], } c.deltas = c.deltas[minCap:] c.hasDifferentTypes = false c.hasLargeDelta = false if len(c.deltas) > 0 { tmp := c.deltas[0] for _, d := range c.deltas { if tmp != d { c.hasDifferentTypes = true } if d == rtcp.TypeTCCPacketReceivedLargeDelta { c.hasLargeDelta = true } } } return svc } func (c *chunk) reset() { c.deltas = []uint16{} c.hasLargeDelta = false c.hasDifferentTypes = false } func min(a, b int) int { if a < b { return a } return b } interceptor-0.1.12/pkg/twcc/twcc_test.go000066400000000000000000000612371426573475500202330ustar00rootroot00000000000000package twcc import ( "fmt" "testing" "github.com/pion/rtcp" "github.com/stretchr/testify/assert" ) func rtcpToTwcc(t *testing.T, in []rtcp.Packet) []*rtcp.TransportLayerCC { out := make([]*rtcp.TransportLayerCC, len(in)) var ok bool for i, pkt := range in { if out[i], ok = pkt.(*rtcp.TransportLayerCC); !ok { t.Fatal("Failed to cast") } } return out } func Test_chunk_add(t *testing.T) { t.Run("fill with not received", func(t *testing.T) { c := &chunk{} for i := 0; i < maxRunLengthCap; i++ { assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived), i) c.add(rtcp.TypeTCCPacketNotReceived) } assert.Equal(t, make([]uint16, maxRunLengthCap), c.deltas) assert.False(t, c.hasDifferentTypes) assert.False(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) assert.False(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) assert.False(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) statusChunk := c.encode() assert.IsType(t, &rtcp.RunLengthChunk{}, statusChunk) buf, err := statusChunk.Marshal() assert.NoError(t, err) assert.Equal(t, []byte{0x1f, 0xff}, buf) }) t.Run("fill with small delta", func(t *testing.T) { c := &chunk{} for i := 0; i < maxOneBitCap; i++ { assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta), i) c.add(rtcp.TypeTCCPacketReceivedSmallDelta) } assert.Equal(t, c.deltas, []uint16{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) assert.False(t, c.hasDifferentTypes) assert.False(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) assert.False(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) statusChunk := c.encode() assert.IsType(t, &rtcp.RunLengthChunk{}, statusChunk) buf, err := statusChunk.Marshal() assert.NoError(t, err) assert.Equal(t, []byte{0x20, 0xe}, buf) }) t.Run("fill with large delta", func(t *testing.T) { c := &chunk{} for i := 0; i < maxTwoBitCap; i++ { assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta), i) c.add(rtcp.TypeTCCPacketReceivedLargeDelta) } assert.Equal(t, c.deltas, []uint16{2, 2, 2, 2, 2, 2, 2}) assert.True(t, c.hasLargeDelta) assert.False(t, c.hasDifferentTypes) assert.False(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) assert.False(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) statusChunk := c.encode() assert.IsType(t, &rtcp.RunLengthChunk{}, statusChunk) buf, err := statusChunk.Marshal() assert.NoError(t, err) assert.Equal(t, []byte{0x40, 0x7}, buf) }) t.Run("fill with different types", func(t *testing.T) { c := &chunk{} assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) c.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) c.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) c.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) c.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) c.add(rtcp.TypeTCCPacketReceivedLargeDelta) assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) c.add(rtcp.TypeTCCPacketReceivedLargeDelta) assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) c.add(rtcp.TypeTCCPacketReceivedLargeDelta) assert.False(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) statusChunk := c.encode() assert.IsType(t, &rtcp.StatusVectorChunk{}, statusChunk) buf, err := statusChunk.Marshal() assert.NoError(t, err) assert.Equal(t, []byte{0xd5, 0x6a}, buf) }) t.Run("overfill and encode", func(t *testing.T) { c := chunk{} assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) c.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) c.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) c.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) c.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) c.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) c.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) c.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, c.canAdd(rtcp.TypeTCCPacketNotReceived)) c.add(rtcp.TypeTCCPacketNotReceived) assert.False(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) statusChunk1 := c.encode() assert.IsType(t, &rtcp.StatusVectorChunk{}, statusChunk1) assert.Equal(t, 1, len(c.deltas)) assert.True(t, c.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) c.add(rtcp.TypeTCCPacketReceivedLargeDelta) statusChunk2 := c.encode() assert.IsType(t, &rtcp.StatusVectorChunk{}, statusChunk2) assert.Equal(t, 0, len(c.deltas)) assert.Equal(t, &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedLargeDelta}, }, statusChunk2) }) } func Test_feedback(t *testing.T) { t.Run("add simple", func(t *testing.T) { f := feedback{} got := f.addReceived(0, 10) assert.True(t, got) }) t.Run("add too large", func(t *testing.T) { f := feedback{} assert.False(t, f.addReceived(12, 8200*1000*250)) }) t.Run("add received 1", func(t *testing.T) { f := &feedback{} f.setBase(1, 1000*1000) got := f.addReceived(1, 1023*1000) assert.True(t, got) assert.Equal(t, uint16(2), f.nextSequenceNumber) assert.Equal(t, int64(15), f.refTimestamp64MS) got = f.addReceived(4, 1086*1000) assert.True(t, got) assert.Equal(t, uint16(5), f.nextSequenceNumber) assert.Equal(t, int64(15), f.refTimestamp64MS) assert.True(t, f.lastChunk.hasDifferentTypes) assert.Equal(t, 4, len(f.lastChunk.deltas)) assert.NotContains(t, f.lastChunk.deltas, rtcp.TypeTCCPacketReceivedLargeDelta) }) t.Run("add received 2", func(t *testing.T) { f := newFeedback(0, 0, 0) f.setBase(5, 320*1000) got := f.addReceived(5, 320*1000) assert.True(t, got) got = f.addReceived(7, 448*1000) assert.True(t, got) got = f.addReceived(8, 512*1000) assert.True(t, got) got = f.addReceived(11, 768*1000) assert.True(t, got) pkt := f.getRTCP() assert.True(t, pkt.Header.Padding) assert.Equal(t, uint16(7), pkt.Header.Length) assert.Equal(t, uint16(5), pkt.BaseSequenceNumber) assert.Equal(t, uint16(7), pkt.PacketStatusCount) assert.Equal(t, uint32(5), pkt.ReferenceTime) assert.Equal(t, uint8(0), pkt.FbPktCount) assert.Equal(t, 1, len(pkt.PacketChunks)) assert.Equal(t, []rtcp.PacketStatusChunk{&rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedLargeDelta, }, }}, pkt.PacketChunks) expectedDeltas := []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0, }, { Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: 0x0200 * rtcp.TypeTCCDeltaScaleFactor, }, { Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: 0x0100 * rtcp.TypeTCCDeltaScaleFactor, }, { Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: 0x0400 * rtcp.TypeTCCDeltaScaleFactor, }, } assert.Equal(t, len(expectedDeltas), len(pkt.RecvDeltas)) for i, d := range expectedDeltas { assert.Equal(t, d, pkt.RecvDeltas[i]) } }) t.Run("add received wrapped sequence number", func(t *testing.T) { f := newFeedback(0, 0, 0) f.setBase(65535, 320*1000) got := f.addReceived(65535, 320*1000) assert.True(t, got) got = f.addReceived(7, 448*1000) assert.True(t, got) got = f.addReceived(8, 512*1000) assert.True(t, got) got = f.addReceived(11, 768*1000) assert.True(t, got) pkt := f.getRTCP() assert.True(t, pkt.Header.Padding) assert.Equal(t, uint16(7), pkt.Header.Length) assert.Equal(t, uint16(65535), pkt.BaseSequenceNumber) assert.Equal(t, uint16(13), pkt.PacketStatusCount) assert.Equal(t, uint32(5), pkt.ReferenceTime) assert.Equal(t, uint8(0), pkt.FbPktCount) assert.Equal(t, 2, len(pkt.PacketChunks)) assert.Equal(t, []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, }, }, &rtcp.StatusVectorChunk{ SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketReceivedLargeDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedLargeDelta, }, }, }, pkt.PacketChunks) expectedDeltas := []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0, }, { Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: 0x0200 * rtcp.TypeTCCDeltaScaleFactor, }, { Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: 0x0100 * rtcp.TypeTCCDeltaScaleFactor, }, { Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: 0x0400 * rtcp.TypeTCCDeltaScaleFactor, }, } assert.Equal(t, len(expectedDeltas), len(pkt.RecvDeltas)) for i, d := range expectedDeltas { assert.Equal(t, d, pkt.RecvDeltas[i]) } }) t.Run("get RTCP", func(t *testing.T) { testcases := []struct { arrivalTS int64 sequenceNumber uint16 wantRefTime uint32 wantBaseSequenceNumber uint16 }{ {320, 1, 5, 1}, {1000, 2, 15, 2}, } for _, tt := range testcases { tt := tt t.Run("set correct base seq and time", func(t *testing.T) { f := newFeedback(0, 0, 0) f.setBase(tt.sequenceNumber, tt.arrivalTS*1000) got := f.getRTCP() assert.Equal(t, tt.wantRefTime, got.ReferenceTime) assert.Equal(t, tt.wantBaseSequenceNumber, got.BaseSequenceNumber) }) } }) } func addRun(t *testing.T, r *Recorder, sequenceNumbers []uint16, arrivalTimes []int64) { assert.Equal(t, len(sequenceNumbers), len(arrivalTimes)) for i := range sequenceNumbers { r.Record(5000, sequenceNumbers[i], arrivalTimes[i]) } } const ( scaleFactorReferenceTime = 64000 ) func increaseTime(arrivalTime *int64, increaseAmount int64) int64 { *arrivalTime += increaseAmount return *arrivalTime } func marshalAll(t *testing.T, pkts []rtcp.Packet) { for _, pkt := range pkts { _, err := pkt.Marshal() assert.NoError(t, err) } } func TestBuildFeedbackPacket(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{0, 1, 2, 3, 4, 5, 6, 7}, []int64{ scaleFactorReferenceTime, increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor*256), }) rtcpPackets := r.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) assert.Equal(t, &rtcp.TransportLayerCC{ Header: rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Padding: true, Length: 8, }, SenderSSRC: 5000, MediaSSRC: 5000, BaseSequenceNumber: 0, ReferenceTime: 1, FbPktCount: 0, PacketStatusCount: 8, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.RunLengthChunk{ Type: rtcp.TypeTCCRunLengthChunk, PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 7, }, &rtcp.RunLengthChunk{ Type: rtcp.TypeTCCRunLengthChunk, PacketStatusSymbol: rtcp.TypeTCCPacketReceivedLargeDelta, RunLength: 1, }, }, RecvDeltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: rtcp.TypeTCCDeltaScaleFactor * 256}, }, }, rtcpToTwcc(t, rtcpPackets)[0]) marshalAll(t, rtcpPackets) } func TestBuildFeedbackPacket_Rolling(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{65535}, []int64{ arrivalTime, }) rtcpPackets := r.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) addRun(t, r, []uint16{4, 8, 9, 10}, []int64{ increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) rtcpPackets = r.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) assert.Equal(t, &rtcp.TransportLayerCC{ Header: rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Padding: true, Length: 6, }, SenderSSRC: 5000, MediaSSRC: 5000, BaseSequenceNumber: 4, ReferenceTime: 1, FbPktCount: 1, PacketStatusCount: 7, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ Type: rtcp.TypeTCCRunLengthChunk, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, }, }, }, RecvDeltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, }, }, rtcpToTwcc(t, rtcpPackets)[0]) marshalAll(t, rtcpPackets) } func TestBuildFeedbackPacketCount(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{0}, []int64{ arrivalTime, }) pkts := r.BuildFeedbackPacket() assert.Len(t, pkts, 1) twcc := rtcpToTwcc(t, pkts)[0] assert.Equal(t, uint8(0), twcc.FbPktCount) addRun(t, r, []uint16{0}, []int64{ arrivalTime, }) pkts = r.BuildFeedbackPacket() assert.Len(t, pkts, 1) twcc = rtcpToTwcc(t, pkts)[0] assert.Equal(t, uint8(1), twcc.FbPktCount) } func TestDuplicatePackets(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{12, 13, 13, 14}, []int64{ arrivalTime, arrivalTime, arrivalTime, arrivalTime, }) rtcpPackets := r.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) assert.Equal(t, &rtcp.TransportLayerCC{ Header: rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Padding: true, Length: 6, }, SenderSSRC: 5000, MediaSSRC: 5000, BaseSequenceNumber: 12, ReferenceTime: 1, FbPktCount: 0, PacketStatusCount: 3, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.RunLengthChunk{ PacketStatusChunk: nil, Type: rtcp.TypeTCCRunLengthChunk, PacketStatusSymbol: rtcp.TypeTCCPacketReceivedSmallDelta, RunLength: 3, }, }, RecvDeltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, }, }, rtcpToTwcc(t, rtcpPackets)[0]) marshalAll(t, rtcpPackets) } func TestShortDeltas(t *testing.T) { t.Run("SplitsOneBitDeltas", func(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{3, 4, 5, 7, 6, 8, 10, 11, 13, 14}, []int64{ arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, }) rtcpPackets := r.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) pkt := rtcpToTwcc(t, rtcpPackets)[0] bs, err := pkt.Marshal() unmarshalled := &rtcp.TransportLayerCC{} assert.NoError(t, err) assert.NoError(t, unmarshalled.Unmarshal(bs)) assert.Equal(t, &rtcp.TransportLayerCC{ Header: rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Padding: true, Length: 8, }, SenderSSRC: 5000, MediaSSRC: 5000, BaseSequenceNumber: 3, ReferenceTime: 1, FbPktCount: 0, PacketStatusCount: 12, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.BitVectorChunkType, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{1, 1, 1, 1, 1, 1, 0}, }, &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.BitVectorChunkType, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{1, 1, 0, 1, 1, 0, 0}, }, }, RecvDeltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, }, }, unmarshalled) marshalAll(t, rtcpPackets) }) t.Run("padsTwoBitDeltas", func(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{3, 4, 5, 7}, []int64{ arrivalTime, arrivalTime, arrivalTime, arrivalTime, }) rtcpPackets := r.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) pkt := rtcpToTwcc(t, rtcpPackets)[0] bs, err := pkt.Marshal() unmarshalled := &rtcp.TransportLayerCC{} assert.NoError(t, err) assert.NoError(t, unmarshalled.Unmarshal(bs)) assert.Equal(t, &rtcp.TransportLayerCC{ Header: rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Padding: true, Length: 6, }, SenderSSRC: 5000, MediaSSRC: 5000, BaseSequenceNumber: 3, ReferenceTime: 1, FbPktCount: 0, PacketStatusCount: 5, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.BitVectorChunkType, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{1, 1, 1, 0, 1, 0, 0}, }, }, RecvDeltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, }, }, unmarshalled) marshalAll(t, rtcpPackets) }) } func TestReorderedPackets(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{3, 4, 5, 7, 6, 8, 10, 11, 13, 14}, []int64{ increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) rtcpPackets := r.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) pkt := rtcpToTwcc(t, rtcpPackets)[0] bs, err := pkt.Marshal() unmarshalled := &rtcp.TransportLayerCC{} assert.NoError(t, err) assert.NoError(t, unmarshalled.Unmarshal(bs)) assert.Equal(t, &rtcp.TransportLayerCC{ Header: rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Padding: true, Length: 8, }, SenderSSRC: 5000, MediaSSRC: 5000, BaseSequenceNumber: 3, ReferenceTime: 1, FbPktCount: 0, PacketStatusCount: 12, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.BitVectorChunkType, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{1, 1, 1, 1, 2, 1, 0}, }, &rtcp.StatusVectorChunk{ PacketStatusChunk: nil, Type: rtcp.BitVectorChunkType, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{1, 1, 0, 1, 1, 0, 0}, }, }, RecvDeltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 2 * rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedLargeDelta, Delta: -rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 2 * rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: rtcp.TypeTCCDeltaScaleFactor}, }, }, unmarshalled) marshalAll(t, rtcpPackets) } func TestInsertSorted(t *testing.T) { cases := []struct { l []pktInfo e pktInfo expected []pktInfo }{ { l: []pktInfo{}, e: pktInfo{}, expected: []pktInfo{{ sequenceNumber: 0, arrivalTime: 0, }}, }, { l: []pktInfo{ { sequenceNumber: 0, arrivalTime: 0, }, { sequenceNumber: 1, arrivalTime: 0, }, }, e: pktInfo{ sequenceNumber: 2, arrivalTime: 0, }, expected: []pktInfo{ { sequenceNumber: 0, arrivalTime: 0, }, { sequenceNumber: 1, arrivalTime: 0, }, { sequenceNumber: 2, arrivalTime: 0, }, }, }, { l: []pktInfo{ { sequenceNumber: 0, arrivalTime: 0, }, { sequenceNumber: 2, arrivalTime: 0, }, }, e: pktInfo{ sequenceNumber: 1, arrivalTime: 0, }, expected: []pktInfo{ { sequenceNumber: 0, arrivalTime: 0, }, { sequenceNumber: 1, arrivalTime: 0, }, { sequenceNumber: 2, arrivalTime: 0, }, }, }, { l: []pktInfo{ { sequenceNumber: 0, arrivalTime: 0, }, { sequenceNumber: 1, arrivalTime: 0, }, { sequenceNumber: 2, arrivalTime: 0, }, }, e: pktInfo{ sequenceNumber: 1, arrivalTime: 0, }, expected: []pktInfo{ { sequenceNumber: 0, arrivalTime: 0, }, { sequenceNumber: 1, arrivalTime: 0, }, { sequenceNumber: 2, arrivalTime: 0, }, }, }, { l: []pktInfo{ { sequenceNumber: 10, arrivalTime: 0, }, }, e: pktInfo{ sequenceNumber: 9, arrivalTime: 0, }, expected: []pktInfo{ { sequenceNumber: 9, arrivalTime: 0, }, { sequenceNumber: 10, arrivalTime: 0, }, }, }, } for i, c := range cases { c := c t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { assert.Equal(t, c.expected, insertSorted(c.l, c.e)) }) } } interceptor-0.1.12/registry.go000066400000000000000000000012021426573475500163450ustar00rootroot00000000000000package interceptor // Registry is a collector for interceptors. type Registry struct { factories []Factory } // Add adds a new Interceptor to the registry. func (r *Registry) Add(f Factory) { r.factories = append(r.factories, f) } // Build constructs a single Interceptor from a InterceptorRegistry func (r *Registry) Build(id string) (Interceptor, error) { if len(r.factories) == 0 { return &NoOp{}, nil } interceptors := []Interceptor{} for _, f := range r.factories { i, err := f.NewInterceptor(id) if err != nil { return nil, err } interceptors = append(interceptors, i) } return NewChain(interceptors), nil } interceptor-0.1.12/renovate.json000066400000000000000000000011661426573475500166750ustar00rootroot00000000000000{ "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" ] } interceptor-0.1.12/streaminfo.go000066400000000000000000000020201426573475500166430ustar00rootroot00000000000000package interceptor // RTPHeaderExtension represents a negotiated RFC5285 RTP header extension. type RTPHeaderExtension struct { URI string ID int } // StreamInfo is the Context passed when a StreamLocal or StreamRemote has been Binded or Unbinded type StreamInfo struct { ID string Attributes Attributes SSRC uint32 PayloadType uint8 RTPHeaderExtensions []RTPHeaderExtension MimeType string ClockRate uint32 Channels uint16 SDPFmtpLine string RTCPFeedback []RTCPFeedback } // RTCPFeedback signals the connection to use additional RTCP packet types. // https://draft.ortc.org/#dom-rtcrtcpfeedback type RTCPFeedback struct { // Type is the type of feedback. // see: https://draft.ortc.org/#dom-rtcrtcpfeedback // valid: ack, ccm, nack, goog-remb, transport-cc Type string // The parameter value depends on the type. // For example, type="nack" parameter="pli" will send Picture Loss Indicator packets. Parameter string }