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


Pion Interceptor

RTP and RTCP processors for building real time communications

Pion Interceptor join us on Discord Follow us on Bluesky
GitHub Workflow Status Go Reference 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) * [Stats](https://github.com/pion/interceptor/tree/master/pkg/stats) A [webrtc-stats](https://www.w3.org/TR/webrtc-stats/) compliant statistics generation * [Interval PLI](https://github.com/pion/interceptor/tree/master/pkg/intervalpli) Generate PLI on a interval. Useful when no decoder is available. * [FlexFec](https://github.com/pion/interceptor/tree/master/pkg/flexfec) – [FlexFEC-03](https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03) encoder implementation ### Planned Interceptors * Bandwidth Estimation - [NADA](https://tools.ietf.org/html/rfc8698) * JitterBuffer, re-order packets and wait for arrival * [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. ### Roadmap The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. ### Community Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. We are always looking to support **your projects**. Please reach out if you have something to build! If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) ### Contributing Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible ### License MIT License - see [LICENSE](LICENSE) for full text interceptor-0.1.42/attributes.go000066400000000000000000000032701510612111000166370ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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[any]any // Get returns the attribute associated with key. func (a Attributes) Get(key any) any { return a[key] } // Set sets the attribute associated with key to the given value. func (a Attributes) Set(key any, val any) { 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 attributes. 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 unmarshalled 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.42/attributes_test.go000066400000000000000000000062161510612111000177010ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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, 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, 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, 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.42/chain.go000066400000000000000000000050331510612111000155320ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/codecov.yml000066400000000000000000000007151510612111000162700ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT coverage: status: project: default: # Allow decreasing 2% of total coverage to avoid noise. threshold: 2% patch: default: target: 70% only_pulls: true ignore: - "examples/*" - "examples/**/*" interceptor-0.1.42/errors.go000066400000000000000000000016231510612111000157650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/errors_test.go000066400000000000000000000015451510612111000170270ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package interceptor import ( "errors" "testing" "github.com/stretchr/testify/assert" ) 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" assert.Equal(t, str, errs.Error(), "String representation doesn't match") errIs, ok := errs.(multiError) //nolint assert.True(t, ok, "FlattenErrs returns non-multiError") for i := 0; i < 3; i++ { assert.True(t, errIs.Is(rawErrs[i]), "Should contain error %d", i) } assert.False(t, errIs.Is(rawErrs[3]), "Should not contain error %d", 3) } interceptor-0.1.42/examples/000077500000000000000000000000001510612111000157365ustar00rootroot00000000000000interceptor-0.1.42/examples/nack/000077500000000000000000000000001510612111000166525ustar00rootroot00000000000000interceptor-0.1.42/examples/nack/README.md000066400000000000000000000025361510612111000201370ustar00rootroot00000000000000# 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.42/examples/nack/main.go000066400000000000000000000102631510612111000201270ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package demonstrates how to use the NACK interceptor package 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 } } } //nolint:cyclop 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, _ 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.42/go.mod000066400000000000000000000006361510612111000152330ustar00rootroot00000000000000module github.com/pion/interceptor go 1.21 require ( github.com/pion/logging v0.2.4 github.com/pion/rtcp v1.2.16 github.com/pion/rtp v1.8.25 github.com/pion/transport/v3 v3.1.1 github.com/stretchr/testify v1.11.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) interceptor-0.1.42/go.sum000066400000000000000000000035071510612111000152600ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= 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.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= github.com/pion/rtp v1.8.25 h1:b8+y44GNbwOJTYWuVan7SglX/hMlicVCAtL50ztyZHw= github.com/pion/rtp v1.8.25/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= interceptor-0.1.42/interceptor.go000066400000000000000000000072031510612111000170070ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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.42/internal/000077500000000000000000000000001510612111000157345ustar00rootroot00000000000000interceptor-0.1.42/internal/cc/000077500000000000000000000000001510612111000163215ustar00rootroot00000000000000interceptor-0.1.42/internal/cc/acknowledgment.go000066400000000000000000000014431510612111000216540ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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("\tTWCC:\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.42/internal/cc/cc.go000066400000000000000000000002731510612111000172370ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package cc implements common constructs used by Congestion Controllers package cc interceptor-0.1.42/internal/cc/feedback_adapter.go000066400000000000000000000167161510612111000221070ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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), //nolint:gosec // G115 } 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( _ 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)) //nolint:gosec // G115 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)) //nolint:gosec // G115 default: return nil, errInvalidFeedback } } return result, nil } // OnRFC8888Feedback converts incoming Congestion Control Feedback RTCP packet // to Acknowledgments. func (f *FeedbackAdapter) OnRFC8888Feedback(_ 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) //nolint:gosec // G115 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.42/internal/cc/feedback_adapter_test.go000066400000000000000000000647101510612111000231430ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 { t.Helper() 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 } //nolint:maintidx,cyclop 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.42/internal/ntp/000077500000000000000000000000001510612111000165355ustar00rootroot00000000000000interceptor-0.1.42/internal/ntp/ntp.go000066400000000000000000000031061510612111000176650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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) //nolint:gosec // G115 } // ToNTP32 converts a time.Time object to a uint32 NTP timestamp. func ToNTP32(t time.Time) uint32 { return uint32(ToNTP(t) >> 16) //nolint:gosec // G115 } // 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) //nolint:gosec // G115 d := time.Duration(seconds)*time.Second + time.Duration(fractional*1e9)*time.Nanosecond return time.Unix(0, 0).Add(-2208988800 * time.Second).Add(d) } // ToTime32 converts a uint32 NTP timestamp to a time.Time object, using the // highest 16 bit of the reference to recover the lost bits. The low 16 bits are // not recovered. func ToTime32(t uint32, reference time.Time) time.Time { referenceNTP := ToNTP(reference) & 0xFFFF000000000000 tu64 := ((uint64(t) << 16) & 0x0000FFFFFFFF0000) | referenceNTP return ToTime(tu64) } interceptor-0.1.42/internal/ntp/ntp_test.go000066400000000000000000000034371510612111000207330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package ntp import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) func TestNTPToTimeConverstion(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())) assert.InDelta( t, cc.ts.UnixNano(), ToTime32(ToNTP32(cc.ts), 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))) }) } } 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, //nolint:gosec // G115 expected: uint32(uint(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 := ToNTP32(cc.input) assert.Equalf(t, cc.expected, res, "%b != %b", cc.expected, res) }) } } interceptor-0.1.42/internal/rtpbuffer/000077500000000000000000000000001510612111000177335ustar00rootroot00000000000000interceptor-0.1.42/internal/rtpbuffer/errors.go000066400000000000000000000012031510612111000215720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package rtpbuffer import "errors" // ErrInvalidSize is returned by newReceiveLog/newRTPBuffer, 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") errPaddingOverflow = errors.New("padding size exceeds payload size") ) interceptor-0.1.42/internal/rtpbuffer/packet_factory.go000066400000000000000000000107421510612111000232640ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package rtpbuffer import ( "encoding/binary" "io" "sync" "github.com/pion/rtp" ) const rtxSsrcByteLength = 2 // PacketFactory allows custom logic around the handle of RTP Packets before they added to the RTPBuffer. // The NoOpPacketFactory doesn't copy packets, while the RetainablePacket will take a copy before adding. type PacketFactory interface { NewPacket(header *rtp.Header, payload []byte, rtxSsrc uint32, rtxPayloadType uint8) (*RetainablePacket, error) } // PacketFactoryCopy is PacketFactory that takes a copy of packets when added to the RTPBuffer. type PacketFactoryCopy struct { headerPool *sync.Pool payloadPool *sync.Pool rtxSequencer rtp.Sequencer } // NewPacketFactoryCopy constructs a PacketFactory that takes a copy of packets when added to the RTPBuffer. func NewPacketFactoryCopy() *PacketFactoryCopy { return &PacketFactoryCopy{ headerPool: &sync.Pool{ New: func() any { return &rtp.Header{} }, }, payloadPool: &sync.Pool{ New: func() any { buf := make([]byte, maxPayloadLen) return &buf }, }, rtxSequencer: rtp.NewRandomSequencer(), } } // NewPacket constructs a new RetainablePacket that can be added to the RTPBuffer. // //nolint:cyclop func (m *PacketFactoryCopy) NewPacket( header *rtp.Header, payload []byte, rtxSsrc uint32, rtxPayloadType uint8, ) (*RetainablePacket, error) { if len(payload) > maxPayloadLen { return nil, io.ErrShortBuffer } retainablePacket := &RetainablePacket{ onRelease: m.releasePacket, sequenceNumber: header.SequenceNumber, // new packets have retain count of 1 count: 1, } var ok bool retainablePacket.header, ok = m.headerPool.Get().(*rtp.Header) if !ok { return nil, errFailedToCastHeaderPool } *retainablePacket.header = header.Clone() if payload != nil { retainablePacket.buffer, ok = m.payloadPool.Get().(*[]byte) if !ok { return nil, errFailedToCastPayloadPool } if rtxSsrc != 0 && rtxPayloadType != 0 { size := copy((*retainablePacket.buffer)[rtxSsrcByteLength:], payload) retainablePacket.payload = (*retainablePacket.buffer)[:size+rtxSsrcByteLength] } else { size := copy(*retainablePacket.buffer, payload) retainablePacket.payload = (*retainablePacket.buffer)[:size] } } if rtxSsrc != 0 && rtxPayloadType != 0 { //nolint:nestif if payload == nil { retainablePacket.buffer, ok = m.payloadPool.Get().(*[]byte) if !ok { return nil, errFailedToCastPayloadPool } retainablePacket.payload = (*retainablePacket.buffer)[:rtxSsrcByteLength] } // Write the original sequence number at the beginning of the payload. binary.BigEndian.PutUint16(retainablePacket.payload, retainablePacket.header.SequenceNumber) // Rewrite the SSRC. retainablePacket.header.SSRC = rtxSsrc // Rewrite the payload type. retainablePacket.header.PayloadType = rtxPayloadType // Rewrite the sequence number. retainablePacket.header.SequenceNumber = m.rtxSequencer.NextSequenceNumber() // Remove padding if present. if retainablePacket.header.Padding { // Older versions of pion/rtp didn't have the Header.PaddingSize field and as a workaround // users had to add padding to the payload. We need to handle this case here. if retainablePacket.header.PaddingSize == 0 && len(retainablePacket.payload) > 0 { paddingLength := int(retainablePacket.payload[len(retainablePacket.payload)-1]) if paddingLength > len(retainablePacket.payload) { return nil, errPaddingOverflow } retainablePacket.payload = (*retainablePacket.buffer)[:len(retainablePacket.payload)-paddingLength] } retainablePacket.header.Padding = false retainablePacket.header.PaddingSize = 0 } } return retainablePacket, nil } func (m *PacketFactoryCopy) releasePacket(header *rtp.Header, payload *[]byte) { m.headerPool.Put(header) if payload != nil { m.payloadPool.Put(payload) } } // PacketFactoryNoOp is a PacketFactory implementation that doesn't copy packets. type PacketFactoryNoOp struct{} // NewPacket constructs a new RetainablePacket that can be added to the RTPBuffer. func (f *PacketFactoryNoOp) NewPacket( header *rtp.Header, payload []byte, _ uint32, _ uint8, ) (*RetainablePacket, error) { return &RetainablePacket{ onRelease: f.releasePacket, count: 1, header: header, payload: payload, sequenceNumber: header.SequenceNumber, }, nil } func (f *PacketFactoryNoOp) releasePacket(_ *rtp.Header, _ *[]byte) { // no-op } interceptor-0.1.42/internal/rtpbuffer/retainable_packet.go000066400000000000000000000023151510612111000237200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package rtpbuffer import ( "sync" "github.com/pion/rtp" ) // RetainablePacket is a referenced counted RTP packet. type RetainablePacket struct { onRelease func(*rtp.Header, *[]byte) countMu sync.Mutex count int header *rtp.Header buffer *[]byte payload []byte sequenceNumber uint16 } // Header returns the RTP Header of the RetainablePacket. func (p *RetainablePacket) Header() *rtp.Header { return p.header } // Payload returns the RTP Payload of the RetainablePacket. func (p *RetainablePacket) Payload() []byte { return p.payload } // Retain increases the reference count of the RetainablePacket. func (p *RetainablePacket) Retain() error { p.countMu.Lock() defer p.countMu.Unlock() if p.count == 0 { // already released return errPacketReleased } p.count++ return nil } // Release decreases the reference count of the RetainablePacket and frees if needed. 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.42/internal/rtpbuffer/rtpbuffer.go000066400000000000000000000041551510612111000222660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package rtpbuffer provides a buffer for storing RTP packets package rtpbuffer import ( "fmt" ) const ( // Uint16SizeHalf is half of a math.Uint16. Uint16SizeHalf = 1 << 15 maxPayloadLen = 1460 ) // RTPBuffer stores RTP packets and allows custom logic // around the lifetime of them via the PacketFactory. type RTPBuffer struct { packets []*RetainablePacket size uint16 highestAdded uint16 started bool } // NewRTPBuffer constructs a new RTPBuffer. func NewRTPBuffer(size uint16) (*RTPBuffer, error) { allowedSizes := make([]uint16, 0) correctSize := false for i := 0; i < 16; i++ { if size == 1<= Uint16SizeHalf { return nil } if diff >= r.size { return nil } pkt := r.packets[seq%r.size] if pkt != nil { if pkt.sequenceNumber != seq { return nil } // already released if err := pkt.Retain(); err != nil { return nil } } return pkt } interceptor-0.1.42/internal/rtpbuffer/rtpbuffer_test.go000066400000000000000000000171051510612111000233240ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package rtpbuffer import ( "bytes" "testing" "github.com/pion/rtp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRTPBuffer(t *testing.T) { pm := NewPacketFactoryCopy() 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 := NewRTPBuffer(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, 0, 0) require.NoError(t, err) sb.Add(pkt) } } assertGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n packet := sb.Get(seq) assert.NotNil(t, packet, "packet not found: %d", seq) assert.Equal(t, seq, packet.Header().SequenceNumber, "packet for %d returned with incorrect SequenceNumber", seq) packet.Release() } } assertNOTGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n packet := sb.Get(seq) assert.Nil(t, packet, "packet found for %d", seq) } } 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 TestRTPBuffer_WithRTX(t *testing.T) { pm := NewPacketFactoryCopy() 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 := NewRTPBuffer(8) require.NoError(t, err) add := func(nums ...uint16) { for _, n := range nums { seq := start + n pkt, err := pm.NewPacket(&rtp.Header{SequenceNumber: seq, PayloadType: 2}, []byte("originalcontent"), 1, 1) require.NoError(t, err) sb.Add(pkt) } } assertGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n packet := sb.Get(seq) assert.NotNil(t, packet, "packet not found: %d", seq) assert.True( t, packet.Header().SSRC == 1 && packet.Header().PayloadType == 1, "packet for %d returned with incorrect SSRC : %d and PayloadType: %d", seq, packet.Header().SSRC, packet.Header().PayloadType, ) packet.Release() } } assertNOTGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n packet := sb.Get(seq) assert.Nil(t, packet, "packet found for %d", seq) } } 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) // A late packet coming in (such as due to RTX) shouldn't invalidate other packets. add(9) assertGet(3, 4, 5, 6, 7, 8, 9, 10) 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 TestRTPBuffer_Overridden(t *testing.T) { // override original packet content and get pm := NewPacketFactoryCopy() sb, err := NewRTPBuffer(1) require.NoError(t, err) require.Equal(t, uint16(1), sb.size) originalBytes := []byte("originalContent") pkt, err := pm.NewPacket(&rtp.Header{SequenceNumber: 1}, originalBytes, 0, 0) 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, 0, 0) require.NoError(t, err) sb.Add(pkt) require.Equal(t, 0, retrieved.count) require.Nil(t, sb.Get(1)) } func TestRTPBuffer_Overridden_WithRTX_AND_Padding(t *testing.T) { // override original packet content and get pm := NewPacketFactoryCopy() sb, err := NewRTPBuffer(1) require.NoError(t, err) require.Equal(t, uint16(1), sb.size) originalBytes := []byte("originalContent\x01") pkt, err := pm.NewPacket(&rtp.Header{SequenceNumber: 1, Padding: true, SSRC: 2, PayloadType: 3}, originalBytes, 1, 1) require.NoError(t, err) sb.Add(pkt) // change payload copy(originalBytes, "altered") retrieved := sb.Get(1) require.NotNil(t, retrieved) require.Equal(t, "\x00\x01originalContent", 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, 1, 1) require.NoError(t, err) sb.Add(pkt) require.Equal(t, 0, retrieved.count) require.Nil(t, sb.Get(1)) } func TestRTPBuffer_Overridden_WithRTX_NILPayload(t *testing.T) { // override original packet content and get pm := NewPacketFactoryCopy() sb, err := NewRTPBuffer(1) require.NoError(t, err) require.Equal(t, uint16(1), sb.size) pkt, err := pm.NewPacket(&rtp.Header{SequenceNumber: 1}, nil, 1, 1) require.NoError(t, err) sb.Add(pkt) // change payload retrieved := sb.Get(1) require.NotNil(t, retrieved) require.Equal(t, "\x00\x01", string(retrieved.Payload())) retrieved.Release() require.Equal(t, 1, retrieved.count) // ensure original packet is released pkt, err = pm.NewPacket(&rtp.Header{SequenceNumber: 2}, []byte("altered"), 1, 1) require.NoError(t, err) sb.Add(pkt) require.Equal(t, 0, retrieved.count) require.Nil(t, sb.Get(1)) } func TestRTPBuffer_Padding(t *testing.T) { pm := NewPacketFactoryCopy() sb, err := NewRTPBuffer(1) require.NoError(t, err) require.Equal(t, uint16(1), sb.size) t.Run("valid padding in payload is stripped", func(t *testing.T) { origPayload := []byte{116, 101, 115, 116} expected := []byte{0, 1, 116, 101, 115, 116} padLen := 120 padded := make([]byte, 0) padded = append(padded, origPayload...) padded = append(padded, bytes.Repeat([]byte{0}, padLen-1)...) padded = append(padded, byte(padLen)) pkt, err := pm.NewPacket(&rtp.Header{ SequenceNumber: 1, Padding: true, PaddingSize: 0, }, padded, 1, 1) require.NoError(t, err) sb.Add(pkt) retrieved := sb.Get(1) require.NotNil(t, retrieved) defer retrieved.Release() require.False(t, retrieved.Header().Padding, "P-bit should be cleared after trimming") actual := retrieved.Payload() require.Equal(t, len(expected), len(actual), "payload length after trimming") require.Equal(t, expected, actual, "payload content after trimming") }) t.Run("valid paddingsize in header is cleared", func(t *testing.T) { origPayload := []byte{116, 101, 115, 116} expected := []byte{0, 1, 116, 101, 115, 116} pkt, err := pm.NewPacket(&rtp.Header{ SequenceNumber: 1, Padding: true, PaddingSize: 120, }, origPayload, 1, 1) require.NoError(t, err) sb.Add(pkt) retrieved := sb.Get(1) require.NotNil(t, retrieved) defer retrieved.Release() require.False(t, retrieved.Header().Padding, "P-bit should be cleared after trimming") actual := retrieved.Payload() require.Equal(t, len(expected), len(actual), "payload length after trimming") require.Equal(t, expected, actual, "payload content after trimming") }) t.Run("overflow padding returns io.ErrShortBuffer", func(t *testing.T) { overflow := []byte{0, 1, 200} _, err := pm.NewPacket(&rtp.Header{ SequenceNumber: 2, Padding: true, }, overflow, 1, 1) require.ErrorIs(t, err, errPaddingOverflow, "factory should reject invalid padding") }) } interceptor-0.1.42/internal/sequencenumber/000077500000000000000000000000001510612111000207555ustar00rootroot00000000000000interceptor-0.1.42/internal/sequencenumber/unwrapper.go000066400000000000000000000021361510612111000233310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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) //nolint:gosec // G115 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.42/internal/sequencenumber/unwrapper_test.go000066400000000000000000000041431510612111000243700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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, 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}, }, { 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.42/internal/test/000077500000000000000000000000001510612111000167135ustar00rootroot00000000000000interceptor-0.1.42/internal/test/mock_stream.go000066400000000000000000000130231510612111000215450ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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 mockStream := &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), } mockStream.rtcpWriter = i.BindRTCPWriter( interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, _ interceptor.Attributes) (int, error) { select { case mockStream.rtcpOutModified <- pkts: default: } return 0, nil }), ) mockStream.rtcpReader = i.BindRTCPReader(interceptor.RTCPReaderFunc( func(b []byte, attrs interceptor.Attributes) (int, interceptor.Attributes, error) { pkts, ok := <-mockStream.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), attrs, err }, )) mockStream.rtpWriter = i.BindLocalStream( info, interceptor.RTPWriterFunc( func(header *rtp.Header, payload []byte, _ interceptor.Attributes) (int, error) { select { case mockStream.rtpOutModified <- &rtp.Packet{Header: *header, Payload: payload}: default: } return 0, nil }, ), ) mockStream.rtpReader = i.BindRemoteStream( info, interceptor.RTPReaderFunc( func(b []byte, attrs interceptor.Attributes) (int, interceptor.Attributes, error) { p, ok := <-mockStream.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), attrs, err }, ), ) go func() { buf := make([]byte, 1500) for { i, _, err := mockStream.rtcpReader.Read(buf, interceptor.Attributes{}) if err != nil { if !errors.Is(err, io.EOF) { mockStream.rtcpInModified <- RTCPWithError{Err: err} } return } pkts, err := rtcp.Unmarshal(buf[:i]) if err != nil { mockStream.rtcpInModified <- RTCPWithError{Err: err} return } mockStream.rtcpInModified <- RTCPWithError{Packets: pkts} } }() go func() { buf := make([]byte, 1500) for { i, _, err := mockStream.rtpReader.Read(buf, interceptor.Attributes{}) if err != nil { if err.Error() == "attempt to pop while buffering" { continue } if errors.Is(err, io.EOF) { mockStream.rtpInModified <- RTPWithError{Err: err} } return } p := &rtp.Packet{} if err := p.Unmarshal(buf[:i]); err != nil { mockStream.rtpInModified <- RTPWithError{Err: err} return } mockStream.rtpInModified <- RTPWithError{Packet: p} } }() return mockStream } // 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 by the stream. func (s *MockStream) ReceiveRTCP(pkts []rtcp.Packet) { s.rtcpIn <- pkts } // ReceiveRTP schedules a rtp packet, so it can be read by 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.42/internal/test/mock_stream_test.go000066400000000000000000000037421510612111000226130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package test import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) //nolint:cyclop func TestMockStream(t *testing.T) { mockStream := NewMockStream(&interceptor.StreamInfo{}, &interceptor.NoOp{}) assert.NoError(t, mockStream.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{}})) select { case <-mockStream.WrittenRTCP(): case <-time.After(10 * time.Millisecond): assert.Fail(t, "rtcp packet written but not found") } select { case <-mockStream.WrittenRTCP(): assert.Fail(t, "single rtcp packet written, but multiple found") case <-time.After(10 * time.Millisecond): } assert.NoError(t, mockStream.WriteRTP(&rtp.Packet{})) select { case <-mockStream.WrittenRTP(): case <-time.After(10 * time.Millisecond): assert.Fail(t, "rtp packet written but not found") } select { case <-mockStream.WrittenRTP(): assert.Fail(t, "single rtp packet written, but multiple found") case <-time.After(10 * time.Millisecond): } mockStream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{}}) select { case r := <-mockStream.ReadRTCP(): assert.NoError(t, r.Err, "read rtcp returned error") case <-time.After(10 * time.Millisecond): assert.Fail(t, "rtcp packet received but not read") } select { case r := <-mockStream.ReadRTCP(): assert.Fail(t, "single rtcp packet received, but multiple read: %v", r) case <-time.After(10 * time.Millisecond): } mockStream.ReceiveRTP(&rtp.Packet{}) select { case r := <-mockStream.ReadRTP(): assert.NoError(t, r.Err, "read rtp returned error") case <-time.After(10 * time.Millisecond): assert.Fail(t, "rtp packet received but not read") } select { case r := <-mockStream.ReadRTP(): assert.Fail(t, "single rtp packet received, but multiple read: %v", r) case <-time.After(10 * time.Millisecond): } assert.NoError(t, mockStream.Close()) } interceptor-0.1.42/internal/test/mock_ticker.go000066400000000000000000000007661510612111000215450ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/internal/test/mock_time.go000066400000000000000000000010141510612111000212050ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/noop.go000066400000000000000000000034021510612111000154210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/000077500000000000000000000000001510612111000147015ustar00rootroot00000000000000interceptor-0.1.42/pkg/cc/000077500000000000000000000000001510612111000152665ustar00rootroot00000000000000interceptor-0.1.42/pkg/cc/interceptor.go000066400000000000000000000077741510612111000201720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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]any 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 } interceptorInstance := &Interceptor{ NoOp: interceptor.NoOp{}, estimator: bwe, feedback: make(chan []rtcp.Packet), close: make(chan struct{}), } for _, opt := range f.opts { if err := opt(interceptorInstance); err != nil { return nil, err } } if f.addPeerConnection != nil { f.addPeerConnection(id, interceptorInstance.estimator) } return interceptorInstance, 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.42/pkg/flexfec/000077500000000000000000000000001510612111000163155ustar00rootroot00000000000000interceptor-0.1.42/pkg/flexfec/encoder_interceptor.go000066400000000000000000000067341510612111000227130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec import ( "errors" "sync" "github.com/pion/interceptor" "github.com/pion/rtp" ) // streamState holds the state for a single stream. type streamState struct { mu sync.Mutex flexFecEncoder FlexEncoder packetBuffer []rtp.Packet } // FecInterceptor implements FlexFec. type FecInterceptor struct { interceptor.NoOp mu sync.Mutex streams map[uint32]*streamState numMediaPackets uint32 numFecPackets uint32 encoderFactory EncoderFactory } // FecInterceptorFactory creates new FecInterceptors. type FecInterceptorFactory struct { opts []FecOption } // NewFecInterceptor returns a new Fec interceptor factory. func NewFecInterceptor(opts ...FecOption) (*FecInterceptorFactory, error) { return &FecInterceptorFactory{opts: opts}, nil } // NewInterceptor constructs a new FecInterceptor. func (r *FecInterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) { interceptor := &FecInterceptor{ streams: make(map[uint32]*streamState), numMediaPackets: 5, numFecPackets: 2, encoderFactory: FlexEncoder03Factory{}, } for _, opt := range r.opts { if err := opt(interceptor); err != nil { return nil, err } } return interceptor, nil } // UnbindLocalStream removes the stream state for a specific SSRC. func (r *FecInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { r.mu.Lock() defer r.mu.Unlock() delete(r.streams, info.SSRC) } // 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 *FecInterceptor) BindLocalStream( info *interceptor.StreamInfo, writer interceptor.RTPWriter, ) interceptor.RTPWriter { if info.PayloadTypeForwardErrorCorrection == 0 || info.SSRCForwardErrorCorrection == 0 { return writer } mediaSSRC := info.SSRC r.mu.Lock() stream := &streamState{ // Chromium supports version flexfec-03 of existing draft, this is the one we will configure by default // although we should support configuring the latest (flexfec-20) as well. flexFecEncoder: r.encoderFactory.NewEncoder(info.PayloadTypeForwardErrorCorrection, info.SSRCForwardErrorCorrection), packetBuffer: make([]rtp.Packet, 0), } r.streams[mediaSSRC] = stream r.mu.Unlock() return interceptor.RTPWriterFunc( func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { // Ignore non-media packets if header.SSRC != mediaSSRC { return writer.Write(header, payload, attributes) } var fecPackets []rtp.Packet stream.mu.Lock() stream.packetBuffer = append(stream.packetBuffer, rtp.Packet{ Header: *header, Payload: payload, }) // Check if we have enough packets to generate FEC if len(stream.packetBuffer) == int(r.numMediaPackets) { fecPackets = stream.flexFecEncoder.EncodeFec(stream.packetBuffer, r.numFecPackets) // Reset the packet buffer now that we've sent the corresponding FEC packets. stream.packetBuffer = nil } stream.mu.Unlock() var errs []error result, err := writer.Write(header, payload, attributes) if err != nil { errs = append(errs, err) } for _, packet := range fecPackets { header := packet.Header _, err = writer.Write(&header, packet.Payload, attributes) if err != nil { errs = append(errs, err) } } return result, errors.Join(errs...) }, ) } interceptor-0.1.42/pkg/flexfec/encoder_interceptor_test.go000066400000000000000000000233041510612111000237420ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec_test import ( "testing" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/interceptor/pkg/flexfec" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) type MockFlexEncoder struct { Called bool MediaPackets []rtp.Packet NumFECPackets uint32 FECPackets []rtp.Packet } func NewMockFlexEncoder(fecPackets []rtp.Packet) *MockFlexEncoder { return &MockFlexEncoder{ Called: false, FECPackets: fecPackets, } } func (m *MockFlexEncoder) EncodeFec(mediaPackets []rtp.Packet, numFecPackets uint32) []rtp.Packet { m.Called = true m.MediaPackets = mediaPackets m.NumFECPackets = numFecPackets return m.FECPackets } type MockEncoderFactory struct { Called bool PayloadType uint8 SSRC uint32 Encoder flexfec.FlexEncoder } func NewMockEncoderFactory(encoder flexfec.FlexEncoder) *MockEncoderFactory { return &MockEncoderFactory{ Called: false, Encoder: encoder, } } func (m *MockEncoderFactory) NewEncoder(payloadType uint8, ssrc uint32) flexfec.FlexEncoder { m.Called = true m.PayloadType = payloadType m.SSRC = ssrc return m.Encoder } func TestFecInterceptor_GenerateAndWriteFecPackets(t *testing.T) { fecPackets := []rtp.Packet{ { Header: rtp.Header{ SSRC: 2000, PayloadType: 100, SequenceNumber: 1000, }, Payload: []byte{0xFE, 0xC0, 0xDE}, }, } mockEncoder := NewMockFlexEncoder(fecPackets) mockFactory := NewMockEncoderFactory(mockEncoder) factory, err := flexfec.NewFecInterceptor( flexfec.FECEncoderFactory(mockFactory), flexfec.NumMediaPackets(2), flexfec.NumFECPackets(1), ) assert.NoError(t, err) i, err := factory.NewInterceptor("") assert.NoError(t, err) info := &interceptor.StreamInfo{ SSRC: 1000, PayloadTypeForwardErrorCorrection: 100, SSRCForwardErrorCorrection: 2000, } stream := test.NewMockStream(info, i) defer assert.NoError(t, stream.Close()) assert.True(t, mockFactory.Called, "NewEncoder should have been called") assert.Equal(t, uint8(100), mockFactory.PayloadType, "Should be called with correct payload type") assert.Equal(t, uint32(2000), mockFactory.SSRC, "Should be called with correct SSRC") for i := uint16(1); i <= 2; i++ { packet := &rtp.Packet{ Header: rtp.Header{ SSRC: 1000, SequenceNumber: i, PayloadType: 96, }, Payload: []byte{0x01, 0x02, 0x03, 0x04}, } err = stream.WriteRTP(packet) assert.NoError(t, err) } var mediaPacketsCount, fecPacketsCount int for i := 0; i < 3; i++ { select { case packet := <-stream.WrittenRTP(): switch packet.PayloadType { case 96: mediaPacketsCount++ case 100: fecPacketsCount++ assert.Equal(t, uint32(2000), packet.SSRC) assert.Equal(t, []byte{0xFE, 0xC0, 0xDE}, packet.Payload) } default: assert.Fail(t, "Not enough packets were written") } } assert.Equal(t, 2, mediaPacketsCount, "Should have written 2 media packets") assert.Equal(t, 1, fecPacketsCount, "Should have written 1 FEC packet") assert.True(t, mockEncoder.Called, "EncodeFec should have been called") assert.Equal(t, uint32(1), mockEncoder.NumFECPackets, "Should be called with correct number of FEC packets") } func TestFecInterceptor_BypassStreamWhenFecPtAndSsrcAreZero(t *testing.T) { fecPackets := []rtp.Packet{ { Header: rtp.Header{ SSRC: 2000, PayloadType: 100, SequenceNumber: 1000, }, Payload: []byte{0xFE, 0xC0, 0xDE}, }, } mockEncoder := NewMockFlexEncoder(fecPackets) mockFactory := NewMockEncoderFactory(mockEncoder) factory, err := flexfec.NewFecInterceptor( flexfec.FECEncoderFactory(mockFactory), flexfec.NumMediaPackets(1), flexfec.NumFECPackets(1), ) assert.NoError(t, err) i, err := factory.NewInterceptor("") assert.NoError(t, err) info := &interceptor.StreamInfo{ SSRC: 1, PayloadTypeForwardErrorCorrection: 0, SSRCForwardErrorCorrection: 0, } stream := test.NewMockStream(info, i) defer assert.NoError(t, stream.Close()) packet := &rtp.Packet{ Header: rtp.Header{ SSRC: 1, PayloadType: 96, }, Payload: []byte{0x01, 0x02, 0x03, 0x04}, } err = stream.WriteRTP(packet) assert.NoError(t, err) select { case writtenPacket := <-stream.WrittenRTP(): assert.Equal(t, packet.SSRC, writtenPacket.SSRC) assert.Equal(t, packet.SequenceNumber, writtenPacket.SequenceNumber) assert.Equal(t, packet.Payload, writtenPacket.Payload) default: assert.Fail(t, "No packet was written") } select { case <-stream.WrittenRTP(): assert.Fail(t, "Only one packet must be received") default: } assert.False(t, mockEncoder.Called, "EncodeFec should not have been called") } func TestFecInterceptor_EncodeOnlyPacketsWithMediaSsrc(t *testing.T) { mockEncoder := NewMockFlexEncoder(nil) mockFactory := NewMockEncoderFactory(mockEncoder) factory, err := flexfec.NewFecInterceptor( flexfec.FECEncoderFactory(mockFactory), flexfec.NumMediaPackets(2), flexfec.NumFECPackets(1), ) assert.NoError(t, err) i, err := factory.NewInterceptor("") assert.NoError(t, err) info := &interceptor.StreamInfo{ SSRC: 1000, PayloadTypeForwardErrorCorrection: 100, SSRCForwardErrorCorrection: 2000, } stream := test.NewMockStream(info, i) defer assert.NoError(t, stream.Close()) mediaPacket := &rtp.Packet{ Header: rtp.Header{ SSRC: 1000, SequenceNumber: 1, PayloadType: 96, }, Payload: []byte{0x01, 0x02, 0x03, 0x04}, } nonMediaPacket := &rtp.Packet{ Header: rtp.Header{ SSRC: 3000, // Different from mediaSSRC SequenceNumber: 2, PayloadType: 96, }, Payload: []byte{0x05, 0x06, 0x07, 0x08}, } err = stream.WriteRTP(mediaPacket) assert.NoError(t, err) err = stream.WriteRTP(nonMediaPacket) assert.NoError(t, err) // The non-media packet should be passed through without being added to the buffer select { case writtenPacket := <-stream.WrittenRTP(): assert.Equal(t, mediaPacket.SSRC, writtenPacket.SSRC) default: assert.Fail(t, "No media packet was written") } select { case writtenPacket := <-stream.WrittenRTP(): assert.Equal(t, nonMediaPacket.SSRC, writtenPacket.SSRC) default: assert.Fail(t, "No non-media packet was written") } assert.False(t, mockEncoder.Called, "EncodeFec should not have been called") } type EncoderFactoryFunc func(payloadType uint8, ssrc uint32) flexfec.FlexEncoder func (f EncoderFactoryFunc) NewEncoder(payloadType uint8, ssrc uint32) flexfec.FlexEncoder { return f(payloadType, ssrc) } // nolint:cyclop func TestFecInterceptor_HandleMultipleStreamsCorrectly(t *testing.T) { fecPackets1 := []rtp.Packet{ { Header: rtp.Header{ SSRC: 2000, PayloadType: 100, SequenceNumber: 1000, }, Payload: []byte{0xFE, 0xC0, 0xDE}, }, } mockEncoder1 := NewMockFlexEncoder(fecPackets1) fecPackets2 := []rtp.Packet{ { Header: rtp.Header{ SSRC: 3000, PayloadType: 101, SequenceNumber: 1000, }, Payload: []byte{0xFE, 0xC0, 0xDE}, }, } mockEncoder2 := NewMockFlexEncoder(fecPackets2) customFactory := EncoderFactoryFunc(func(payloadType uint8, ssrc uint32) flexfec.FlexEncoder { if payloadType == 100 && ssrc == 2000 { return mockEncoder1 } else if payloadType == 101 && ssrc == 3000 { return mockEncoder2 } return nil }) factory, err := flexfec.NewFecInterceptor( flexfec.FECEncoderFactory(customFactory), flexfec.NumMediaPackets(2), ) assert.NoError(t, err) fecInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) info1 := &interceptor.StreamInfo{ SSRC: 1000, PayloadTypeForwardErrorCorrection: 100, SSRCForwardErrorCorrection: 2000, } info2 := &interceptor.StreamInfo{ SSRC: 1001, PayloadTypeForwardErrorCorrection: 101, SSRCForwardErrorCorrection: 3000, } stream1 := test.NewMockStream(info1, fecInterceptor) defer assert.NoError(t, stream1.Close()) stream2 := test.NewMockStream(info2, fecInterceptor) defer assert.NoError(t, stream2.Close()) for idx := uint16(1); idx <= 2; idx++ { packet1 := &rtp.Packet{ Header: rtp.Header{ SSRC: 1000, SequenceNumber: idx, PayloadType: 96, }, Payload: []byte{0x01, 0x02, 0x03, 0x04}, } err = stream1.WriteRTP(packet1) assert.NoError(t, err) packet2 := &rtp.Packet{ Header: rtp.Header{ SSRC: 1001, SequenceNumber: idx, PayloadType: 97, }, Payload: []byte{0x05, 0x06, 0x07, 0x08}, } err = stream2.WriteRTP(packet2) assert.NoError(t, err) } assert.True(t, mockEncoder1.Called, "First encoder's EncodeFec should have been called") assert.True(t, mockEncoder2.Called, "Second encoder's EncodeFec should have been called") mediaPacketsCount1 := 0 fecPacketsCount1 := 0 for i := 0; i < 3; i++ { select { case packet := <-stream1.WrittenRTP(): switch packet.SSRC { case 1000: mediaPacketsCount1++ case 2000: fecPacketsCount1++ } default: assert.Fail(t, "No packet from stream1") } } assert.Equal(t, 2, mediaPacketsCount1, "Expected 2 media packets for stream1") assert.Equal(t, 1, fecPacketsCount1, "Expected 1 FEC packet for stream1") mediaPacketsCount2 := 0 fecPacketsCount2 := 0 for i := 0; i < 3; i++ { select { case packet := <-stream2.WrittenRTP(): switch packet.SSRC { case 1001: mediaPacketsCount2++ case 3000: fecPacketsCount2++ } default: assert.Fail(t, "No packet from stream2") } } assert.Equal(t, 2, mediaPacketsCount2, "Expected 2 media packets for stream2") assert.Equal(t, 1, fecPacketsCount2, "Expected 1 FEC packet for stream2") } interceptor-0.1.42/pkg/flexfec/flexfec_03_test.go000066400000000000000000000101321510612111000216160ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec import ( "testing" "github.com/pion/rtp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( payloadType = uint8(49) ssrc = uint32(867589674) protectedStreamSSRC = uint32(476325762) ) func checkAnyPacketCanBeRecovered(t *testing.T, mediaPackets []rtp.Packet, fecPackets []rtp.Packet) { t.Helper() for lost := 0; lost < len(mediaPackets); lost++ { decoder := newFECDecoder(ssrc, protectedStreamSSRC) recoveredPackets := make([]rtp.Packet, 0) // lose one packet for _, mediaPacket := range mediaPackets[:lost] { recoveredPackets = append(recoveredPackets, decoder.DecodeFec(mediaPacket)...) } for _, mediaPacket := range mediaPackets[lost+1:] { recoveredPackets = append(recoveredPackets, decoder.DecodeFec(mediaPacket)...) } assert.Empty(t, recoveredPackets) for _, fecPacket := range fecPackets { recoveredPackets = append(recoveredPackets, decoder.DecodeFec(fecPacket)...) } require.Len(t, recoveredPackets, 1) assert.Equal(t, mediaPackets[lost], recoveredPackets[0]) } } func generatePackets(t *testing.T, seqs []uint16) ([]rtp.Packet, []rtp.Packet) { t.Helper() return generatePacketsWithFecCount(t, seqs, 2) } func generatePacketsWithFecCount(t *testing.T, seqs []uint16, fecCount uint32) ([]rtp.Packet, []rtp.Packet) { t.Helper() mediaPackets := make([]rtp.Packet, 0) for i, seq := range seqs { payload := []byte{ // Payload 1, 2, 3, 4, 5, byte(i), } packet := rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: false, Version: 2, PayloadType: 96, SequenceNumber: seq, Timestamp: 3653407706, SSRC: protectedStreamSSRC, }, Payload: payload, } extension := []byte{0xAA, 0xAA} err := packet.SetExtension(1, extension) require.NoError(t, err) mediaPackets = append(mediaPackets, packet) } encoder := FlexEncoder03Factory{}.NewEncoder(payloadType, ssrc) fecPackets := encoder.EncodeFec(mediaPackets, fecCount) return mediaPackets, fecPackets } func runEncoderDecoderParityTest(t *testing.T, seqStart uint16, seqLen int, fecCount uint32) { t.Helper() seqs := make([]uint16, seqLen) for i := 0; i < seqLen; i++ { seqs[i] = seqStart + uint16(i) //nolint:gosec // G115 } mediaPackets, fecPackets := generatePacketsWithFecCount(t, seqs, fecCount) require.Len(t, mediaPackets, len(seqs)) require.Len(t, fecPackets, int(fecCount)) checkAnyPacketCanBeRecovered(t, mediaPackets, fecPackets) } func TestFlexFec03_SimpleRoundTrip(t *testing.T) { tests := []struct { name string seqs []uint16 }{ { name: "first", seqs: []uint16{1, 2, 3, 4, 5}, }, { name: "last", seqs: []uint16{65531, 65532, 65533, 65534, 65535}, }, { name: "wrap", seqs: []uint16{65533, 65534, 65535, 0, 1}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { mediaPackets, fecPackets := generatePackets(t, test.seqs) require.Len(t, mediaPackets, len(test.seqs)) checkAnyPacketCanBeRecovered(t, mediaPackets, fecPackets) }) } } func TestFlexFec03_WholeRangeRoundTrip(t *testing.T) { var seqs []uint16 const maxFlexFEC03MediaPackets = 109 for i := 0; i < maxFlexFEC03MediaPackets; i++ { seqs = append(seqs, uint16(i)) //nolint:gosec } mediaPackets, fecPackets := generatePackets(t, seqs) require.Len(t, mediaPackets, len(seqs)) checkAnyPacketCanBeRecovered(t, mediaPackets, fecPackets) } func TestFlexFec03_EncoderDecoderParitySingleFECShortWindow(t *testing.T) { runEncoderDecoderParityTest(t, 42, 8, 1) } func TestFlexFec03_EncoderDecoderParityDualFECMask2Coverage(t *testing.T) { runEncoderDecoderParityTest(t, 5, 40, 2) } func TestFlexFec03_EncoderDecoderParityTripleFECMask3Coverage(t *testing.T) { runEncoderDecoderParityTest(t, 120, 90, 3) } func TestFlexFec03_EncoderDecoderParityWrapAroundSequences(t *testing.T) { runEncoderDecoderParityTest(t, 65510, 60, 4) } func TestFlexFec03_EncoderDecoderParityHighMediaCount(t *testing.T) { runEncoderDecoderParityTest(t, 41000, 100, 10) } interceptor-0.1.42/pkg/flexfec/flexfec_benchmark_test.go000066400000000000000000000066271510612111000233440ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec_test import ( "testing" "github.com/pion/interceptor/pkg/flexfec" "github.com/pion/rtp" ) const ( payloadType = uint8(49) ssrc = uint32(867589674) protectedStreamSSRC = uint32(476325762) ) // generateMediaPackets creates a slice of RTP packets with fixed-size payloads. func generateMediaPackets(n int, startSeq uint16) []rtp.Packet { mediaPackets := make([]rtp.Packet, 0, n) for i := 0; i < n; i++ { payload := []byte{ // Payload with some random data 1, 2, 3, 4, 5, byte(i), } packet := rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: false, Version: 2, PayloadType: 96, SequenceNumber: startSeq + uint16(i), //nolint:gosec // G115 Timestamp: 3653407706, SSRC: protectedStreamSSRC, }, Payload: payload, } mediaPackets = append(mediaPackets, packet) } return mediaPackets } // generateMediaPacketsWithSizes creates a slice of RTP packets with varying payload sizes. func generateMediaPacketsWithSizes(n int, startSeq uint16, minSize, maxSize int) []rtp.Packet { mediaPackets := make([]rtp.Packet, 0, n) for i := 0; i < n; i++ { // Calculate a size that varies between minSize and maxSize based on the packet index size := minSize + (i % (maxSize - minSize + 1)) // Create a payload of the calculated size payload := make([]byte, size) // Fill with some pattern data for j := 0; j < size; j++ { payload[j] = byte((j + i) % 256) } packet := rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: false, Version: 2, PayloadType: 96, SequenceNumber: startSeq + uint16(i), //nolint:gosec // G115 Timestamp: 3653407706, SSRC: protectedStreamSSRC, }, Payload: payload, } mediaPackets = append(mediaPackets, packet) } return mediaPackets } // BenchmarkFlexEncoder03_EncodeFec benchmarks the FEC encoding with fixed configurations. func BenchmarkFlexEncoder03_EncodeFec(b *testing.B) { benchmarks := []struct { name string mediaPackets int fecPackets uint32 sequenceStart uint16 }{ {"Small_2FEC", 5, 2, 1000}, {"Medium_3FEC", 10, 3, 1000}, } for _, bm := range benchmarks { b.Run(bm.name, func(b *testing.B) { mediaPackets := generateMediaPackets(bm.mediaPackets, bm.sequenceStart) encoder := flexfec.NewFlexEncoder03(payloadType, ssrc) b.ResetTimer() for i := 0; i < b.N; i++ { _ = encoder.EncodeFec(mediaPackets, bm.fecPackets) } }) } } // BenchmarkFlexEncoder03_EncodeFecVaryingSizes benchmarks the FEC encoding with varying packet sizes. func BenchmarkFlexEncoder03_EncodeFecVaryingSizes(b *testing.B) { benchmarks := []struct { name string mediaPackets int fecPackets uint32 minSize int maxSize int sequenceStart uint16 }{ {"ManySmall_2FEC", 20, 2, 50, 150, 1000}, {"ManyMedium_3FEC", 30, 3, 200, 800, 1000}, {"ManyLarge_2FEC", 40, 2, 900, 1400, 1000}, } for _, bm := range benchmarks { b.Run(bm.name, func(b *testing.B) { mediaPackets := generateMediaPacketsWithSizes(bm.mediaPackets, bm.sequenceStart, bm.minSize, bm.maxSize) encoder := flexfec.NewFlexEncoder03(payloadType, ssrc) b.ResetTimer() for i := 0; i < b.N; i++ { _ = encoder.EncodeFec(mediaPackets, bm.fecPackets) } }) } } interceptor-0.1.42/pkg/flexfec/flexfec_coverage.go000066400000000000000000000144461510612111000221440ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec import ( "github.com/pion/interceptor/pkg/flexfec/util" "github.com/pion/rtp" ) // Maximum number of media packets that can be protected by a single FEC packet. // We are not supporting the possibility of having an FEC packet protect multiple // SSRC source packets for now. // https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1 const ( MaxMediaPackets uint32 = 110 MaxFecPackets uint32 = MaxMediaPackets ) // ProtectionCoverage defines the map of RTP packets that individual Fec packets protect. type ProtectionCoverage struct { // Array of masks, each mask capable of covering up to maxMediaPkts = 110. // A mask is represented as a grouping of bytes where each individual bit // represents the coverage for the media packet at the corresponding index. packetMasks [MaxFecPackets]util.BitArray numFecPackets uint32 numMediaPackets uint32 mediaPackets []rtp.Packet } // NewCoverage returns a new ProtectionCoverage object. numFecPackets represents the number of // Fec packets that we will be generating to cover the list of mediaPackets. This allows us to know // how big the underlying map should be. func NewCoverage(mediaPackets []rtp.Packet, numFecPackets uint32) *ProtectionCoverage { numMediaPackets := uint32(len(mediaPackets)) //nolint:gosec // G115 // Basic sanity checks if numMediaPackets <= 0 || numMediaPackets > MaxMediaPackets { return nil } // We allocate the biggest array of bitmasks that respects the max constraints. var packetMasks [MaxFecPackets]util.BitArray for i := 0; i < int(MaxFecPackets); i++ { packetMasks[i] = util.BitArray{} } coverage := &ProtectionCoverage{ packetMasks: packetMasks, numFecPackets: 0, numMediaPackets: 0, mediaPackets: nil, } coverage.UpdateCoverage(mediaPackets, numFecPackets) return coverage } // UpdateCoverage updates the ProtectionCoverage object with new bitmasks accounting for the numFecPackets // we want to use to protect the batch media packets. func (p *ProtectionCoverage) UpdateCoverage(mediaPackets []rtp.Packet, numFecPackets uint32) { numMediaPackets := uint32(len(mediaPackets)) //nolint:gosec // G115 // Basic sanity checks if numMediaPackets <= 0 || numMediaPackets > MaxMediaPackets { return } p.mediaPackets = mediaPackets if numFecPackets == p.numFecPackets && numMediaPackets == p.numMediaPackets { // We have the same number of FEC packets covering the same number of media packets, we can simply // reuse the previous coverage map with the updated media packets. return } p.numFecPackets = numFecPackets p.numMediaPackets = numMediaPackets // The number of FEC packets and/or the number of packets has changed, we need to update the coverage map // to reflect these new values. p.resetCoverage() // Generate FEC bit mask where numFecPackets FEC packets are covering numMediaPackets Media packets. // In the packetMasks array, each FEC packet is represented by a single BitArray, each bit in a given BitArray // corresponds to a specific Media packet. // Ex: Row I, Col J is set to 1 -> FEC packet I will protect media packet J. for fecPacketIndex := uint32(0); fecPacketIndex < numFecPackets; fecPacketIndex++ { // We use an interleaved method to determine coverage. Given N FEC packets, Media packet X will be // covered by FEC packet X % N. coveredMediaPacketIndex := fecPacketIndex for coveredMediaPacketIndex < numMediaPackets { p.packetMasks[fecPacketIndex].SetBit(coveredMediaPacketIndex) coveredMediaPacketIndex += numFecPackets } } } // ResetCoverage clears the underlying map so that we can reuse it for new batches of RTP packets. func (p *ProtectionCoverage) resetCoverage() { for i := uint32(0); i < MaxFecPackets; i++ { p.packetMasks[i].Reset() } } // GetCoveredBy returns an iterator over RTP packets that are protected by the specified Fec packet index. func (p *ProtectionCoverage) GetCoveredBy(fecPacketIndex uint32) *util.MediaPacketIterator { coverage := make([]uint32, 0, p.numMediaPackets) for mediaPacketIndex := uint32(0); mediaPacketIndex < p.numMediaPackets; mediaPacketIndex++ { if p.packetMasks[fecPacketIndex].GetBit(mediaPacketIndex) == 1 { coverage = append(coverage, mediaPacketIndex) } } return util.NewMediaPacketIterator(p.mediaPackets, coverage) } // ExtractMask1 returns the first section of the bitmask as defined by the FEC header. // https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1 func (p *ProtectionCoverage) ExtractMask1(fecPacketIndex uint32) uint16 { return extractMask1(p.packetMasks[fecPacketIndex]) } // ExtractMask2 returns the second section of the bitmask as defined by the FEC header. // https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1 func (p *ProtectionCoverage) ExtractMask2(fecPacketIndex uint32) uint32 { return extractMask2(p.packetMasks[fecPacketIndex]) } // ExtractMask3 returns the third section of the bitmask as defined by the FEC header. // https://datatracker.ietf.org/doc/html/rfc8627#section-4.2.2.1 func (p *ProtectionCoverage) ExtractMask3(fecPacketIndex uint32) uint64 { return extractMask3(p.packetMasks[fecPacketIndex]) } // ExtractMask3_03 returns the third section of the bitmask as defined by the FEC header. // https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03#section-4.2 func (p *ProtectionCoverage) ExtractMask3_03(fecPacketIndex uint32) uint64 { return extractMask3_03(p.packetMasks[fecPacketIndex]) } func extractMask1(mask util.BitArray) uint16 { // We get the first 16 bits (64 - 16 -> shift by 48) and we shift once more for K field mask1 := mask.Lo >> 49 return uint16(mask1) //nolint:gosec // G115 } func extractMask2(mask util.BitArray) uint32 { // We remove the first 15 bits mask2 := mask.Lo << 15 // We get the first 31 bits (64 - 32 -> shift by 32) and we shift once more for K field mask2 >>= 33 return uint32(mask2) //nolint:gosec } func extractMask3(mask util.BitArray) uint64 { // We remove the first 46 bits maskLo := mask.Lo << 46 maskHi := mask.Hi >> 18 mask3 := maskLo | maskHi return mask3 } func extractMask3_03(mask util.BitArray) uint64 { // We remove the first 46 bits maskLo := mask.Lo << 46 maskHi := mask.Hi >> 18 mask3 := maskLo | maskHi // We shift once for the K bit. mask3 >>= 1 return mask3 } interceptor-0.1.42/pkg/flexfec/flexfec_coverage_test.go000066400000000000000000000036651510612111000232040ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec import ( "testing" "github.com/pion/interceptor/pkg/flexfec/util" "github.com/stretchr/testify/assert" ) func TestMaskExtractors(t *testing.T) { tests := []struct { name string setBits []uint32 mask1 uint16 mask2 uint32 mask3 uint64 mask3_03 uint64 }{ { name: "Empty mask", setBits: []uint32{}, mask1: 0, mask2: 0, mask3: 0, mask3_03: 0, }, { name: "Single bit in each mask", setBits: []uint32{5, 20, 50}, mask1: 0x200, // bit 5 mask2: 0x2000000, // bit 20 mask3: 0x800000000000000, // bit 50 mask3_03: 0x400000000000000, // bit 50 }, { name: "Multiple bits in each mask", setBits: []uint32{0, 7, 14, 15, 30, 45, 46, 80, 108, 109}, mask1: 0x4081, // bits 0, 7, 14 mask2: 0x40008001, // bits 15, 30, 45 mask3: 0x8000000020000003, // bits 46, 80, 108, 109 mask3_03: 0x4000000010000001, // bits 46, 80, 108 }, { name: "Boundary values", setBits: []uint32{0, 14, 15, 45, 46, 108, 109}, mask1: 0x4001, // bits 0, 14 mask2: 0x40000001, // bits 15, 45 mask3: 0x8000000000000003, // bits 46, 108, 109 mask3_03: 0x4000000000000001, // bits 46, 108 }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mask := util.BitArray{} for _, bit := range tt.setBits { mask.SetBit(bit) } actualMask1 := extractMask1(mask) actualMask2 := extractMask2(mask) actualMask3 := extractMask3(mask) actualMask3_03 := extractMask3_03(mask) assert.Equal(t, tt.mask1, actualMask1, "Mask1 mismatch") assert.Equal(t, tt.mask2, actualMask2, "Mask2 mismatch") assert.Equal(t, tt.mask3, actualMask3, "Mask3 mismatch") assert.Equal(t, tt.mask3_03, actualMask3_03, "Mask3_03 mismatch") }) } } interceptor-0.1.42/pkg/flexfec/flexfec_decoder_03.go000066400000000000000000000300611510612111000222470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package flexfec implements FlexFEC-03 to recover missing RTP packets due to packet loss. // https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03 package flexfec import ( "encoding/binary" "errors" "fmt" "sort" "github.com/pion/logging" "github.com/pion/rtp" ) // Static errors for the flexfec package. var ( errPacketTruncated = errors.New("packet truncated") errRetransmissionBitSet = errors.New("packet with retransmission bit set not supported") errInflexibleGeneratorMatrix = errors.New("packet with inflexible generator matrix not supported") errMultipleSSRCProtection = errors.New("multiple ssrc protection not supported") errLastOptionalMaskKBitSetToFalse = errors.New("k-bit of last optional mask is set to false") ) // fecDecoder is a WIP implementation decoder used for testing purposes. type fecDecoder struct { logger logging.LeveledLogger ssrc uint32 protectedStreamSSRC uint32 maxMediaPackets int maxFECPackets int recoveredPackets []rtp.Packet receivedFECPackets []fecPacketState } func newFECDecoder(ssrc uint32, protectedStreamSSRC uint32) *fecDecoder { return &fecDecoder{ logger: logging.NewDefaultLoggerFactory().NewLogger("fec_decoder"), ssrc: ssrc, protectedStreamSSRC: protectedStreamSSRC, maxMediaPackets: 100, maxFECPackets: 100, recoveredPackets: make([]rtp.Packet, 0), receivedFECPackets: make([]fecPacketState, 0), } } func (d *fecDecoder) DecodeFec(receivedPacket rtp.Packet) []rtp.Packet { if len(d.recoveredPackets) == d.maxMediaPackets { backRecoveredPacket := d.recoveredPackets[len(d.recoveredPackets)-1] if backRecoveredPacket.SSRC == receivedPacket.SSRC { seqDiffVal := seqDiff(receivedPacket.SequenceNumber, backRecoveredPacket.SequenceNumber) if seqDiffVal > uint16(d.maxMediaPackets) { //nolint:gosec d.logger.Info("big gap in media sequence numbers - resetting buffers") d.recoveredPackets = nil d.receivedFECPackets = nil } } } d.insertPacket(receivedPacket) return d.attemptRecovery() } func (d *fecDecoder) insertPacket(receivedPkt rtp.Packet) { // Discard old FEC packets such that the sequence numbers in // `received_fec_packets_` span at most 1/2 of the sequence number space. // This is important for keeping `received_fec_packets_` sorted, and may // also reduce the possibility of incorrect decoding due to sequence number // wrap-around. if len(d.receivedFECPackets) > 0 && receivedPkt.SSRC == d.ssrc { toRemove := 0 for _, fecPkt := range d.receivedFECPackets { if abs(int(receivedPkt.SequenceNumber)-int(fecPkt.packet.SequenceNumber)) > 0x3fff { toRemove++ } else { // No need to keep iterating, since |received_fec_packets_| is sorted. break } } if toRemove > 0 { d.receivedFECPackets = d.receivedFECPackets[toRemove:] } } switch receivedPkt.SSRC { case d.ssrc: d.insertFECPacket(receivedPkt) case d.protectedStreamSSRC: d.insertMediaPacket(receivedPkt) } d.discardOldRecoveredPackets() } func (d *fecDecoder) insertMediaPacket(receivedPkt rtp.Packet) { for _, recoveredPacket := range d.recoveredPackets { if recoveredPacket.SequenceNumber == receivedPkt.SequenceNumber { return } } d.recoveredPackets = append(d.recoveredPackets, receivedPkt) sort.Slice(d.recoveredPackets, func(i, j int) bool { return isNewerSeq(d.recoveredPackets[i].SequenceNumber, d.recoveredPackets[j].SequenceNumber) }) d.updateCoveringFecPackets(receivedPkt) } func (d *fecDecoder) updateCoveringFecPackets(receivedPkt rtp.Packet) { for _, fecPkt := range d.receivedFECPackets { for _, protectedPacket := range fecPkt.protectedPackets { if protectedPacket.seq == receivedPkt.SequenceNumber { protectedPacket.packet = &receivedPkt } } } } func (d *fecDecoder) insertFECPacket(fecPkt rtp.Packet) { //nolint:cyclop for _, existingFECPacket := range d.receivedFECPackets { if existingFECPacket.packet.SequenceNumber == fecPkt.SequenceNumber { return } } fec, err := parseFlexFEC03Header(fecPkt.Payload) if err != nil { d.logger.Errorf("failed to parse flexfec03 header: %v", err) return } if fec.protectedSSRC != d.protectedStreamSSRC { d.logger.Errorf("fec is protecting unknown ssrc, expected %d, got %d", fec.protectedSSRC, d.protectedStreamSSRC) return } protectedSeqs := decodeMask(uint64(fec.mask0), 15, fec.seqNumBase) if fec.mask1 != 0 { protectedSeqs = append(protectedSeqs, decodeMask(uint64(fec.mask1), 31, fec.seqNumBase+15)...) } if fec.mask2 != 0 { protectedSeqs = append(protectedSeqs, decodeMask(fec.mask2, 63, fec.seqNumBase+46)...) } if len(protectedSeqs) == 0 { d.logger.Warn("empty fec packet mask") return } protectedPackets := make([]*protectedPacket, 0, len(protectedSeqs)) protectedSeqIt := 0 recoveredPacketIt := 0 for protectedSeqIt < len(protectedSeqs) && recoveredPacketIt < len(d.recoveredPackets) { switch { case isNewerSeq(protectedSeqs[protectedSeqIt], d.recoveredPackets[recoveredPacketIt].SequenceNumber): protectedPackets = append(protectedPackets, &protectedPacket{ seq: protectedSeqs[protectedSeqIt], packet: nil, }) protectedSeqIt++ case isNewerSeq(d.recoveredPackets[recoveredPacketIt].SequenceNumber, protectedSeqs[protectedSeqIt]): recoveredPacketIt++ default: protectedPackets = append(protectedPackets, &protectedPacket{ seq: protectedSeqs[protectedSeqIt], packet: &d.recoveredPackets[recoveredPacketIt], }) protectedSeqIt++ recoveredPacketIt++ } } for protectedSeqIt < len(protectedSeqs) { protectedPackets = append(protectedPackets, &protectedPacket{ seq: protectedSeqs[protectedSeqIt], packet: nil, }) protectedSeqIt++ } d.receivedFECPackets = append(d.receivedFECPackets, fecPacketState{ packet: fecPkt, flexFec: fec, protectedPackets: protectedPackets, }) sort.Slice(d.receivedFECPackets, func(i, j int) bool { return isNewerSeq(d.receivedFECPackets[i].packet.SequenceNumber, d.receivedFECPackets[j].packet.SequenceNumber) }) if len(d.receivedFECPackets) > d.maxFECPackets { d.receivedFECPackets = d.receivedFECPackets[1:] } } func (d *fecDecoder) attemptRecovery() []rtp.Packet { recoveredPackets := make([]rtp.Packet, 0) for { packetsRecovered := 0 for _, fecPkt := range d.receivedFECPackets { packetsMissing := 0 for _, pkt := range fecPkt.protectedPackets { if pkt.packet == nil { packetsMissing++ if packetsMissing > 1 { break } } } if packetsMissing != 1 { continue } recovered, err := d.recoverPacket(&fecPkt) //nolint:gosec if err != nil { d.logger.Errorf("failed to recover packet: %v", err) } recoveredPackets = append(recoveredPackets, recovered) d.recoveredPackets = append(d.recoveredPackets, recovered) sort.Slice(d.recoveredPackets, func(i, j int) bool { return isNewerSeq(d.recoveredPackets[i].SequenceNumber, d.recoveredPackets[j].SequenceNumber) }) d.updateCoveringFecPackets(recovered) d.discardOldRecoveredPackets() packetsRecovered++ } if packetsRecovered == 0 { break } } return recoveredPackets } func (d *fecDecoder) recoverPacket(fec *fecPacketState) (rtp.Packet, error) { // https://datatracker.ietf.org/doc/html/draft-ietf-payload-flexible-fec-scheme-03#section-6.3.2 // 2. For the repair packet in T, extract the FEC bit string as the // first 80 bits of the FEC header. headerRecovery := make([]byte, 12) copy(headerRecovery, fec.packet.Payload[:10]) var seqnum uint16 for _, protectedPacket := range fec.protectedPackets { if protectedPacket.packet != nil { // 1. For each of the source packets that are successfully received in // T, compute the 80-bit string by concatenating the first 64 bits // of their RTP header and the unsigned network-ordered 16-bit // representation of their length in bytes minus 12. receivedHeader, err := protectedPacket.packet.Header.Marshal() if err != nil { return rtp.Packet{}, fmt.Errorf("marshal received header: %w", err) } binary.BigEndian.PutUint16(receivedHeader[2:4], uint16(protectedPacket.packet.MarshalSize()-12)) //nolint:gosec for i := 0; i < 8; i++ { headerRecovery[i] ^= receivedHeader[i] } } else { seqnum = protectedPacket.seq } } // set version to 2 headerRecovery[0] |= 0x80 headerRecovery[0] &= 0xbf payloadLength := binary.BigEndian.Uint16(headerRecovery[2:4]) binary.BigEndian.PutUint16(headerRecovery[2:4], seqnum) binary.BigEndian.PutUint32(headerRecovery[8:12], d.protectedStreamSSRC) payloadRecovery := make([]byte, payloadLength) copy(payloadRecovery, fec.flexFec.payload) for _, protectedPacket := range fec.protectedPackets { if protectedPacket.packet != nil { packet, err := protectedPacket.packet.Marshal() if err != nil { return rtp.Packet{}, fmt.Errorf("marshal protected packet: %w", err) } for i := 0; i < min(int(payloadLength), len(packet)-12); i++ { payloadRecovery[i] ^= packet[12+i] } } } headerRecovery = append(headerRecovery, payloadRecovery...) //nolint:makezero var packet rtp.Packet err := packet.Unmarshal(headerRecovery) if err != nil { return rtp.Packet{}, fmt.Errorf("unmarshal recovered: %w", err) } return packet, nil } func (d *fecDecoder) discardOldRecoveredPackets() { const limit = 192 if len(d.recoveredPackets) > limit { d.recoveredPackets = d.recoveredPackets[len(d.recoveredPackets)-192:] } } func decodeMask(mask uint64, bitCount uint16, seqNumBase uint16) []uint16 { res := make([]uint16, 0) for i := uint16(0); i < bitCount; i++ { if (mask>>(bitCount-1-i))&1 == 1 { res = append(res, seqNumBase+i) } } return res } type fecPacketState struct { packet rtp.Packet flexFec flexFec protectedPackets []*protectedPacket } type flexFec struct { protectedSSRC uint32 seqNumBase uint16 mask0 uint16 mask1 uint32 mask2 uint64 payload []byte } type protectedPacket struct { seq uint16 packet *rtp.Packet } func parseFlexFEC03Header(data []byte) (flexFec, error) { if len(data) < 20 { return flexFec{}, fmt.Errorf("%w: length %d", errPacketTruncated, len(data)) } rBit := (data[0] & 0x80) != 0 if rBit { return flexFec{}, errRetransmissionBitSet } fBit := (data[0] & 0x40) != 0 if fBit { return flexFec{}, errInflexibleGeneratorMatrix } ssrcCount := data[8] if ssrcCount != 1 { return flexFec{}, fmt.Errorf("%w: count %d", errMultipleSSRCProtection, ssrcCount) } protectedSSRC := binary.BigEndian.Uint32(data[12:]) seqNumBase := binary.BigEndian.Uint16(data[16:]) rawPacketMask := data[18:] var payload []byte kBit0 := (rawPacketMask[0] & 0x80) != 0 maskPart0 := binary.BigEndian.Uint16(rawPacketMask[0:2]) & 0x7FFF var maskPart1 uint32 var maskPart2 uint64 if kBit0 { //nolint:nestif payload = rawPacketMask[2:] } else { if len(data) < 24 { return flexFec{}, fmt.Errorf("%w: length %d", errPacketTruncated, len(data)) } kBit1 := (rawPacketMask[2] & 0x80) != 0 maskPart1 = binary.BigEndian.Uint32(rawPacketMask[2:]) & 0x7FFFFFFF if kBit1 { payload = rawPacketMask[6:] } else { if len(data) < 32 { return flexFec{}, fmt.Errorf("%w: length %d", errPacketTruncated, len(data)) } kBit2 := (rawPacketMask[6] & 0x80) != 0 maskPart2 = binary.BigEndian.Uint64(rawPacketMask[6:]) & 0x7FFFFFFFFFFFFFFF if kBit2 { payload = rawPacketMask[14:] } else { return flexFec{}, errLastOptionalMaskKBitSetToFalse } } } return flexFec{ protectedSSRC: protectedSSRC, seqNumBase: seqNumBase, mask0: maskPart0, mask1: maskPart1, mask2: maskPart2, payload: payload, }, nil } func seqDiff(a, b uint16) uint16 { return min(a-b, b-a) } func abs(x int) int { if x >= 0 { return x } return -x } func isNewerSeq(prevValue, value uint16) bool { // half-way mark breakpoint := uint16(0x8000) if value-prevValue == breakpoint { return value > prevValue } return value != prevValue && (value-prevValue) < breakpoint } interceptor-0.1.42/pkg/flexfec/flexfec_decoder_03_test.go000066400000000000000000000045121510612111000233100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec import ( "encoding/binary" "testing" "github.com/pion/rtp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( testDecoderSSRC = uint32(1234) testProtectedStreamSSRC = uint32(5678) ) func TestFECDecoderInsertPacketRemovesOldFEC(t *testing.T) { decoder := newFECDecoder(testDecoderSSRC, testProtectedStreamSSRC) decoder.receivedFECPackets = []fecPacketState{ newFecPacketState(1), newFecPacketState(500), newFecPacketState(1500), newFecPacketState(25000), } pkt := rtp.Packet{ Header: rtp.Header{ SequenceNumber: 40000, SSRC: testDecoderSSRC, }, Payload: buildTestFlexFecPayload(40000), } decoder.insertPacket(pkt) require.Len(t, decoder.receivedFECPackets, 2) assert.Equal(t, uint16(25000), decoder.receivedFECPackets[0].packet.SequenceNumber) assert.Equal(t, uint16(40000), decoder.receivedFECPackets[1].packet.SequenceNumber) } func TestFECDecoderInsertPacketKeepsRecentFEC(t *testing.T) { decoder := newFECDecoder(testDecoderSSRC, testProtectedStreamSSRC) initialStates := []fecPacketState{ newFecPacketState(1), newFecPacketState(500), newFecPacketState(1500), } decoder.receivedFECPackets = append(decoder.receivedFECPackets, initialStates...) pkt := rtp.Packet{ Header: rtp.Header{ SequenceNumber: 2000, SSRC: testDecoderSSRC, }, Payload: buildTestFlexFecPayload(2000), } decoder.insertPacket(pkt) require.Len(t, decoder.receivedFECPackets, len(initialStates)+1) for i, state := range initialStates { assert.Equal(t, state.packet.SequenceNumber, decoder.receivedFECPackets[i].packet.SequenceNumber) } assert.Equal(t, uint16(2000), decoder.receivedFECPackets[len(initialStates)].packet.SequenceNumber) } func newFecPacketState(seq uint16) fecPacketState { return fecPacketState{ packet: rtp.Packet{ Header: rtp.Header{ SequenceNumber: seq, SSRC: testDecoderSSRC, }, }, } } func buildTestFlexFecPayload(seqNumBase uint16) []byte { payload := make([]byte, BaseFec03HeaderSize+4) payload[8] = 1 binary.BigEndian.PutUint32(payload[12:], testProtectedStreamSSRC) binary.BigEndian.PutUint16(payload[16:], seqNumBase) binary.BigEndian.PutUint16(payload[18:], 0x8001) return payload } interceptor-0.1.42/pkg/flexfec/flexfec_encoder.go000066400000000000000000000162011510612111000217570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package flexfec implements FlexFEC to recover missing RTP packets due to packet loss. // https://datatracker.ietf.org/doc/html/rfc8627 package flexfec import ( "encoding/binary" "github.com/pion/interceptor/pkg/flexfec/util" "github.com/pion/rtp" ) const ( // BaseRTPHeaderSize represents the minium RTP packet header size in bytes. BaseRTPHeaderSize = 12 // BaseFecHeaderSize represents the minium FEC payload's header size including the // required first mask. BaseFecHeaderSize = 12 ) // EncoderFactory is an interface for generic FEC encoders. type EncoderFactory interface { NewEncoder(payloadType uint8, ssrc uint32) FlexEncoder } // FlexEncoder is the interface that FecInterceptor uses to encode Fec packets. type FlexEncoder interface { EncodeFec(mediaPackets []rtp.Packet, numFecPackets uint32) []rtp.Packet } // FlexEncoder20 implementation is WIP, contains bugs and no tests. Check out FlexEncoder03. type FlexEncoder20 struct { fecBaseSn uint16 payloadType uint8 ssrc uint32 coverage *ProtectionCoverage } // NewFlexEncoder returns a new FlexEncoder20. // FlexEncoder20 implementation is WIP, contains bugs and no tests. Check out FlexEncoder03. func NewFlexEncoder(payloadType uint8, ssrc uint32) *FlexEncoder20 { return &FlexEncoder20{ payloadType: payloadType, ssrc: ssrc, fecBaseSn: uint16(1000), } } // EncodeFec returns a list of generated RTP packets with FEC payloads that protect the specified mediaPackets. // This method does not account for missing RTP packets in the mediaPackets array nor does it account for // them being passed out of order. func (flex *FlexEncoder20) EncodeFec(mediaPackets []rtp.Packet, numFecPackets uint32) []rtp.Packet { // Start by defining which FEC packets cover which media packets if flex.coverage == nil { flex.coverage = NewCoverage(mediaPackets, numFecPackets) } else { flex.coverage.UpdateCoverage(mediaPackets, numFecPackets) } if flex.coverage == nil { return nil } // Generate FEC payloads fecPackets := make([]rtp.Packet, numFecPackets) for fecPacketIndex := uint32(0); fecPacketIndex < numFecPackets; fecPacketIndex++ { fecPackets[fecPacketIndex] = flex.encodeFlexFecPacket(fecPacketIndex, mediaPackets[0].SequenceNumber) } return fecPackets } func (flex *FlexEncoder20) encodeFlexFecPacket(fecPacketIndex uint32, mediaBaseSn uint16) rtp.Packet { mediaPacketsIt := flex.coverage.GetCoveredBy(fecPacketIndex) flexFecHeader := flex.encodeFlexFecHeader( mediaPacketsIt, flex.coverage.ExtractMask1(fecPacketIndex), flex.coverage.ExtractMask2(fecPacketIndex), flex.coverage.ExtractMask3(fecPacketIndex), mediaBaseSn, ) flexFecRepairPayload := flex.encodeFlexFecRepairPayload(mediaPacketsIt.Reset()) packet := rtp.Packet{ Header: rtp.Header{ Version: 2, Padding: false, Extension: false, Marker: false, PayloadType: flex.payloadType, SequenceNumber: flex.fecBaseSn, Timestamp: 54243243, SSRC: flex.ssrc, CSRC: []uint32{}, }, Payload: append(flexFecHeader, flexFecRepairPayload...), } flex.fecBaseSn++ return packet } func (flex *FlexEncoder20) encodeFlexFecHeader( mediaPackets *util.MediaPacketIterator, mask1 uint16, optionalMask2 uint32, optionalMask3 uint64, mediaBaseSn uint16, ) []byte { /* 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |0|0|P|X| CC |M| PT recovery | length recovery | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | TS recovery | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SN base_i |k| Mask [0-14] | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |k| Mask [15-45] (optional) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Mask [46-109] (optional) | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ... next SN base and Mask for CSRC_i in CSRC list ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : Repair "Payload" follows FEC Header : : : */ // Get header size - This depends on the size of the bitmask. headerSize := BaseFecHeaderSize if optionalMask2 > 0 { headerSize += 4 } if optionalMask3 > 0 { headerSize += 8 } // Allocate the FlexFec header flexFecHeader := make([]byte, headerSize) // XOR the relevant fields for the header // TO DO - CHECK TO SEE IF THE MARSHALTO() call works with this. tmpMediaPacketBuf := make([]byte, headerSize) for mediaPackets.HasNext() { mediaPacket := mediaPackets.Next() n, err := mediaPacket.MarshalTo(tmpMediaPacketBuf) if n == 0 || err != nil { return nil } // XOR the first 2 bytes of the header: V, P, X, CC, M, PT fields flexFecHeader[0] ^= tmpMediaPacketBuf[0] flexFecHeader[1] ^= tmpMediaPacketBuf[1] // XOR the length recovery field lengthRecoveryVal := uint16(mediaPacket.MarshalSize() - BaseRTPHeaderSize) //nolint:gosec // G115 flexFecHeader[2] ^= uint8(lengthRecoveryVal >> 8) //nolint:gosec // G115 flexFecHeader[3] ^= uint8(lengthRecoveryVal) //nolint:gosec // G115 // XOR the 5th to 8th bytes of the header: the timestamp field flexFecHeader[4] ^= flexFecHeader[4] flexFecHeader[5] ^= flexFecHeader[5] flexFecHeader[6] ^= flexFecHeader[6] flexFecHeader[7] ^= flexFecHeader[7] } // Write the base SN for the batch of media packets binary.BigEndian.PutUint16(flexFecHeader[8:10], mediaBaseSn) // Write the bitmasks to the header binary.BigEndian.PutUint16(flexFecHeader[10:12], mask1) if optionalMask2 > 0 { binary.BigEndian.PutUint32(flexFecHeader[12:16], optionalMask2) flexFecHeader[10] |= 0b10000000 } if optionalMask3 > 0 { binary.BigEndian.PutUint64(flexFecHeader[16:24], optionalMask3) flexFecHeader[12] |= 0b10000000 } return flexFecHeader } func (flex *FlexEncoder20) encodeFlexFecRepairPayload(mediaPackets *util.MediaPacketIterator) []byte { flexFecPayload := make([]byte, len(mediaPackets.First().Payload)) for mediaPackets.HasNext() { mediaPacketPayload := mediaPackets.Next().Payload if len(flexFecPayload) < len(mediaPacketPayload) { // Expected FEC packet payload is bigger that what we can currently store, // we need to resize. flexFecPayloadTmp := make([]byte, len(mediaPacketPayload)) copy(flexFecPayloadTmp, flexFecPayload) flexFecPayload = flexFecPayloadTmp } for byteIndex := 0; byteIndex < len(mediaPacketPayload); byteIndex++ { flexFecPayload[byteIndex] ^= mediaPacketPayload[byteIndex] } } return flexFecPayload } interceptor-0.1.42/pkg/flexfec/flexfec_encoder_03.go000066400000000000000000000177041510612111000222720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package flexfec implements FlexFEC to recover missing RTP packets due to packet loss. // https://datatracker.ietf.org/doc/html/rfc8627 package flexfec import ( "encoding/binary" "sync" "github.com/pion/rtp" ) const ( // BaseFec03HeaderSize represents the minium FEC payload's header size including the // required first mask. BaseFec03HeaderSize = 20 // maxRTPPacketSize represents the maximum size of an RTP packet buffer. // This is a reasonable upper bound for typical RTP packets. maxRTPPacketSize = 1500 ) var bufferPool = sync.Pool{ //nolint:gochecknoglobals New: func() any { b := make([]byte, maxRTPPacketSize) return &b }, } // FlexEncoder03 implements the Fec encoding mechanism for the "Flex" variant of FlexFec. type FlexEncoder03 struct { fecBaseSn uint16 payloadType uint8 ssrc uint32 coverage *ProtectionCoverage } // FlexEncoder03Factory is a factory for FlexFEC-03 encoders. type FlexEncoder03Factory struct{} // NewEncoder creates new FlexFEC-03 encoder. func (f FlexEncoder03Factory) NewEncoder(payloadType uint8, ssrc uint32) FlexEncoder { return NewFlexEncoder03(payloadType, ssrc) } // NewFlexEncoder03 creates new FlexFEC-03 encoder. func NewFlexEncoder03(payloadType uint8, ssrc uint32) *FlexEncoder03 { return &FlexEncoder03{ payloadType: payloadType, ssrc: ssrc, fecBaseSn: uint16(1000), } } // EncodeFec returns a list of generated RTP packets with FEC payloads that protect the specified mediaPackets. // This method returns nil in case of missing RTP packets in the mediaPackets array or packets passed out of order. func (flex *FlexEncoder03) EncodeFec(mediaPackets []rtp.Packet, numFecPackets uint32) []rtp.Packet { // Check if mediaPackets is empty if len(mediaPackets) == 0 { return nil } // Check if RTP packets are in order by comparing sequence numbers for i := 1; i < len(mediaPackets); i++ { if mediaPackets[i].SequenceNumber != mediaPackets[i-1].SequenceNumber+1 { // Packets are not in order or there are missing packets return nil } } // Start by defining which FEC packets cover which media packets if flex.coverage == nil { flex.coverage = NewCoverage(mediaPackets, numFecPackets) } else { flex.coverage.UpdateCoverage(mediaPackets, numFecPackets) } if flex.coverage == nil { return nil } // Generate FEC payloads fecPackets := make([]rtp.Packet, 0, numFecPackets) for fecPacketIndex := uint32(0); fecPacketIndex < numFecPackets; fecPacketIndex++ { fecPacket, ok := flex.encodeFlexFecPacket(fecPacketIndex, mediaPackets[0].SequenceNumber) if ok { fecPackets = append(fecPackets, fecPacket) } } return fecPackets } //nolint:cyclop func (flex *FlexEncoder03) encodeFlexFecPacket(fecPacketIndex uint32, mediaBaseSn uint16) (rtp.Packet, bool) { mediaPackets := flex.coverage.GetCoveredBy(fecPacketIndex) mask1 := flex.coverage.ExtractMask1(fecPacketIndex) optionalMask2 := flex.coverage.ExtractMask2(fecPacketIndex) optionalMask3 := flex.coverage.ExtractMask3_03(fecPacketIndex) /* FlexFEC Header Format: 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |0|0| P|X| CC |M| PT recovery | length recovery | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | TS recovery | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRCCount | reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC_i | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SN base_i |k| Mask [0-14] | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |k| Mask [15-45] (optional) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |k| | +-+ Mask [46-108] (optional) | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ... next in SSRC_i ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ if !mediaPackets.HasNext() { return rtp.Packet{}, false } // Get header size - This depends on the size of the bitmask. headerSize := BaseFec03HeaderSize if optionalMask2 > 0 || optionalMask3 > 0 { headerSize += 4 } if optionalMask3 > 0 { headerSize += 8 } // Find the maximum payload size among all media packets maxPayloadSize := 0 for mediaPackets.HasNext() { maxPayloadSize = max(maxPayloadSize, mediaPackets.Next().MarshalSize()-BaseRTPHeaderSize) } mediaPackets.Reset() flexFecPayload := make([]byte, headerSize+maxPayloadSize) flexFecHeader := flexFecPayload[:headerSize] flexFecRepairPayload := flexFecPayload[headerSize : headerSize+maxPayloadSize] bufferFromPool := bufferPool.Get().(*[]byte) //nolint:forcetypeassert defer bufferPool.Put(bufferFromPool) tmpMediaPacketBuf := *bufferFromPool for mediaPackets.HasNext() { mediaPacket := mediaPackets.Next() packetSize := mediaPacket.MarshalSize() if packetSize > len(tmpMediaPacketBuf) { // Packet is too large for our fixed buffer, fallback to dynamic allocation tmpMediaPacketBuf = make([]byte, packetSize) } n, err := mediaPacket.MarshalTo(tmpMediaPacketBuf[:packetSize]) if n == 0 || err != nil { return rtp.Packet{}, false } // XOR the first 2 bytes of the header: V, P, X, CC, M, PT fields flexFecHeader[0] ^= tmpMediaPacketBuf[0] flexFecHeader[1] ^= tmpMediaPacketBuf[1] // Clear the first 2 bits flexFecHeader[0] &= 0b00111111 // XOR the length recovery field lengthRecoveryVal := uint16(mediaPacket.MarshalSize() - BaseRTPHeaderSize) //nolint:gosec // G115 flexFecHeader[2] ^= uint8(lengthRecoveryVal >> 8) //nolint:gosec // G115 flexFecHeader[3] ^= uint8(lengthRecoveryVal) //nolint:gosec // G115 // XOR the 5th to 8th bytes of the header: the timestamp field flexFecHeader[4] ^= tmpMediaPacketBuf[4] flexFecHeader[5] ^= tmpMediaPacketBuf[5] flexFecHeader[6] ^= tmpMediaPacketBuf[6] flexFecHeader[7] ^= tmpMediaPacketBuf[7] // Process FlexFEC Repair Payload (bytes after RTP header) for byteIndex := 0; byteIndex < packetSize-BaseRTPHeaderSize; byteIndex++ { flexFecRepairPayload[byteIndex] ^= tmpMediaPacketBuf[byteIndex+BaseRTPHeaderSize] } } // Write the SSRC count flexFecHeader[8] = 1 // Write 0s in reserved flexFecHeader[9] = 0 flexFecHeader[10] = 0 flexFecHeader[11] = 0 // Write the SSRC of media packets protected by this FEC packet binary.BigEndian.PutUint32(flexFecHeader[12:16], mediaPackets.First().SSRC) // Write the base SN for the batch of media packets binary.BigEndian.PutUint16(flexFecHeader[16:18], mediaBaseSn) // Write the bitmasks to the header binary.BigEndian.PutUint16(flexFecHeader[18:20], mask1) if optionalMask2 == 0 && optionalMask3 == 0 { flexFecHeader[18] |= 0b10000000 } else { binary.BigEndian.PutUint32(flexFecHeader[20:24], optionalMask2) if optionalMask3 == 0 { flexFecHeader[20] |= 0b10000000 } else { binary.BigEndian.PutUint64(flexFecHeader[24:32], optionalMask3) flexFecHeader[24] |= 0b10000000 } } packet := rtp.Packet{ Header: rtp.Header{ Version: 2, Padding: false, Extension: false, Marker: false, PayloadType: flex.payloadType, SequenceNumber: flex.fecBaseSn, Timestamp: 54243243, SSRC: flex.ssrc, CSRC: []uint32{}, }, Payload: flexFecPayload, } flex.fecBaseSn++ return packet, true } interceptor-0.1.42/pkg/flexfec/flexfec_encoder_03_test.go000066400000000000000000000045541510612111000233300ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec import ( "testing" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestFlexEncoder03_EncodeFec_EmptyMediaPackets(t *testing.T) { encoder := FlexEncoder03Factory{}.NewEncoder(96, 1234) var mediaPackets []rtp.Packet result := encoder.EncodeFec(mediaPackets, 1) assert.Nil(t, result, "EncodeFec should return nil when mediaPackets is empty") } func TestFlexEncoder03_EncodeFec_OutOfOrderPackets(t *testing.T) { encoder := FlexEncoder03Factory{}.NewEncoder(96, 1234) mediaPackets := []rtp.Packet{ { Header: rtp.Header{ SequenceNumber: 2, SSRC: 1234, }, }, { Header: rtp.Header{ SequenceNumber: 1, // Out of order (should be 3) SSRC: 1234, }, }, } result := encoder.EncodeFec(mediaPackets, 1) assert.Nil(t, result, "EncodeFec should return nil when packets are out of order") } func TestFlexEncoder03_EncodeFec_MissingPackets(t *testing.T) { encoder := FlexEncoder03Factory{}.NewEncoder(96, 1234) mediaPackets := []rtp.Packet{ { Header: rtp.Header{ SequenceNumber: 1, SSRC: 1234, }, }, { Header: rtp.Header{ SequenceNumber: 3, // Missing packet with sequence number 2 SSRC: 1234, }, }, } result := encoder.EncodeFec(mediaPackets, 1) assert.Nil(t, result, "EncodeFec should return nil when there are missing packets") } func TestFlexEncoder03_EncodeFec_DifferentPayloadSizes(t *testing.T) { encoder := FlexEncoder03Factory{}.NewEncoder(96, 1234) smallPayload := []byte{1, 2, 3} largePayload := []byte{1, 2, 3, 4, 5, 6, 7, 8} mediaPackets := []rtp.Packet{ { Header: rtp.Header{ SequenceNumber: 1, SSRC: 1234, }, Payload: smallPayload, }, { Header: rtp.Header{ SequenceNumber: 2, SSRC: 1234, }, Payload: largePayload, }, } fecPackets := encoder.EncodeFec(mediaPackets, 1) assert.NotNil(t, fecPackets, "EncodeFec should return FEC packets") assert.Equal(t, 1, len(fecPackets), "EncodeFec should return 1 FEC packet") expectedPayloadSize := len(largePayload) actualPayloadSize := len(fecPackets[0].Payload) - BaseFec03HeaderSize assert.Equal(t, expectedPayloadSize, actualPayloadSize, "FEC payload size should match the size of the largest media packet payload") } interceptor-0.1.42/pkg/flexfec/option.go000066400000000000000000000017171510612111000201620ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package flexfec // FecOption can be used to set initial options on Fec encoder interceptors. type FecOption func(d *FecInterceptor) error // NumMediaPackets sets the number of media packets to accumulate before generating another FEC packets batch. func NumMediaPackets(numMediaPackets uint32) FecOption { return func(f *FecInterceptor) error { f.numMediaPackets = numMediaPackets return nil } } // NumFECPackets sets the number of FEC packets to generate for each batch of media packets. func NumFECPackets(numFecPackets uint32) FecOption { return func(f *FecInterceptor) error { f.numFecPackets = numFecPackets return nil } } // FECEncoderFactory sets the custom factory for constructing the FEC Encoders. func FECEncoderFactory(factory EncoderFactory) FecOption { return func(f *FecInterceptor) error { f.encoderFactory = factory return nil } } interceptor-0.1.42/pkg/flexfec/util/000077500000000000000000000000001510612111000172725ustar00rootroot00000000000000interceptor-0.1.42/pkg/flexfec/util/bitarray.go000066400000000000000000000020351510612111000214360ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package util implements utilities to better support Fec decoding / encoding. package util // BitArray provides support for bitmask manipulations. type BitArray struct { Lo uint64 // leftmost 64 bits Hi uint64 // rightmost 64 bits } // SetBit sets a bit to the specified bit value on the bitmask. func (b *BitArray) SetBit(bitIndex uint32) { if bitIndex < 64 { b.Lo |= uint64(0b1) << (63 - bitIndex) } else { hiBitIndex := bitIndex - 64 b.Hi |= uint64(0b1) << (63 - hiBitIndex) } } // Reset clears the bitmask. func (b *BitArray) Reset() { b.Lo = 0 b.Hi = 0 } // GetBit returns the bit value at a specified index of the bitmask. func (b *BitArray) GetBit(bitIndex uint32) uint8 { if bitIndex < 64 { result := (b.Lo & (uint64(0b1) << (63 - bitIndex))) if result > 0 { return 1 } return 0 } hiBitIndex := bitIndex - 64 result := (b.Hi & (uint64(0b1) << (63 - hiBitIndex))) if result > 0 { return 1 } return 0 } interceptor-0.1.42/pkg/flexfec/util/media_packet_iterator.go000066400000000000000000000027271510612111000241500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // nolint: revive, staticcheck package util import "github.com/pion/rtp" // MediaPacketIterator supports iterating through a list of media packets protected by // a specific Fec packet. type MediaPacketIterator struct { mediaPackets []rtp.Packet coveredIndices []uint32 nextIndex int } // NewMediaPacketIterator returns a new MediaPacketIterator. func NewMediaPacketIterator(mediaPackets []rtp.Packet, coveredIndices []uint32) *MediaPacketIterator { return &MediaPacketIterator{ mediaPackets: mediaPackets, coveredIndices: coveredIndices, nextIndex: 0, } } // Reset sets the starting iterating index back to 0. func (m *MediaPacketIterator) Reset() *MediaPacketIterator { m.nextIndex = 0 return m } // HasNext indicates whether or not there are more media packets // that can be iterated through. func (m *MediaPacketIterator) HasNext() bool { return m.nextIndex < len(m.coveredIndices) } // Next returns the next media packet to iterate through. func (m *MediaPacketIterator) Next() *rtp.Packet { if m.nextIndex == len(m.coveredIndices) { return nil } packet := m.mediaPackets[m.coveredIndices[m.nextIndex]] m.nextIndex++ return &packet } // First returns the first media packet to iterate through. func (m *MediaPacketIterator) First() *rtp.Packet { if len(m.coveredIndices) == 0 { return nil } return &m.mediaPackets[m.coveredIndices[0]] } interceptor-0.1.42/pkg/flexfec/util/media_packet_iterator_test.go000066400000000000000000000071471510612111000252100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package util_test import ( "testing" "github.com/pion/interceptor/pkg/flexfec/util" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func createTestMediaPackets() []rtp.Packet { return []rtp.Packet{ {Header: rtp.Header{SequenceNumber: 1, SSRC: 1}}, {Header: rtp.Header{SequenceNumber: 2, SSRC: 1}}, {Header: rtp.Header{SequenceNumber: 3, SSRC: 1}}, {Header: rtp.Header{SequenceNumber: 4, SSRC: 1}}, {Header: rtp.Header{SequenceNumber: 5, SSRC: 1}}, } } func TestNewMediaPacketIterator(t *testing.T) { mediaPackets := createTestMediaPackets() coveredIndices := []uint32{1, 2, 4} iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices) assert.NotNil(t, iterator) } func TestMediaPacketIteratorReset(t *testing.T) { mediaPackets := createTestMediaPackets() coveredIndices := []uint32{1, 2, 4} iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices) // Advance the iterator _ = iterator.Next() _ = iterator.Next() // Reset the iterator result := iterator.Reset() // After reset, HasNext should be true again assert.True(t, iterator.HasNext()) // Reset should return the iterator for chaining assert.Equal(t, iterator, result, "Reset should return the iterator for chaining") } func TestMediaPacketIteratorHasNext(t *testing.T) { mediaPackets := createTestMediaPackets() coveredIndices := []uint32{1, 2, 4} iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices) // Initially, HasNext should be true assert.True(t, iterator.HasNext()) // Advance to the last element _ = iterator.Next() _ = iterator.Next() assert.True(t, iterator.HasNext()) // Advance past the last element _ = iterator.Next() assert.False(t, iterator.HasNext()) } func TestMediaPacketIteratorNext(t *testing.T) { mediaPackets := createTestMediaPackets() coveredIndices := []uint32{1, 2, 4} iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices) // First call to Next should return the first covered packet packet := iterator.Next() assert.NotNil(t, packet) assert.Equal(t, uint16(2), packet.SequenceNumber) // Second call to Next should return the second covered packet packet = iterator.Next() assert.NotNil(t, packet) assert.Equal(t, uint16(3), packet.SequenceNumber) // Third call to Next should return the third covered packet packet = iterator.Next() assert.NotNil(t, packet) assert.Equal(t, uint16(5), packet.SequenceNumber) // Fourth call to Next should return nil packet = iterator.Next() assert.Nil(t, packet) } func TestMediaPacketIteratorFirst(t *testing.T) { mediaPackets := createTestMediaPackets() coveredIndices := []uint32{1, 2, 4} iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices) // First should return the first covered packet packet := iterator.First() assert.NotNil(t, packet) assert.Equal(t, uint16(2), packet.SequenceNumber) // First should not advance the iterator, so Next should still return the first packet nextPacket := iterator.Next() assert.NotNil(t, nextPacket) assert.Equal(t, uint16(2), nextPacket.SequenceNumber) // Even after advancing the iterator, First should still return the first packet _ = iterator.Next() packet = iterator.First() assert.NotNil(t, packet) assert.Equal(t, uint16(2), packet.SequenceNumber) } func TestMediaPacketIteratorEmptyCoveredIndices(t *testing.T) { mediaPackets := createTestMediaPackets() coveredIndices := []uint32{} iterator := util.NewMediaPacketIterator(mediaPackets, coveredIndices) assert.False(t, iterator.HasNext()) assert.Nil(t, iterator.Next()) } interceptor-0.1.42/pkg/gcc/000077500000000000000000000000001510612111000154355ustar00rootroot00000000000000interceptor-0.1.42/pkg/gcc/adaptive_threshold.go000066400000000000000000000062301510612111000216360ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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, _ time.Duration) (usage, time.Duration, time.Duration) { a.numDeltas++ if a.numDeltas < 2 { return usageNormal, estimate, a.max } t := time.Duration(min(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( min(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.42/pkg/gcc/adaptive_threshold_test.go000066400000000000000000000055371510612111000227060ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/arrival_group.go000066400000000000000000000015371510612111000206460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package gcc import ( "fmt" "time" "github.com/pion/interceptor/internal/cc" ) type arrivalGroup struct { packets []cc.Acknowledgment departure time.Time arrival time.Time } func newArrivalGroup(a cc.Acknowledgment) arrivalGroup { return arrivalGroup{ packets: []cc.Acknowledgment{a}, departure: a.Departure, arrival: a.Arrival, } } func (g *arrivalGroup) add(a cc.Acknowledgment) { g.packets = append(g.packets, a) g.arrival = a.Arrival } 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.42/pkg/gcc/arrival_group_accumulator.go000066400000000000000000000042151510612111000232410ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 = newArrivalGroup(next) init = true continue } if next.Arrival.Before(group.arrival) { // ignore out of order arrivals continue } if next.Departure.After(group.departure) { // A sequence of packets which are sent within a burst_time interval // constitute a group. if interDepartureTimePkt(group, next) <= a.interDepartureThreshold { group.add(next) continue } // A Packet which has an inter-arrival time less than burst_time and // an inter-group delay variation d(i) less than 0 is considered // being part of the current group of packets. if interArrivalTimePkt(group, next) <= a.interArrivalThreshold && interGroupDelayVariationPkt(group, next) < a.interGroupDelayVariationTreshold { group.add(next) continue } agWriter(group) group = newArrivalGroup(next) } } } } func interArrivalTimePkt(group arrivalGroup, ack cc.Acknowledgment) time.Duration { return ack.Arrival.Sub(group.arrival) } func interDepartureTimePkt(group arrivalGroup, ack cc.Acknowledgment) time.Duration { if len(group.packets) == 0 { return 0 } return ack.Departure.Sub(group.departure) } func interGroupDelayVariationPkt(group arrivalGroup, ack cc.Acknowledgment) time.Duration { return ack.Arrival.Sub(group.arrival) - ack.Departure.Sub(group.departure) } interceptor-0.1.42/pkg/gcc/arrival_group_accumulator_test.go000066400000000000000000000137541510612111000243100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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{}, }}, }, { 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(0 * 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), }, }, }, { name: "newGroupBecauseOfInterDepartureTime", log: []cc.Acknowledgment{ { SequenceNumber: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(4 * time.Millisecond), }, { SequenceNumber: 1, Departure: time.Time{}.Add(3 * time.Millisecond), Arrival: time.Time{}.Add(4 * time.Millisecond), }, { SequenceNumber: 2, Departure: time.Time{}.Add(6 * time.Millisecond), Arrival: time.Time{}.Add(10 * time.Millisecond), }, { SequenceNumber: 3, Departure: time.Time{}.Add(9 * time.Millisecond), Arrival: time.Time{}.Add(10 * time.Millisecond), }, triggerNewGroupElement, }, exp: []arrivalGroup{ { packets: []cc.Acknowledgment{ { SequenceNumber: 0, Departure: time.Time{}, Arrival: time.Time{}.Add(4 * time.Millisecond), }, { SequenceNumber: 1, Departure: time.Time{}.Add(3 * time.Millisecond), Arrival: time.Time{}.Add(4 * time.Millisecond), }, }, departure: time.Time{}, arrival: time.Time{}.Add(4 * time.Millisecond), }, { packets: []cc.Acknowledgment{ { SequenceNumber: 2, Departure: time.Time{}.Add(6 * time.Millisecond), Arrival: time.Time{}.Add(10 * time.Millisecond), }, { SequenceNumber: 3, Departure: time.Time{}.Add(9 * time.Millisecond), Arrival: time.Time{}.Add(10 * time.Millisecond), }, }, departure: time.Time{}.Add(6 * time.Millisecond), arrival: time.Time{}.Add(10 * 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.42/pkg/gcc/arrival_group_test.go000066400000000000000000000064411510612111000217040ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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{}, }, }, { name: "departure time of group is the departure time of the first packet in the group", acks: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 0, Departure: time.Time{}.Add(27 * time.Millisecond), Arrival: time.Time{}, }, { SequenceNumber: 1, Size: 1, Departure: time.Time{}.Add(32 * time.Millisecond), Arrival: time.Time{}.Add(37 * time.Millisecond), }, { SequenceNumber: 2, Size: 2, Departure: time.Time{}.Add(50 * time.Millisecond), Arrival: time.Time{}.Add(56 * time.Millisecond), }}, expected: arrivalGroup{ packets: []cc.Acknowledgment{{ SequenceNumber: 0, Size: 0, Departure: time.Time{}.Add(27 * time.Millisecond), Arrival: time.Time{}, }, { SequenceNumber: 1, Size: 1, Departure: time.Time{}.Add(32 * time.Millisecond), Arrival: time.Time{}.Add(37 * time.Millisecond), }, { SequenceNumber: 2, Size: 2, Departure: time.Time{}.Add(50 * time.Millisecond), Arrival: time.Time{}.Add(56 * time.Millisecond), }}, arrival: time.Time{}.Add(56 * time.Millisecond), departure: time.Time{}.Add(27 * time.Millisecond), }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { ag := arrivalGroup{} for i, ack := range tc.acks { if i == 0 { ag = newArrivalGroup(ack) } else { ag.add(ack) } } assert.Equal(t, tc.expected, ag) }) } } interceptor-0.1.42/pkg/gcc/delay_based_bwe.go000066400000000000000000000052141510612111000210570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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(delayConfig 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( delayConfig.nowFn, delayConfig.initialBitrate, delayConfig.minBitrate, delayConfig.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.42/pkg/gcc/gcc.go000066400000000000000000000006511510612111000165220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package gcc implements Google Congestion Control for bandwidth estimation package gcc import "time" func clampInt(b, minVal, maxVal int) int { return max(minVal, min(maxVal, b)) } func clampDuration(d, minVal, maxVal time.Duration) time.Duration { return time.Duration(clampInt(int(d), int(minVal), int(maxVal))) } interceptor-0.1.42/pkg/gcc/gcc_test.go000066400000000000000000000021611510612111000175570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package gcc import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) 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) minVal := time.Duration(tt.min) maxVal := time.Duration(tt.max) expected := time.Duration(tt.expected) assert.Equal(t, expected, clampDuration(x, minVal, maxVal)) }) } } interceptor-0.1.42/pkg/gcc/kalman.go000066400000000000000000000043761510612111000172410ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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) } else { 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.42/pkg/gcc/kalman_test.go000066400000000000000000000043771510612111000203010ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/leaky_bucket_pacer.go000066400000000000000000000077751510612111000216200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 { pacer := &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{}, } pacer.pool = &sync.Pool{ New: func() any { b := make([]byte, 1460) return &b }, } go pacer.Run() return pacer } // 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.42/pkg/gcc/loss_based_bwe.go000066400000000000000000000062031510612111000207400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 = min(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.42/pkg/gcc/noop_pacer.go000066400000000000000000000030171510612111000201120ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/overuse_detector.go000066400000000000000000000036511510612111000213520ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 { //nolint:nestif if d.increasingDuration == 0 { d.increasingDuration = delta / 2 } else { d.increasingDuration += delta } d.increasingCounter++ if (d.overuseTime == 0 && d.increasingCounter > 1) || (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.42/pkg/gcc/overuse_detector_test.go000066400000000000000000000055201510612111000224060ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package gcc import ( "runtime" "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) if tc.delay == 0 { // avoid time.Sleep(0) since it's broken on windows. runtime.Gosched() } else { 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.42/pkg/gcc/rate_calculator.go000066400000000000000000000024721510612111000211350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/rate_calculator_test.go000066400000000000000000000044501510612111000221720ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/rate_controller.go000066400000000000000000000110251510612111000211610ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/rate_controller_test.go000066400000000000000000000032511510612111000222220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/send_side_bwe.go000066400000000000000000000170651510612111000205670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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) { send := &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(send); err != nil { return nil, err } } if send.pacer == nil { send.pacer = NewLeakyBucketPacer(send.latestBitrate) } send.lossController = newLossBasedBWE(send.latestBitrate) send.delayController = newDelayController(delayControllerConfig{ nowFn: time.Now, initialBitrate: send.latestBitrate, minBitrate: send.minBitrate, maxBitrate: send.maxBitrate, }) send.delayController.onUpdate(send.onDelayUpdate) return send, 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) //nolint:gosec // G115 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. // //nolint:cyclop func (e *SendSideBWE) WriteRTCP(pkts []rtcp.Packet, _ 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(min(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]any { e.lock.Lock() defer e.lock.Unlock() return map[string]any{ "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 := min(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.42/pkg/gcc/send_side_bwe_test.go000066400000000000000000000100351510612111000216140ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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/assert" "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, _ 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 t *testing.T } func (m *mockGCCWriteStream) Write(header *rtp.Header, payload []byte, _ interceptor.Attributes) (int, error) { m.t.Helper() pkt, err := (&rtp.Packet{Header: *header, Payload: payload}).Marshal() assert.NoError(m.t, 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) gccMock := &mockGCCWriteStream{ twccResponder: &mockTWCCResponder{ bwe, make(chan []byte, 500), }, t: t, } twccSender, err := (&twcc.SenderInterceptorFactory{}).NewInterceptor("") require.NoError(t, err) require.NotNil(t, twccSender) twccInboundRTP := twccSender.BindRemoteStream(streamInfo, gccMock.twccResponder) twccSender.BindRTCPWriter(gccMock.twccResponder) require.Equal(t, latestBitrate, bwe.GetTargetBitrate()) require.NotEqual(t, 0, len(bwe.GetStats())) rtpWriter := bwe.AddStream(streamInfo, gccMock) require.NotNil(t, rtpWriter) twccWriter := twcc.HeaderExtensionInterceptor{} rtpWriter = twccWriter.BindLocalStream(streamInfo, rtpWriter) for i := 0; i <= 100; i++ { _, err = rtpWriter.Write(&rtp.Header{SSRC: 1, Extensions: []rtp.Extension{}}, rtpPayload, nil) assert.NoError(t, err) _, _, err = twccInboundRTP.Read(buffer, nil) assert.NoError(t, 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) recorder := 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 arrivalTime += int64(rtcp.TypeTCCDeltaScaleFactor * (rand.Intn(128) + 1)) //nolint:gosec recorder.Record(5000, seq, arrivalTime) } } rtcpPackets := recorder.BuildFeedbackPacket() require.Equal(b, 1, len(rtcpPackets)) require.NoError(b, bwe.WriteRTCP(rtcpPackets, nil)) } require.NoError(b, bwe.Close()) }) } } interceptor-0.1.42/pkg/gcc/slope_estimator.go000066400000000000000000000023421510612111000211760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/slope_estimator_test.go000066400000000000000000000047121510612111000222400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/gcc/state.go000066400000000000000000000017751510612111000171160ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package gcc import "fmt" type state int const ( stateIncrease state = iota stateDecrease stateHold ) //nolint:cyclop func (s state) transition(use usage) state { switch s { case stateHold: switch use { case usageOver: return stateDecrease case usageNormal: return stateIncrease case usageUnder: return stateHold } case stateIncrease: switch use { case usageOver: return stateDecrease case usageNormal: return stateIncrease case usageUnder: return stateHold } case stateDecrease: switch use { 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.42/pkg/gcc/usage.go000066400000000000000000000006451510612111000170750ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/intervalpli/000077500000000000000000000000001510612111000172325ustar00rootroot00000000000000interceptor-0.1.42/pkg/intervalpli/generator_interceptor.go000066400000000000000000000113471510612111000241730ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package intervalpli 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 []GeneratorOption } // NewReceiverInterceptor returns a new ReceiverInterceptor. func NewReceiverInterceptor(opts ...GeneratorOption) (*ReceiverInterceptorFactory, error) { return &ReceiverInterceptorFactory{ opts: opts, }, nil } // NewInterceptor returns a new ReceiverInterceptor interceptor. func (r *ReceiverInterceptorFactory) NewInterceptor(string) (interceptor.Interceptor, error) { return NewGeneratorInterceptor(r.opts...) } // GeneratorInterceptor interceptor sends PLI packets. // Implements PLI in a naive way: sends a PLI for each new track that support PLI, periodically. type GeneratorInterceptor struct { interceptor.NoOp interval time.Duration streams sync.Map immediatePLINeeded chan []uint32 log logging.LeveledLogger m sync.Mutex wg sync.WaitGroup close chan struct{} } // NewGeneratorInterceptor returns a new GeneratorInterceptor interceptor. func NewGeneratorInterceptor(opts ...GeneratorOption) (*GeneratorInterceptor, error) { generatorInterceptor := &GeneratorInterceptor{ interval: 3 * time.Second, log: logging.NewDefaultLoggerFactory().NewLogger("pli_generator"), immediatePLINeeded: make(chan []uint32, 1), close: make(chan struct{}), } for _, opt := range opts { if err := opt(generatorInterceptor); err != nil { return nil, err } } return generatorInterceptor, nil } func (r *GeneratorInterceptor) isClosed() bool { select { case <-r.close: return true default: return false } } // Close closes the interceptor. func (r *GeneratorInterceptor) 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 *GeneratorInterceptor) 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 *GeneratorInterceptor) loop(rtcpWriter interceptor.RTCPWriter) { defer r.wg.Done() ticker, tickerChan := r.createLoopTicker() defer func() { if ticker != nil { ticker.Stop() } }() for { select { case ssrcs := <-r.immediatePLINeeded: r.writePLIs(rtcpWriter, ssrcs) case <-tickerChan: ssrcs := make([]uint32, 0) r.streams.Range(func(k, _ any) bool { key, ok := k.(uint32) if !ok { return false } ssrcs = append(ssrcs, key) return true }) r.writePLIs(rtcpWriter, ssrcs) case <-r.close: return } } } func (r *GeneratorInterceptor) createLoopTicker() (*time.Ticker, <-chan time.Time) { if r.interval > 0 { ticker := time.NewTicker(r.interval) return ticker, ticker.C } return nil, make(chan time.Time) } func (r *GeneratorInterceptor) writePLIs(rtcpWriter interceptor.RTCPWriter, ssrcs []uint32) { if len(ssrcs) == 0 { return } pkts := []rtcp.Packet{} for _, ssrc := range ssrcs { pkts = append(pkts, &rtcp.PictureLossIndication{MediaSSRC: ssrc}) } if _, err := rtcpWriter.Write(pkts, interceptor.Attributes{}); err != nil { r.log.Warnf("failed sending: %+v", err) } } // 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 *GeneratorInterceptor) BindRemoteStream( info *interceptor.StreamInfo, reader interceptor.RTPReader, ) interceptor.RTPReader { if !streamSupportPli(info) { return reader } r.streams.Store(info.SSRC, nil) // New streams need to receive a PLI as soon as possible. r.ForcePLI(info.SSRC) return reader } // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (r *GeneratorInterceptor) 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 *GeneratorInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader { return reader } // ForcePLI sends a PLI request to the tracks matching the provided SSRCs. func (r *GeneratorInterceptor) ForcePLI(ssrc ...uint32) { r.immediatePLINeeded <- ssrc } interceptor-0.1.42/pkg/intervalpli/generator_interceptor_test.go000066400000000000000000000042651510612111000252330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package intervalpli import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/stretchr/testify/assert" ) func TestPLIGeneratorInterceptor_Unsupported(t *testing.T) { i, err := NewGeneratorInterceptor( GeneratorInterval(time.Millisecond*10), GeneratorLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ) assert.Nil(t, err) streamSSRC := uint32(123456) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: streamSSRC, MimeType: "video/h264", }, i) defer func() { assert.NoError(t, stream.Close()) }() timeout := time.NewTimer(100 * time.Millisecond) defer timeout.Stop() select { case <-timeout.C: return case <-stream.WrittenRTCP(): assert.FailNow(t, "should not receive any PIL") } } func TestPLIGeneratorInterceptor(t *testing.T) { generatorInterceptor, err := NewGeneratorInterceptor( GeneratorInterval(time.Second*1), GeneratorLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ) assert.Nil(t, err) streamSSRC := uint32(123456) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: streamSSRC, ClockRate: 90000, MimeType: "video/h264", RTCPFeedback: []interceptor.RTCPFeedback{ {Type: "nack", Parameter: "pli"}, }, }, generatorInterceptor) defer func() { assert.NoError(t, stream.Close()) }() pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) sr, ok := pkts[0].(*rtcp.PictureLossIndication) assert.True(t, ok) assert.Equal(t, &rtcp.PictureLossIndication{MediaSSRC: streamSSRC}, sr) // Should not have another packet immediately... func() { timeout := time.NewTimer(100 * time.Millisecond) defer timeout.Stop() select { case <-timeout.C: return case <-stream.WrittenRTCP(): assert.FailNow(t, "should not receive any PIL") } }() // ... but should receive one 1sec later. pkts = <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) sr, ok = pkts[0].(*rtcp.PictureLossIndication) assert.True(t, ok) assert.Equal(t, &rtcp.PictureLossIndication{MediaSSRC: streamSSRC}, sr) } interceptor-0.1.42/pkg/intervalpli/generator_option.go000066400000000000000000000013001510612111000231310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package intervalpli import ( "time" "github.com/pion/logging" ) // GeneratorOption can be used to configure GeneratorInterceptor. type GeneratorOption func(r *GeneratorInterceptor) error // 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 send interval for the interceptor. func GeneratorInterval(interval time.Duration) GeneratorOption { return func(r *GeneratorInterceptor) error { r.interval = interval return nil } } interceptor-0.1.42/pkg/intervalpli/pli.go000066400000000000000000000007631510612111000203530ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package intervalpli is an interceptor that requests PLI on a static interval. // Useful when bridging protocols that don't have receiver feedback. package intervalpli import "github.com/pion/interceptor" func streamSupportPli(info *interceptor.StreamInfo) bool { for _, fb := range info.RTCPFeedback { if fb.Type == "nack" && fb.Parameter == "pli" { return true } } return false } interceptor-0.1.42/pkg/jitterbuffer/000077500000000000000000000000001510612111000173745ustar00rootroot00000000000000interceptor-0.1.42/pkg/jitterbuffer/jitter_buffer.go000066400000000000000000000176051510612111000225660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package jitterbuffer implements a buffer for RTP packets designed to help // counteract non-deterministic sources of latency package jitterbuffer import ( "errors" "sync" "github.com/pion/rtp" ) // State tracks a JitterBuffer as either Buffering or Emitting. type State uint16 // Event represents all events a JitterBuffer can emit. type Event string var ( // ErrBufferUnderrun is returned when the buffer has no items. ErrBufferUnderrun = errors.New("invalid Peek: Empty jitter buffer") // ErrPopWhileBuffering is returned if a jitter buffer is not in a playback state. ErrPopWhileBuffering = errors.New("attempt to pop while buffering") ) const ( // Buffering is the state when the jitter buffer has not started emitting yet, // or has hit an underflow and needs to re-buffer packets. Buffering State = iota // Emitting is the state when the jitter buffer is operating nominally Emitting ) const ( // StartBuffering is emitted when the buffer receives its first packet. StartBuffering Event = "startBuffering" // BeginPlayback is emitted when the buffer has satisfied its buffer length. BeginPlayback = "playing" // BufferUnderflow is emitted when the buffer does not have enough packets to Pop. BufferUnderflow = "underflow" // BufferOverflow is emitted when the buffer has exceeded its limit. BufferOverflow = "overflow" ) func (jbs State) String() string { switch jbs { case Buffering: return "Buffering" case Emitting: return "Emitting" } return "unknown" } type ( // Option will Override JitterBuffer's defaults. Option func(jb *JitterBuffer) // EventListener will be called when the corresponding Event occurs. EventListener func(event Event, jb *JitterBuffer) ) // A JitterBuffer will accept Pushed packets, put them in sequence number // order, and allows removing in either sequence number order or via a // provided timestamp. type JitterBuffer struct { packets *PriorityQueue minStartCount uint16 overflowLen uint16 lastSequence uint16 playoutHead uint16 playoutReady bool state State stats Stats listeners map[Event][]EventListener mutex sync.Mutex } // Stats Track interesting statistics for the life of this JitterBuffer // outOfOrderCount will provide the number of times a packet was Pushed // // without its predecessor being present // // underflowCount will provide the count of attempts to Pop an empty buffer // overflowCount will track the number of times the jitter buffer exceeds its limit. type Stats struct { outOfOrderCount uint32 underflowCount uint32 overflowCount uint32 } // New will initialize a jitter buffer and its associated statistics. func New(opts ...Option) *JitterBuffer { jb := &JitterBuffer{ state: Buffering, stats: Stats{0, 0, 0}, minStartCount: 50, overflowLen: 100, packets: NewQueue(), listeners: make(map[Event][]EventListener), } for _, o := range opts { o(jb) } return jb } // WithMinimumPacketCount will set the required number of packets to be received before // any attempt to pop a packet can succeed. func WithMinimumPacketCount(count uint16) Option { return func(jb *JitterBuffer) { jb.minStartCount = count } } // Listen will register an event listener // The jitter buffer may emit events correspnding, interested listerns should // look at Event for available events. func (jb *JitterBuffer) Listen(event Event, cb EventListener) { jb.listeners[event] = append(jb.listeners[event], cb) } // PlayoutHead returns the SequenceNumber that will be attempted to Pop next. func (jb *JitterBuffer) PlayoutHead() uint16 { jb.mutex.Lock() defer jb.mutex.Unlock() return jb.playoutHead } // SetPlayoutHead allows you to manually specify the packet you wish to pop next // If you have encountered a packet that hasn't resolved you can skip it. func (jb *JitterBuffer) SetPlayoutHead(playoutHead uint16) { jb.mutex.Lock() defer jb.mutex.Unlock() jb.playoutHead = playoutHead } func (jb *JitterBuffer) updateStats(lastPktSeqNo uint16) { // If we have at least one packet, and the next packet being pushed in is not // at the expected sequence number increment the out of order count if jb.packets.Length() > 0 && lastPktSeqNo != (jb.lastSequence+1) { jb.stats.outOfOrderCount++ } jb.lastSequence = lastPktSeqNo } // Push an RTP packet into the jitter buffer, this does not clone // the data so if the memory is expected to be reused, the caller should // take this in to account and pass a copy of the packet they wish to buffer. func (jb *JitterBuffer) Push(packet *rtp.Packet) { jb.mutex.Lock() defer jb.mutex.Unlock() if jb.packets.Length() == 0 { jb.emit(StartBuffering) } if jb.packets.Length() > jb.overflowLen { jb.stats.overflowCount++ jb.emit(BufferOverflow) } if !jb.playoutReady && jb.packets.Length() == 0 { jb.playoutHead = packet.SequenceNumber } jb.updateStats(packet.SequenceNumber) jb.packets.Push(packet, packet.SequenceNumber) jb.updateState() } func (jb *JitterBuffer) emit(event Event) { for _, l := range jb.listeners[event] { l(event, jb) } } func (jb *JitterBuffer) updateState() { // For now, we only look at the number of packets captured in the play buffer if jb.packets.Length() >= jb.minStartCount && jb.state == Buffering { jb.state = Emitting jb.playoutReady = true jb.emit(BeginPlayback) } } // Peek at the packet which is either: // // At the playout head when we are emitting, and the playoutHead flag is true // // or else // // At the last sequence received func (jb *JitterBuffer) Peek(playoutHead bool) (*rtp.Packet, error) { jb.mutex.Lock() defer jb.mutex.Unlock() if jb.packets.Length() < 1 { return nil, ErrBufferUnderrun } if playoutHead && jb.state == Emitting { return jb.packets.Find(jb.playoutHead) } return jb.packets.Find(jb.lastSequence) } // Pop an RTP packet from the jitter buffer at the current playout head. func (jb *JitterBuffer) Pop() (*rtp.Packet, error) { jb.mutex.Lock() defer jb.mutex.Unlock() if jb.state != Emitting { return nil, ErrPopWhileBuffering } packet, err := jb.packets.PopAt(jb.playoutHead) if err != nil { jb.stats.underflowCount++ jb.emit(BufferUnderflow) return nil, err } jb.playoutHead = (jb.playoutHead + 1) jb.updateState() return packet, nil } // PopAtSequence will pop an RTP packet from the jitter buffer at the specified Sequence. func (jb *JitterBuffer) PopAtSequence(sq uint16) (*rtp.Packet, error) { jb.mutex.Lock() defer jb.mutex.Unlock() if jb.state != Emitting { return nil, ErrPopWhileBuffering } packet, err := jb.packets.PopAt(sq) if err != nil { jb.stats.underflowCount++ jb.emit(BufferUnderflow) return nil, err } jb.playoutHead = (jb.playoutHead + 1) jb.updateState() return packet, nil } // PeekAtSequence will return an RTP packet from the jitter buffer at the specified Sequence // without removing it from the buffer. func (jb *JitterBuffer) PeekAtSequence(sq uint16) (*rtp.Packet, error) { jb.mutex.Lock() defer jb.mutex.Unlock() packet, err := jb.packets.Find(sq) if err != nil { return nil, err } return packet, nil } // PopAtTimestamp pops an RTP packet from the jitter buffer with the provided timestamp // Call this method repeatedly to drain the buffer at the timestamp. func (jb *JitterBuffer) PopAtTimestamp(ts uint32) (*rtp.Packet, error) { jb.mutex.Lock() defer jb.mutex.Unlock() if jb.state != Emitting { return nil, ErrPopWhileBuffering } packet, err := jb.packets.PopAtTimestamp(ts) if err != nil { jb.stats.underflowCount++ jb.emit(BufferUnderflow) return nil, err } jb.updateState() return packet, nil } // Clear will empty the buffer and optionally reset the state. func (jb *JitterBuffer) Clear(resetState bool) { jb.mutex.Lock() defer jb.mutex.Unlock() jb.packets.Clear() if resetState { jb.lastSequence = 0 jb.state = Buffering jb.stats = Stats{0, 0, 0} jb.minStartCount = 50 } } interceptor-0.1.42/pkg/jitterbuffer/jitter_buffer_test.go000066400000000000000000000235371510612111000236260ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package jitterbuffer import ( "math" "testing" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) //nolint:cyclop,maintidx func TestJitterBuffer(t *testing.T) { assert := assert.New(t) t.Run("Appends packets in order", func(*testing.T) { jb := New() assert.Equal(jb.lastSequence, uint16(0)) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 501}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 502}, Payload: []byte{0x02}}) assert.Equal(jb.lastSequence, uint16(5002)) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5012, Timestamp: 512}, Payload: []byte{0x02}}) assert.Equal(jb.stats.outOfOrderCount, uint32(1)) assert.Equal(jb.packets.Length(), uint16(4)) assert.Equal(jb.lastSequence, uint16(5012)) }) t.Run("Appends packets and wraps", func(*testing.T) { jb := New(WithMinimumPacketCount(1)) assert.Equal(jb.lastSequence, uint16(0)) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 65535, Timestamp: 500}, Payload: []byte{0x02}}) assert.Equal(jb.lastSequence, uint16(65535)) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 0, Timestamp: 512}, Payload: []byte{0x02}}) assert.Equal(jb.packets.Length(), uint16(2)) assert.Equal(jb.lastSequence, uint16(0)) head, err := jb.Pop() assert.Equal(head.SequenceNumber, uint16(65535)) assert.Equal(err, nil) head, err = jb.Pop() assert.Equal(head.SequenceNumber, uint16(0)) assert.Equal(err, nil) }) t.Run("Appends packets and begins playout", func(*testing.T) { jb := New() for i := 0; i < 100; i++ { jb.Push( &rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(5012 + i), //nolint:gosec // G115 Timestamp: uint32(512 + i), //nolint:gosec // G115 }, Payload: []byte{0x02}, }, ) } assert.Equal(jb.packets.Length(), uint16(100)) assert.Equal(jb.state, Emitting) assert.Equal(jb.playoutHead, uint16(5012)) head, err := jb.Pop() assert.Equal(head.SequenceNumber, uint16(5012)) assert.Equal(err, nil) }) t.Run("Appends packets and begins playout", func(*testing.T) { jb := New(WithMinimumPacketCount(1)) events := make([]Event, 0) jb.Listen(BeginPlayback, func(event Event, _ *JitterBuffer) { events = append(events, event) }) for i := 0; i < 2; i++ { //nolint:gosec // G115 jb.Push( &rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(5012 + i), Timestamp: uint32(512 + i), }, Payload: []byte{0x02}, }, ) } assert.Equal(jb.packets.Length(), uint16(2)) assert.Equal(jb.state, Emitting) assert.Equal(jb.playoutHead, uint16(5012)) head, err := jb.Pop() assert.Equal(head.SequenceNumber, uint16(5012)) assert.Equal(err, nil) assert.Equal(1, len(events)) assert.Equal(Event(BeginPlayback), events[0]) }) t.Run("Wraps playout correctly", func(*testing.T) { jb := New() for i := 0; i < 100; i++ { sqnum := uint16(math.MaxUint16 - 32 + i) //nolint:gosec // G115 //nolint:gosec // G115 jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: sqnum, Timestamp: uint32(512 + i)}, Payload: []byte{0x02}}) } assert.Equal(jb.packets.Length(), uint16(100)) assert.Equal(jb.state, Emitting) assert.Equal(jb.playoutHead, uint16(math.MaxUint16-32)) head, err := jb.Pop() assert.Equal(head.SequenceNumber, uint16(math.MaxUint16-32)) assert.Equal(err, nil) for i := 0; i < 100; i++ { head, err := jb.Pop() if i < 99 { assert.Equal(head.SequenceNumber, uint16((math.MaxUint16 - 31 + i))) //nolint:gosec // G115 assert.Equal(err, nil) } else { assert.Equal(head, (*rtp.Packet)(nil)) } } }) t.Run("Pops at timestamp correctly", func(*testing.T) { jb := New() for i := 0; i < 100; i++ { sqnum := uint16((math.MaxUint16 - 32 + i)) //nolint:gosec // G115 //nolint:gosec // G115 jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: sqnum, Timestamp: uint32(512 + i)}, Payload: []byte{0x02}}) } assert.Equal(jb.packets.Length(), uint16(100)) assert.Equal(jb.state, Emitting) head, err := jb.PopAtTimestamp(uint32(513)) assert.Equal(head.SequenceNumber, uint16(math.MaxUint16-32+1)) assert.Equal(err, nil) head, err = jb.PopAtTimestamp(uint32(513)) assert.Equal(head, (*rtp.Packet)(nil)) assert.NotEqual(err, nil) head, err = jb.Pop() assert.Equal(head.SequenceNumber, uint16(math.MaxUint16-32)) assert.Equal(err, nil) }) t.Run("Can peek at a packet", func(*testing.T) { jb := New() jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 501}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 502}, Payload: []byte{0x02}}) pkt, err := jb.Peek(false) assert.Equal(pkt.SequenceNumber, uint16(5002)) assert.Equal(err, nil) for i := 0; i < 100; i++ { sqnum := uint16((math.MaxUint16 - 32 + i)) //nolint:gosec // G115 //nolint:gosec // G115 jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: sqnum, Timestamp: uint32(512 + i)}, Payload: []byte{0x02}}) } pkt, err = jb.Peek(true) assert.Equal(pkt.SequenceNumber, uint16(5000)) assert.Equal(err, nil) }) t.Run("Pops at sequence with an invalid sequence number", func(*testing.T) { jb := New() for i := 0; i < 50; i++ { sqnum := uint16((math.MaxUint16 - 32 + i)) //nolint:gosec // G115 //nolint:gosec // G115 jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: sqnum, Timestamp: uint32(512 + i)}, Payload: []byte{0x02}}) } jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1019, Timestamp: uint32(9000)}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1020, Timestamp: uint32(9000)}, Payload: []byte{0x02}}) assert.Equal(jb.packets.Length(), uint16(52)) assert.Equal(jb.state, Emitting) head, err := jb.PopAtSequence(uint16(9000)) assert.Equal(head, (*rtp.Packet)(nil)) assert.NotEqual(err, nil) }) t.Run("Pops at timestamp with multiple packets", func(*testing.T) { jb := New() for i := 0; i < 50; i++ { sqnum := uint16((math.MaxUint16 - 32 + i)) //nolint:gosec // G115 //nolint:gosec // G115 jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: sqnum, Timestamp: uint32(512 + i)}, Payload: []byte{0x02}}) } jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1019, Timestamp: uint32(9000)}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1020, Timestamp: uint32(9000)}, Payload: []byte{0x02}}) assert.Equal(jb.packets.Length(), uint16(52)) assert.Equal(jb.state, Emitting) head, err := jb.PopAtTimestamp(uint32(9000)) assert.Equal(head.SequenceNumber, uint16(1019)) assert.Equal(err, nil) head, err = jb.PopAtTimestamp(uint32(9000)) assert.Equal(head.SequenceNumber, uint16(1020)) assert.Equal(err, nil) head, err = jb.Pop() assert.Equal(head.SequenceNumber, uint16(math.MaxUint16-32)) assert.Equal(err, nil) }) t.Run("Peeks at timestamp with multiple packets", func(*testing.T) { jb := New() for i := 0; i < 50; i++ { sqnum := uint16((math.MaxUint16 - 32 + i)) //nolint:gosec // G115 //nolint:gosec // G115 jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: sqnum, Timestamp: uint32(512 + i)}, Payload: []byte{0x02}}) } jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1019, Timestamp: uint32(9000)}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1020, Timestamp: uint32(9000)}, Payload: []byte{0x02}}) assert.Equal(jb.packets.Length(), uint16(52)) assert.Equal(jb.state, Emitting) head, err := jb.PeekAtSequence(uint16(1019)) assert.Equal(head.SequenceNumber, uint16(1019)) assert.Equal(err, nil) head, err = jb.PeekAtSequence(uint16(1020)) assert.Equal(head.SequenceNumber, uint16(1020)) assert.Equal(err, nil) head, err = jb.PopAtSequence(uint16(math.MaxUint16 - 32)) assert.Equal(head.SequenceNumber, uint16(math.MaxUint16-32)) assert.Equal(err, nil) }) t.Run("SetPlayoutHead", func(*testing.T) { jb := New(WithMinimumPacketCount(1)) // Push packets 0-9, but no packet 4 for i := uint16(0); i < 10; i++ { if i == 4 { continue } jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: i, Timestamp: uint32(512 + i)}, Payload: []byte{0x00}}) } // The first 3 packets will be able to popped for i := 0; i < 4; i++ { pkt, err := jb.Pop() assert.NoError(err) assert.NotNil(pkt) } // The next pop will fail because of gap pkt, err := jb.Pop() assert.ErrorIs(err, ErrNotFound) assert.Nil(pkt) assert.Equal(jb.PlayoutHead(), uint16(4)) // Assert that PlayoutHead isn't modified with pushing/popping again jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 10, Timestamp: uint32(522)}, Payload: []byte{0x00}}) pkt, err = jb.Pop() assert.ErrorIs(err, ErrNotFound) assert.Nil(pkt) assert.Equal(jb.PlayoutHead(), uint16(4)) // Increment the PlayoutHead and popping will work again jb.SetPlayoutHead(jb.PlayoutHead() + 1) for i := 0; i < 6; i++ { pkt, err := jb.Pop() assert.NoError(err) assert.NotNil(pkt) } }) t.Run("Allows clearing the buffer", func(*testing.T) { jb := New() jb.Clear(false) assert.Equal(jb.lastSequence, uint16(0)) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 501}, Payload: []byte{0x02}}) jb.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 502}, Payload: []byte{0x02}}) assert.Equal(jb.lastSequence, uint16(5002)) jb.Clear(true) assert.Equal(jb.lastSequence, uint16(0)) assert.Equal(jb.stats.outOfOrderCount, uint32(0)) assert.Equal(jb.packets.Length(), uint16(0)) }) } interceptor-0.1.42/pkg/jitterbuffer/option.go000066400000000000000000000007511510612111000212360ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package jitterbuffer import ( "github.com/pion/logging" ) // ReceiverInterceptorOption can be used to configure ReceiverInterceptor. type ReceiverInterceptorOption func(d *ReceiverInterceptor) error // Log sets a logger for the interceptor. func Log(log logging.LeveledLogger) ReceiverInterceptorOption { return func(d *ReceiverInterceptor) error { d.log = log return nil } } interceptor-0.1.42/pkg/jitterbuffer/priority_queue.go000066400000000000000000000077461510612111000230260ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package jitterbuffer import ( "errors" "github.com/pion/rtp" ) // PriorityQueue provides a linked list sorting of RTP packets by SequenceNumber. type PriorityQueue struct { next *node length uint16 } type node struct { val *rtp.Packet next *node prev *node priority uint16 } var ( // ErrInvalidOperation may be returned if a Pop or Find operation is performed on an empty queue. ErrInvalidOperation = errors.New("attempt to find or pop on an empty list") // ErrNotFound will be returned if the packet cannot be found in the queue. ErrNotFound = errors.New("priority not found") ) // NewQueue will create a new PriorityQueue whose order relies on monotonically // increasing Sequence Number, wrapping at MaxUint16, so // a packet with sequence number MaxUint16 - 1 will be after 0. func NewQueue() *PriorityQueue { return &PriorityQueue{ next: nil, length: 0, } } func newNode(val *rtp.Packet, priority uint16) *node { return &node{ val: val, prev: nil, next: nil, priority: priority, } } // Find a packet in the queue with the provided sequence number, // regardless of position (the packet is retained in the queue). func (q *PriorityQueue) Find(sqNum uint16) (*rtp.Packet, error) { next := q.next for next != nil { if next.priority == sqNum { return next.val, nil } next = next.next } return nil, ErrNotFound } // Push will insert a packet in to the queue in order of sequence number. func (q *PriorityQueue) Push(val *rtp.Packet, priority uint16) { newPq := newNode(val, priority) if q.next == nil { q.next = newPq q.length++ return } if priority < q.next.priority { newPq.next = q.next q.next.prev = newPq q.next = newPq q.length++ return } head := q.next prev := q.next for head != nil { if priority <= head.priority { break } prev = head head = head.next } if head == nil { if prev != nil { prev.next = newPq } newPq.prev = prev } else { newPq.next = head newPq.prev = prev if prev != nil { prev.next = newPq } head.prev = newPq } q.length++ } // Length will get the total length of the queue. func (q *PriorityQueue) Length() uint16 { return q.length } // Pop removes the first element from the queue, regardless // sequence number. func (q *PriorityQueue) Pop() (*rtp.Packet, error) { if q.next == nil { return nil, ErrInvalidOperation } val := q.next.val q.next.val = nil q.length-- q.next = q.next.next return val, nil } // PopAt removes an element at the specified sequence number (priority). func (q *PriorityQueue) PopAt(sqNum uint16) (*rtp.Packet, error) { if q.next == nil { return nil, ErrInvalidOperation } if q.next.priority == sqNum { val := q.next.val q.next.val = nil q.next = q.next.next q.length-- return val, nil } pos := q.next prev := q.next.prev for pos != nil { if pos.priority == sqNum { val := pos.val pos.val = nil prev.next = pos.next if prev.next != nil { prev.next.prev = prev } q.length-- return val, nil } prev = pos pos = pos.next } return nil, ErrNotFound } // PopAtTimestamp removes and returns a packet at the given RTP Timestamp, regardless // sequence number order. func (q *PriorityQueue) PopAtTimestamp(timestamp uint32) (*rtp.Packet, error) { if q.next == nil { return nil, ErrInvalidOperation } if q.next.val.Timestamp == timestamp { val := q.next.val q.next.val = nil q.next = q.next.next q.length-- return val, nil } pos := q.next prev := q.next.prev for pos != nil { if pos.val.Timestamp == timestamp { val := pos.val pos.val = nil prev.next = pos.next if prev.next != nil { prev.next.prev = prev } q.length-- return val, nil } prev = pos pos = pos.next } return nil, ErrNotFound } // Clear will empty a PriorityQueue. func (q *PriorityQueue) Clear() { next := q.next q.length = 0 for next != nil { next.prev = nil next = next.next } } interceptor-0.1.42/pkg/jitterbuffer/priority_queue_test.go000066400000000000000000000134431510612111000240540ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package jitterbuffer import ( "runtime" "sync/atomic" "testing" "time" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) func TestPriorityQueue(t *testing.T) { assert := assert.New(t) t.Run("Appends packets in order", func(*testing.T) { pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}} q := NewQueue() q.Push(pkt, pkt.SequenceNumber) pkt2 := &rtp.Packet{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 500}, Payload: []byte{0x02}} q.Push(pkt2, pkt2.SequenceNumber) assert.Equal(q.next.next.val, pkt2) assert.Equal(q.next.priority, uint16(5000)) assert.Equal(q.next.next.priority, uint16(5004)) }) t.Run("Appends many in order", func(*testing.T) { queue := NewQueue() for i := 0; i < 100; i++ { //nolint:gosec // G115 queue.Push( &rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(5012 + i), Timestamp: uint32(512 + i), }, Payload: []byte{0x02}, }, uint16(5012+i), ) } assert.Equal(uint16(100), queue.Length()) last := (*node)(nil) cur := queue.next for cur != nil { last = cur cur = cur.next if cur != nil { assert.Equal(cur.priority, last.priority+1) } } assert.Equal(queue.next.priority, uint16(5012)) assert.Equal(last.priority, uint16(5012+99)) }) t.Run("Can remove an element", func(*testing.T) { pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}} queue := NewQueue() queue.Push(pkt, pkt.SequenceNumber) pkt2 := &rtp.Packet{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 500}, Payload: []byte{0x02}} queue.Push(pkt2, pkt2.SequenceNumber) for i := 0; i < 100; i++ { //nolint:gosec // G115 queue.Push( &rtp.Packet{ Header: rtp.Header{SequenceNumber: uint16(5012 + i), Timestamp: uint32(512 + i)}, Payload: []byte{0x02}, }, uint16(5012+i), ) } popped, _ := queue.Pop() assert.Equal(popped.SequenceNumber, uint16(5000)) _, _ = queue.Pop() nextPop, _ := queue.Pop() assert.Equal(nextPop.SequenceNumber, uint16(5012)) }) t.Run("Appends in order", func(*testing.T) { queue := NewQueue() for i := 0; i < 100; i++ { queue.Push( &rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(5012 + i), //nolint:gosec // G115 Timestamp: uint32(512 + i), //nolint:gosec // G115 }, Payload: []byte{0x02}, }, uint16(5012+i), //nolint:gosec // G115 ) } assert.Equal(uint16(100), queue.Length()) pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}} queue.Push(pkt, pkt.SequenceNumber) assert.Equal(pkt, queue.next.val) assert.Equal(uint16(101), queue.Length()) assert.Equal(queue.next.priority, uint16(5000)) }) t.Run("Can find", func(*testing.T) { queue := NewQueue() for i := 0; i < 100; i++ { //nolint:gosec // G115 queue.Push( &rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(5012 + i), Timestamp: uint32(512 + i), }, Payload: []byte{0x02}, }, uint16(5012+i), ) } pkt, err := queue.Find(5012) assert.Equal(pkt.SequenceNumber, uint16(5012)) assert.Equal(err, nil) }) t.Run("Updates the length when PopAt* are called", func(*testing.T) { pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}} queue := NewQueue() queue.Push(pkt, pkt.SequenceNumber) pkt2 := &rtp.Packet{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 500}, Payload: []byte{0x02}} queue.Push(pkt2, pkt2.SequenceNumber) for i := 0; i < 100; i++ { //nolint:gosec // G115 queue.Push( &rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(5012 + i), Timestamp: uint32(512 + i), }, Payload: []byte{0x02}, }, uint16(5012+i), ) } assert.Equal(uint16(102), queue.Length()) popped, _ := queue.PopAt(uint16(5012)) assert.Equal(popped.SequenceNumber, uint16(5012)) assert.Equal(uint16(101), queue.Length()) popped, err := queue.PopAtTimestamp(uint32(500)) assert.Equal(popped.SequenceNumber, uint16(5000)) assert.Equal(uint16(100), queue.Length()) assert.Equal(err, nil) }) } func TestPriorityQueue_Find(t *testing.T) { packets := NewQueue() packets.Push(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: 1000, Timestamp: 5, SSRC: 5, }, Payload: []uint8{0xA}, }, 1000) _, err := packets.PopAt(1000) assert.NoError(t, err) _, err = packets.Find(1001) assert.Error(t, err) } func TestPriorityQueue_Clean(t *testing.T) { packets := NewQueue() packets.Clear() packets.Push(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: 1000, Timestamp: 5, SSRC: 5, }, Payload: []uint8{0xA}, }, 1000) assert.EqualValues(t, 1, packets.Length()) packets.Clear() } func TestPriorityQueue_Unreference(t *testing.T) { packets := NewQueue() var refs int64 finalizer := func(*rtp.Packet) { atomic.AddInt64(&refs, -1) } numPkts := 100 for i := 0; i < numPkts; i++ { atomic.AddInt64(&refs, 1) seq := uint16(i) //nolint:gosec // G115 p := rtp.Packet{ Header: rtp.Header{ SequenceNumber: seq, Timestamp: uint32(i + 42), //nolint:gosec // G115 }, Payload: []byte{byte(i)}, } runtime.SetFinalizer(&p, finalizer) packets.Push(&p, seq) } for i := 0; i < numPkts-1; i++ { switch i % 3 { case 0: packets.Pop() //nolint case 1: packets.PopAt(uint16(i)) //nolint case 2: packets.PopAtTimestamp(uint32(i + 42)) //nolint } } runtime.GC() time.Sleep(10 * time.Millisecond) remainedRefs := atomic.LoadInt64(&refs) runtime.KeepAlive(packets) // only the last packet should be still referenced assert.Equal(t, int64(1), remainedRefs) } interceptor-0.1.42/pkg/jitterbuffer/receiver_interceptor.go000066400000000000000000000064141510612111000241520ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package jitterbuffer import ( "sync" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtp" ) // InterceptorFactory is a interceptor.Factory for a GeneratorInterceptor. type InterceptorFactory struct { opts []ReceiverInterceptorOption } // NewInterceptor constructs a new ReceiverInterceptor. func (g *InterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) { i := &ReceiverInterceptor{ close: make(chan struct{}), log: logging.NewDefaultLoggerFactory().NewLogger("jitterbuffer"), buffer: New(), } for _, opt := range g.opts { if err := opt(i); err != nil { return nil, err } } return i, nil } // ReceiverInterceptor places a JitterBuffer in the chain to smooth packet arrival // and allow for network jitter // // The Interceptor is designed to fit in a RemoteStream // pipeline and buffer incoming packets for a short period (currently // defaulting to 50 packets) before emitting packets to be consumed by the // next step in the pipeline. // // The caller must ensure they are prepared to handle an // ErrPopWhileBuffering in the case that insufficient packets have been // received by the jitter buffer. The caller should retry the operation // at some point later as the buffer may have been filled in the interim. // // The caller should also be aware that an ErrBufferUnderrun may be // returned in the case that the initial buffering was sufficient and // playback began but the caller is consuming packets (or they are not // arriving) quickly enough. type ReceiverInterceptor struct { interceptor.NoOp buffer *JitterBuffer m sync.Mutex wg sync.WaitGroup close chan struct{} log logging.LeveledLogger } // NewInterceptor returns a new InterceptorFactory. func NewInterceptor(opts ...ReceiverInterceptorOption) (*InterceptorFactory, error) { return &InterceptorFactory{opts}, nil } // 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 *ReceiverInterceptor) BindRemoteStream( _ *interceptor.StreamInfo, reader interceptor.RTPReader, ) interceptor.RTPReader { return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { buf := make([]byte, len(b)) n, attr, err := reader.Read(buf, a) if err != nil { return n, attr, err } packet := &rtp.Packet{} if err := packet.Unmarshal(buf); err != nil { return 0, nil, err } i.m.Lock() defer i.m.Unlock() i.buffer.Push(packet) if i.buffer.state == Emitting { newPkt, err := i.buffer.Pop() if err != nil { return 0, nil, err } nlen, err := newPkt.MarshalTo(b) return nlen, attr, err } return n, attr, ErrPopWhileBuffering }) } // UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (i *ReceiverInterceptor) UnbindRemoteStream(_ *interceptor.StreamInfo) { defer i.wg.Wait() i.m.Lock() defer i.m.Unlock() i.buffer.Clear(true) } // Close closes the interceptor. func (i *ReceiverInterceptor) Close() error { defer i.wg.Wait() i.m.Lock() defer i.m.Unlock() i.buffer.Clear(true) return nil } interceptor-0.1.42/pkg/jitterbuffer/receiver_interceptor_test.go000066400000000000000000000044731510612111000252140ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package jitterbuffer 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 TestBufferStart(t *testing.T) { buf := bytes.Buffer{} factory, err := NewInterceptor( Log(logging.NewDefaultLoggerFactory().NewLogger("test")), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.Zero(t, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) 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) select { case pkt := <-stream.ReadRTP(): assert.EqualValues(t, nil, pkt) default: // No data ready to read, this is what we expect } err = testInterceptor.Close() assert.NoError(t, err) assert.Zero(t, buf.Len()) } func TestReceiverBuffersAndPlaysout(t *testing.T) { buf := bytes.Buffer{} factory, err := NewInterceptor( Log(logging.NewDefaultLoggerFactory().NewLogger("test")), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 456, }}) for s := 0; s < 61; s++ { stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(s), //nolint:gosec // G115 }}) } // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) for s := 0; s < 10; s++ { read := <-stream.ReadRTP() seq := read.Packet.Header.SequenceNumber assert.EqualValues(t, uint16(s), seq) //nolint:gosec // G115 } assert.NoError(t, stream.Close()) err = testInterceptor.Close() assert.NoError(t, err) } interceptor-0.1.42/pkg/mock/000077500000000000000000000000001510612111000156325ustar00rootroot00000000000000interceptor-0.1.42/pkg/mock/factory.go000066400000000000000000000006721510612111000176350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/mock/interceptor.go000066400000000000000000000070461510612111000205260ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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.42/pkg/mock/interceptor_test.go000066400000000000000000000103121510612111000215530ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package mock import ( "sync/atomic" "testing" "github.com/pion/interceptor" "github.com/stretchr/testify/assert" ) //nolint:cyclop func TestInterceptor(t *testing.T) { dummyRTPWriter := &RTPWriter{} dummyRTPReader := &RTPReader{} dummyRTCPWriter := &RTCPWriter{} dummyRTCPReader := &RTCPReader{} dummyStreamInfo := &interceptor.StreamInfo{} t.Run("Default", func(t *testing.T) { testInterceptor := &Interceptor{} assert.Equal( t, dummyRTCPWriter, testInterceptor.BindRTCPWriter(dummyRTCPWriter), "Default BindRTCPWriter should return given writer", ) assert.Equal( t, dummyRTCPReader, testInterceptor.BindRTCPReader(dummyRTCPReader), "Default BindRTCPReader should return given reader", ) assert.Equal( t, dummyRTPWriter, testInterceptor.BindLocalStream(dummyStreamInfo, dummyRTPWriter), "Default BindLocalStream should return given writer", ) testInterceptor.UnbindLocalStream(dummyStreamInfo) assert.Equal( t, dummyRTPReader, testInterceptor.BindRemoteStream(dummyStreamInfo, dummyRTPReader), "Default BindRemoteStream should return given writer", ) testInterceptor.UnbindRemoteStream(dummyStreamInfo) assert.NoError(t, testInterceptor.Close(), "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 ) testInterceptor := &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(_ *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { atomic.AddUint32(&cntBindLocalStream, 1) return writer }, UnbindLocalStreamFn: func(*interceptor.StreamInfo) { atomic.AddUint32(&cntUnbindLocalStream, 1) }, BindRemoteStreamFn: func(_ *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { atomic.AddUint32(&cntBindRemoteStream, 1) return reader }, UnbindRemoteStreamFn: func(*interceptor.StreamInfo) { atomic.AddUint32(&cntUnbindRemoteStream, 1) }, CloseFn: func() error { atomic.AddUint32(&cntClose, 1) return nil }, } assert.Equal( t, dummyRTCPWriter, testInterceptor.BindRTCPWriter(dummyRTCPWriter), "Mocked BindRTCPWriter should return given writer", ) assert.Equal( t, dummyRTCPReader, testInterceptor.BindRTCPReader(dummyRTCPReader), "Mocked BindRTCPReader should return given reader", ) assert.Equal( t, dummyRTPWriter, testInterceptor.BindLocalStream(dummyStreamInfo, dummyRTPWriter), "Mocked BindLocalStream should return given writer", ) testInterceptor.UnbindLocalStream(dummyStreamInfo) assert.Equal( t, dummyRTPReader, testInterceptor.BindRemoteStream(dummyStreamInfo, dummyRTPReader), "Mocked BindRemoteStream should return given writer", ) testInterceptor.UnbindRemoteStream(dummyStreamInfo) assert.NoError(t, testInterceptor.Close(), "Mocked Close should return nil") assert.Equal(t, uint32(1), atomic.LoadUint32(&cntBindRTCPWriter), "BindRTCPWriterFn is expected to be called once") assert.Equal(t, uint32(1), atomic.LoadUint32(&cntBindRTCPReader), "BindRTCPReaderFn is expected to be called once") assert.Equal(t, uint32(1), atomic.LoadUint32(&cntBindLocalStream), "BindLocalStreamFn is expected to be called once") assert.Equal( t, uint32(1), atomic.LoadUint32(&cntUnbindLocalStream), "UnbindLocalStreamFn is expected to be called once", ) assert.Equal( t, uint32(1), atomic.LoadUint32(&cntBindRemoteStream), "BindRemoteStreamFn is expected to be called once", ) assert.Equal( t, uint32(1), atomic.LoadUint32(&cntUnbindRemoteStream), "UnbindRemoteStreamFn is expected to be called once", ) assert.Equal(t, uint32(1), atomic.LoadUint32(&cntClose), "CloseFn is expected to be called once") }) } interceptor-0.1.42/pkg/nack/000077500000000000000000000000001510612111000156155ustar00rootroot00000000000000interceptor-0.1.42/pkg/nack/errors.go000066400000000000000000000005031510612111000174560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import "github.com/pion/interceptor/internal/rtpbuffer" // ErrInvalidSize is returned by newReceiveLog/newRTPBuffer, when an incorrect buffer size is supplied. var ErrInvalidSize = rtpbuffer.ErrInvalidSize interceptor-0.1.42/pkg/nack/generator_interceptor.go000066400000000000000000000133041510612111000225510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import ( "math/rand" "slices" "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(_ string) (interceptor.Interceptor, error) { generatorInterceptor := &GeneratorInterceptor{ streamsFilter: streamSupportNack, size: 512, skipLastN: 0, maxNacksPerPacket: 0, interval: time.Millisecond * 100, receiveLogs: map[uint32]*receiveLog{}, nackCountLogs: map[uint32]map[uint16]uint16{}, close: make(chan struct{}), log: logging.NewDefaultLoggerFactory().NewLogger("nack_generator"), } for _, opt := range g.opts { if err := opt(generatorInterceptor); err != nil { return nil, err } } if _, err := newReceiveLog(generatorInterceptor.size); err != nil { return nil, err } return generatorInterceptor, nil } // GeneratorInterceptor interceptor generates nack feedback messages. type GeneratorInterceptor struct { interceptor.NoOp streamsFilter func(info *interceptor.StreamInfo) bool size uint16 skipLastN uint16 maxNacksPerPacket uint16 interval time.Duration m sync.Mutex wg sync.WaitGroup close chan struct{} log logging.LeveledLogger nackCountLogs map[uint32]map[uint16]uint16 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 !n.streamsFilter(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 }) } // UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (n *GeneratorInterceptor) UnbindRemoteStream(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 } // nolint:gocognit,cyclop func (n *GeneratorInterceptor) loop(rtcpWriter interceptor.RTCPWriter) { defer n.wg.Done() senderSSRC := rand.Uint32() // #nosec missingPacketSeqNums := make([]uint16, n.size) filteredMissingPacket := make([]uint16, n.size) 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, missingPacketSeqNums) if len(missing) == 0 || n.nackCountLogs[ssrc] == nil { n.nackCountLogs[ssrc] = map[uint16]uint16{} } if len(missing) == 0 { continue } nack := &rtcp.TransportLayerNack{} // nolint:ineffassign,wastedassign c := 0 // nolint:varnamelen, if n.maxNacksPerPacket > 0 { for _, missingSeq := range missing { if n.nackCountLogs[ssrc][missingSeq] < n.maxNacksPerPacket { filteredMissingPacket[c] = missingSeq c++ } n.nackCountLogs[ssrc][missingSeq]++ } if c == 0 { continue } nack = &rtcp.TransportLayerNack{ SenderSSRC: senderSSRC, MediaSSRC: ssrc, Nacks: rtcp.NackPairsFromSequenceNumbers(filteredMissingPacket[:c]), } } else { nack = &rtcp.TransportLayerNack{ SenderSSRC: senderSSRC, MediaSSRC: ssrc, Nacks: rtcp.NackPairsFromSequenceNumbers(missing), } } for nackSeq := range n.nackCountLogs[ssrc] { isMissing := slices.Contains(missing, nackSeq) if !isMissing { delete(n.nackCountLogs[ssrc], nackSeq) } } 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.42/pkg/nack/generator_interceptor_test.go000066400000000000000000000075701510612111000236200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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), GeneratorMaxNacksPerPacket(10), 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(50 * time.Millisecond): assert.FailNow(t, "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) // we want packets: 13, 15 (not packet 17, because skipLastN is setReceived to 2) assert.Equal(t, rtcp.PacketBitmap(0b10), p.Nacks[0].LostPackets) case <-time.After(10 * time.Millisecond): assert.FailNow(t, "written rtcp packet not found") } } func TestGeneratorInterceptor_InvalidSize(t *testing.T) { f, _ := NewGeneratorInterceptor(GeneratorSize(5)) _, err := f.NewInterceptor("") assert.Error(t, err, ErrInvalidSize) } func TestGeneratorInterceptor_StreamFilter(t *testing.T) { const interval = time.Millisecond * 10 f, err := NewGeneratorInterceptor( GeneratorSize(64), GeneratorSkipLastN(2), GeneratorInterval(interval), GeneratorLog(logging.NewDefaultLoggerFactory().NewLogger("test")), GeneratorStreamsFilter(func(info *interceptor.StreamInfo) bool { return info.SSRC != 1 // enable nacks only for ssrc 2 }), ) assert.NoError(t, err) testInterceptor, err := f.NewInterceptor("") assert.NoError(t, err) streamWithoutNacks := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 1, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, testInterceptor) defer func() { assert.NoError(t, streamWithoutNacks.Close()) }() streamWithNacks := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 2, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, testInterceptor) defer func() { assert.NoError(t, streamWithNacks.Close()) }() for _, seqNum := range []uint16{10, 11, 12, 14, 16, 18} { streamWithNacks.ReceiveRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum}}) streamWithoutNacks.ReceiveRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum}}) } time.Sleep(interval * 2) // wait for at least 2 nack packets // both test streams receive RTCP packets about both test streams (as they both call BindRTCPWriter), so we // can check only one rtcpStream := streamWithNacks.WrittenRTCP() for { select { case pkts := <-rtcpStream: for _, pkt := range pkts { if nack, isNack := pkt.(*rtcp.TransportLayerNack); isNack { assert.NotEqual(t, uint32(1), nack.MediaSSRC) // check there are no nacks for ssrc 1 } } default: return } } } interceptor-0.1.42/pkg/nack/generator_option.go000066400000000000000000000036101510612111000215220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import ( "time" "github.com/pion/interceptor" "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 } } // GeneratorMaxNacksPerPacket sets the maximum number of NACKs sent per missing packet, e.g. if set to 2, a missing // packet will only be NACKed at most twice. If set to 0 (default), max number of NACKs is unlimited. func GeneratorMaxNacksPerPacket(maxNacks uint16) GeneratorOption { return func(r *GeneratorInterceptor) error { r.maxNacksPerPacket = maxNacks 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 } } // GeneratorStreamsFilter sets filter for generator streams. func GeneratorStreamsFilter(filter func(info *interceptor.StreamInfo) bool) GeneratorOption { return func(r *GeneratorInterceptor) error { r.streamsFilter = filter return nil } } interceptor-0.1.42/pkg/nack/nack.go000066400000000000000000000006671510612111000170710ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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.42/pkg/nack/receive_log.go000066400000000000000000000057651510612111000204440ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import ( "fmt" "sync" "github.com/pion/interceptor/internal/rtpbuffer" ) 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 >= rtpbuffer.Uint16SizeHalf { return false } if diff >= s.size { return false } return s.getReceived(seq) } func (s *receiveLog) missingSeqNumbers(skipLastN uint16, missingPacketSeqNums []uint16) []uint16 { s.m.RLock() defer s.m.RUnlock() until := s.end - skipLastN if until-s.lastConsecutive >= rtpbuffer.Uint16SizeHalf { // until < s.lastConsecutive (counting for rollover) return nil } c := 0 for i := s.lastConsecutive + 1; i != until+1; i++ { if !s.getReceived(i) { missingPacketSeqNums[c] = i c++ } } return missingPacketSeqNums[:c] } 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++ { //nolint:revive // find all consecutive packets } s.lastConsecutive = i - 1 } interceptor-0.1.42/pkg/nack/receive_log_test.go000066400000000000000000000101461510612111000214700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) //nolint:cyclop 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) assert.NoError(t, err) all := func(minVal uint16, maxVal uint16) []uint16 { result := make([]uint16, 0) for i := minVal; i != maxVal+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 assert.True(t, rl.get(seq), "packet not found: %d", seq) } } assertNOTGet := func(nums ...uint16) { t.Helper() for _, n := range nums { seq := start + n assert.False(t, rl.get(seq), "packet found for %d", seq) } } assertMissing := func(skipLastN uint16, nums []uint16) { t.Helper() missingPacketSeqNums := make([]uint16, rl.size) missing := rl.missingSeqNumbers(skipLastN, missingPacketSeqNums) if missing == nil { missing = []uint16{} } want := make([]uint16, 0, len(nums)) for _, n := range nums { want = append(want, start+n) } assert.Equal(t, want, missing, "missing packets don't match") } assertLastConsecutive := func(lastConsecutive uint16) { want := lastConsecutive + start assert.Equal(t, want, rl.lastConsecutive, "lastConsecutive doesn't match") } 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 add(all(330, 332)...) assertMissing(0, []uint16{}) assertLastConsecutive(334) // Add a packet beyond the current missing range to trigger buffer overflow behavior. // Ensure that when the number of missing packets exceeds the buffer size, // only the latest (rl.size - 1) entries are considered for NACKs. add(466) assertGet(466) missing := all(335, 465) if len(missing) > int(rl.size) { assertLastConsecutive(missing[len(missing)-int(rl.size)]) assertMissing(0, missing[len(missing)-(int(rl.size-1)):]) } else { assertMissing(0, all(335, 465)) } }) } } interceptor-0.1.42/pkg/nack/responder_interceptor.go000066400000000000000000000112721510612111000225660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import ( "sync" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/rtpbuffer" "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 } // NewInterceptor constructs a new ResponderInterceptor. func (r *ResponderInterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) { responderInterceptor := &ResponderInterceptor{ streamsFilter: streamSupportNack, size: 1024, log: logging.NewDefaultLoggerFactory().NewLogger("nack_responder"), streams: map[uint32]*localStream{}, } for _, opt := range r.opts { if err := opt(responderInterceptor); err != nil { return nil, err } } if responderInterceptor.packetFactory == nil { responderInterceptor.packetFactory = rtpbuffer.NewPacketFactoryCopy() } if _, err := rtpbuffer.NewRTPBuffer(responderInterceptor.size); err != nil { return nil, err } return responderInterceptor, nil } // ResponderInterceptor responds to nack feedback messages. type ResponderInterceptor struct { interceptor.NoOp streamsFilter func(info *interceptor.StreamInfo) bool size uint16 log logging.LeveledLogger packetFactory rtpbuffer.PacketFactory streams map[uint32]*localStream streamsMu sync.Mutex } type localStream struct { rtpBuffer *rtpbuffer.RTPBuffer rtpBufferMutex sync.RWMutex 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 !n.streamsFilter(info) { return writer } // error is already checked in NewGeneratorInterceptor rtpBuffer, _ := rtpbuffer.NewRTPBuffer(n.size) stream := &localStream{ rtpBuffer: rtpBuffer, rtpWriter: writer, } n.streamsMu.Lock() n.streams[info.SSRC] = stream n.streamsMu.Unlock() return interceptor.RTPWriterFunc( func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { // If this packet doesn't belong to the main SSRC, do not add it to rtpBuffer if header.SSRC != info.SSRC { return writer.Write(header, payload, attributes) } pkt, err := n.packetFactory.NewPacket(header, payload, info.SSRCRetransmission, info.PayloadTypeRetransmission) if err != nil { return 0, err } stream.rtpBufferMutex.Lock() defer stream.rtpBufferMutex.Unlock() rtpBuffer.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 { stream.rtpBufferMutex.Lock() defer stream.rtpBufferMutex.Unlock() if p := stream.rtpBuffer.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.42/pkg/nack/responder_interceptor_test.go000066400000000000000000000255431510612111000236330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import ( "encoding/binary" "sync" "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/rtpbuffer" "github.com/pion/interceptor/internal/test" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" "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, SSRC: 1}})) select { case p := <-stream.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) case <-time.After(10 * time.Millisecond): assert.FailNow(t, "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): assert.Fail(t, "written rtp packet not found") } } select { case p := <-stream.WrittenRTP(): assert.Fail(t, "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.(*rtpbuffer.PacketFactoryNoOp) 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}, }, }, }) } } } // this test is only useful when being run with the race detector, it won't fail otherwise: // // go test -race ./pkg/nack // . func TestResponderInterceptor_RaceConcurrentStreams(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) var wg sync.WaitGroup for j := 0; j < 5; j++ { stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 1, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, i) wg.Add(1) go func() { for seqNum := uint16(0); seqNum < 500; seqNum++ { require.NoError(t, stream.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum}})) } wg.Done() }() } wg.Wait() } func TestResponderInterceptor_StreamFilter(t *testing.T) { f, err := NewResponderInterceptor( ResponderSize(8), ResponderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), ResponderStreamsFilter(func(info *interceptor.StreamInfo) bool { return info.SSRC != 1 // enable nacks only for ssrc 2 })) require.NoError(t, err) testInterceptor, err := f.NewInterceptor("") require.NoError(t, err) streamWithoutNacks := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 1, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, testInterceptor) defer func() { require.NoError(t, streamWithoutNacks.Close()) }() streamWithNacks := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 2, RTCPFeedback: []interceptor.RTCPFeedback{{Type: "nack"}}, }, testInterceptor) defer func() { require.NoError(t, streamWithNacks.Close()) }() for _, seqNum := range []uint16{10, 11, 12, 14, 15} { require.NoError(t, streamWithoutNacks.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum, SSRC: 1}})) require.NoError(t, streamWithNacks.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum, SSRC: 2}})) select { case p := <-streamWithoutNacks.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) case <-time.After(10 * time.Millisecond): assert.Fail(t, "written rtp packet not found") } select { case p := <-streamWithNacks.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) case <-time.After(10 * time.Millisecond): assert.Fail(t, "written rtp packet not found") } } streamWithoutNacks.ReceiveRTCP([]rtcp.Packet{ &rtcp.TransportLayerNack{ MediaSSRC: 1, SenderSSRC: 2, Nacks: []rtcp.NackPair{ {PacketID: 11, LostPackets: 0b1011}, // sequence numbers: 11, 12, 13, 15 }, }, }) streamWithNacks.ReceiveRTCP([]rtcp.Packet{ &rtcp.TransportLayerNack{ MediaSSRC: 2, SenderSSRC: 2, Nacks: []rtcp.NackPair{ {PacketID: 11, LostPackets: 0b1011}, // sequence numbers: 11, 12, 13, 15 }, }, }) select { case <-streamWithNacks.WrittenRTP(): case <-time.After(10 * time.Millisecond): assert.Fail(t, "nack response expected") } select { case <-streamWithoutNacks.WrittenRTP(): assert.Fail(t, "no nack response expected") case <-time.After(10 * time.Millisecond): } } func TestResponderInterceptor_RFC4588(t *testing.T) { f, err := NewResponderInterceptor() require.NoError(t, err) i, err := f.NewInterceptor("") require.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 1, SSRCRetransmission: 2, PayloadTypeRetransmission: 2, 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, SSRC: 1}})) select { case p := <-stream.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) case <-time.After(10 * time.Millisecond): assert.Fail(t, "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 present for _, seqNum := range []uint16{11, 12, 15} { select { case p := <-stream.WrittenRTP(): require.Equal(t, uint32(2), p.SSRC) require.Equal(t, uint8(2), p.PayloadType) require.Equal(t, binary.BigEndian.Uint16(p.Payload), seqNum) case <-time.After(10 * time.Millisecond): assert.Fail(t, "written rtp packet not found") } } select { case p := <-stream.WrittenRTP(): assert.Fail(t, "no more rtp packets expected, found sequence number: %v", p.SequenceNumber) case <-time.After(10 * time.Millisecond): } } //nolint:cyclop func TestResponderInterceptor_BypassUnknownSSRCs(t *testing.T) { f, err := NewResponderInterceptor( ResponderSize(8), 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) defer func() { require.NoError(t, stream.Close()) }() // Send some packets with both SSRCs to check that only SSRC=1 added to the buffer for _, seqNum := range []uint16{10, 11, 12, 14, 15} { require.NoError(t, stream.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum, SSRC: 1}})) // This packet should be bypassed and not added to the buffer. require.NoError(t, stream.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: seqNum, SSRC: 2}})) select { case p := <-stream.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) require.Equal(t, uint32(1), p.SSRC) case <-time.After(10 * time.Millisecond): assert.Fail(t, "written rtp packet not found") } select { case p := <-stream.WrittenRTP(): require.Equal(t, seqNum, p.SequenceNumber) require.Equal(t, uint32(2), p.SSRC) case <-time.After(10 * time.Millisecond): assert.Fail(t, "written rtp packet not found") } } // This packet should be bypassed and not added to the buffer. require.NoError(t, stream.WriteRTP(&rtp.Packet{Header: rtp.Header{SequenceNumber: 13, SSRC: 2}})) select { case p := <-stream.WrittenRTP(): require.Equal(t, uint16(13), p.SequenceNumber) case <-time.After(10 * time.Millisecond): assert.Fail(t, "written rtp packet not found") } stream.ReceiveRTCP([]rtcp.Packet{ &rtcp.TransportLayerNack{ MediaSSRC: 1, SenderSSRC: 1, Nacks: []rtcp.NackPair{ {PacketID: 11, LostPackets: 0b1011}, // sequence numbers: 11, 12, 13, 15 }, }, }) // seq number 13 was sent with different ssrc, it should not be present for _, seqNum := range []uint16{11, 12, 15} { select { case p := <-stream.WrittenRTP(): require.Equal(t, uint32(1), p.SSRC) require.Equal(t, seqNum, p.SequenceNumber) case <-time.After(10 * time.Millisecond): assert.Fail(t, "written rtp packet not found") } } } interceptor-0.1.42/pkg/nack/responder_option.go000066400000000000000000000025541510612111000215430ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package nack import ( "github.com/pion/interceptor" "github.com/pion/interceptor/internal/rtpbuffer" "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 = &rtpbuffer.PacketFactoryNoOp{} return nil } } // ResponderStreamsFilter sets filter for local streams. func ResponderStreamsFilter(filter func(info *interceptor.StreamInfo) bool) ResponderOption { return func(r *ResponderInterceptor) error { r.streamsFilter = filter return nil } } interceptor-0.1.42/pkg/packetdump/000077500000000000000000000000001510612111000170365ustar00rootroot00000000000000interceptor-0.1.42/pkg/packetdump/default_packet_logger.go000066400000000000000000000063101510612111000236770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2025 The Pion community // SPDX-License-Identifier: MIT package packetdump import ( "fmt" "io" "sync" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" ) type defaultPacketLogger struct { log logging.LeveledLogger wg sync.WaitGroup close chan struct{} rtpChan chan *rtpDump rtcpChan chan *rtcpDump rtpStream io.Writer rtcpStream io.Writer rtpFormatBinary RTPBinaryFormatCallback rtcpFormatBinary RTCPBinaryFormatCallback rtpFormat RTPFormatCallback rtcpFormat RTCPFormatCallback rtpFilter RTPFilterCallback rtcpFilter RTCPFilterCallback rtcpPerPacketFilter RTCPPerPacketFilterCallback } func (d *defaultPacketLogger) run() { d.wg.Add(1) go d.loop() } func (d *defaultPacketLogger) 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 *defaultPacketLogger) LogRTCPPackets(pkts []rtcp.Packet, attributes interceptor.Attributes) { select { case d.rtcpChan <- &rtcpDump{ attributes: attributes, packets: pkts, }: case <-d.close: } } func (d *defaultPacketLogger) writeDumpedRTP(dump *rtpDump) error { if !d.rtpFilter(dump.packet) { return nil } if d.rtpFormatBinary != nil { dumped, err := d.rtpFormatBinary(dump.packet, dump.attributes) if err != nil { return fmt.Errorf("rtp format binary: %w", err) } _, err = d.rtpStream.Write(dumped) if err != nil { return fmt.Errorf("rtp stream write: %w", err) } } if d.rtpFormat != nil { if _, err := fmt.Fprint(d.rtpStream, d.rtpFormat(dump.packet, dump.attributes)); err != nil { return fmt.Errorf("rtp stream Fprint: %w", err) } } return nil } func (d *defaultPacketLogger) writeDumpedRTCP(dump *rtcpDump) error { if !d.rtcpFilter(dump.packets) { return nil } for _, pkt := range dump.packets { if !d.rtcpPerPacketFilter(pkt) { continue } if d.rtcpFormatBinary != nil { dumped, err := d.rtcpFormatBinary(pkt, dump.attributes) if err != nil { return fmt.Errorf("rtcp format binary: %w", err) } _, err = d.rtcpStream.Write(dumped) if err != nil { return fmt.Errorf("rtcp stream write: %w", err) } } } if d.rtcpFormat != nil { if _, err := fmt.Fprint(d.rtcpStream, d.rtcpFormat(dump.packets, dump.attributes)); err != nil { return fmt.Errorf("rtcp stream Fprint: %w", err) } } return nil } // Close closes the PacketDumper. func (d *defaultPacketLogger) Close() error { defer d.wg.Wait() if !d.isClosed() { close(d.close) } return nil } func (d *defaultPacketLogger) isClosed() bool { select { case <-d.close: return true default: return false } } func (d *defaultPacketLogger) loop() { defer d.wg.Done() for { select { case <-d.close: return case dump := <-d.rtpChan: err := d.writeDumpedRTP(dump) if err != nil { d.log.Errorf("could not dump RTP packet: %v", err) } case dump := <-d.rtcpChan: err := d.writeDumpedRTCP(dump) if err != nil { d.log.Errorf("could not dump RTCP packets: %v", err) } } } } interceptor-0.1.42/pkg/packetdump/filter.go000066400000000000000000000015331510612111000206540ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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. // Deprecated: prefer RTCPPerPacketFilterCallback. type RTCPFilterCallback func(pkt []rtcp.Packet) bool // RTCPPerPacketFilterCallback can be used to filter RTCP packets to dump. // It's called once per every packet opposing to RTCPFilterCallback which is called once per packet batch. type RTCPPerPacketFilterCallback func(pkt rtcp.Packet) bool interceptor-0.1.42/pkg/packetdump/format.go000066400000000000000000000032551510612111000206620ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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. // Deprecated: prefer RTPBinaryFormatCallback. 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. // Deprecated: prefer RTCPBinaryFormatCallback. type RTCPFormatCallback func([]rtcp.Packet, interceptor.Attributes) string // DefaultRTPFormatter returns the default log format for RTP packets // Deprecated: useless export since set by default. func DefaultRTPFormatter(pkt *rtp.Packet, _ interceptor.Attributes) string { return fmt.Sprintf("%s\n", pkt) } // DefaultRTCPFormatter returns the default log format for RTCP packets // Deprecated: useless export since set by default. func DefaultRTCPFormatter(pkts []rtcp.Packet, _ interceptor.Attributes) string { return fmt.Sprintf("%s\n", pkts) } // RTPBinaryFormatCallback can be used to apply custom formatting or marshaling to each dumped RTP packet. type RTPBinaryFormatCallback func(*rtp.Packet, interceptor.Attributes) ([]byte, error) // RTCPBinaryFormatCallback can be used to apply custom formatting or marshaling to each dumped RTCP packet. type RTCPBinaryFormatCallback func(rtcp.Packet, interceptor.Attributes) ([]byte, error) interceptor-0.1.42/pkg/packetdump/option.go000066400000000000000000000055661510612111000207110ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 } } // PacketLog sets the packet logger of a packet dumper. Use this to replace the // default logger with yout own logger implementation. func PacketLog(logger PacketLogger) PacketDumperOption { return func(d *PacketDumper) error { d.packetLogger = logger return nil } } // RTPWriter sets the io.Writer on which RTP packets will be dumped by the // default packet logger. 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 by the // default packet logger. func RTCPWriter(w io.Writer) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpStream = w return nil } } // RTPFormatter sets the RTP format used by the default packet logger. // Deprecated: prefer RTPBinaryFormatter. func RTPFormatter(f RTPFormatCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtpFormat = f return nil } } // RTCPFormatter sets the RTCP format used by the default packet logger. // Deprecated: prefer RTCPBinaryFormatter. func RTCPFormatter(f RTCPFormatCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpFormat = f return nil } } // RTPBinaryFormatter sets the RTP binary formatter used by the default packet // logger. func RTPBinaryFormatter(f RTPBinaryFormatCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtpFormatBinary = f return nil } } // RTCPBinaryFormatter sets the RTCP binary formatter used by the default packet // logger. func RTCPBinaryFormatter(f RTCPBinaryFormatCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpFormatBinary = f return nil } } // RTPFilter sets the RTP filter used by the default packet logger. func RTPFilter(callback RTPFilterCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtpFilter = callback return nil } } // RTCPFilter sets the RTCP filter used by the default packet logger. // Deprecated: prefer RTCPPerPacketFilter. func RTCPFilter(callback RTCPFilterCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpFilter = callback return nil } } // RTCPPerPacketFilter sets the RTCP per-packet filter used by the default // packet logger. func RTCPPerPacketFilter(callback RTCPPerPacketFilterCallback) PacketDumperOption { return func(d *PacketDumper) error { d.rtcpPerPacketFilter = callback return nil } } interceptor-0.1.42/pkg/packetdump/packet_dump.go000066400000000000000000000006721510612111000216660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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.42/pkg/packetdump/packet_dumper.go000066400000000000000000000063711510612111000222170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package packetdump import ( "fmt" "io" "os" "sync" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" ) // ErrBothBinaryAndDeprecatedFormat is returned when both binary and deprecated format callbacks are set. var ErrBothBinaryAndDeprecatedFormat = fmt.Errorf("both binary and deprecated format callbacks are set") // PacketDumper dumps packet to a io.Writer. type PacketDumper struct { packetLogger PacketLogger // Default Logger Options log logging.LeveledLogger rtpStream io.Writer rtcpStream io.Writer rtpFormatBinary RTPBinaryFormatCallback rtcpFormatBinary RTCPBinaryFormatCallback rtpFormat RTPFormatCallback rtcpFormat RTCPFormatCallback rtpFilter RTPFilterCallback rtcpFilter RTCPFilterCallback rtcpPerPacketFilter RTCPPerPacketFilterCallback } // NewPacketDumper creates a new PacketDumper. func NewPacketDumper(opts ...PacketDumperOption) (*PacketDumper, error) { dumper := &PacketDumper{ packetLogger: nil, log: logging.NewDefaultLoggerFactory().NewLogger("packet_dumper"), rtpStream: os.Stdout, rtcpStream: os.Stdout, rtpFormatBinary: nil, rtcpFormatBinary: nil, rtpFormat: nil, rtcpFormat: nil, rtpFilter: func(*rtp.Packet) bool { return true }, rtcpFilter: func([]rtcp.Packet) bool { return true }, rtcpPerPacketFilter: func(rtcp.Packet) bool { return true }, } if dumper.rtpFormat != nil && dumper.rtpFormatBinary != nil { return nil, ErrBothBinaryAndDeprecatedFormat } for _, opt := range opts { if err := opt(dumper); err != nil { return nil, err } } // If we get a custom packet logger, we don't need to set any default logger // options. if dumper.packetLogger != nil { return dumper, nil } if dumper.rtpFormat == nil && dumper.rtpFormatBinary == nil { dumper.rtpFormat = DefaultRTPFormatter } if dumper.rtcpFormat == nil && dumper.rtcpFormatBinary == nil { dumper.rtcpFormat = DefaultRTCPFormatter } dpl := &defaultPacketLogger{ log: dumper.log, wg: sync.WaitGroup{}, close: make(chan struct{}), rtpChan: make(chan *rtpDump), rtcpChan: make(chan *rtcpDump), rtpStream: dumper.rtpStream, rtcpStream: dumper.rtcpStream, rtpFormatBinary: dumper.rtpFormatBinary, rtcpFormatBinary: dumper.rtcpFormatBinary, rtpFormat: dumper.rtpFormat, rtcpFormat: dumper.rtcpFormat, rtpFilter: dumper.rtpFilter, rtcpFilter: dumper.rtcpFilter, rtcpPerPacketFilter: dumper.rtcpPerPacketFilter, } dpl.run() dumper.packetLogger = dpl return dumper, nil } func (d *PacketDumper) logRTPPacket(header *rtp.Header, payload []byte, attributes interceptor.Attributes) { d.packetLogger.LogRTPPacket(header, payload, attributes) } func (d *PacketDumper) logRTCPPackets(pkts []rtcp.Packet, attributes interceptor.Attributes) { d.packetLogger.LogRTCPPackets(pkts, attributes) } // Close the packetdumper. func (d *PacketDumper) Close() error { dpl, ok := d.packetLogger.(*defaultPacketLogger) if ok { return dpl.Close() } return nil } interceptor-0.1.42/pkg/packetdump/packet_dumper_test.go000066400000000000000000000027031510612111000232510ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2025 The Pion community // SPDX-License-Identifier: MIT package packetdump import ( "testing" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) type customLogger struct { rtpLog chan rtpDump rtcpLog chan rtcpDump } // LogRTCPPackets implements PacketLogger. func (c *customLogger) LogRTCPPackets(pkts []rtcp.Packet, attributes interceptor.Attributes) { c.rtcpLog <- rtcpDump{ attributes: attributes, packets: pkts, } } // LogRTPPacket implements PacketLogger. func (c *customLogger) LogRTPPacket(header *rtp.Header, payload []byte, attributes interceptor.Attributes) { c.rtpLog <- rtpDump{ attributes: attributes, packet: &rtp.Packet{ Header: *header, Payload: payload, }, } } func TestCustomLogger(t *testing.T) { cl := &customLogger{ rtpLog: make(chan rtpDump, 1), rtcpLog: make(chan rtcpDump, 1), } dumper, err := NewPacketDumper(PacketLog(cl)) assert.NoError(t, err) dumper.logRTPPacket(&rtp.Header{}, []byte{1, 2, 3, 4}, nil) dumper.logRTCPPackets([]rtcp.Packet{ &rtcp.RawPacket{0, 1, 2, 3}, }, nil) rtpL := <-cl.rtpLog assert.Equal(t, rtpDump{ attributes: nil, packet: &rtp.Packet{ Header: rtp.Header{}, Payload: []byte{1, 2, 3, 4}, }, }, rtpL) rtcpL := <-cl.rtcpLog assert.Equal(t, rtcpDump{ attributes: nil, packets: []rtcp.Packet{ &rtcp.RawPacket{0, 1, 2, 3}, }, }, rtcpL) } interceptor-0.1.42/pkg/packetdump/packet_logger.go000066400000000000000000000006731510612111000222010ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2025 The Pion community // SPDX-License-Identifier: MIT package packetdump import ( "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" ) // PacketLogger logs RTP and RTCP Packets. type PacketLogger interface { LogRTPPacket(header *rtp.Header, payload []byte, attributes interceptor.Attributes) LogRTCPPackets(pkts []rtcp.Packet, attributes interceptor.Attributes) } interceptor-0.1.42/pkg/packetdump/receiver_interceptor.go000066400000000000000000000051541510612111000236140ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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(_ 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( _ *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.42/pkg/packetdump/receiver_interceptor_test.go000066400000000000000000000123321510612111000246470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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(*rtp.Packet) bool { return false }), RTCPFilter(func([]rtcp.Packet) bool { return false }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.Zero(t, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) 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 = testInterceptor.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(*rtp.Packet) bool { return true }), RTCPFilter(func([]rtcp.Packet) bool { return true }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) 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 = testInterceptor.Close() assert.NoError(t, err) assert.NotZero(t, buf.Len()) } func TestReceiverCustomBinaryFormatter(t *testing.T) { rtpBuf := bytes.Buffer{} rtcpBuf := bytes.Buffer{} factory, err := NewReceiverInterceptor( RTPWriter(&rtpBuf), RTCPWriter(&rtcpBuf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), // custom binary formatter to dump only seqno mod 256 RTPBinaryFormatter(func(p *rtp.Packet, _ interceptor.Attributes) ([]byte, error) { return []byte{byte(p.SequenceNumber)}, nil }), // custom binary formatter to dump only DestinationSSRCs mod 256 RTCPBinaryFormatter(func(p rtcp.Packet, _ interceptor.Attributes) ([]byte, error) { b := make([]byte, 0) for _, ssrc := range p.DestinationSSRC() { b = append(b, byte(ssrc)) } return b, nil }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, rtpBuf.Len()) assert.EqualValues(t, 0, rtcpBuf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) defer func() { assert.NoError(t, stream.Close()) }() stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 45, }}) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(123), }}) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = testInterceptor.Close() assert.NoError(t, err) // check that there is custom formatter results in buffer assert.Equal(t, []byte{123}, rtpBuf.Bytes()) assert.Equal(t, []byte{45}, rtcpBuf.Bytes()) } func TestReceiverRTCPPerPacketFilter(t *testing.T) { buf := bytes.Buffer{} factory, err := NewReceiverInterceptor( RTCPWriter(&buf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), RTCPPerPacketFilter(func(packet rtcp.Packet) bool { _, isPli := packet.(*rtcp.PictureLossIndication) return isPli }), RTCPBinaryFormatter(func(p rtcp.Packet, _ interceptor.Attributes) ([]byte, error) { assert.IsType(t, &rtcp.PictureLossIndication{}, p) return []byte{123}, nil }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) defer func() { assert.NoError(t, stream.Close()) }() stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 456, }}) stream.ReceiveRTCP([]rtcp.Packet{&rtcp.ReceiverReport{ SSRC: 789, }}) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = testInterceptor.Close() assert.NoError(t, err) // Only single PictureLossIndication should have been written. assert.Equal(t, []byte{123}, buf.Bytes()) } interceptor-0.1.42/pkg/packetdump/sender_interceptor.go000066400000000000000000000040651510612111000232700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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(_ 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( _ *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.42/pkg/packetdump/sender_interceptor_test.go000066400000000000000000000126571510612111000243350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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(*rtp.Packet) bool { return false }), RTCPFilter(func([]rtcp.Packet) bool { return false }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.Zero(t, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) 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 = testInterceptor.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(*rtp.Packet) bool { return true }), RTCPFilter(func([]rtcp.Packet) bool { return true }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) 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 = testInterceptor.Close() assert.NoError(t, err) assert.NotZero(t, buf.Len()) } func TestSenderCustomBinaryFormatter(t *testing.T) { rtpBuf := bytes.Buffer{} rtcpBuf := bytes.Buffer{} factory, err := NewSenderInterceptor( RTPWriter(&rtpBuf), RTCPWriter(&rtcpBuf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), // custom binary formatter to dump only seqno mod 256 RTPBinaryFormatter(func(p *rtp.Packet, _ interceptor.Attributes) ([]byte, error) { return []byte{byte(p.SequenceNumber)}, nil }), // custom binary formatter to dump only DestinationSSRCs mod 256 RTCPBinaryFormatter(func(p rtcp.Packet, _ interceptor.Attributes) ([]byte, error) { b := make([]byte, 0) for _, ssrc := range p.DestinationSSRC() { b = append(b, byte(ssrc)) } return b, nil }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, rtpBuf.Len()) assert.EqualValues(t, 0, rtcpBuf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) defer func() { assert.NoError(t, stream.Close()) }() err = stream.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 45, }}) assert.NoError(t, err) err = stream.WriteRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: uint16(123), }}) assert.NoError(t, err) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = testInterceptor.Close() assert.NoError(t, err) // check that there is custom formatter results in buffer assert.Equal(t, []byte{123}, rtpBuf.Bytes()) assert.Equal(t, []byte{45}, rtcpBuf.Bytes()) } func TestSenderRTCPPerPacketFilter(t *testing.T) { buf := bytes.Buffer{} factory, err := NewSenderInterceptor( RTCPWriter(&buf), Log(logging.NewDefaultLoggerFactory().NewLogger("test")), RTCPPerPacketFilter(func(packet rtcp.Packet) bool { _, isPli := packet.(*rtcp.PictureLossIndication) return isPli }), RTCPBinaryFormatter(func(p rtcp.Packet, _ interceptor.Attributes) ([]byte, error) { assert.IsType(t, &rtcp.PictureLossIndication{}, p) return []byte{123}, nil }), ) assert.NoError(t, err) testInterceptor, err := factory.NewInterceptor("") assert.NoError(t, err) assert.EqualValues(t, 0, buf.Len()) stream := test.NewMockStream(&interceptor.StreamInfo{ SSRC: 123456, ClockRate: 90000, }, testInterceptor) defer func() { assert.NoError(t, stream.Close()) }() err = stream.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ SenderSSRC: 123, MediaSSRC: 456, }}) assert.NoError(t, err) err = stream.WriteRTCP([]rtcp.Packet{&rtcp.ReceiverReport{ SSRC: 789, }}) assert.NoError(t, err) // Give time for packets to be handled and stream written to. time.Sleep(50 * time.Millisecond) err = testInterceptor.Close() assert.NoError(t, err) // Only single PictureLossIndication should have been written. assert.Equal(t, []byte{123}, buf.Bytes()) } interceptor-0.1.42/pkg/report/000077500000000000000000000000001510612111000162145ustar00rootroot00000000000000interceptor-0.1.42/pkg/report/receiver_interceptor.go000066400000000000000000000112101510612111000227600ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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(_ string) (interceptor.Interceptor, error) { receiverInterceptor := &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(receiverInterceptor); err != nil { return nil, err } } return receiverInterceptor, 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(_, value any) 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 }) } // UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (r *ReceiverInterceptor) UnbindRemoteStream(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.42/pkg/report/receiver_interceptor_test.go000066400000000000000000000271261510612111000240340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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" ) //nolint:maintidx 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), //nolint:gosec // G115 }}) } 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), //nolint:gosec // G115 }}) } 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, }}) stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ SequenceNumber: 0xfffe, }}) 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, 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.42/pkg/report/receiver_option.go000066400000000000000000000015601510612111000217410ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/report/receiver_stream.go000066400000000000000000000111701510612111000217220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package report import ( "math/rand" "sync" "time" "github.com/pion/rtcp" "github.com/pion/rtp" ) const ( // packetsPerHistoryEntry represents how many packets are in the bitmask for // each entry in the `packets` slice in the receiver stream. Because we use // a uint64, we can keep track of 64 packets per entry. packetsPerHistoryEntry = 64 ) 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() //nolint:nestif 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 := pktHeader.SequenceNumber - stream.lastSeqnum if diff > 0 && diff < (1<<15) { // wrap around if pktHeader.SequenceNumber < stream.lastSeqnum { 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 * packetsPerHistoryEntry) stream.packets[pos/packetsPerHistoryEntry] |= 1 << (pos % packetsPerHistoryEntry) } func (stream *receiverStream) delReceived(seq uint16) { pos := seq % (stream.size * packetsPerHistoryEntry) stream.packets[pos/packetsPerHistoryEntry] &^= 1 << (pos % packetsPerHistoryEntry) } func (stream *receiverStream) getReceived(seq uint16) bool { pos := seq % (stream.size * packetsPerHistoryEntry) return (stream.packets[pos/packetsPerHistoryEntry] & (1 << (pos % packetsPerHistoryEntry))) != 0 } func (stream *receiverStream) processSenderReport(now time.Time, sr *rtcp.SenderReport) { stream.m.Lock() defer stream.m.Unlock() stream.lastSenderReport = uint32(sr.NTPTime >> 16) //nolint:gosec // G115 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 } receiverReport := &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 receiverReport } interceptor-0.1.42/pkg/report/receiver_stream_test.go000066400000000000000000000020231510612111000227560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package report import ( "testing" "github.com/stretchr/testify/require" ) func TestReceiverStream(t *testing.T) { t.Run("can use entire history size", func(t *testing.T) { stream := newReceiverStream(12345, 90000) maxPackets := stream.size * packetsPerHistoryEntry // We shouldn't wrap around so long as we only try maxPackets worth. for seq := uint16(0); seq < maxPackets; seq++ { require.False(t, stream.getReceived(seq), "packet with SN %v shouldn't be received yet", seq) stream.setReceived(seq) require.True(t, stream.getReceived(seq), "packet with SN %v should now be received", seq) } // Delete should also work. for seq := uint16(0); seq < maxPackets; seq++ { require.True(t, stream.getReceived(seq), "packet with SN %v should still be marked as received", seq) stream.delReceived(seq) require.False(t, stream.getReceived(seq), "packet with SN %v should no longer be received", seq) } }) } interceptor-0.1.42/pkg/report/report.go000066400000000000000000000003171510612111000200570ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package report provides interceptors to implement sending sender and receiver reports. package report interceptor-0.1.42/pkg/report/sender_interceptor.go000066400000000000000000000076211510612111000224470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package report import ( "sync" "time" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/rtp" ) // 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 []SenderOption } // NewInterceptor constructs a new SenderInterceptor. func (s *SenderInterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) { senderInterceptor := &SenderInterceptor{ interval: 1 * time.Second, now: time.Now, newTicker: func(d time.Duration) Ticker { return &timeTicker{time.NewTicker(d)} }, log: logging.NewDefaultLoggerFactory().NewLogger("sender_interceptor"), close: make(chan struct{}), } for _, opt := range s.opts { if err := opt(senderInterceptor); err != nil { return nil, err } } return senderInterceptor, 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 newTicker TickerFactory streams sync.Map log logging.LeveledLogger m sync.Mutex wg sync.WaitGroup close chan struct{} started chan struct{} useLatestPacket bool } 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 := s.newTicker(s.interval) defer ticker.Stop() if s.started != nil { // This lets us synchronize in tests to know whether the loop has begun or not. // It only happens if started was initialized, which should not occur in non-tests. close(s.started) } for { select { case <-ticker.Ch(): now := s.now() s.streams.Range(func(_, value any) 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.useLatestPacket) 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) }) } // UnbindLocalStream is called when the Stream is removed. It can be used to clean up any data related to that track. func (s *SenderInterceptor) UnbindLocalStream(info *interceptor.StreamInfo) { s.streams.Delete(info.SSRC) } interceptor-0.1.42/pkg/report/sender_interceptor_test.go000066400000000000000000000145531510612111000235100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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)}, //nolint:gosec // G115 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) }) t.Run("out of order 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()) }() // Write several packets for i := 0; i < 10; i++ { assert.NoError(t, stream.WriteRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), //nolint:gosec // G115 Timestamp: uint32(i), //nolint:gosec // G115 }, Payload: []byte("\x00\x00"), })) } // Skip a packet, then redeliver it out-of-order assert.NoError(t, stream.WriteRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: 12, Timestamp: 12, }, Payload: []byte("\x00\x00"), })) assert.NoError(t, stream.WriteRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: 11, Timestamp: 11, }, Payload: []byte("\x00\x00"), })) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) sr, ok := pkts[0].(*rtcp.SenderReport) assert.True(t, ok) // The out-of-order packet is included in PacketCount and OctetCount, but the RTP // timestamp of the last in-order packet is used for RTPTime assert.Equal(t, &rtcp.SenderReport{ SSRC: 123456, NTPTime: ntp.ToNTP(mt.Now()), RTPTime: 12, PacketCount: 12, OctetCount: 24, }, sr) }) t.Run("out of order RTP packets with SenderUseLatestPacket", func(t *testing.T) { mt := &test.MockTime{} f, err := NewSenderInterceptor( SenderInterval(time.Millisecond*50), SenderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), SenderNow(mt.Now), SenderUseLatestPacket(), ) 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()) }() // Write several packets for i := 0; i < 10; i++ { assert.NoError(t, stream.WriteRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), //nolint:gosec // G115 Timestamp: uint32(i), //nolint:gosec // G115 }, Payload: []byte("\x00\x00"), })) } // Skip a packet, then redeliver it out-of-order assert.NoError(t, stream.WriteRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: 12, Timestamp: 12, }, Payload: []byte("\x00\x00"), })) assert.NoError(t, stream.WriteRTP(&rtp.Packet{ Header: rtp.Header{ SequenceNumber: 11, Timestamp: 11, }, Payload: []byte("\x00\x00"), })) pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) sr, ok := pkts[0].(*rtcp.SenderReport) assert.True(t, ok) // The out-of-order packet *is* used for RTPTime assert.Equal(t, &rtcp.SenderReport{ SSRC: 123456, NTPTime: ntp.ToNTP(mt.Now()), RTPTime: 11, PacketCount: 12, OctetCount: 24, }, sr) }) t.Run("inject ticker", func(t *testing.T) { mNow := &test.MockTime{} mTick := &test.MockTicker{ C: make(chan time.Time), } advanceTicker := func() { mNow.SetNow(mNow.Now().Add(50 * time.Millisecond)) mTick.Tick(mNow.Now()) } loopStarted := make(chan struct{}) f, err := NewSenderInterceptor( SenderInterval(time.Millisecond*50), SenderLog(logging.NewDefaultLoggerFactory().NewLogger("test")), SenderNow(mNow.Now), SenderTicker(func(time.Duration) Ticker { return mTick }), enableStartTracking(loopStarted), ) 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()) }() <-loopStarted for i := 0; i < 5; i++ { advanceTicker() pkts := <-stream.WrittenRTCP() assert.Equal(t, len(pkts), 1) } }) } interceptor-0.1.42/pkg/report/sender_option.go000066400000000000000000000030721510612111000214150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 } } // SenderTicker sets an alternative for the time.NewTicker function. func SenderTicker(f TickerFactory) SenderOption { return func(r *SenderInterceptor) error { r.newTicker = f return nil } } // SenderUseLatestPacket sets the interceptor to always use the latest packet, even // if it appears to be out-of-order. func SenderUseLatestPacket() SenderOption { return func(r *SenderInterceptor) error { r.useLatestPacket = true return nil } } // enableStartTracking is used by tests to synchronize whether the loop() has begun // and it's safe to start sending ticks to the ticker. func enableStartTracking(startedCh chan struct{}) SenderOption { return func(r *SenderInterceptor) error { r.started = startedCh return nil } } interceptor-0.1.42/pkg/report/sender_stream.go000066400000000000000000000035621510612111000214040ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 useLatestPacket bool // data from rtp packets lastRTPTimeRTP uint32 lastRTPTimeTime time.Time lastRTPSN uint16 packetCount uint32 octetCount uint32 } func newSenderStream(ssrc uint32, clockRate uint32, useLatestPacket bool) *senderStream { return &senderStream{ ssrc: ssrc, clockRate: float64(clockRate), useLatestPacket: useLatestPacket, } } func (stream *senderStream) processRTP(now time.Time, header *rtp.Header, payload []byte) { stream.m.Lock() defer stream.m.Unlock() diff := header.SequenceNumber - stream.lastRTPSN if stream.useLatestPacket || stream.packetCount == 0 || (diff > 0 && diff < (1<<15)) { // Told to consider every packet, or this was the first packet, or it's in-order stream.lastRTPSN = header.SequenceNumber // update only on first packet of a frame to ensure sender report does not get affected by // processing delay of pushing a large frame which could span multiple packets if header.Timestamp != stream.lastRTPTimeRTP { stream.lastRTPTimeRTP = header.Timestamp stream.lastRTPTimeTime = now } } stream.packetCount++ stream.octetCount += uint32(len(payload)) //nolint:gosec // G115 } 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.42/pkg/report/ticker.go000066400000000000000000000005651510612111000200320ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package report import "time" // Ticker is an interface for *time.Ticker for use with the SenderTicker option. 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.42/pkg/rfc8888/000077500000000000000000000000001510612111000160135ustar00rootroot00000000000000interceptor-0.1.42/pkg/rfc8888/interceptor.go000066400000000000000000000110521510612111000206770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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(_ string) (interceptor.Interceptor, error) { senderInterceptor := &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(senderInterceptor) if err != nil { return nil, err } } return senderInterceptor, 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( _ *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 <-t.Ch(): now := s.now() 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.42/pkg/rfc8888/interceptor_test.go000066400000000000000000000151571510612111000217500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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" ) //nolint:maintidx,cyclop 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), //nolint:gosec // G115 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(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), //nolint:gosec // G115 SSRC: 123456, }, }) select { case r := <-stream.ReadRTP(): assert.NoError(t, r.Err) case <-time.After(10 * time.Millisecond): assert.Fail(t, "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(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), //nolint:gosec // G115 SSRC: 123456, }, }) select { case r := <-stream.ReadRTP(): assert.NoError(t, r.Err) case <-time.After(10 * time.Millisecond): assert.Fail(t, "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.42/pkg/rfc8888/option.go000066400000000000000000000014631510612111000176560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/rfc8888/recorder.go000066400000000000000000000030301510612111000201430ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package rfc8888 import ( "time" "github.com/pion/interceptor/internal/ntp" "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: ntp.ToNTP32(now), } maxReportBlocks := (maxSize - 12 - (8 * len(r.streams))) / 2 maxReportBlocksPerStream := maxReportBlocks / len(r.streams) for _, log := range r.streams { block := log.metricsAfter(now, int64(maxReportBlocksPerStream)) report.ReportBlocks = append(report.ReportBlocks, block) } return report } interceptor-0.1.42/pkg/rfc8888/recorder_test.go000066400000000000000000000112211510612111000212030ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package rfc8888 import ( "math/rand" "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 3 streams", func(t *testing.T) { recorder := NewRecorder() now := time.Time{} maxSize := 1200 streams := 3 packets := 1000 // Add 1000 packets on 3 different streams for i := 0; i < streams; i++ { ssrc := rand.Uint32() //nolint:gosec for j := 0; j < packets; j++ { recorder.AddPacket(now, ssrc, uint16(j), 0) //nolint:gosec // G115 } } reports := recorder.BuildReport(time.Time{}, maxSize) blocks := 0 for i := 0; i < streams; i++ { blocks += len(reports.ReportBlocks[i].MetricBlocks) } assert.Less(t, blocks*2, maxSize) }) t.Run("MaxreportsPerStream 10 streams", func(t *testing.T) { recorder := NewRecorder() now := time.Time{} maxSize := 1300 streams := 10 packets := 1000 // Add 1000 packets on 10 different streams for i := 0; i < streams; i++ { ssrc := rand.Uint32() //nolint:gosec for j := 0; j < packets; j++ { recorder.AddPacket(now, ssrc, uint16(j), 0) //nolint:gosec // G115 } } reports := recorder.BuildReport(time.Time{}, maxSize) blocks := 0 for i := 0; i < streams; i++ { blocks += len(reports.ReportBlocks[i].MetricBlocks) } assert.Less(t, blocks*2, maxSize) }) } interceptor-0.1.42/pkg/rfc8888/stream_log.go000066400000000000000000000062341510612111000205030ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package rfc8888 import ( "time" "github.com/pion/interceptor/internal/sequencenumber" "github.com/pion/rtcp" ) const maxReportsPerReportBlock = 16384 type streamLog struct { ssrc uint32 sequence sequencenumber.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: sequencenumber.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), //nolint:gosec // G115 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++ { //nolint:varnamelen // i int64 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), //nolint:gosec // G115 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.42/pkg/rfc8888/stream_log_test.go000066400000000000000000000301111510612111000215310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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) }) } } //nolint:maintidx 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) //nolint:gosec // G115 } 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.42/pkg/rfc8888/ticker.go000066400000000000000000000004451510612111000176260ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/stats/000077500000000000000000000000001510612111000160375ustar00rootroot00000000000000interceptor-0.1.42/pkg/stats/interceptor.go000066400000000000000000000146411510612111000207320ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // 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) { interceptor := &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(interceptor); err != nil { return nil, err } } if r.addPeerConnection != nil { r.addPeerConnection(id, interceptor) } return interceptor, 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.42/pkg/stats/interceptor_test.go000066400000000000000000000125101510612111000217620ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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" ) //nolint:cyclop 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() testInterceptor, err := NewInterceptor( SetRecorderFactory(func(uint32, float64) Recorder { return mockRecorder }), SetNowFunc(func() time.Time { return now }), ) assert.NoError(t, err) statsCh := make(chan Getter) testInterceptor.OnNewPeerConnection(func(_ string, g Getter) { go func() { statsCh <- g }() }) i, err := testInterceptor.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[any]any{}, } assert.Equal(t, expectedIncomingRTP, riRTP) buf, err = rtcp.Marshal(incomingRTCP) assert.NoError(t, err) expectedIncomingRTCP := recordedIncomingRTCP{ ts: now, buf: buf, attr: map[any]any{}, } assert.Equal(t, expectedIncomingRTCP, riRTCP) expectedOutgoingRTP := recordedOutgoingRTP{ ts: now, header: &rtp.Header{}, payload: outgoingRTP.Payload, attr: map[any]any{}, } assert.Equal(t, expectedOutgoingRTP, roRTP) expectedOutgoingRTCP := recordedOutgoingRTCP{ ts: now, pkts: outgoingRTCP, attr: map[any]any{}, } 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.42/pkg/stats/received_stats.go000066400000000000000000000045351510612111000214010ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/stats/sent_stats.go000066400000000000000000000040631510612111000205600ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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.42/pkg/stats/stats_recorder.go000066400000000000000000000313411510612111000214130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stats import ( "slices" "sync" "sync/atomic" "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 inboundLastArrivalRTP uint32 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 latestStats *internalStats ms *sync.Mutex // Locks latestStats running uint32 } func newRecorder(ssrc uint32, clockRate float64) *recorder { return &recorder{ logger: logging.NewDefaultLoggerFactory().NewLogger("stats_recorder"), ssrc: ssrc, clockRate: clockRate, maxLastSenderReports: 5, maxLastReceiverReferenceTimes: 5, latestStats: &internalStats{}, ms: &sync.Mutex{}, } } func (r *recorder) Stop() { atomic.StoreUint32(&r.running, 0) } func (r *recorder) GetStats() Stats { r.ms.Lock() defer r.ms.Unlock() return Stats{ InboundRTPStreamStats: r.latestStats.InboundRTPStreamStats, OutboundRTPStreamStats: r.latestStats.OutboundRTPStreamStats, RemoteInboundRTPStreamStats: r.latestStats.RemoteInboundRTPStreamStats, RemoteOutboundRTPStreamStats: r.latestStats.RemoteOutboundRTPStreamStats, } } func (r *recorder) recordIncomingRTP(latestStats internalStats, incoming *incomingRTP) internalStats { if incoming.header.SSRC != r.ssrc { return latestStats } sequenceNumber := latestStats.inboundSequencerNumber.Unwrap(incoming.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 //nolint:gosec // G115 latestStats.InboundRTPStreamStats.PacketsLost = expectedPackets - int64(latestStats.InboundRTPStreamStats.PacketsReceived) if !latestStats.inboundLastArrivalInitialized { latestStats.inboundLastArrival = incoming.ts latestStats.inboundLastArrivalRTP = incoming.header.Timestamp latestStats.inboundLastArrivalInitialized = true } else { rtpUnitsSinceLastArrival := incoming.ts.Sub(latestStats.inboundLastArrival).Seconds() * r.clockRate arrival := latestStats.inboundLastArrivalRTP + uint32(rtpUnitsSinceLastArrival) transit := int(arrival) - int(incoming.header.Timestamp) d := transit - latestStats.inboundLastTransit if d < 0 { d = -d } dSec := float64(d) / r.clockRate latestStats.inboundLastTransit = transit latestStats.InboundRTPStreamStats.Jitter += (1.0 / 16.0) * (dSec - latestStats.InboundRTPStreamStats.Jitter) latestStats.inboundLastArrival = incoming.ts latestStats.inboundLastArrivalRTP = incoming.header.Timestamp } latestStats.LastPacketReceivedTimestamp = incoming.ts latestStats.HeaderBytesReceived += uint64(incoming.header.MarshalSize()) //nolint:gosec // G115 latestStats.BytesReceived += uint64(incoming.header.MarshalSize() + incoming.payloadLen) //nolint:gosec // G115 return latestStats } //nolint:cyclop func (r *recorder) recordOutgoingRTCP(latestStats internalStats, v *outgoingRTCP) internalStats { for _, pkt := range v.pkts { // The SSRC check is performed for most of the cases but not all. The // reason is that ReceiverReferenceTimeReportBlocks don't have // destination SSRCs but must still be recorded. switch rtcpPkt := pkt.(type) { case *rtcp.FullIntraRequest: if !contains(pkt.DestinationSSRC(), r.ssrc) { r.logger.Debugf("skipping outgoing RTCP pkt: %v", pkt) continue } latestStats.InboundRTPStreamStats.FIRCount++ case *rtcp.PictureLossIndication: if !contains(pkt.DestinationSSRC(), r.ssrc) { r.logger.Debugf("skipping outgoing RTCP pkt: %v", pkt) continue } latestStats.InboundRTPStreamStats.PLICount++ case *rtcp.TransportLayerNack: if !contains(pkt.DestinationSSRC(), r.ssrc) { r.logger.Debugf("skipping outgoing RTCP pkt: %v", pkt) continue } latestStats.InboundRTPStreamStats.NACKCount++ case *rtcp.SenderReport: if !contains(pkt.DestinationSSRC(), r.ssrc) { r.logger.Debugf("skipping outgoing RTCP pkt: %v", pkt) continue } 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 { if v.header.SSRC != r.ssrc { return latestStats } headerSize := v.header.MarshalSize() latestStats.OutboundRTPStreamStats.PacketsSent++ latestStats.OutboundRTPStreamStats.BytesSent += uint64(headerSize + v.payloadLen) //nolint:gosec // G115 latestStats.HeaderBytesSent += uint64(headerSize) //nolint:gosec // G115 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 latestStats.remoteInboundFirstSequenceNumberInitialized { cycles := uint64(report.LastSequenceNumber&0xFFFF0000) >> 16 nr := uint64(report.LastSequenceNumber & 0x0000FFFF) highest := cycles*(0xFFFF+1) + nr //nolint:gosec // G115 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(float64(xrReport.DLRR) / 65536.0 * float64(time.Second)) latestStats.RemoteOutboundRTPStreamStats.RoundTripTime = (ts.Add(-dlrr)).Sub(ntp.ToTime(lastRR)) //nolint:lll latestStats.RemoteOutboundRTPStreamStats.TotalRoundTripTime += latestStats.RemoteOutboundRTPStreamStats.RoundTripTime latestStats.RemoteOutboundRTPStreamStats.RoundTripTimeMeasurements++ } } } } } } return latestStats } func contains(ls []uint32, e uint32) bool { return slices.Contains(ls, e) } func (r *recorder) recordIncomingRTCP(latestStats internalStats, incoming *incomingRTCP) internalStats { for _, pkt := range incoming.pkts { if !contains(pkt.DestinationSSRC(), r.ssrc) { r.logger.Debugf("skipping incoming RTCP pkt: %v", pkt) continue } 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: latestStats = r.recordIncomingRR(latestStats, pkt, incoming.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, incoming.ts) } } return latestStats } func (r *recorder) Start() { atomic.StoreUint32(&r.running, 1) } func (r *recorder) QueueIncomingRTP(ts time.Time, buf []byte, attr interceptor.Attributes) { if atomic.LoadUint32(&r.running) == 0 { return } 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.ms.Lock() *r.latestStats = r.recordIncomingRTP(*r.latestStats, &incomingRTP{ ts: ts, header: hdr, payloadLen: len(buf) - hdr.MarshalSize(), attr: attr, }) r.ms.Unlock() } func (r *recorder) QueueIncomingRTCP(ts time.Time, buf []byte, attr interceptor.Attributes) { if atomic.LoadUint32(&r.running) == 0 { return } 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.ms.Lock() *r.latestStats = r.recordIncomingRTCP(*r.latestStats, &incomingRTCP{ ts: ts, pkts: pkts, attr: attr, }) r.ms.Unlock() } func (r *recorder) QueueOutgoingRTP(ts time.Time, header *rtp.Header, payload []byte, attr interceptor.Attributes) { if atomic.LoadUint32(&r.running) == 0 { return } hdr := header.Clone() r.ms.Lock() *r.latestStats = r.recordOutgoingRTP(*r.latestStats, &outgoingRTP{ ts: ts, header: hdr, payloadLen: len(payload), attr: attr, }) r.ms.Unlock() } func (r *recorder) QueueOutgoingRTCP(ts time.Time, pkts []rtcp.Packet, attr interceptor.Attributes) { if atomic.LoadUint32(&r.running) == 0 { return } r.ms.Lock() *r.latestStats = r.recordOutgoingRTCP(*r.latestStats, &outgoingRTCP{ ts: ts, pkts: pkts, attr: attr, }) r.ms.Unlock() } interceptor-0.1.42/pkg/stats/stats_recorder_test.go000066400000000000000000000501751510612111000224600ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stats import ( "context" "errors" "fmt" "testing" "time" "github.com/pion/interceptor" "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 { t.Helper() buf, err := pkt.Marshal() assert.NoError(t, err) return buf } func mustMarshalRTCPs(t *testing.T, pkt rtcp.Packet) []byte { t.Helper() buf, err := pkt.Marshal() assert.NoError(t, err) return buf } //nolint:maintidx 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 any } 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: 0, }, 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: "issue#193", records: []record{ { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 65535, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 0, }, }, }, { ts: now, content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{ SSRC: 0, Reports: []rtcp.ReceptionReport{ { SSRC: 0, FractionLost: 0, TotalLost: 0, LastSequenceNumber: 1 << 16, Jitter: 45000, }, }, }, cname, }, }, }, }, expectedOutboundRTPStreamStats: OutboundRTPStreamStats{ SentRTPStreamStats: SentRTPStreamStats{ PacketsSent: 2, BytesSent: 24, }, HeaderBytesSent: 24, }, expectedRemoteInboundRTPStreamStats: RemoteInboundRTPStreamStats{ ReceivedRTPStreamStats: ReceivedRTPStreamStats{ PacketsReceived: 2, PacketsLost: 0, Jitter: 0.5, }, FractionLost: 0.0, }, }, { 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, //nolint:gosec // G115 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{ SSRC: 9999, Reports: []rtcp.ReceptionReport{ {SSRC: 0}, }, }, &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, //nolint:gosec // G115 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, }, }, { name: "RecordIncomingNACKAfterRR", records: []record{ { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 1, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 2, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 3, }, }, }, { ts: now.Add(time.Second), content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{}, cname, &rtcp.TransportLayerNack{ SenderSSRC: 9999, MediaSSRC: 0, Nacks: rtcp.NackPairsFromSequenceNumbers([]uint16{2}), }, }, }, }, }, expectedOutboundRTPStreamStats: OutboundRTPStreamStats{ SentRTPStreamStats: SentRTPStreamStats{ PacketsSent: 3, BytesSent: 36, }, HeaderBytesSent: 36, NACKCount: 1, }, }, { name: "IgnoreUnknownOutgoingSSRCs", records: []record{ { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 1, SSRC: 0, }, }, }, { ts: now.Add(33 * time.Millisecond), content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 2, SSRC: 0, }, }, }, { ts: now.Add(66 * time.Millisecond), content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 3, SSRC: 0, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 1, SSRC: 1, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 2, SSRC: 1, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 3, SSRC: 1, }, }, }, { ts: now.Add(time.Second), content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{ SSRC: 9999, Reports: []rtcp.ReceptionReport{}, }, cname, &rtcp.TransportLayerNack{ SenderSSRC: 9999, MediaSSRC: 0, Nacks: rtcp.NackPairsFromSequenceNumbers([]uint16{2}), }, &rtcp.TransportLayerNack{ SenderSSRC: 9999, MediaSSRC: 1, Nacks: rtcp.NackPairsFromSequenceNumbers([]uint16{2}), }, }, }, }, }, expectedOutboundRTPStreamStats: OutboundRTPStreamStats{ SentRTPStreamStats: SentRTPStreamStats{ PacketsSent: 3, BytesSent: 36, }, HeaderBytesSent: 36, NACKCount: 1, }, }, { name: "IgnoreIncomingNACKForUnknownSSRC", records: []record{ { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 1, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 2, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 3, }, }, }, { ts: now.Add(time.Second), content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{}, cname, &rtcp.TransportLayerNack{ SenderSSRC: 9999, MediaSSRC: 1, Nacks: rtcp.NackPairsFromSequenceNumbers([]uint16{2}), }, }, }, }, }, expectedOutboundRTPStreamStats: OutboundRTPStreamStats{ SentRTPStreamStats: SentRTPStreamStats{ PacketsSent: 3, BytesSent: 36, }, HeaderBytesSent: 36, NACKCount: 0, }, }, { name: "IgnoreIncomingFIRForUnknownSSRC", records: []record{ { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 1, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 2, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 3, }, }, }, { ts: now.Add(time.Second), content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{}, cname, &rtcp.FullIntraRequest{ SenderSSRC: 9999, MediaSSRC: 1, }, }, }, }, }, expectedOutboundRTPStreamStats: OutboundRTPStreamStats{ SentRTPStreamStats: SentRTPStreamStats{ PacketsSent: 3, BytesSent: 36, }, HeaderBytesSent: 36, FIRCount: 0, }, }, { name: "IgnoreIncomingPLIForUnknownSSRC", records: []record{ { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 1, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 2, }, }, }, { ts: now, content: outgoingRTP{ header: rtp.Header{ SequenceNumber: 3, }, }, }, { ts: now.Add(time.Second), content: incomingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{}, cname, &rtcp.PictureLossIndication{ SenderSSRC: 9999, MediaSSRC: 1, }, }, }, }, }, expectedOutboundRTPStreamStats: OutboundRTPStreamStats{ SentRTPStreamStats: SentRTPStreamStats{ PacketsSent: 3, BytesSent: 36, }, HeaderBytesSent: 36, PLICount: 0, }, }, { name: "IgnoreUnknownIncomingSSRCs", records: []record{ { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 1, SSRC: 0, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 2, SSRC: 0, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 3, SSRC: 0, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 1, SSRC: 1, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 2, SSRC: 1, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 3, SSRC: 1, }, }, }, }, expectedInboundRTPStreamStats: InboundRTPStreamStats{ ReceivedRTPStreamStats: ReceivedRTPStreamStats{ PacketsReceived: 3, }, LastPacketReceivedTimestamp: now, HeaderBytesReceived: 36, BytesReceived: 36, }, }, { name: "IgnoreOutgoingNACKForUnknownSSRC", records: []record{ { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 1, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 2, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 3, }, }, }, { ts: now.Add(time.Second), content: outgoingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{}, cname, &rtcp.PictureLossIndication{ SenderSSRC: 9999, MediaSSRC: 1, }, }, }, }, }, expectedInboundRTPStreamStats: InboundRTPStreamStats{ ReceivedRTPStreamStats: ReceivedRTPStreamStats{ PacketsReceived: 3, }, LastPacketReceivedTimestamp: now, HeaderBytesReceived: 36, BytesReceived: 36, PLICount: 0, }, }, { name: "IgnoreOutgoingFIRForUnknownSSRC", records: []record{ { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 1, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 2, }, }, }, { ts: now, content: incomingRTP{ header: rtp.Header{ SequenceNumber: 3, }, }, }, { ts: now.Add(time.Second), content: outgoingRTCP{ pkts: []rtcp.Packet{ &rtcp.ReceiverReport{}, cname, &rtcp.FullIntraRequest{ SenderSSRC: 9999, MediaSSRC: 1, }, }, }, }, }, expectedInboundRTPStreamStats: InboundRTPStreamStats{ ReceivedRTPStreamStats: ReceivedRTPStreamStats{ PacketsReceived: 3, }, LastPacketReceivedTimestamp: now, HeaderBytesReceived: 36, BytesReceived: 36, FIRCount: 0, }, }, { name: "incomingRTPWithJitter", 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: 8, Timestamp: 90000, }, }, }, { ts: now.Add(2*time.Second + 100*time.Millisecond), content: incomingRTP{ header: rtp.Header{ SequenceNumber: 9, Timestamp: 2 * 90000, }, }, }, }, expectedInboundRTPStreamStats: InboundRTPStreamStats{ ReceivedRTPStreamStats: ReceivedRTPStreamStats{ PacketsReceived: 3, PacketsLost: 0, Jitter: 0.00625, }, LastPacketReceivedTimestamp: now.Add(2*time.Second + 100*time.Millisecond), HeaderBytesReceived: 36, BytesReceived: 36, }, }, } { t.Run(fmt.Sprintf("%v:%v", i, cc.name), func(t *testing.T) { recorder := newRecorder(0, 90_000) recorder.Start() for _, record := range cc.records { switch v := record.content.(type) { case incomingRTP: recorder.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) recorder.QueueIncomingRTCP(record.ts, mustMarshalRTCPs(t, &pkts), v.attr) case outgoingRTP: recorder.QueueOutgoingRTP(record.ts, &v.header, []byte{}, v.attr) case outgoingRTCP: recorder.QueueOutgoingRTCP(record.ts, v.pkts, v.attr) default: assert.FailNow(t, "invalid test case") } } s := recorder.GetStats() recorder.Stop() 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) }) } } func TestStatsRecorder_DLRR_Precision(t *testing.T) { recorder := newRecorder(0, 90_000) report := &rtcp.ExtendedReport{ Reports: []rtcp.ReportBlock{ &rtcp.DLRRReportBlock{ Reports: []rtcp.DLRRReport{ { SSRC: 5000, LastRR: 762, DLRR: 30000, }, }, }, }, } s := recorder.recordIncomingXR(internalStats{ lastReceiverReferenceTimes: []uint64{50000000}, }, report, time.Time{}) assert.Equal(t, int64(s.RemoteOutboundRTPStreamStats.RoundTripTime), int64(-9223372036854775808)) } func TestGetStatsNotBlocking(t *testing.T) { r := newRecorder(0, 90_000) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() go func() { defer cancel() r.Start() r.GetStats() }() go r.Stop() <-ctx.Done() assert.False(t, errors.Is(ctx.Err(), context.DeadlineExceeded), "it shouldn't block") } func TestQueueNotBlocking(t *testing.T) { for _, testCase := range []struct { f func(r *recorder) name string }{ { f: func(r *recorder) { r.QueueIncomingRTP(time.Now(), mustMarshalRTP(t, rtp.Packet{}), interceptor.Attributes{}) }, name: "QueueIncomingRTP", }, { f: func(r *recorder) { r.QueueOutgoingRTP(time.Now(), &rtp.Header{}, mustMarshalRTP(t, rtp.Packet{}), interceptor.Attributes{}) }, name: "QueueOutgoingRTP", }, { f: func(r *recorder) { r.QueueIncomingRTCP(time.Now(), mustMarshalRTCPs(t, &rtcp.CCFeedbackReport{}), interceptor.Attributes{}) }, name: "QueueIncomingRTCP", }, { f: func(r *recorder) { r.QueueOutgoingRTCP(time.Now(), []rtcp.Packet{}, interceptor.Attributes{}) }, name: "QueueOutgoingRTCP", }, } { t.Run(testCase.name+"NotBlocking", func(t *testing.T) { r := newRecorder(0, 90_000) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() go func() { defer cancel() r.Start() testCase.f(r) }() go r.Stop() <-ctx.Done() assert.False(t, errors.Is(ctx.Err(), context.DeadlineExceeded), "it shouldn't block") }) } } func TestContains(t *testing.T) { for i, tc := range []struct { list []uint32 element uint32 expected bool }{ {list: []uint32{}, element: 0, expected: false}, {list: []uint32{0}, element: 0, expected: true}, {list: []uint32{0, 1, 2}, element: 1, expected: true}, {list: []uint32{0, 1, 2}, element: 3, expected: false}, } { t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { assert.Equal(t, tc.expected, contains(tc.list, tc.element)) }) } } interceptor-0.1.42/pkg/twcc/000077500000000000000000000000001510612111000156415ustar00rootroot00000000000000interceptor-0.1.42/pkg/twcc/arrival_time_map.go000066400000000000000000000151201510612111000215020ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package twcc const ( minCapacity = 128 maxNumberOfPackets = 1 << 15 ) // packetArrivalTimeMap is adapted from Chrome's implementation of TWCC, and keeps track // of the arrival times of packets. It is used by the TWCC interceptor to build feedback // packets. // See https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/webrtc/modules/remote_bitrate_estimator/packet_arrival_map.h;drc=b5cd13bb6d5d157a5fbe3628b2dd1c1e106203c6 // //nolint:lll type packetArrivalTimeMap struct { // arrivalTimes is a circular buffer, where the packet with sequence number sn is stored // in slot sn % len(arrivalTimes) arrivalTimes []int64 // The unwrapped sequence numbers for the range of valid sequence numbers in arrivalTimes. // beginSequenceNumber is inclusive, and endSequenceNumber is exclusive. beginSequenceNumber, endSequenceNumber int64 } // AddPacket records the fact that the packet with sequence number sequenceNumber arrived // at arrivalTime. func (m *packetArrivalTimeMap) AddPacket(sequenceNumber int64, arrivalTime int64) { if m.arrivalTimes == nil { // First packet m.reallocate(minCapacity) m.beginSequenceNumber = sequenceNumber m.endSequenceNumber = sequenceNumber + 1 m.arrivalTimes[m.index(sequenceNumber)] = arrivalTime return } if sequenceNumber >= m.beginSequenceNumber && sequenceNumber < m.endSequenceNumber { // The packet is within the buffer, no need to resize. m.arrivalTimes[m.index(sequenceNumber)] = arrivalTime return } if sequenceNumber < m.beginSequenceNumber { // The packet goes before the current buffer. Expand to add packet, // but only if it fits within the maximum number of packets. newSize := int(m.endSequenceNumber - sequenceNumber) if newSize > maxNumberOfPackets { // Don't expand the buffer back for this packet, as it would remove newer received // packets. return } m.adjustToSize(newSize) m.arrivalTimes[m.index(sequenceNumber)] = arrivalTime m.setNotReceived(sequenceNumber+1, m.beginSequenceNumber) m.beginSequenceNumber = sequenceNumber return } // The packet goes after the buffer. newEndSequenceNumber := sequenceNumber + 1 if newEndSequenceNumber >= m.endSequenceNumber+maxNumberOfPackets { // All old packets have to be removed. m.beginSequenceNumber = sequenceNumber m.endSequenceNumber = newEndSequenceNumber m.arrivalTimes[m.index(sequenceNumber)] = arrivalTime return } if m.beginSequenceNumber < newEndSequenceNumber-maxNumberOfPackets { // Remove oldest entries. m.beginSequenceNumber = newEndSequenceNumber - maxNumberOfPackets } m.adjustToSize(int(newEndSequenceNumber - m.beginSequenceNumber)) // Packets can be received out of order. If this isn't the next expected packet, // add enough placeholders to fill the gap. m.setNotReceived(m.endSequenceNumber, sequenceNumber) m.endSequenceNumber = newEndSequenceNumber m.arrivalTimes[m.index(sequenceNumber)] = arrivalTime } func (m *packetArrivalTimeMap) setNotReceived(startInclusive, endExclusive int64) { for sn := startInclusive; sn < endExclusive; sn++ { m.arrivalTimes[m.index(sn)] = -1 } } // BeginSequenceNumber returns the first valid sequence number in the map. func (m *packetArrivalTimeMap) BeginSequenceNumber() int64 { return m.beginSequenceNumber } // EndSequenceNumber returns the first sequence number after the last valid sequence number in the map. func (m *packetArrivalTimeMap) EndSequenceNumber() int64 { return m.endSequenceNumber } // FindNextAtOrAfter returns the sequence number and timestamp of the first received packet that has a sequence number // greater or equal to sequenceNumber. func (m *packetArrivalTimeMap) FindNextAtOrAfter(sequenceNumber int64) ( int64, int64, bool, ) { for seq := m.Clamp(sequenceNumber); seq < m.endSequenceNumber; seq++ { if arrivalTime := m.get(seq); arrivalTime >= 0 { return seq, arrivalTime, true } } return -1, -1, false } // EraseTo erases all elements from the beginning of the map until sequenceNumber. func (m *packetArrivalTimeMap) EraseTo(sequenceNumber int64) { if sequenceNumber < m.beginSequenceNumber { return } if sequenceNumber >= m.endSequenceNumber { // Erase all. m.beginSequenceNumber = m.endSequenceNumber return } // Remove some m.beginSequenceNumber = sequenceNumber m.adjustToSize(int(m.endSequenceNumber - m.beginSequenceNumber)) } // RemoveOldPackets removes packets from the beginning of the map as long as they are before // sequenceNumber and with an age older than arrivalTimeLimit. func (m *packetArrivalTimeMap) RemoveOldPackets(sequenceNumber int64, arrivalTimeLimit int64) { checkTo := min(sequenceNumber, m.endSequenceNumber) for m.beginSequenceNumber < checkTo && m.get(m.beginSequenceNumber) <= arrivalTimeLimit { m.beginSequenceNumber++ } m.adjustToSize(int(m.endSequenceNumber - m.beginSequenceNumber)) } // HasReceived returns whether a packet with the sequence number has been received. func (m *packetArrivalTimeMap) HasReceived(sequenceNumber int64) bool { return m.get(sequenceNumber) >= 0 } // Clamp returns sequenceNumber clamped to [beginSequenceNumber, endSequenceNumber]. func (m *packetArrivalTimeMap) Clamp(sequenceNumber int64) int64 { if sequenceNumber < m.beginSequenceNumber { return m.beginSequenceNumber } if m.endSequenceNumber < sequenceNumber { return m.endSequenceNumber } return sequenceNumber } func (m *packetArrivalTimeMap) get(sequenceNumber int64) int64 { if sequenceNumber < m.beginSequenceNumber || sequenceNumber >= m.endSequenceNumber { return -1 } return m.arrivalTimes[m.index(sequenceNumber)] } func (m *packetArrivalTimeMap) index(sequenceNumber int64) int { // Sequence number might be negative, and we always guarantee that arrivalTimes // length is a power of 2, so it's easier to use "&" instead of "%" return int(sequenceNumber & int64(m.capacity()-1)) } func (m *packetArrivalTimeMap) adjustToSize(newSize int) { if newSize > m.capacity() { newCapacity := m.capacity() for newCapacity < newSize { newCapacity *= 2 } m.reallocate(newCapacity) } if m.capacity() > max(minCapacity, newSize*4) { newCapacity := m.capacity() for newCapacity >= 2*max(newSize, minCapacity) { newCapacity /= 2 } m.reallocate(newCapacity) } } func (m *packetArrivalTimeMap) capacity() int { return len(m.arrivalTimes) } func (m *packetArrivalTimeMap) reallocate(newCapacity int) { newBuffer := make([]int64, newCapacity) for sn := m.beginSequenceNumber; sn < m.endSequenceNumber; sn++ { newBuffer[int(sn&(int64(newCapacity-1)))] = m.get(sn) } m.arrivalTimes = newBuffer } interceptor-0.1.42/pkg/twcc/arrival_time_map_test.go000066400000000000000000000236111510612111000225450ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package twcc import ( "testing" "github.com/stretchr/testify/assert" ) //nolint:maintidx func TestArrivalTimeMap(t *testing.T) { t.Run("consistent when empty", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap assert.Equal(t, arrivalTimeMap.BeginSequenceNumber(), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(0)) assert.Equal(t, int64(0), arrivalTimeMap.Clamp(-5)) assert.Equal(t, int64(0), arrivalTimeMap.Clamp(5)) }) t.Run("inserts first item into map", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 10) assert.Equal(t, int64(42), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(43), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(41)) assert.True(t, arrivalTimeMap.HasReceived(42)) assert.False(t, arrivalTimeMap.HasReceived(43)) assert.False(t, arrivalTimeMap.HasReceived(44)) assert.Equal(t, int64(42), arrivalTimeMap.Clamp(-100)) assert.Equal(t, int64(42), arrivalTimeMap.Clamp(42)) assert.Equal(t, int64(43), arrivalTimeMap.Clamp(100)) }) t.Run("inserts with gaps", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 0) arrivalTimeMap.AddPacket(45, 11) assert.Equal(t, int64(42), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(46), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(41)) assert.True(t, arrivalTimeMap.HasReceived(42)) assert.False(t, arrivalTimeMap.HasReceived(43)) assert.False(t, arrivalTimeMap.HasReceived(44)) assert.True(t, arrivalTimeMap.HasReceived(45)) assert.False(t, arrivalTimeMap.HasReceived(46)) assert.Equal(t, int64(0), arrivalTimeMap.get(42)) assert.Less(t, arrivalTimeMap.get(43), int64(0)) assert.Less(t, arrivalTimeMap.get(44), int64(0)) assert.Equal(t, int64(11), arrivalTimeMap.get(45)) assert.Equal(t, int64(42), arrivalTimeMap.Clamp(-100)) assert.Equal(t, int64(44), arrivalTimeMap.Clamp(44)) assert.Equal(t, int64(46), arrivalTimeMap.Clamp(100)) }) t.Run("find next at or after with gaps", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 0) arrivalTimeMap.AddPacket(45, 11) seq, ts, ok := arrivalTimeMap.FindNextAtOrAfter(42) assert.Equal(t, int64(42), seq) assert.Equal(t, int64(0), ts) assert.True(t, ok) seq, ts, ok = arrivalTimeMap.FindNextAtOrAfter(43) assert.Equal(t, int64(45), seq) assert.Equal(t, int64(11), ts) assert.True(t, ok) }) t.Run("inserts within buffer", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(45, 11) arrivalTimeMap.AddPacket(43, 12) arrivalTimeMap.AddPacket(44, 13) assert.False(t, arrivalTimeMap.HasReceived(41)) assert.True(t, arrivalTimeMap.HasReceived(42)) assert.True(t, arrivalTimeMap.HasReceived(43)) assert.True(t, arrivalTimeMap.HasReceived(44)) assert.True(t, arrivalTimeMap.HasReceived(45)) assert.False(t, arrivalTimeMap.HasReceived(46)) assert.Equal(t, int64(10), arrivalTimeMap.get(42)) assert.Equal(t, int64(12), arrivalTimeMap.get(43)) assert.Equal(t, int64(13), arrivalTimeMap.get(44)) assert.Equal(t, int64(11), arrivalTimeMap.get(45)) }) t.Run("grows buffer and removes old", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap var largeSeqNum int64 = 42 + maxNumberOfPackets arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(43, 11) arrivalTimeMap.AddPacket(44, 12) arrivalTimeMap.AddPacket(45, 13) arrivalTimeMap.AddPacket(largeSeqNum, 12) assert.Equal(t, int64(43), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, largeSeqNum+1, arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(41)) assert.False(t, arrivalTimeMap.HasReceived(42)) assert.True(t, arrivalTimeMap.HasReceived(43)) assert.True(t, arrivalTimeMap.HasReceived(44)) assert.True(t, arrivalTimeMap.HasReceived(45)) assert.False(t, arrivalTimeMap.HasReceived(46)) assert.True(t, arrivalTimeMap.HasReceived(largeSeqNum)) assert.False(t, arrivalTimeMap.HasReceived(largeSeqNum+1)) }) t.Run("sequence number jump deletes all", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap var largeSeqNum int64 = 42 + 2*maxNumberOfPackets arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(largeSeqNum, 12) assert.Equal(t, largeSeqNum, arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, largeSeqNum+1, arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(42)) assert.True(t, arrivalTimeMap.HasReceived(largeSeqNum)) assert.False(t, arrivalTimeMap.HasReceived(largeSeqNum+1)) }) t.Run("expands before beginning", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(-1000, 13) assert.Equal(t, int64(-1000), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(43), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(-1001)) assert.True(t, arrivalTimeMap.HasReceived(-1000)) assert.False(t, arrivalTimeMap.HasReceived(-999)) assert.True(t, arrivalTimeMap.HasReceived(42)) assert.False(t, arrivalTimeMap.HasReceived(43)) }) t.Run("expanding before beginning keeps received", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap var smallSeqNum int64 = 42 - 2*maxNumberOfPackets arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(smallSeqNum, 13) assert.Equal(t, int64(42), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(43), arrivalTimeMap.EndSequenceNumber()) }) t.Run("erase to removes elements", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(43, 11) arrivalTimeMap.AddPacket(44, 12) arrivalTimeMap.AddPacket(45, 13) arrivalTimeMap.EraseTo(44) assert.Equal(t, int64(44), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(46), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(43)) assert.True(t, arrivalTimeMap.HasReceived(44)) assert.True(t, arrivalTimeMap.HasReceived(45)) assert.False(t, arrivalTimeMap.HasReceived(46)) }) t.Run("erases in empty map", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap assert.Equal(t, arrivalTimeMap.BeginSequenceNumber(), arrivalTimeMap.EndSequenceNumber()) arrivalTimeMap.EraseTo(arrivalTimeMap.EndSequenceNumber()) assert.Equal(t, arrivalTimeMap.BeginSequenceNumber(), arrivalTimeMap.EndSequenceNumber()) }) t.Run("is tolerant to wrong arguments for erase", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(43, 11) arrivalTimeMap.EraseTo(1) assert.Equal(t, int64(42), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(44), arrivalTimeMap.EndSequenceNumber()) arrivalTimeMap.EraseTo(100) assert.Equal(t, int64(44), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(44), arrivalTimeMap.EndSequenceNumber()) }) //nolint:dupl t.Run("erase all remembers beginning sequence number", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(42, 10) arrivalTimeMap.AddPacket(43, 11) arrivalTimeMap.AddPacket(44, 12) arrivalTimeMap.AddPacket(45, 13) arrivalTimeMap.EraseTo(46) arrivalTimeMap.AddPacket(50, 10) assert.Equal(t, int64(46), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(51), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(45)) assert.False(t, arrivalTimeMap.HasReceived(46)) assert.False(t, arrivalTimeMap.HasReceived(47)) assert.False(t, arrivalTimeMap.HasReceived(48)) assert.False(t, arrivalTimeMap.HasReceived(49)) assert.True(t, arrivalTimeMap.HasReceived(50)) assert.False(t, arrivalTimeMap.HasReceived(51)) }) //nolint:dupl t.Run("erase to missing sequence number", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(37, 10) arrivalTimeMap.AddPacket(39, 11) arrivalTimeMap.AddPacket(40, 12) arrivalTimeMap.AddPacket(41, 13) arrivalTimeMap.EraseTo(38) arrivalTimeMap.AddPacket(42, 40) assert.Equal(t, int64(38), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(43), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(37)) assert.False(t, arrivalTimeMap.HasReceived(38)) assert.True(t, arrivalTimeMap.HasReceived(39)) assert.True(t, arrivalTimeMap.HasReceived(40)) assert.True(t, arrivalTimeMap.HasReceived(41)) assert.True(t, arrivalTimeMap.HasReceived(42)) assert.False(t, arrivalTimeMap.HasReceived(43)) }) t.Run("remove old packets", func(t *testing.T) { var arrivalTimeMap packetArrivalTimeMap arrivalTimeMap.AddPacket(37, 10) arrivalTimeMap.AddPacket(39, 11) arrivalTimeMap.AddPacket(40, 12) arrivalTimeMap.AddPacket(41, 13) arrivalTimeMap.RemoveOldPackets(42, 11) assert.Equal(t, int64(40), arrivalTimeMap.BeginSequenceNumber()) assert.Equal(t, int64(42), arrivalTimeMap.EndSequenceNumber()) assert.False(t, arrivalTimeMap.HasReceived(39)) assert.True(t, arrivalTimeMap.HasReceived(40)) assert.True(t, arrivalTimeMap.HasReceived(41)) assert.False(t, arrivalTimeMap.HasReceived(42)) }) t.Run("shrinks buffer when necessary", func(t *testing.T) { var m packetArrivalTimeMap var largeSeqNum int64 = 100 + maxNumberOfPackets - 1 m.AddPacket(100, 10) m.AddPacket(largeSeqNum, 11) m.EraseTo(largeSeqNum - 1) assert.Equal(t, largeSeqNum-1, m.BeginSequenceNumber()) assert.Equal(t, largeSeqNum+1, m.EndSequenceNumber()) assert.Equal(t, minCapacity, m.capacity()) }) t.Run("find next at or after with invalid sequence", func(t *testing.T) { var m packetArrivalTimeMap m.AddPacket(100, 10) _, _, ok := m.FindNextAtOrAfter(101) assert.False(t, ok) }) } interceptor-0.1.42/pkg/twcc/header_extension_interceptor.go000066400000000000000000000043101510612111000241300ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package twcc import ( "errors" "sync/atomic" "github.com/pion/interceptor" "github.com/pion/rtp" ) var errHeaderIsNil = errors.New("header is nil") // HeaderExtensionInterceptorFactory is a interceptor.Factory for a HeaderExtensionInterceptor. type HeaderExtensionInterceptorFactory struct{} // NewInterceptor constructs a new HeaderExtensionInterceptor. func (h *HeaderExtensionInterceptorFactory) NewInterceptor(_ 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) //nolint:gosec // G115 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 //nolint:gosec // G115 tcc, err := (&rtp.TransportCCExtension{TransportSequence: uint16(sequenceNumber)}).Marshal() if err != nil { return 0, err } if header == nil { return 0, errHeaderIsNil } err = header.SetExtension(hdrExtID, tcc) if err != nil { return 0, err } return writer.Write(header, payload, attributes) }, ) } interceptor-0.1.42/pkg/twcc/header_extension_interceptor_test.go000066400000000000000000000050471510612111000251770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package twcc import ( "io" "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("if header is nil, return error", func(t *testing.T) { factory, err := NewHeaderExtensionInterceptor() assert.NoError(t, err) inter, err := factory.NewInterceptor("") assert.NoError(t, err) fn := inter.BindLocalStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, interceptor.RTPWriterFunc(func(*rtp.Header, []byte, interceptor.Attributes) (int, error) { return 0, io.EOF })) _, err = fn.Write(nil, []byte{}, interceptor.Attributes{}) assert.Equal(t, errHeaderIsNil, err) }) 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): assert.FailNow(t, "written rtp packet not found") } } }(pChan, uint16(i+1)) //nolint:gosec // G115 } 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.42/pkg/twcc/sender_interceptor.go000066400000000000000000000115621510612111000220730ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package twcc import ( "errors" "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 } var errClosed = errors.New("interceptor is closed") // NewInterceptor constructs a new SenderInterceptor. func (s *SenderInterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) { senderInterceptor := &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(senderInterceptor) if err != nil { return nil, err } } return senderInterceptor, 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. // //nolint:cyclop 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) //nolint:gosec // G115 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 } p := packet{ hdr: header, sequenceNumber: tccExt.TransportSequence, arrivalTime: time.Since(s.startTime).Microseconds(), ssrc: info.SSRC, } select { case <-s.close: return 0, nil, errClosed case s.packetChan <- p: } } 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(writer 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 len(pkts) == 0 { continue } if _, err := writer.Write(pkts, nil); err != nil { s.log.Error(err.Error()) } } } } interceptor-0.1.42/pkg/twcc/sender_interceptor_test.go000066400000000000000000000205251510612111000231310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package twcc import ( "testing" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/internal/test" "github.com/pion/rtcp" "github.com/pion/rtp" transportTest "github.com/pion/transport/v3/test" "github.com/stretchr/testify/assert" ) //nolint:maintidx 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{} //nolint:gosec // G115 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{} //nolint:gosec // G115 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{} //nolint:gosec // G115 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{} //nolint:gosec // G115 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) }) } func TestSenderInterceptor_Leak(t *testing.T) { lim := transportTest.TimeOut(time.Second * 10) defer lim.Stop() report := transportTest.CheckRoutines(t) defer report() f, err := NewSenderInterceptor(SendInterval(200 * time.Millisecond)) assert.NoError(t, err) testInterceptor, err := f.NewInterceptor("") assert.NoError(t, err) stream := test.NewMockStream(&interceptor.StreamInfo{RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ { URI: transportCCURI, ID: 1, }, }}, testInterceptor) defer func() { assert.NoError(t, stream.Close()) }() assert.NoError(t, testInterceptor.Close()) for _, i := range []int{0, 1, 2, 3, 4, 5} { hdr := rtp.Header{} //nolint:gosec // G115 tcc, err := (&rtp.TransportCCExtension{TransportSequence: uint16(i)}).Marshal() assert.NoError(t, err) assert.NoError(t, hdr.SetExtension(1, tcc)) stream.ReceiveRTP(&rtp.Packet{Header: hdr}) } } interceptor-0.1.42/pkg/twcc/twcc.go000066400000000000000000000243511510612111000171350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package twcc provides interceptors to implement transport wide congestion control. package twcc import ( "math" "github.com/pion/interceptor/internal/sequencenumber" "github.com/pion/rtcp" ) const ( packetWindowMicroseconds = 500_000 maxMissingSequenceNumbers = 0x7FFE ) // 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 { arrivalTimeMap packetArrivalTimeMap sequenceUnwrapper sequencenumber.Unwrapper // startSequenceNumber is the first sequence number that will be included in the the // next feedback packet. startSequenceNumber *int64 senderSSRC uint32 mediaSSRC uint32 fbPktCnt uint8 packetsHeld int } // NewRecorder creates a new Recorder which uses the given senderSSRC in the created // feedback packets. func NewRecorder(senderSSRC uint32) *Recorder { return &Recorder{ 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 // "Unwrap" the sequence number to get a monotonically increasing sequence number that // won't wrap around after math.MaxUint16. unwrappedSN := r.sequenceUnwrapper.Unwrap(sequenceNumber) r.maybeCullOldPackets(unwrappedSN, arrivalTime) if r.startSequenceNumber == nil || unwrappedSN < *r.startSequenceNumber { r.startSequenceNumber = &unwrappedSN } // We are only interested in the first time a packet is received. if r.arrivalTimeMap.HasReceived(unwrappedSN) { return } r.arrivalTimeMap.AddPacket(unwrappedSN, arrivalTime) r.packetsHeld++ // Limit the range of sequence numbers to send feedback for. if *r.startSequenceNumber < r.arrivalTimeMap.BeginSequenceNumber() { sn := r.arrivalTimeMap.BeginSequenceNumber() r.startSequenceNumber = &sn } } func (r *Recorder) maybeCullOldPackets(sequenceNumber int64, arrivalTime int64) { if r.startSequenceNumber != nil && *r.startSequenceNumber >= r.arrivalTimeMap.EndSequenceNumber() && arrivalTime >= packetWindowMicroseconds { r.arrivalTimeMap.RemoveOldPackets(sequenceNumber, arrivalTime-packetWindowMicroseconds) } } // PacketsHeld returns the number of received packets currently held by the recorder. func (r *Recorder) PacketsHeld() int { return r.packetsHeld } // BuildFeedbackPacket creates a new RTCP packet containing a TWCC feedback report. func (r *Recorder) BuildFeedbackPacket() []rtcp.Packet { if r.startSequenceNumber == nil { return nil } endSN := r.arrivalTimeMap.EndSequenceNumber() var feedbacks []rtcp.Packet for *r.startSequenceNumber < endSN { feedback := r.maybeBuildFeedbackPacket(*r.startSequenceNumber, endSN) if feedback == nil { break } feedbacks = append(feedbacks, feedback.getRTCP()) // NOTE: we don't erase packets from the history in case they need to be resent // after a reordering. They will be removed instead in Record when they get too // old. } r.packetsHeld = 0 return feedbacks } // maybeBuildFeedbackPacket builds a feedback packet starting from startSN (inclusive) until // endSN (exclusive). func (r *Recorder) maybeBuildFeedbackPacket(beginSeqNumInclusive, endSeqNumExclusive int64) *feedback { // NOTE: The logic of this method is inspired by the implementation in Chrome. // See https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/webrtc/modules/remote_bitrate_estimator/remote_estimator_proxy.cc;l=276;drc=b5cd13bb6d5d157a5fbe3628b2dd1c1e106203c6 //nolint:lll startSNInclusive, endSNExclusive := r.arrivalTimeMap.Clamp(beginSeqNumInclusive), r.arrivalTimeMap.Clamp(endSeqNumExclusive) // Create feedback on demand, as we don't yet know if there are packets in the range that have been // received. var fb *feedback nextSequenceNumber := beginSeqNumInclusive for seq := startSNInclusive; seq < endSNExclusive; seq++ { foundSeq, arrivalTime, ok := r.arrivalTimeMap.FindNextAtOrAfter(seq) seq = foundSeq if !ok || seq >= endSNExclusive { break } if fb == nil { fb = newFeedback(r.senderSSRC, r.mediaSSRC, r.fbPktCnt) r.fbPktCnt++ // It should be possible to add seq to this new packet. // If the difference between seq and beginSeqNumInclusive is too large, discard // reporting too old missing packets. baseSequenceNumber := max(beginSeqNumInclusive, seq-maxMissingSequenceNumbers) // baseSequenceNumber is the expected first sequence number. This is known, // but we may not have actually received it, so the base time should be the time // of the first received packet in the feedback. fb.setBase(uint16(baseSequenceNumber), arrivalTime) //nolint:gosec // G115 if !fb.addReceived(uint16(seq), arrivalTime) { //nolint:gosec // G115 // Could not add a single received packet to the feedback. // This is unexpected to actually occur, but if it does, we'll // try again after skipping any missing packets. // NOTE: It's fine that we already incremented fbPktCnt, as in essence // we did actually "skip" a feedback (and this matches Chrome's behavior). r.startSequenceNumber = &seq return nil } } else if !fb.addReceived(uint16(seq), arrivalTime) { //nolint:gosec // G115 // Could not add timestamp. Packet may be full. Return // and try again with a fresh packet. break } nextSequenceNumber = seq + 1 } r.startSequenceNumber = &nextSequenceNumber return fb } 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) //nolint:gosec // G115 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 // 4 bytes header + 16 bytes twcc header + 2 bytes for each chunk + length of deltas padLen := 20 + len(f.rtcp.PacketChunks)*2 + f.len 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), //nolint:gosec // G115 } return f.rtcp } func (f *feedback) addReceived(sequenceNumber uint16, timestampUS int64) bool { deltaUS := timestampUS - f.lastTimestampUS var delta250US int64 if deltaUS >= 0 { delta250US = (deltaUS + rtcp.TypeTCCDeltaScaleFactor/2) / rtcp.TypeTCCDeltaScaleFactor } else { delta250US = (deltaUS - rtcp.TypeTCCDeltaScaleFactor/2) / rtcp.TypeTCCDeltaScaleFactor } // delta doesn't fit into 16 bit, need to create new packet if delta250US < math.MinInt16 || delta250US > math.MaxInt16 { return false } deltaUSRounded := delta250US * rtcp.TypeTCCDeltaScaleFactor 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: deltaUSRounded, }) f.lastTimestampUS += deltaUSRounded 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)), //nolint:gosec // G115 } } 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 } interceptor-0.1.42/pkg/twcc/twcc_test.go000066400000000000000000000713451510612111000202010ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package twcc import ( "testing" "github.com/pion/rtcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func rtcpToTwcc(t *testing.T, in []rtcp.Packet) []*rtcp.TransportLayerCC { t.Helper() out := make([]*rtcp.TransportLayerCC, len(in)) var ok bool for i, pkt := range in { out[i], ok = pkt.(*rtcp.TransportLayerCC) assert.True(t, ok, "Expected TransportLayerCC, got %T", pkt) } return out } func Test_chunk_add(t *testing.T) { t.Run("fill with not received", func(t *testing.T) { testChunk := &chunk{} for i := 0; i < maxRunLengthCap; i++ { assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived), i) testChunk.add(rtcp.TypeTCCPacketNotReceived) } assert.Equal(t, make([]uint16, maxRunLengthCap), testChunk.deltas) assert.False(t, testChunk.hasDifferentTypes) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) statusChunk := testChunk.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) { testChunk := &chunk{} for i := 0; i < maxOneBitCap; i++ { assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta), i) testChunk.add(rtcp.TypeTCCPacketReceivedSmallDelta) } assert.Equal(t, testChunk.deltas, []uint16{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) assert.False(t, testChunk.hasDifferentTypes) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) statusChunk := testChunk.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) { testChunk := &chunk{} for i := 0; i < maxTwoBitCap; i++ { assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta), i) testChunk.add(rtcp.TypeTCCPacketReceivedLargeDelta) } assert.Equal(t, testChunk.deltas, []uint16{2, 2, 2, 2, 2, 2, 2}) assert.True(t, testChunk.hasLargeDelta) assert.False(t, testChunk.hasDifferentTypes) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) statusChunk := testChunk.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) { testChunk := &chunk{} assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedLargeDelta) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedLargeDelta) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedLargeDelta) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) statusChunk := testChunk.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) { testChunk := chunk{} assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedSmallDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedSmallDelta) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) testChunk.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) testChunk.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) testChunk.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) testChunk.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) testChunk.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) testChunk.add(rtcp.TypeTCCPacketNotReceived) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketNotReceived)) testChunk.add(rtcp.TypeTCCPacketNotReceived) assert.False(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) statusChunk1 := testChunk.encode() assert.IsType(t, &rtcp.StatusVectorChunk{}, statusChunk1) assert.Equal(t, 1, len(testChunk.deltas)) assert.True(t, testChunk.canAdd(rtcp.TypeTCCPacketReceivedLargeDelta)) testChunk.add(rtcp.TypeTCCPacketReceivedLargeDelta) statusChunk2 := testChunk.encode() assert.IsType(t, &rtcp.StatusVectorChunk{}, statusChunk2) assert.Equal(t, 0, len(testChunk.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) { testFeedback := &feedback{} testFeedback.setBase(1, 1000*1000) got := testFeedback.addReceived(1, 1023*1000) assert.True(t, got) assert.Equal(t, uint16(2), testFeedback.nextSequenceNumber) assert.Equal(t, int64(15), testFeedback.refTimestamp64MS) got = testFeedback.addReceived(4, 1086*1000) assert.True(t, got) assert.Equal(t, uint16(5), testFeedback.nextSequenceNumber) assert.Equal(t, int64(15), testFeedback.refTimestamp64MS) assert.True(t, testFeedback.lastChunk.hasDifferentTypes) assert.Equal(t, 4, len(testFeedback.lastChunk.deltas)) assert.NotContains(t, testFeedback.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 small deltas", func(t *testing.T) { f := newFeedback(0, 0, 0) base := int64(320 * 1000) deltaUS := int64(200) f.setBase(5, base) for i := int64(0); i < 5; i++ { got := f.addReceived(5+uint16(i+1), base+deltaUS*i) //nolint:gosec // G115 assert.True(t, got) } pkt := f.getRTCP() expectedDeltas := []*rtcp.RecvDelta{ { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, // NOTE: The delta is less than the scale factor, but it should be rounded up. // (rtcp.RecvDelta).Marshal() simply truncates to an interval of the scale factor, // so we want to make sure that the deltas have any rounding applied when building // the feedback. Delta: 1 * rtcp.TypeTCCDeltaScaleFactor, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 1 * rtcp.TypeTCCDeltaScaleFactor, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, // NOTE: This is zero because even though the deltas are all the same, the rounding error has // built up enough by this packet to cause it to be rounded down. Delta: 0 * rtcp.TypeTCCDeltaScaleFactor, }, { Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 1 * 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) { t.Helper() 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) { t.Helper() for _, pkt := range pkts { marshaled, err := pkt.Marshal() assert.NoError(t, err) // Chrome expects feedback packets to always be 18 bytes or more. // https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/modules/rtp_rtcp/source/rtcp_packet/transport_feedback.cc;l=423?q=transport_feedback.cc&ss=chromium%2Fchromium%2Fsrc //nolint:lll assert.GreaterOrEqual(t, len(marshaled), 18) } } func TestBuildFeedbackPacket(t *testing.T) { recoder := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, recoder, []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 := recoder.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) { recoder := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, recoder, []uint16{65534, 65535}, []int64{ arrivalTime, increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) rtcpPackets := recoder.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) addRun(t, recoder, []uint16{0, 4, 5, 6}, []int64{ increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) rtcpPackets = recoder.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: 0, 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 * 2}, {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 TestBuildFeedbackPacket_MinInput(t *testing.T) { r := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, r, []uint16{0}, []int64{ arrivalTime, }) pkts := r.BuildFeedbackPacket() assert.Equal(t, 1, len(pkts)) assert.Equal(t, &rtcp.TransportLayerCC{ Header: rtcp.Header{ Count: rtcp.FormatTCC, Type: rtcp.TypeTransportSpecificFeedback, Length: 5, Padding: true, }, SenderSSRC: 5000, MediaSSRC: 5000, BaseSequenceNumber: 0, ReferenceTime: 1, FbPktCount: 0, PacketStatusCount: 1, PacketChunks: []rtcp.PacketStatusChunk{ &rtcp.RunLengthChunk{ PacketStatusSymbol: 1, Type: rtcp.TypeTCCRunLengthChunk, RunLength: 1, }, }, RecvDeltas: []*rtcp.RecvDelta{ {Type: rtcp.TypeTCCPacketReceivedSmallDelta, Delta: 0}, }, }, rtcpToTwcc(t, pkts)[0]) marshalAll(t, pkts) } func TestBuildFeedbackPacket_MissingPacketsBetweenFeedbacks(t *testing.T) { recorder := NewRecorder(5000) // Create a run of received packets. arrivalTime := int64(scaleFactorReferenceTime) addRun(t, recorder, []uint16{0, 1, 2, 3}, []int64{ scaleFactorReferenceTime, increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) rtcpPackets := recorder.BuildFeedbackPacket() assert.Equal(t, 1, len(rtcpPackets)) // Now create another run of received packets, but with a gap. addRun(t, recorder, []uint16{7, 8, 9}, []int64{ increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor*256), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) rtcpPackets = recorder.BuildFeedbackPacket() require.Equal(t, 1, len(rtcpPackets)) twccPacket := rtcpToTwcc(t, rtcpPackets)[0] assert.Equal( t, uint16(4), twccPacket.BaseSequenceNumber, "Base sequence should be one after the end of the previous feedback", ) assert.Equal( t, uint16(6), twccPacket.PacketStatusCount, "Feedback should include status for both the lost and received packets", ) expectedPacketChunks := []rtcp.PacketStatusChunk{ &rtcp.StatusVectorChunk{ Type: rtcp.TypeTCCRunLengthChunk, SymbolSize: rtcp.TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketNotReceived, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, rtcp.TypeTCCPacketReceivedSmallDelta, }, }, } assert.Equal(t, expectedPacketChunks, twccPacket.PacketChunks) marshalAll(t, rtcpPackets) } func TestBuildFeedbackPacketCount(t *testing.T) { recorder := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, recorder, []uint16{0, 1}, []int64{ arrivalTime, arrivalTime, }) pkts := recorder.BuildFeedbackPacket() assert.Len(t, pkts, 1) twcc := rtcpToTwcc(t, pkts)[0] assert.Equal(t, uint8(0), twcc.FbPktCount) addRun(t, recorder, []uint16{0, 1}, []int64{ arrivalTime, arrivalTime, }) pkts = recorder.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) { recorder := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, recorder, []uint16{3, 4, 5, 7, 6, 8, 10, 11, 13, 14}, []int64{ arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, arrivalTime, }) rtcpPackets := recorder.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) { recorder := NewRecorder(5000) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, recorder, []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 := recorder.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 TestPacketsHheld(t *testing.T) { recorder := NewRecorder(5000) assert.Zero(t, recorder.PacketsHeld()) arrivalTime := int64(scaleFactorReferenceTime) addRun(t, recorder, []uint16{0, 1, 2}, []int64{ arrivalTime, increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) assert.Equal(t, recorder.PacketsHeld(), 3) addRun(t, recorder, []uint16{3, 4}, []int64{ increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), increaseTime(&arrivalTime, rtcp.TypeTCCDeltaScaleFactor), }) assert.Equal(t, recorder.PacketsHeld(), 5) recorder.BuildFeedbackPacket() assert.Zero(t, recorder.PacketsHeld()) } interceptor-0.1.42/registry.go000066400000000000000000000014021510612111000163140ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 := make([]Interceptor, 0, len(r.factories)) 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.42/renovate.json000066400000000000000000000001731510612111000166370ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>pion/renovate-config" ] } interceptor-0.1.42/streaminfo.go000066400000000000000000000026511510612111000166220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package 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 SSRCRetransmission uint32 SSRCForwardErrorCorrection uint32 PayloadType uint8 PayloadTypeRetransmission uint8 PayloadTypeForwardErrorCorrection 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 }