Repository: pion/rtcp Branch: main Commit: d61b52c3dcee Files: 76 Total size: 301.9 KB Directory structure: gitextract_env3fvgh/ ├── .github/ │ ├── .gitignore │ ├── fetch-scripts.sh │ ├── install-hooks.sh │ └── workflows/ │ ├── api.yaml │ ├── codeql-analysis.yml │ ├── fuzz.yaml │ ├── lint.yaml │ ├── release.yml │ ├── renovate-go-sum-fix.yaml │ ├── reuse.yml │ ├── test.yaml │ └── tidy-check.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .reuse/ │ └── dep5 ├── LICENSE ├── LICENSES/ │ ├── CC0-1.0.txt │ └── MIT.txt ├── README.md ├── application_defined.go ├── application_defined_test.go ├── codecov.yml ├── compound_packet.go ├── compound_packet_test.go ├── doc.go ├── errors.go ├── extended_report.go ├── extended_report_test.go ├── full_intra_request.go ├── full_intra_request_test.go ├── fuzz_test.go ├── go.mod ├── go.sum ├── goodbye.go ├── goodbye_test.go ├── header.go ├── header_test.go ├── packet.go ├── packet_buffer.go ├── packet_buffer_test.go ├── packet_stringifier.go ├── packet_stringifier_test.go ├── packet_test.go ├── picture_loss_indication.go ├── picture_loss_indication_test.go ├── rapid_resynchronization_request.go ├── rapid_resynchronization_request_test.go ├── raw_packet.go ├── raw_packet_test.go ├── receiver_estimated_maximum_bitrate.go ├── receiver_estimated_maximum_bitrate_test.go ├── receiver_report.go ├── receiver_report_test.go ├── reception_report.go ├── renovate.json ├── rfc8888.go ├── rfc8888_test.go ├── sender_report.go ├── sender_report_test.go ├── slice_loss_indication.go ├── slice_loss_indication_test.go ├── source_description.go ├── source_description_test.go ├── testdata/ │ └── fuzz/ │ └── FuzzUnmarshal/ │ ├── 0b954a73147600a3 │ ├── 16c369bd58290097 │ ├── 5eaf215c68e1ddb3 │ ├── 60753346a105d3c3 │ ├── 6366fbb9980fa33a │ └── e1a48af9f8e7db71 ├── transport_layer_cc.go ├── transport_layer_cc_test.go ├── transport_layer_nack.go ├── transport_layer_nack_test.go ├── util.go └── util_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/.gitignore ================================================ # SPDX-FileCopyrightText: 2026 The Pion community # SPDX-License-Identifier: MIT .goassets ================================================ FILE: .github/fetch-scripts.sh ================================================ #!/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: 2026 The Pion community # SPDX-License-Identifier: MIT set -eu SCRIPT_PATH="$(realpath "$(dirname "$0")")" GOASSETS_PATH="${SCRIPT_PATH}/.goassets" GOASSETS_REF=${GOASSETS_REF:-main} 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 ================================================ FILE: .github/install-hooks.sh ================================================ #!/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: 2026 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" ================================================ FILE: .github/workflows/api.yaml ================================================ # # 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: 2026 The Pion community # SPDX-License-Identifier: MIT name: API on: pull_request: jobs: check: uses: pion/.goassets/.github/workflows/api.reusable.yml@main ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # # 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: 2026 The Pion community # SPDX-License-Identifier: MIT name: CodeQL on: workflow_dispatch: schedule: - cron: '23 5 * * 0' pull_request: branches: - main paths: - '**.go' jobs: analyze: uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@main ================================================ FILE: .github/workflows/fuzz.yaml ================================================ # # 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: 2026 The Pion community # SPDX-License-Identifier: MIT name: Fuzz on: push: branches: - main schedule: - cron: "0 */8 * * *" jobs: fuzz: uses: pion/.goassets/.github/workflows/fuzz.reusable.yml@main with: go-version: "1.25" # auto-update/latest-go-version fuzz-time: "60s" ================================================ FILE: .github/workflows/lint.yaml ================================================ # # 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: 2026 The Pion community # SPDX-License-Identifier: MIT name: Lint on: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/lint.reusable.yml@main with: golangci-lint-version: v2.10.1 ================================================ FILE: .github/workflows/release.yml ================================================ # # 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: 2026 The Pion community # SPDX-License-Identifier: MIT name: Release on: push: tags: - 'v*' jobs: release: uses: pion/.goassets/.github/workflows/release.reusable.yml@main with: go-version: "1.25" # auto-update/latest-go-version ================================================ FILE: .github/workflows/renovate-go-sum-fix.yaml ================================================ # # 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: 2026 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@main secrets: token: ${{ secrets.PIONBOT_PRIVATE_KEY }} ================================================ FILE: .github/workflows/reuse.yml ================================================ # # 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: 2026 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@main ================================================ FILE: .github/workflows/test.yaml ================================================ # # 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: 2026 The Pion community # SPDX-License-Identifier: MIT name: Test on: push: branches: - main pull_request: jobs: test: uses: pion/.goassets/.github/workflows/test.reusable.yml@main 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@main 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@main 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@main 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@main with: go-version: "1.25" # auto-update/latest-go-version secrets: inherit ================================================ FILE: .github/workflows/tidy-check.yaml ================================================ # # 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: 2026 The Pion community # SPDX-License-Identifier: MIT name: Go mod tidy on: pull_request: push: branches: - main jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@main with: go-version: "1.25" # auto-update/latest-go-version ================================================ FILE: .gitignore ================================================ # SPDX-FileCopyrightText: 2026 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 ================================================ FILE: .golangci.yml ================================================ # SPDX-FileCopyrightText: 2026 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 - modernize # Replace and suggests simplifications to code - 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 issues: max-issues-per-linter: 0 max-same-issues: 0 ================================================ FILE: .goreleaser.yml ================================================ # SPDX-FileCopyrightText: 2026 The Pion community # SPDX-License-Identifier: MIT builds: - skip: true ================================================ FILE: .reuse/dep5 ================================================ Format: 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: 2026 The Pion community License: MIT Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt Copyright: 2026 The Pion community License: CC0-1.0 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2026 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. ================================================ FILE: LICENSES/CC0-1.0.txt ================================================ Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. ================================================ FILE: LICENSES/MIT.txt ================================================ MIT 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. ================================================ FILE: README.md ================================================


Pion RTCP

A Go implementation of RTCP

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


See [DESIGN.md](DESIGN.md) for an overview of features and future goals. ### 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 ================================================ FILE: application_defined.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" ) // ApplicationDefined represents an RTCP application-defined packet. type ApplicationDefined struct { SubType uint8 SSRC uint32 Name string Data []byte } // DestinationSSRC returns the SSRC value for this packet. func (a ApplicationDefined) DestinationSSRC() []uint32 { return []uint32{a.SSRC} } // Marshal serializes the application-defined struct into a byte slice with padding. func (a ApplicationDefined) Marshal() ([]byte, error) { dataLength := len(a.Data) if dataLength > 0xFFFF-12 { return nil, errAppDefinedDataTooLarge } if len(a.Name) != 4 { return nil, errAppDefinedInvalidName } // Calculate the padding size to be added to make the packet length a multiple of 4 bytes. paddingSize := 4 - (dataLength % 4) if paddingSize == 4 { paddingSize = 0 } packetSize := a.MarshalSize() header := Header{ Type: TypeApplicationDefined, Length: uint16((packetSize / 4) - 1), //nolint:gosec // G115 Padding: paddingSize != 0, Count: a.SubType, } headerBytes, err := header.Marshal() if err != nil { return nil, err } rawPacket := make([]byte, packetSize) copy(rawPacket, headerBytes) binary.BigEndian.PutUint32(rawPacket[4:8], a.SSRC) copy(rawPacket[8:12], a.Name) copy(rawPacket[12:], a.Data) // Add padding if necessary. if paddingSize > 0 { for i := 0; i < paddingSize; i++ { rawPacket[12+dataLength+i] = byte(paddingSize) } } return rawPacket, nil } // Unmarshal parses the given raw packet into an application-defined struct, handling padding. func (a *ApplicationDefined) Unmarshal(rawPacket []byte) error { /* 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P| subtype | PT=APP=204 | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC/CSRC | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | name (ASCII) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | application-dependent data ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ header := Header{} err := header.Unmarshal(rawPacket) if err != nil { return err } if len(rawPacket) < 12 { return errPacketTooShort } if int(header.Length+1)*4 != len(rawPacket) { return errAppDefinedInvalidLength } a.SubType = header.Count a.SSRC = binary.BigEndian.Uint32(rawPacket[4:8]) a.Name = string(rawPacket[8:12]) // Check for padding. paddingSize := 0 if header.Padding { paddingSize = int(rawPacket[len(rawPacket)-1]) if paddingSize > len(rawPacket)-12 { return errWrongPadding } } a.Data = rawPacket[12 : len(rawPacket)-paddingSize] return nil } // MarshalSize returns the size of the packet once marshaled. func (a *ApplicationDefined) MarshalSize() int { dataLength := len(a.Data) // Calculate the padding size to be added to make the packet length a multiple of 4 bytes. paddingSize := 4 - (dataLength % 4) if paddingSize == 4 { paddingSize = 0 } return 12 + dataLength + paddingSize } ================================================ FILE: application_defined_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) func TestTApplicationPacketUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want ApplicationDefined WantError error }{ { Name: "valid", Data: []byte{ // Application Packet Type + Length(0x0003) 0x80, 0xcc, 0x00, 0x03, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCD' 0x41, 0x42, 0x43, 0x44, }, Want: ApplicationDefined{ SubType: 0, SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44}, }, }, { Name: "validCustomSsubType", Data: []byte{ // Application Packet Type (SubType 31) + Length(0x0003) 0x9f, 0xcc, 0x00, 0x03, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCD' 0x41, 0x42, 0x43, 0x44, }, Want: ApplicationDefined{ SubType: 31, SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44}, }, }, { Name: "validWithPadding", Data: []byte{ // Application Packet Type + Length(0x0002) (0xA0 has padding bit set) 0xA0, 0xcc, 0x00, 0x04, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCDE' 0x41, 0x42, 0x43, 0x44, 0x45, // 3 bytes padding as packet length must be a division of 4 0x03, 0x03, 0x03, }, Want: ApplicationDefined{ SubType: 0, SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44, 0x45}, }, }, { Name: "invalidAppPacketLengthField", Data: []byte{ // Application Packet Type + invalid Length(0x00FF) 0x80, 0xcc, 0x00, 0xFF, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCD' 0x41, 0x42, 0x43, 0x44, }, WantError: errAppDefinedInvalidLength, }, { Name: "invalidPacketLengthTooShort", Data: []byte{ // Application Packet Type + Length(0x0002). Total packet length is less than 12 bytes 0x80, 0xcc, 0x00, 0x2, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='SUI' 0x53, 0x55, 0x49, }, WantError: errPacketTooShort, }, { Name: "wrongPaddingSize", Data: []byte{ // Application Packet Type + Length(0x0002) (0xA0 has padding bit set) 0xA0, 0xcc, 0x00, 0x04, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCDE' 0x41, 0x42, 0x43, 0x44, 0x45, // 3 bytes padding as packet length must be a division of 4 0x03, 0x03, 0x09, // last byte has padding size 0x09 which is more than the data + padding bytes }, WantError: errWrongPadding, }, { Name: "invalidHeader", Data: []byte{ // Application Packet Type + invalid Length(0x00FF) 0xFF, }, WantError: errPacketTooShort, }, } { var apk ApplicationDefined err := apk.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, apk, "Unmarshal %q", test.Name) // Check SSRC is matching assert.Equalf(t, uint32(0x4baae1ab), apk.SSRC, "%q SSRC mismatch", test.Name) assert.Equalf(t, uint32(0x4baae1ab), apk.DestinationSSRC()[0], "%q DestinationSSRC mismatch", test.Name) } } func TestTApplicationPacketMarshal(t *testing.T) { for _, test := range []struct { Name string Want []byte Packet ApplicationDefined WantError error }{ { Name: "valid", Want: []byte{ // Application Packet Type + Length(0x0003) 0x80, 0xcc, 0x00, 0x03, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCD' 0x41, 0x42, 0x43, 0x44, }, Packet: ApplicationDefined{ SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44}, }, }, { Name: "validCustomSubType", Want: []byte{ // Application Packet Type (SubType 31) + Length(0x0003) 0x9f, 0xcc, 0x00, 0x03, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCD' 0x41, 0x42, 0x43, 0x44, }, Packet: ApplicationDefined{ SubType: 31, SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44}, }, }, { Name: "validWithPadding", Want: []byte{ // Application Packet Type + Length(0x0002) (0xA0 has padding bit set) 0xA0, 0xcc, 0x00, 0x04, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCDE' 0x41, 0x42, 0x43, 0x44, 0x45, // 3 bytes padding as packet length must be a division of 4 0x03, 0x03, 0x03, }, Packet: ApplicationDefined{ SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44, 0x45}, }, }, { Name: "invalidDataTooLarge", WantError: errAppDefinedDataTooLarge, Packet: ApplicationDefined{ SSRC: 0x4baae1ab, Name: "NAME", Data: make([]byte, 0xFFFF-12+1), // total max packet size is 0xFFFF including header and other fields. }, }, { Name: "invalidName", WantError: errAppDefinedInvalidName, Packet: ApplicationDefined{ SSRC: 0x4baae1ab, Name: "NOT4CHARS", Data: []byte{0x41, 0x42, 0x43, 0x44}, }, }, { Name: "InvalidSubType", WantError: errInvalidHeader, Packet: ApplicationDefined{ SubType: 32, // Must be up to 31 SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44}, }, }, } { rawPacket, err := test.Packet.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, rawPacket, "Marshal %q", test.Name) marshalSize := test.Packet.MarshalSize() assert.Equalf(t, marshalSize, len(rawPacket), "MarshalSize %q", test.Name) } } ================================================ FILE: codecov.yml ================================================ # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # SPDX-FileCopyrightText: 2026 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/**/*" ================================================ FILE: compound_packet.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "fmt" "strings" ) // A CompoundPacket is a collection of RTCP packets transmitted as a single packet with // the underlying protocol (for example UDP). // // To maximize the resolution of receiption statistics, the first Packet in a CompoundPacket // must always be either a SenderReport or a ReceiverReport. This is true even if no data // has been sent or received, in which case an empty ReceiverReport must be sent, and even // if the only other RTCP packet in the compound packet is a Goodbye. // // Next, a SourceDescription containing a CNAME item must be included in each CompoundPacket // to identify the source and to begin associating media for purposes such as lip-sync. // // Other RTCP packet types may follow in any order. Packet types may appear more than once. type CompoundPacket []Packet // Validate returns an error if this is not an RFC-compliant CompoundPacket. // //nolint:cyclop func (c CompoundPacket) Validate() error { if len(c) == 0 { return errEmptyCompound } // SenderReport and ReceiverReport are the only types that // are allowed to be the first packet in a compound datagram switch c[0].(type) { case *SenderReport, *ReceiverReport: // ok default: return errBadFirstPacket } for _, pkt := range c[1:] { switch p := pkt.(type) { // If the number of RecetpionReports exceeds 31 additional ReceiverReports // can be included here. case *ReceiverReport: continue // A SourceDescription containing a CNAME must be included in every // CompoundPacket. case *SourceDescription: var hasCNAME bool for _, c := range p.Chunks { for _, it := range c.Items { if it.Type == SDESCNAME { hasCNAME = true } } } if !hasCNAME { return errMissingCNAME } return nil // Other packets are not permitted before the CNAME default: return errPacketBeforeCNAME } } // CNAME never reached return errMissingCNAME } // CNAME returns the CNAME that *must* be present in every CompoundPacket. func (c CompoundPacket) CNAME() (string, error) { var err error if len(c) < 1 { return "", errEmptyCompound } for _, pkt := range c[1:] { sdes, ok := pkt.(*SourceDescription) if ok { for _, c := range sdes.Chunks { for _, it := range c.Items { if it.Type == SDESCNAME { return it.Text, err } } } } else { _, ok := pkt.(*ReceiverReport) if !ok { err = errPacketBeforeCNAME } } } return "", errMissingCNAME } // Marshal encodes the CompoundPacket as binary. func (c CompoundPacket) Marshal() ([]byte, error) { if err := c.Validate(); err != nil { return nil, err } p := []Packet(c) return Marshal(p) } // MarshalSize returns the size of the packet once marshaled. func (c CompoundPacket) MarshalSize() int { l := 0 for _, p := range c { l += p.MarshalSize() } return l } // Unmarshal decodes a CompoundPacket from binary. func (c *CompoundPacket) Unmarshal(rawData []byte) error { out := make(CompoundPacket, 0) for len(rawData) != 0 { p, processed, err := unmarshal(rawData) if err != nil { return err } out = append(out, p) rawData = rawData[processed:] } *c = out return c.Validate() } // DestinationSSRC returns the synchronization sources associated with this // CompoundPacket's reception report. func (c CompoundPacket) DestinationSSRC() []uint32 { if len(c) == 0 { return nil } return c[0].DestinationSSRC() } func (c CompoundPacket) String() string { out := "CompoundPacket\n" for _, p := range c { stringer, canString := p.(fmt.Stringer) if canString { out += stringer.String() } else { out += stringify(p) } } out = strings.TrimSuffix(strings.ReplaceAll(out, "\n", "\n\t"), "\t") return out } ================================================ FILE: compound_packet_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*CompoundPacket)(nil) // assert is a Packet func TestReadEOF(t *testing.T) { shortHeader := []byte{ 0x81, 0xc9, // missing type & len } _, err := Unmarshal(shortHeader) assert.Error(t, err) } func TestBadCompound(t *testing.T) { // trailing data! badcompound := realPacket()[:34] packets, err := Unmarshal(badcompound) assert.Error(t, err) assert.Nil(t, packets) badcompound = realPacket()[84:104] packets, err = Unmarshal(badcompound) assert.NoError(t, err) compound := CompoundPacket(packets) // this should return an error, // it violates the "must start with RR or SR" rule assert.ErrorIs(t, compound.Validate(), errBadFirstPacket) assert.Equal(t, 2, len(compound)) _, ok := compound[0].(*Goodbye) assert.True(t, ok) _, ok = compound[1].(*PictureLossIndication) assert.True(t, ok) } func TestValidPacket(t *testing.T) { cname := NewCNAMESourceDescription(1234, "cname") for _, test := range []struct { Name string Packet CompoundPacket Err error }{ { Name: "empty", Packet: CompoundPacket{}, Err: errEmptyCompound, }, { Name: "no cname", Packet: CompoundPacket{ &SenderReport{}, }, Err: errMissingCNAME, }, { Name: "just BYE", Packet: CompoundPacket{ &Goodbye{}, }, Err: errBadFirstPacket, }, { Name: "SDES / no cname", Packet: CompoundPacket{ &SenderReport{}, &SourceDescription{}, }, Err: errMissingCNAME, }, { Name: "just SR", Packet: CompoundPacket{ &SenderReport{}, cname, }, Err: nil, }, { Name: "multiple SRs", Packet: CompoundPacket{ &SenderReport{}, &SenderReport{}, cname, }, Err: errPacketBeforeCNAME, }, { Name: "just RR", Packet: CompoundPacket{ &ReceiverReport{}, cname, }, Err: nil, }, { Name: "multiple RRs", Packet: CompoundPacket{ &ReceiverReport{}, &ReceiverReport{}, cname, }, Err: nil, }, { Name: "goodbye", Packet: CompoundPacket{ &ReceiverReport{}, cname, &Goodbye{}, }, Err: nil, }, } { assert.ErrorIsf(t, test.Packet.Validate(), test.Err, "Validate(%s)", test.Name) } } func TestCNAME(t *testing.T) { cname := NewCNAMESourceDescription(1234, "cname") for _, test := range []struct { Name string Packet CompoundPacket Err error Text string }{ { Name: "no cname", Packet: CompoundPacket{ &SenderReport{}, }, Err: errMissingCNAME, }, { Name: "SDES / no cname", Packet: CompoundPacket{ &SenderReport{}, &SourceDescription{}, }, Err: errMissingCNAME, }, { Name: "just SR", Packet: CompoundPacket{ &SenderReport{}, cname, }, Err: nil, Text: "cname", }, { Name: "multiple SRs", Packet: CompoundPacket{ &SenderReport{}, &SenderReport{}, cname, }, Err: errPacketBeforeCNAME, Text: "cname", }, { Name: "just RR", Packet: CompoundPacket{ &ReceiverReport{}, cname, }, Err: nil, Text: "cname", }, { Name: "multiple RRs", Packet: CompoundPacket{ &ReceiverReport{}, &ReceiverReport{}, cname, }, Err: nil, Text: "cname", }, { Name: "goodbye", Packet: CompoundPacket{ &ReceiverReport{}, cname, &Goodbye{}, }, Err: nil, Text: "cname", }, } { assert.ErrorIsf(t, test.Packet.Validate(), test.Err, "Validate(%s)", test.Name) name, err := test.Packet.CNAME() assert.ErrorIsf(t, err, test.Err, "CNAME(%s)", test.Name) assert.Equalf(t, test.Text, name, "CNAME(%s)", test.Name) } } func TestCompoundPacketRoundTrip(t *testing.T) { cname := NewCNAMESourceDescription(1234, "cname") for _, test := range []struct { Name string Packet CompoundPacket Err error }{ { Name: "bye", Packet: CompoundPacket{ &ReceiverReport{}, cname, &Goodbye{ Sources: []uint32{1234}, }, }, }, { Name: "no cname", Packet: CompoundPacket{ &ReceiverReport{}, }, Err: errMissingCNAME, }, } { data, err := test.Packet.Marshal() assert.ErrorIsf(t, err, test.Err, "Marshal(%v)", test.Name) if err != nil { continue } var c CompoundPacket assert.NoErrorf(t, c.Unmarshal(data), "Unmarshal(%v)", test.Name) data2, err := c.Marshal() assert.NoErrorf(t, err, "Marshal(%v)", test.Name) assert.Equalf(t, data, data2, "Marshal(%v) mismatch", test.Name) } } ================================================ FILE: doc.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT /* Package rtcp implements encoding and decoding of RTCP packets according to RFCs 3550 and 5506. RTCP is a sister protocol of the Real-time Transport Protocol (RTP). Its basic functionality and packet structure is defined in RFC 3550. RTCP provides out-of-band statistics and control information for an RTP session. It partners with RTP in the delivery and packaging of multimedia data, but does not transport any media data itself. The primary function of RTCP is to provide feedback on the quality of service (QoS) in media distribution by periodically sending statistics information such as transmitted octet and packet counts, packet loss, packet delay variation, and round-trip delay time to participants in a streaming multimedia session. An application may use this information to control quality of service parameters, perhaps by limiting flow, or using a different codec. Decoding RTCP packets: pkts, err := rtcp.Unmarshal(rtcpData) // ... for _, pkt := range pkts { switch p := pkt.(type) { case *rtcp.CompoundPacket: ... case *rtcp.PictureLossIndication: ... default: ... } } Encoding RTCP packets: pkt := &rtcp.PictureLossIndication{ SenderSSRC: senderSSRC, MediaSSRC: mediaSSRC } pliData, err := pkt.Marshal() // ... */ package rtcp ================================================ FILE: errors.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import "errors" var ( errWrongMarshalSize = errors.New("rtcp: wrong marshal size") errInvalidTotalLost = errors.New("rtcp: invalid total lost count") errInvalidHeader = errors.New("rtcp: invalid header") errEmptyCompound = errors.New("rtcp: empty compound packet") errBadFirstPacket = errors.New("rtcp: first packet in compound must be SR or RR") errMissingCNAME = errors.New("rtcp: compound missing SourceDescription with CNAME") errPacketBeforeCNAME = errors.New("rtcp: feedback packet seen before CNAME") errTooManySSRCs = errors.New("rtcp: too many SSRCs") errTooManyReports = errors.New("rtcp: too many reports") errTooManyChunks = errors.New("rtcp: too many chunks") errTooManySources = errors.New("rtcp: too many sources") errPacketTooShort = errors.New("rtcp: packet too short") errWrongType = errors.New("rtcp: wrong packet type") errSDESTextTooLong = errors.New("rtcp: sdes must be < 255 octets long") errSDESMissingType = errors.New("rtcp: sdes item missing type") errReasonTooLong = errors.New("rtcp: reason must be < 255 octets long") errBadVersion = errors.New("rtcp: invalid packet version") errBadLength = errors.New("rtcp: invalid packet length") errWrongPadding = errors.New("rtcp: invalid padding value") errWrongFeedbackType = errors.New("rtcp: wrong feedback message type") errWrongPayloadType = errors.New("rtcp: wrong payload type") errHeaderTooSmall = errors.New("rtcp: header length is too small") errSSRCMustBeZero = errors.New("rtcp: media SSRC must be 0") errMissingREMBidentifier = errors.New("missing REMB identifier") errSSRCNumAndLengthMismatch = errors.New("SSRC num and length do not match") errInvalidSizeOrStartIndex = errors.New("invalid size or startIndex") errInvalidBitrate = errors.New("invalid bitrate") errWrongChunkType = errors.New("rtcp: wrong chunk type") errBadStructMemberType = errors.New("rtcp: struct contains unexpected member type") errBadReadParameter = errors.New("rtcp: cannot read into non-pointer") errAppDefinedInvalidLength = errors.New("rtcp: application defined type invalid length") errAppDefinedDataTooLarge = errors.New("rtcp: application defined data is too large") errAppDefinedInvalidName = errors.New("rtcp: application defined name must be 4 ASCII chars") ) ================================================ FILE: extended_report.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "fmt" ) // The ExtendedReport packet is an Implementation of RTCP Extended // Reports defined in RFC 3611. It is used to convey detailed // information about an RTP stream. Each packet contains one or // more report blocks, each of which conveys a different kind of // information. // // 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 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |V=2|P|reserved | PT=XR=207 | length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // : report blocks : // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type ExtendedReport struct { SenderSSRC uint32 `fmt:"0x%X"` Reports []ReportBlock } // ReportBlock represents a single report within an ExtendedReport // packet. type ReportBlock interface { DestinationSSRC() []uint32 setupBlockHeader() unpackBlockHeader() } // TypeSpecificField as described in RFC 3611 section 4.5. In typical // cases, users of ExtendedReports shouldn't need to access this, // and should instead use the corresponding fields in the actual // report blocks themselves. type TypeSpecificField uint8 // XRHeader defines the common fields that must appear at the start // of each report block. In typical cases, users of ExtendedReports // shouldn't need to access this. For locally-constructed report // blocks, these values will not be accurate until the corresponding // packet is marshaled. type XRHeader struct { BlockType BlockTypeType TypeSpecific TypeSpecificField `fmt:"0x%X"` BlockLength uint16 } // BlockTypeType specifies the type of report in a report block. type BlockTypeType uint8 // Extended Report block types from RFC 3611. const ( LossRLEReportBlockType = 1 // RFC 3611, section 4.1 DuplicateRLEReportBlockType = 2 // RFC 3611, section 4.2 PacketReceiptTimesReportBlockType = 3 // RFC 3611, section 4.3 ReceiverReferenceTimeReportBlockType = 4 // RFC 3611, section 4.4 DLRRReportBlockType = 5 // RFC 3611, section 4.5 StatisticsSummaryReportBlockType = 6 // RFC 3611, section 4.6 VoIPMetricsReportBlockType = 7 // RFC 3611, section 4.7 ) // String converts the Extended report block types into readable strings. func (t BlockTypeType) String() string { switch t { case LossRLEReportBlockType: return "LossRLEReportBlockType" case DuplicateRLEReportBlockType: return "DuplicateRLEReportBlockType" case PacketReceiptTimesReportBlockType: return "PacketReceiptTimesReportBlockType" case ReceiverReferenceTimeReportBlockType: return "ReceiverReferenceTimeReportBlockType" case DLRRReportBlockType: return "DLRRReportBlockType" case StatisticsSummaryReportBlockType: return "StatisticsSummaryReportBlockType" case VoIPMetricsReportBlockType: return "VoIPMetricsReportBlockType" } return fmt.Sprintf("invalid value %d", t) } // rleReportBlock defines the common structure used by both // Loss RLE report blocks (RFC 3611 §4.1) and Duplicate RLE // report blocks (RFC 3611 §4.2). // // 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 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | BT = 1 or 2 | rsvd. | T | block length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of source | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | begin_seq | end_seq | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | chunk 1 | chunk 2 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // : ... : // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | chunk n-1 | chunk n | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type rleReportBlock struct { XRHeader T uint8 `encoding:"omit"` SSRC uint32 `fmt:"0x%X"` BeginSeq uint16 EndSeq uint16 Chunks []Chunk } // Chunk as defined in RFC 3611, section 4.1. These represent information // about packet losses and packet duplication. They have three representations: // // Run Length Chunk: // // 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |C|R| run length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // // Bit Vector Chunk: // // 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |C| bit vector | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // // Terminating Null Chunk: // // 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0| // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ type Chunk uint16 // LossRLEReportBlock is used to report information about packet // losses, as described in RFC 3611, section 4.1. type LossRLEReportBlock rleReportBlock // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *LossRLEReportBlock) DestinationSSRC() []uint32 { return []uint32{b.SSRC} } func (b *LossRLEReportBlock) setupBlockHeader() { b.XRHeader.BlockType = LossRLEReportBlockType b.XRHeader.TypeSpecific = TypeSpecificField(b.T & 0x0F) b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *LossRLEReportBlock) unpackBlockHeader() { b.T = uint8(b.XRHeader.TypeSpecific) & 0x0F } // DuplicateRLEReportBlock is used to report information about packet // duplication, as described in RFC 3611, section 4.1. type DuplicateRLEReportBlock rleReportBlock // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *DuplicateRLEReportBlock) DestinationSSRC() []uint32 { return []uint32{b.SSRC} } func (b *DuplicateRLEReportBlock) setupBlockHeader() { b.XRHeader.BlockType = DuplicateRLEReportBlockType b.XRHeader.TypeSpecific = TypeSpecificField(b.T & 0x0F) b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *DuplicateRLEReportBlock) unpackBlockHeader() { b.T = uint8(b.XRHeader.TypeSpecific) & 0x0F } // ChunkType enumerates the three kinds of chunks described in RFC 3611 section 4.1. type ChunkType uint8 // These are the valid values that ChunkType can assume. const ( RunLengthChunkType = 0 BitVectorChunkType = 1 TerminatingNullChunkType = 2 ) func (c Chunk) String() string { switch c.Type() { case RunLengthChunkType: runType, _ := c.RunType() return fmt.Sprintf("[RunLength type=%d, length=%d]", runType, c.Value()) case BitVectorChunkType: return fmt.Sprintf("[BitVector 0b%015b]", c.Value()) case TerminatingNullChunkType: return "[TerminatingNull]" } return fmt.Sprintf("[0x%X]", uint16(c)) } // Type returns the ChunkType that this Chunk represents. func (c Chunk) Type() ChunkType { if c == 0 { return TerminatingNullChunkType } return ChunkType(c >> 15) //nolint:gosec // G115 } // RunType returns the RunType that this Chunk represents. It is // only valid if ChunkType is RunLengthChunkType. func (c Chunk) RunType() (uint, error) { if c.Type() != RunLengthChunkType { return 0, errWrongChunkType } return uint((c >> 14) & 0x01), nil } // Value returns the value represented in this Chunk. func (c Chunk) Value() uint { switch c.Type() { case RunLengthChunkType: return uint(c & 0x3FFF) case BitVectorChunkType: return uint(c & 0x7FFF) case TerminatingNullChunkType: return 0 } return uint(c) } // PacketReceiptTimesReportBlock represents a Packet Receipt Times // report block, as described in RFC 3611 section 4.3. // // 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 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | BT=3 | rsvd. | T | block length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of source | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | begin_seq | end_seq | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Receipt time of packet begin_seq | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Receipt time of packet (begin_seq + 1) mod 65536 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // : ... : // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Receipt time of packet (end_seq - 1) mod 65536 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type PacketReceiptTimesReportBlock struct { XRHeader T uint8 `encoding:"omit"` SSRC uint32 `fmt:"0x%X"` BeginSeq uint16 EndSeq uint16 ReceiptTime []uint32 } // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *PacketReceiptTimesReportBlock) DestinationSSRC() []uint32 { return []uint32{b.SSRC} } func (b *PacketReceiptTimesReportBlock) setupBlockHeader() { b.XRHeader.BlockType = PacketReceiptTimesReportBlockType b.XRHeader.TypeSpecific = TypeSpecificField(b.T & 0x0F) b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *PacketReceiptTimesReportBlock) unpackBlockHeader() { b.T = uint8(b.XRHeader.TypeSpecific) & 0x0F } // ReceiverReferenceTimeReportBlock encodes a Receiver Reference Time // report block as described in RFC 3611 section 4.4. // // 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 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | BT=4 | reserved | block length = 2 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | NTP timestamp, most significant word | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | NTP timestamp, least significant word | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type ReceiverReferenceTimeReportBlock struct { XRHeader NTPTimestamp uint64 } // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *ReceiverReferenceTimeReportBlock) DestinationSSRC() []uint32 { return []uint32{} } func (b *ReceiverReferenceTimeReportBlock) setupBlockHeader() { b.XRHeader.BlockType = ReceiverReferenceTimeReportBlockType b.XRHeader.TypeSpecific = 0 b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *ReceiverReferenceTimeReportBlock) unpackBlockHeader() { } // DLRRReportBlock encodes a DLRR Report Block as described in // RFC 3611 section 4.5. // // 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 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | BT=5 | reserved | block length | // +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ // | SSRC_1 (SSRC of first receiver) | sub- // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ block // | last RR (LRR) | 1 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | delay since last RR (DLRR) | // +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ // | SSRC_2 (SSRC of second receiver) | sub- // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ block // : ... : 2 // +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ // . type DLRRReportBlock struct { XRHeader Reports []DLRRReport } // DLRRReport encodes a single report inside a DLRRReportBlock. type DLRRReport struct { SSRC uint32 `fmt:"0x%X"` LastRR uint32 DLRR uint32 } // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *DLRRReportBlock) DestinationSSRC() []uint32 { ssrc := make([]uint32, len(b.Reports)) for i, r := range b.Reports { ssrc[i] = r.SSRC } return ssrc } func (b *DLRRReportBlock) setupBlockHeader() { b.XRHeader.BlockType = DLRRReportBlockType b.XRHeader.TypeSpecific = 0 b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *DLRRReportBlock) unpackBlockHeader() { } // StatisticsSummaryReportBlock encodes a Statistics Summary Report // Block as described in RFC 3611, section 4.6. // // 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 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | BT=6 |L|D|J|ToH|rsvd.| block length = 9 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of source | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | begin_seq | end_seq | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | lost_packets | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | dup_packets | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | min_jitter | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | max_jitter | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | mean_jitter | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | dev_jitter | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | min_ttl_or_hl | max_ttl_or_hl |mean_ttl_or_hl | dev_ttl_or_hl | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type StatisticsSummaryReportBlock struct { XRHeader LossReports bool `encoding:"omit"` DuplicateReports bool `encoding:"omit"` JitterReports bool `encoding:"omit"` TTLorHopLimit TTLorHopLimitType `encoding:"omit"` SSRC uint32 `fmt:"0x%X"` BeginSeq uint16 EndSeq uint16 LostPackets uint32 DupPackets uint32 MinJitter uint32 MaxJitter uint32 MeanJitter uint32 DevJitter uint32 MinTTLOrHL uint8 MaxTTLOrHL uint8 MeanTTLOrHL uint8 DevTTLOrHL uint8 } // TTLorHopLimitType encodes values for the ToH field in // a StatisticsSummaryReportBlock. type TTLorHopLimitType uint8 // Values for TTLorHopLimitType. const ( ToHMissing = 0 ToHIPv4 = 1 ToHIPv6 = 2 ) func (t TTLorHopLimitType) String() string { switch t { case ToHMissing: return "[ToH Missing]" case ToHIPv4: return "[ToH = IPv4]" case ToHIPv6: return "[ToH = IPv6]" } return "[ToH Flag is Invalid]" } // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *StatisticsSummaryReportBlock) DestinationSSRC() []uint32 { return []uint32{b.SSRC} } func (b *StatisticsSummaryReportBlock) setupBlockHeader() { b.XRHeader.BlockType = StatisticsSummaryReportBlockType b.XRHeader.TypeSpecific = 0x00 if b.LossReports { b.XRHeader.TypeSpecific |= 0x80 } if b.DuplicateReports { b.XRHeader.TypeSpecific |= 0x40 } if b.JitterReports { b.XRHeader.TypeSpecific |= 0x20 } b.XRHeader.TypeSpecific |= TypeSpecificField((b.TTLorHopLimit & 0x03) << 3) b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *StatisticsSummaryReportBlock) unpackBlockHeader() { b.LossReports = b.XRHeader.TypeSpecific&0x80 != 0 b.DuplicateReports = b.XRHeader.TypeSpecific&0x40 != 0 b.JitterReports = b.XRHeader.TypeSpecific&0x20 != 0 b.TTLorHopLimit = TTLorHopLimitType((b.XRHeader.TypeSpecific & 0x18) >> 3) } // VoIPMetricsReportBlock encodes a VoIP Metrics Report Block as described // in RFC 3611, section 4.7. // // 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 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | BT=7 | reserved | block length = 8 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of source | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | loss rate | discard rate | burst density | gap density | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | burst duration | gap duration | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | round trip delay | end system delay | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | signal level | noise level | RERL | Gmin | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | R factor | ext. R factor | MOS-LQ | MOS-CQ | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | RX config | reserved | JB nominal | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | JB maximum | JB abs max | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type VoIPMetricsReportBlock struct { XRHeader SSRC uint32 `fmt:"0x%X"` LossRate uint8 DiscardRate uint8 BurstDensity uint8 GapDensity uint8 BurstDuration uint16 GapDuration uint16 RoundTripDelay uint16 EndSystemDelay uint16 SignalLevel uint8 NoiseLevel uint8 RERL uint8 Gmin uint8 RFactor uint8 ExtRFactor uint8 MOSLQ uint8 MOSCQ uint8 RXConfig uint8 _ uint8 JBNominal uint16 JBMaximum uint16 JBAbsMax uint16 } // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *VoIPMetricsReportBlock) DestinationSSRC() []uint32 { return []uint32{b.SSRC} } func (b *VoIPMetricsReportBlock) setupBlockHeader() { b.XRHeader.BlockType = VoIPMetricsReportBlockType b.XRHeader.TypeSpecific = 0 b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *VoIPMetricsReportBlock) unpackBlockHeader() { } // UnknownReportBlock is used to store bytes for any report block // that has an unknown Report Block Type. type UnknownReportBlock struct { XRHeader Bytes []byte } // DestinationSSRC returns an array of SSRC values that this report block refers to. func (b *UnknownReportBlock) DestinationSSRC() []uint32 { return []uint32{} } func (b *UnknownReportBlock) setupBlockHeader() { b.XRHeader.BlockLength = uint16(wireSize(b)/4 - 1) //nolint:gosec // G115 } func (b *UnknownReportBlock) unpackBlockHeader() { } // MarshalSize returns the size of the packet once marshaled. func (x ExtendedReport) MarshalSize() int { return wireSize(x) } // Marshal encodes the ExtendedReport in binary. func (x ExtendedReport) Marshal() ([]byte, error) { for _, p := range x.Reports { p.setupBlockHeader() } length := wireSize(x) // RTCP Header header := Header{ Type: TypeExtendedReport, Length: uint16(length / 4), //nolint:gosec // G115 } headerBuffer, err := header.Marshal() if err != nil { return []byte{}, err } length += len(headerBuffer) rawPacket := make([]byte, length) buffer := packetBuffer{bytes: rawPacket} err = buffer.write(headerBuffer) if err != nil { return []byte{}, err } err = buffer.write(x) if err != nil { return []byte{}, err } return rawPacket, nil } // Unmarshal decodes the ExtendedReport from binary. // //nolint:cyclop func (x *ExtendedReport) Unmarshal(b []byte) error { var header Header if err := header.Unmarshal(b); err != nil { return err } if header.Type != TypeExtendedReport { return errWrongType } buffer := packetBuffer{bytes: b[headerLength:]} err := buffer.read(&x.SenderSSRC) if err != nil { return err } for len(buffer.bytes) > 0 { var block ReportBlock headerBuffer := buffer xrHeader := XRHeader{} err = headerBuffer.read(&xrHeader) if err != nil { return err } switch xrHeader.BlockType { case LossRLEReportBlockType: block = new(LossRLEReportBlock) case DuplicateRLEReportBlockType: block = new(DuplicateRLEReportBlock) case PacketReceiptTimesReportBlockType: block = new(PacketReceiptTimesReportBlock) case ReceiverReferenceTimeReportBlockType: block = new(ReceiverReferenceTimeReportBlock) case DLRRReportBlockType: block = new(DLRRReportBlock) case StatisticsSummaryReportBlockType: block = new(StatisticsSummaryReportBlock) case VoIPMetricsReportBlockType: block = new(VoIPMetricsReportBlock) default: block = new(UnknownReportBlock) } // We need to limit the amount of data available to // this block to the actual length of the block blockLength := (int(xrHeader.BlockLength) + 1) * 4 blockBuffer := buffer.split(blockLength) err = blockBuffer.read(block) if err != nil { return err } block.unpackBlockHeader() x.Reports = append(x.Reports, block) } return nil } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (x *ExtendedReport) DestinationSSRC() []uint32 { ssrc := make([]uint32, 0, len(x.Reports)+1) ssrc = append(ssrc, x.SenderSSRC) for _, p := range x.Reports { ssrc = append(ssrc, p.DestinationSSRC()...) } return ssrc } func (x *ExtendedReport) String() string { return stringify(x) } ================================================ FILE: extended_report_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) // Assert that ExtendedReport is a Packet. var _ Packet = (*ExtendedReport)(nil) // Assert that all the extended report blocks implement the interface. var ( _ ReportBlock = (*LossRLEReportBlock)(nil) _ ReportBlock = (*DuplicateRLEReportBlock)(nil) _ ReportBlock = (*PacketReceiptTimesReportBlock)(nil) _ ReportBlock = (*ReceiverReferenceTimeReportBlock)(nil) _ ReportBlock = (*DLRRReportBlock)(nil) _ ReportBlock = (*StatisticsSummaryReportBlock)(nil) _ ReportBlock = (*VoIPMetricsReportBlock)(nil) _ ReportBlock = (*UnknownReportBlock)(nil) ) func testPacket() Packet { return &ExtendedReport{ SenderSSRC: 0x01020304, Reports: []ReportBlock{ &LossRLEReportBlock{ XRHeader: XRHeader{ BlockType: LossRLEReportBlockType, }, T: 12, SSRC: 0x12345689, BeginSeq: 5, EndSeq: 12, Chunks: []Chunk{ Chunk(0x4006), Chunk(0x0006), Chunk(0x8765), Chunk(0x0000), }, }, &DuplicateRLEReportBlock{ XRHeader: XRHeader{ BlockType: DuplicateRLEReportBlockType, }, T: 6, SSRC: 0x12345689, BeginSeq: 5, EndSeq: 12, Chunks: []Chunk{ Chunk(0x4123), Chunk(0x3FFF), Chunk(0xFFFF), Chunk(0x0000), }, }, &PacketReceiptTimesReportBlock{ XRHeader: XRHeader{ BlockType: PacketReceiptTimesReportBlockType, }, T: 3, SSRC: 0x98765432, BeginSeq: 15432, EndSeq: 15577, ReceiptTime: []uint32{ 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, }, }, &ReceiverReferenceTimeReportBlock{ XRHeader: XRHeader{ BlockType: ReceiverReferenceTimeReportBlockType, }, NTPTimestamp: 0x0102030405060708, }, &DLRRReportBlock{ XRHeader: XRHeader{ BlockType: DLRRReportBlockType, }, Reports: []DLRRReport{ { SSRC: 0x88888888, LastRR: 0x12345678, DLRR: 0x99999999, }, { SSRC: 0x09090909, LastRR: 0x12345678, DLRR: 0x99999999, }, { SSRC: 0x11223344, LastRR: 0x12345678, DLRR: 0x99999999, }, }, }, &StatisticsSummaryReportBlock{ XRHeader{ BlockType: StatisticsSummaryReportBlockType, }, true, true, true, ToHIPv4, 0xFEDCBA98, 0x1234, 0x5678, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x01, 0x02, 0x03, 0x04, }, &VoIPMetricsReportBlock{ XRHeader{ BlockType: VoIPMetricsReportBlockType, }, 0x89ABCDEF, 0x05, 0x06, 0x07, 0x08, 0x1111, 0x2222, 0x3333, 0x4444, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x1122, 0x3344, 0x5566, }, }, } } func encodedPacket() []byte { return []byte{ // RTP Header 0x80, 0xCF, 0x00, 0x33, // byte 0 - 3 // Sender SSRC 0x01, 0x02, 0x03, 0x04, // Loss RLE Report Block 0x01, 0x0C, 0x00, 0x04, // byte 8 - 11 // Source SSRC 0x12, 0x34, 0x56, 0x89, // Begin & End Seq 0x00, 0x05, 0x00, 0x0C, // byte 16 - 19 // Chunks 0x40, 0x06, 0x00, 0x06, 0x87, 0x65, 0x00, 0x00, // byte 24 - 27 // Duplicate RLE Report Block 0x02, 0x06, 0x00, 0x04, // Source SSRC 0x12, 0x34, 0x56, 0x89, // byte 32 - 35 // Begin & End Seq 0x00, 0x05, 0x00, 0x0C, // Chunks 0x41, 0x23, 0x3F, 0xFF, // byte 40 - 43 0xFF, 0xFF, 0x00, 0x00, // Packet Receipt Times Report Block 0x03, 0x03, 0x00, 0x07, // byte 48 - 51 // Source SSRC 0x98, 0x76, 0x54, 0x32, // Begin & End Seq 0x3C, 0x48, 0x3C, 0xD9, // byte 56 - 59 // Receipt times 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, // byte 64 - 67 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44, // byte 72 - 75 0x55, 0x55, 0x55, 0x55, // Receiver Reference Time Report 0x04, 0x00, 0x00, 0x02, // byte 80 - 83 // Timestamp 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // byte 88 - 91 // DLRR Report 0x05, 0x00, 0x00, 0x09, // SSRC 1 0x88, 0x88, 0x88, 0x88, // byte 96 - 99 // LastRR 1 0x12, 0x34, 0x56, 0x78, // DLRR 1 0x99, 0x99, 0x99, 0x99, // byte 104 - 107 // SSRC 2 0x09, 0x09, 0x09, 0x09, // LastRR 2 0x12, 0x34, 0x56, 0x78, // byte 112 - 115 // DLRR 2 0x99, 0x99, 0x99, 0x99, // SSRC 3 0x11, 0x22, 0x33, 0x44, // byte 120 - 123 // LastRR 3 0x12, 0x34, 0x56, 0x78, // DLRR 3 0x99, 0x99, 0x99, 0x99, // byte 128 - 131 // Statistics Summary Report 0x06, 0xE8, 0x00, 0x09, // SSRC 0xFE, 0xDC, 0xBA, 0x98, // byte 136 - 139 // Various statistics 0x12, 0x34, 0x56, 0x78, 0x11, 0x11, 0x11, 0x11, // byte 144 - 147 0x22, 0x22, 0x22, 0x22, 0x33, 0x33, 0x33, 0x33, // byte 152 - 155 0x44, 0x44, 0x44, 0x44, 0x55, 0x55, 0x55, 0x55, // byte 160 - 163 0x66, 0x66, 0x66, 0x66, 0x01, 0x02, 0x03, 0x04, // byte 168 - 171 // VoIP Metrics Report 0x07, 0x00, 0x00, 0x08, // SSRC 0x89, 0xAB, 0xCD, 0xEF, // byte 176 - 179 // Various statistics 0x05, 0x06, 0x07, 0x08, 0x11, 0x11, 0x22, 0x22, // byte 184 - 187 0x33, 0x33, 0x44, 0x44, 0x11, 0x22, 0x33, 0x44, // byte 192 - 195 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, // byte 200 - 203 0x33, 0x44, 0x55, 0x66, // byte 204 - 207 } } func TestEncode(t *testing.T) { expected := encodedPacket() packet := testPacket() rawPacket, err := packet.Marshal() assert.NoError(t, err) assert.Equal(t, len(expected), len(rawPacket), "Encoded message length does not match expected length") for i := range rawPacket { assert.Equalf(t, expected[i], rawPacket[i], "Byte %d of encoded packet does not match", i) } } func TestDecode(t *testing.T) { encoded := encodedPacket() expected := testPacket() // We need to make sure the header has been set up correctly // before we test for equality extendedReports, ok := expected.(*ExtendedReport) assert.True(t, ok) for _, p := range extendedReports.Reports { p.setupBlockHeader() } report := new(ExtendedReport) err := report.Unmarshal(encoded) assert.NoError(t, err) assert.Equal(t, expected, report) pktStringer, ok := expected.(fmt.Stringer) assert.True(t, ok) assert.Equal(t, report.String(), pktStringer.String(), "Decoded packet does not match expected packet") var includeSenderSSRC bool for _, ssrc := range report.DestinationSSRC() { if ssrc == report.SenderSSRC { includeSenderSSRC = true } } assert.True(t, includeSenderSSRC, "DestinationSSRC does not include the SenderSSRC") } ================================================ FILE: full_intra_request.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" "strings" ) // A FIREntry is a (SSRC, seqno) pair, as carried by FullIntraRequest. type FIREntry struct { SSRC uint32 SequenceNumber uint8 } // The FullIntraRequest packet is used to reliably request an Intra frame // in a video stream. See RFC 5104 Section 3.5.1. This is not for loss // recovery, which should use PictureLossIndication (PLI) instead. type FullIntraRequest struct { SenderSSRC uint32 MediaSSRC uint32 FIR []FIREntry } const ( firOffset = 8 ) var _ Packet = (*FullIntraRequest)(nil) // Marshal encodes the FullIntraRequest. func (p FullIntraRequest) Marshal() ([]byte, error) { rawPacket := make([]byte, firOffset+(len(p.FIR)*8)) binary.BigEndian.PutUint32(rawPacket, p.SenderSSRC) binary.BigEndian.PutUint32(rawPacket[4:], p.MediaSSRC) for i, fir := range p.FIR { binary.BigEndian.PutUint32(rawPacket[firOffset+8*i:], fir.SSRC) rawPacket[firOffset+8*i+4] = fir.SequenceNumber } h := p.Header() hData, err := h.Marshal() if err != nil { return nil, err } return append(hData, rawPacket...), nil } // Unmarshal decodes the TransportLayerNack. func (p *FullIntraRequest) Unmarshal(rawPacket []byte) error { if len(rawPacket) < (headerLength + ssrcLength) { return errPacketTooShort } var header Header if err := header.Unmarshal(rawPacket); err != nil { return err } if len(rawPacket) < (headerLength + int(4*header.Length)) { return errPacketTooShort } if header.Type != TypePayloadSpecificFeedback || header.Count != FormatFIR { return errWrongType } // The FCI field MUST contain one or more FIR entries if 4*header.Length-firOffset <= 0 || (4*header.Length)%8 != 0 { return errBadLength } p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:]) p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:]) for i := headerLength + firOffset; i < (headerLength + int(header.Length*4)); i += 8 { p.FIR = append(p.FIR, FIREntry{ binary.BigEndian.Uint32(rawPacket[i:]), rawPacket[i+4], }) } return nil } // Header returns the Header associated with this packet. func (p *FullIntraRequest) Header() Header { return Header{ Count: FormatFIR, Type: TypePayloadSpecificFeedback, Length: uint16((p.MarshalSize() / 4) - 1), //nolint:gosec // G115 } } // MarshalSize returns the size of the packet once marshaled. func (p *FullIntraRequest) MarshalSize() int { return headerLength + firOffset + len(p.FIR)*8 } func (p *FullIntraRequest) String() string { var out strings.Builder fmt.Fprintf(&out, "FullIntraRequest %x %x", p.SenderSSRC, p.MediaSSRC) for _, e := range p.FIR { fmt.Fprintf(&out, " (%x %v)", e.SSRC, e.SequenceNumber) } return out.String() } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (p *FullIntraRequest) DestinationSSRC() []uint32 { ssrcs := make([]uint32, 0, len(p.FIR)) for _, entry := range p.FIR { ssrcs = append(ssrcs, entry.SSRC) } return ssrcs } ================================================ FILE: full_intra_request_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) func TestFullIntraRequestUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want FullIntraRequest WantError error }{ { Name: "valid", Data: []byte{ // v=2, p=0, FMT=4, PSFB, len=4 0x84, 0xce, 0x00, 0x04, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x12345678 0x12, 0x34, 0x56, 0x78, // Seqno=0x42 0x42, 0x00, 0x00, 0x00, }, Want: FullIntraRequest{ SenderSSRC: 0x0, MediaSSRC: 0x4bc4fcb4, FIR: []FIREntry{ { SSRC: 0x12345678, SequenceNumber: 0x42, }, }, }, }, { Name: "also valid", Data: []byte{ // v=2, p=0, FMT=4, PSFB, len=6 0x84, 0xce, 0x00, 0x06, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x12345678 0x12, 0x34, 0x56, 0x78, // Seqno=0x42 0x42, 0x00, 0x00, 0x00, // ssrc=0x98765432 0x98, 0x76, 0x54, 0x32, // Seqno=0x57 0x57, 0x00, 0x00, 0x00, }, Want: FullIntraRequest{ SenderSSRC: 0x0, MediaSSRC: 0x4bc4fcb4, FIR: []FIREntry{ { SSRC: 0x12345678, SequenceNumber: 0x42, }, { SSRC: 0x98765432, SequenceNumber: 0x57, }, }, }, }, { Name: "packet too short", Data: []byte{ 0x00, 0x00, 0x00, 0x00, }, WantError: errPacketTooShort, }, { Name: "invalid header", Data: []byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, WantError: errBadVersion, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, FMT=4, RR, len=4 0x84, 0xc9, 0x00, 0x04, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x12345678 0x12, 0x34, 0x56, 0x78, // Seqno=0x42 0x42, 0x00, 0x00, 0x00, }, WantError: errWrongType, }, { Name: "wrong fmt", Data: []byte{ // v=2, p=0, FMT=2, PSFB, len=4 0x82, 0xce, 0x00, 0x04, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x12345678 0x12, 0x34, 0x56, 0x78, // Seqno=0x42 0x42, 0x00, 0x00, 0x00, }, WantError: errWrongType, }, { Name: "wrong length", Data: []byte{ // v=2, p=0, FMT=4, PSFB, len=3 0x84, 0xce, 0x00, 0x03, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x12345678 0x12, 0x34, 0x56, 0x78, }, WantError: errBadLength, }, } { var fir FullIntraRequest err := fir.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q rr mismatch", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, fir, "Unmarshal %q rr mismatch", test.Name) } } func TestFullIntraRequestRoundTrip(t *testing.T) { for _, test := range []struct { Name string Packet FullIntraRequest WantError error }{ { Name: "valid", Packet: FullIntraRequest{ SenderSSRC: 1, MediaSSRC: 2, FIR: []FIREntry{{ SSRC: 3, SequenceNumber: 42, }}, }, }, { Name: "also valid", Packet: FullIntraRequest{ SenderSSRC: 5000, MediaSSRC: 6000, FIR: []FIREntry{{ SSRC: 3, SequenceNumber: 57, }}, }, }, } { data, err := test.Packet.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded FullIntraRequest assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Packet, decoded, "%q rr header mismatch", test.Name) } } func TestFullIntraRequestUnmarshalHeader(t *testing.T) { for _, test := range []struct { Name string Data []byte Want Header WantError error }{ { Name: "valid header", Data: []byte{ // v=2, p=0, FMT=1, PSFB, len=4 0x84, 0xce, 0x00, 0x04, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, // ssrc=0x00000000 0x00, 0x00, 0x00, 0x00, // Seqno=0x22 0x22, 0x00, 0x00, 0x00, }, Want: Header{ Count: FormatFIR, Type: TypePayloadSpecificFeedback, Length: 4, }, }, } { var fir FullIntraRequest err := fir.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal header %q rr mismatch", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, fir.Header(), "Unmarshal header %q rr mismatch", test.Name) } } ================================================ FILE: fuzz_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" ) func FuzzUnmarshal(f *testing.F) { f.Add([]byte{}) f.Fuzz(func(_ *testing.T, data []byte) { packets, err := Unmarshal(data) if err != nil { return } for _, packet := range packets { _, err = packet.Marshal() if err != nil { return } } }) } ================================================ FILE: go.mod ================================================ module github.com/pion/rtcp go 1.24.0 require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: goodbye.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" "strings" ) // The Goodbye packet indicates that one or more sources are no longer active. type Goodbye struct { // The SSRC/CSRC identifiers that are no longer active Sources []uint32 // Optional text indicating the reason for leaving, e.g., "camera malfunction" or "RTP loop detected" Reason string } // Marshal encodes the Goodbye packet in binary. func (g Goodbye) Marshal() ([]byte, error) { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |V=2|P| SC | PT=BYE=203 | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SSRC/CSRC | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * : ... : * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * (opt) | length | reason for leaving ... * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ rawPacket := make([]byte, g.MarshalSize()) packetBody := rawPacket[headerLength:] if len(g.Sources) > countMax { return nil, errTooManySources } for i, s := range g.Sources { binary.BigEndian.PutUint32(packetBody[i*ssrcLength:], s) } if g.Reason != "" { reason := []byte(g.Reason) if len(reason) > sdesMaxOctetCount { return nil, errReasonTooLong } reasonOffset := len(g.Sources) * ssrcLength packetBody[reasonOffset] = uint8(len(reason)) //nolint:gosec // G115 copy(packetBody[reasonOffset+1:], reason) } hData, err := g.Header().Marshal() if err != nil { return nil, err } copy(rawPacket, hData) return rawPacket, nil } // Unmarshal decodes the Goodbye packet from binary. func (g *Goodbye) Unmarshal(rawPacket []byte) error { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |V=2|P| SC | PT=BYE=203 | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SSRC/CSRC | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * : ... : * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * (opt) | length | reason for leaving ... * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ var header Header if err := header.Unmarshal(rawPacket); err != nil { return err } if header.Type != TypeGoodbye { return errWrongType } if getPadding(len(rawPacket)) != 0 { return errPacketTooShort } g.Sources = make([]uint32, header.Count) reasonOffset := int(headerLength + header.Count*ssrcLength) if reasonOffset > len(rawPacket) { return errPacketTooShort } for i := 0; i < int(header.Count); i++ { offset := headerLength + i*ssrcLength g.Sources[i] = binary.BigEndian.Uint32(rawPacket[offset:]) } if reasonOffset < len(rawPacket) { reasonLen := int(rawPacket[reasonOffset]) reasonEnd := reasonOffset + 1 + reasonLen if reasonEnd > len(rawPacket) { return errPacketTooShort } g.Reason = string(rawPacket[reasonOffset+1 : reasonEnd]) } return nil } // Header returns the Header associated with this packet. func (g *Goodbye) Header() Header { return Header{ Padding: false, Count: uint8(len(g.Sources)), //nolint:gosec //G115 Type: TypeGoodbye, Length: uint16((g.MarshalSize() / 4) - 1), //nolint:gosec //G115 } } // MarshalSize returns the size of the packet once marshaled. func (g *Goodbye) MarshalSize() int { srcsLength := len(g.Sources) * ssrcLength // reason is optional reasonLength := len(g.Reason) if reasonLength > 0 { reasonLength++ } l := headerLength + srcsLength + reasonLength // align to 32-bit boundary return l + getPadding(l) } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (g *Goodbye) DestinationSSRC() []uint32 { out := make([]uint32, len(g.Sources)) copy(out, g.Sources) return out } func (g Goodbye) String() string { var out strings.Builder out.WriteString("Goodbye\n") for i, s := range g.Sources { fmt.Fprintf(&out, "\tSource %d: %x\n", i, s) } fmt.Fprintf(&out, "\tReason: %s\n", g.Reason) return out.String() } ================================================ FILE: goodbye_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "strings" "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*Goodbye)(nil) // assert is a Packet func TestGoodbyeUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want Goodbye WantError error }{ { Name: "valid", Data: []byte{ // v=2, p=0, count=1, BYE, len=12 0x81, 0xcb, 0x00, 0x0c, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // len=3, text=FOO 0x03, 0x46, 0x4f, 0x4f, }, Want: Goodbye{ Sources: []uint32{0x902f9e2e}, Reason: "FOO", }, }, { Name: "invalid octet count", Data: []byte{ // v=2, p=0, count=1, BYE, len=12 0x81, 0xcb, 0x00, 0x0c, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // len=4, text=FOO 0x04, 0x46, 0x4f, 0x4f, }, WantError: errPacketTooShort, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, count=1, SDES, len=12 0x81, 0xca, 0x00, 0x0c, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // len=3, text=FOO 0x03, 0x46, 0x4f, 0x4f, }, WantError: errWrongType, }, { Name: "short reason", Data: []byte{ // v=2, p=0, count=1, BYE, len=12 0x81, 0xcb, 0x00, 0x0c, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // len=3, text=F + padding 0x01, 0x46, 0x00, 0x00, }, Want: Goodbye{ Sources: []uint32{0x902f9e2e}, Reason: "F", }, }, { Name: "not byte aligned", Data: []byte{ // v=2, p=0, count=1, BYE, len=10 0x81, 0xcb, 0x00, 0x0a, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // len=1, text=F 0x01, 0x46, }, WantError: errPacketTooShort, }, { Name: "bad count in header", Data: []byte{ // v=2, p=0, count=2, BYE, len=8 0x82, 0xcb, 0x00, 0x0c, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, }, WantError: errPacketTooShort, }, { Name: "empty packet", Data: []byte{ // v=2, p=0, count=0, BYE, len=4 0x80, 0xcb, 0x00, 0x04, }, Want: Goodbye{ Sources: []uint32{}, Reason: "", }, }, { Name: "nil", Data: nil, WantError: errPacketTooShort, }, } { var bye Goodbye err := bye.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q bye mismatch", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, bye, "Unmarshal %q bye mismatch", test.Name) } } func TestGoodbyeRoundTrip(t *testing.T) { // a slice with enough sources to overflow an 5-bit int var tooManySources []uint32 var tooLongText strings.Builder for range 1 << 5 { tooManySources = append(tooManySources, 0x00) } for range 1 << 8 { tooLongText.WriteString("x") } for _, test := range []struct { Name string Bye Goodbye WantError error }{ { Name: "empty", Bye: Goodbye{ Sources: []uint32{}, }, }, { Name: "valid", Bye: Goodbye{ Sources: []uint32{ 0x01020304, 0x05060708, }, Reason: "because", }, }, { Name: "empty reason", Bye: Goodbye{ Sources: []uint32{0x01020304}, Reason: "", }, }, { Name: "reason no source", Bye: Goodbye{ Sources: []uint32{}, Reason: "foo", }, }, { Name: "short reason", Bye: Goodbye{ Sources: []uint32{}, Reason: "f", }, }, { Name: "count overflow", Bye: Goodbye{ Sources: tooManySources, }, WantError: errTooManySources, }, { Name: "reason too long", Bye: Goodbye{ Sources: []uint32{}, Reason: tooLongText.String(), }, WantError: errReasonTooLong, }, } { data, err := test.Bye.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var bye Goodbye assert.NoErrorf(t, bye.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Bye, bye, "%q bye round trip mismatch", test.Name) } } ================================================ FILE: header.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" ) // PacketType specifies the type of an RTCP packet. type PacketType uint8 // RTCP packet types registered with IANA. See: // // https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-4 const ( TypeSenderReport PacketType = 200 // RFC 3550, 6.4.1 TypeReceiverReport PacketType = 201 // RFC 3550, 6.4.2 TypeSourceDescription PacketType = 202 // RFC 3550, 6.5 TypeGoodbye PacketType = 203 // RFC 3550, 6.6 TypeApplicationDefined PacketType = 204 // RFC 3550, 6.7 (unimplemented) TypeTransportSpecificFeedback PacketType = 205 // RFC 4585, 6051 TypePayloadSpecificFeedback PacketType = 206 // RFC 4585, 6.3 TypeExtendedReport PacketType = 207 // RFC 3611 ) // Transport and Payload specific feedback messages overload the count field to act as a message type. // those are listed here. const ( FormatSLI uint8 = 2 FormatPLI uint8 = 1 FormatFIR uint8 = 4 FormatTLN uint8 = 1 FormatRRR uint8 = 5 FormatCCFB uint8 = 11 FormatREMB uint8 = 15 // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-5 FormatTCC uint8 = 15 ) func (p PacketType) String() string { switch p { case TypeSenderReport: return "SR" case TypeReceiverReport: return "RR" case TypeSourceDescription: return "SDES" case TypeGoodbye: return "BYE" case TypeApplicationDefined: return "APP" case TypeTransportSpecificFeedback: return "TSFB" case TypePayloadSpecificFeedback: return "PSFB" case TypeExtendedReport: return "XR" default: return string(p) } } const rtpVersion = 2 // A Header is the common header shared by all RTCP packets. type Header struct { // If the padding bit is set, this individual RTCP packet contains // some additional padding octets at the end which are not part of // the control information but are included in the length field. Padding bool // The number of reception reports, sources contained or FMT in this packet (depending on the Type) Count uint8 // The RTCP packet type for this packet Type PacketType // The length of this RTCP packet in 32-bit words minus one, // including the header and any padding. Length uint16 } const ( headerLength = 4 versionShift = 6 versionMask = 0x3 paddingShift = 5 paddingMask = 0x1 countShift = 0 countMask = 0x1f countMax = (1 << 5) - 1 ) // Marshal encodes the Header in binary. func (h Header) Marshal() ([]byte, error) { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |V=2|P| RC | PT=SR=200 | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ rawPacket := make([]byte, headerLength) rawPacket[0] |= rtpVersion << versionShift if h.Padding { rawPacket[0] |= 1 << paddingShift } if h.Count > 31 { return nil, errInvalidHeader } rawPacket[0] |= h.Count << countShift //nolint:gosec // rawPacket is created with length headerLength (4) rawPacket[1] = uint8(h.Type) //nolint:gosec // rawPacket is created with length headerLength (4) binary.BigEndian.PutUint16(rawPacket[2:], h.Length) return rawPacket, nil } // Unmarshal decodes the Header from binary. func (h *Header) Unmarshal(rawPacket []byte) error { if len(rawPacket) < headerLength { return errPacketTooShort } /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |V=2|P| RC | PT | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ version := rawPacket[0] >> versionShift & versionMask if version != rtpVersion { return errBadVersion } h.Padding = (rawPacket[0] >> paddingShift & paddingMask) > 0 h.Count = rawPacket[0] >> countShift & countMask h.Type = PacketType(rawPacket[1]) h.Length = binary.BigEndian.Uint16(rawPacket[2:]) return nil } ================================================ FILE: header_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) func TestHeaderUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want Header WantError error }{ { Name: "valid", Data: []byte{ // v=2, p=0, count=1, RR, len=7 0x81, 0xc9, 0x00, 0x07, }, Want: Header{ Padding: false, Count: 1, Type: TypeReceiverReport, Length: 7, }, }, { Name: "also valid", Data: []byte{ // v=2, p=1, count=1, BYE, len=7 0xa1, 0xcc, 0x00, 0x07, }, Want: Header{ Padding: true, Count: 1, Type: TypeApplicationDefined, Length: 7, }, }, { Name: "bad version", Data: []byte{ // v=0, p=0, count=0, RR, len=4 0x00, 0xc9, 0x00, 0x04, }, WantError: errBadVersion, }, } { var h Header err := h.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q header mispmatch", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, h, "Unmarshal %q header mismatch", test.Name) } } func TestHeaderRoundTrip(t *testing.T) { for _, test := range []struct { Name string Header Header WantError error }{ { Name: "valid", Header: Header{ Padding: true, Count: 31, Type: TypeSenderReport, Length: 4, }, }, { Name: "also valid", Header: Header{ Padding: false, Count: 28, Type: TypeReceiverReport, Length: 65535, }, }, { Name: "invalid count", Header: Header{ Count: 40, }, WantError: errInvalidHeader, }, } { data, err := test.Header.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded Header assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Header, decoded, "%q header round trip mismatch", test.Name) } } ================================================ FILE: packet.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp // Packet represents an RTCP packet, a protocol used for out-of-band statistics // and control information for an RTP session. type Packet interface { // DestinationSSRC returns an array of SSRC values that this packet refers to. DestinationSSRC() []uint32 Marshal() ([]byte, error) Unmarshal(rawPacket []byte) error MarshalSize() int } // Unmarshal takes an entire udp datagram (which may consist of multiple RTCP packets) and // returns the unmarshaled packets it contains. // // If this is a reduced-size RTCP packet a feedback packet (Goodbye, SliceLossIndication, etc) // will be returned. Otherwise, the underlying type of the returned packet will be // CompoundPacket. func Unmarshal(rawData []byte) ([]Packet, error) { var packets []Packet for len(rawData) != 0 { p, processed, err := unmarshal(rawData) if err != nil { return nil, err } packets = append(packets, p) rawData = rawData[processed:] } switch len(packets) { // Empty packet case 0: return nil, errInvalidHeader // Multiple Packets default: return packets, nil } } // Marshal takes an array of Packets and serializes them to a single buffer. func Marshal(packets []Packet) ([]byte, error) { out := make([]byte, 0) for _, p := range packets { data, err := p.Marshal() if err != nil { return nil, err } out = append(out, data...) } return out, nil } // unmarshal is a factory which pulls the first RTCP packet from a bytestream, // and returns it's parsed representation, and the amount of data that was processed. // //nolint:cyclop func unmarshal(rawData []byte) (packet Packet, bytesprocessed int, err error) { var header Header err = header.Unmarshal(rawData) if err != nil { return nil, 0, err } bytesprocessed = int(header.Length+1) * 4 if bytesprocessed > len(rawData) { return nil, 0, errPacketTooShort } inPacket := rawData[:bytesprocessed] switch header.Type { case TypeSenderReport: packet = new(SenderReport) case TypeReceiverReport: packet = new(ReceiverReport) case TypeSourceDescription: packet = new(SourceDescription) case TypeGoodbye: packet = new(Goodbye) case TypeTransportSpecificFeedback: switch header.Count { case FormatTLN: packet = new(TransportLayerNack) case FormatRRR: packet = new(RapidResynchronizationRequest) case FormatTCC: packet = new(TransportLayerCC) case FormatCCFB: packet = new(CCFeedbackReport) default: packet = new(RawPacket) } case TypePayloadSpecificFeedback: switch header.Count { case FormatPLI: packet = new(PictureLossIndication) case FormatSLI: packet = new(SliceLossIndication) case FormatREMB: packet = new(ReceiverEstimatedMaximumBitrate) case FormatFIR: packet = new(FullIntraRequest) default: packet = new(RawPacket) } case TypeExtendedReport: packet = new(ExtendedReport) case TypeApplicationDefined: packet = new(ApplicationDefined) default: packet = new(RawPacket) } err = packet.Unmarshal(inPacket) return packet, bytesprocessed, err } ================================================ FILE: packet_buffer.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "reflect" "unsafe" ) // These functions implement an introspective structure // serializer/deserializer, designed to allow RTCP packet // Structs to be self-describing. They currently work with // fields of type uint8, uint16, uint32, and uint64 (and // types derived from them). // // - Unexported fields will take up space in the encoded // array, but wil be set to zero when written, and ignore // when read. // // - Fields that are marked with the tag `encoding:"omit"` // will be ignored when reading and writing data. // // For example: // // type Example struct { // A uint32 // B bool `encoding:"omit"` // _ uint64 // C uint16 // } // // "A" will be encoded as four bytes, in network order. "B" // will not be encoded at all. The anonymous uint64 will // encode as 8 bytes of value "0", followed by two bytes // encoding "C" in network order. type packetBuffer struct { bytes []byte } const omit = "omit" // Writes the structure passed to into the buffer that // PacketBuffer is initialized with. This function will // modify the PacketBuffer.bytes slice to exclude those // bytes that have been written into. // //nolint:gocognit,cyclop func (b *packetBuffer) write(v any) error { value := reflect.ValueOf(v) // Indirect is safe to call on non-pointers, and // will simply return the same value in such cases value = reflect.Indirect(value) switch value.Kind() { case reflect.Uint8: if len(b.bytes) < 1 { return errWrongMarshalSize } if value.CanInterface() { b.bytes[0] = byte(value.Uint()) //nolint:gosec // value.Kind() == reflect.Uint8 guarantees range } b.bytes = b.bytes[1:] case reflect.Uint16: if len(b.bytes) < 2 { return errWrongMarshalSize } if value.CanInterface() { binary.BigEndian.PutUint16(b.bytes, uint16(value.Uint())) //nolint:gosec // G115 } b.bytes = b.bytes[2:] case reflect.Uint32: if len(b.bytes) < 4 { return errWrongMarshalSize } if value.CanInterface() { binary.BigEndian.PutUint32(b.bytes, uint32(value.Uint())) //nolint:gosec // G115 } b.bytes = b.bytes[4:] case reflect.Uint64: if len(b.bytes) < 8 { return errWrongMarshalSize } if value.CanInterface() { binary.BigEndian.PutUint64(b.bytes, value.Uint()) } b.bytes = b.bytes[8:] case reflect.Slice: for i := 0; i < value.Len(); i++ { if value.Index(i).CanInterface() { if err := b.write(value.Index(i).Interface()); err != nil { return err } } else { b.bytes = b.bytes[value.Index(i).Type().Size():] } } case reflect.Struct: for i := 0; i < value.NumField(); i++ { encoding := value.Type().Field(i).Tag.Get("encoding") if encoding == omit { continue } if value.Field(i).CanInterface() { if err := b.write(value.Field(i).Interface()); err != nil { return err } } else { advance := int(value.Field(i).Type().Size()) //nolint:gosec // RTCP struct field sizes are small and controlled if len(b.bytes) < advance { return errWrongMarshalSize } b.bytes = b.bytes[advance:] } } default: return errBadStructMemberType } return nil } // Reads bytes from the buffer as necessary to populate // the structure passed as a parameter. This function will // modify the PacketBuffer.bytes slice to exclude those // bytes that have already been read. // //nolint:gocognit,cyclop func (b *packetBuffer) read(v any) error { ptr := reflect.ValueOf(v) if ptr.Kind() != reflect.Ptr { return errBadReadParameter } value := reflect.Indirect(ptr) // If this is an interface, we need to make it concrete before using it if value.Kind() == reflect.Interface { value = reflect.ValueOf(value.Interface()) } value = reflect.Indirect(value) switch value.Kind() { case reflect.Uint8: if len(b.bytes) < 1 { return errWrongMarshalSize } value.SetUint(uint64(b.bytes[0])) b.bytes = b.bytes[1:] case reflect.Uint16: if len(b.bytes) < 2 { return errWrongMarshalSize } value.SetUint(uint64(binary.BigEndian.Uint16(b.bytes))) b.bytes = b.bytes[2:] case reflect.Uint32: if len(b.bytes) < 4 { return errWrongMarshalSize } value.SetUint(uint64(binary.BigEndian.Uint32(b.bytes))) b.bytes = b.bytes[4:] case reflect.Uint64: if len(b.bytes) < 8 { return errWrongMarshalSize } value.SetUint(binary.BigEndian.Uint64(b.bytes)) b.bytes = b.bytes[8:] case reflect.Slice: // If we encounter a slice, we consume the rest of the data // in the buffer and load it into the slice. for len(b.bytes) > 0 { newElementPtr := reflect.New(value.Type().Elem()) if err := b.read(newElementPtr.Interface()); err != nil { return err } if value.CanSet() { value.Set(reflect.Append(value, reflect.Indirect(newElementPtr))) } } case reflect.Struct: for i := 0; i < value.NumField(); i++ { encoding := value.Type().Field(i).Tag.Get("encoding") if encoding == omit { continue } if value.Field(i).CanInterface() { field := value.Field(i) newFieldPtr := reflect.NewAt( //nolint:gosec // This is the only way to get a typed pointer to a structure's field field.Type(), unsafe.Pointer(field.UnsafeAddr()), ) if err := b.read(newFieldPtr.Interface()); err != nil { return err } } else { advance := int(value.Field(i).Type().Size()) //nolint:gosec //Size comes from type system and is bounded if len(b.bytes) < advance { return errWrongMarshalSize } b.bytes = b.bytes[advance:] } } default: return errBadStructMemberType } return nil } // Consumes `size` bytes and returns them as an // independent PacketBuffer. func (b *packetBuffer) split(size int) packetBuffer { if size > len(b.bytes) { size = len(b.bytes) } newBuffer := packetBuffer{bytes: b.bytes[:size]} b.bytes = b.bytes[size:] return newBuffer } // Returns the size that a structure will encode into. // This fuction doesn't check that Write() will succeed, // and may return unexpectedly large results for those // structures that Write() will fail on. func wireSize(v any) int { value := reflect.ValueOf(v) // Indirect is safe to call on non-pointers, and // will simply return the same value in such cases value = reflect.Indirect(value) size := int(0) switch value.Kind() { case reflect.Slice: for i := 0; i < value.Len(); i++ { if value.Index(i).CanInterface() { size += wireSize(value.Index(i).Interface()) } else { size += int(value.Index(i).Type().Size()) //nolint:gosec // RTCP element sizes are small and bounded } } case reflect.Struct: for i := 0; i < value.NumField(); i++ { encoding := value.Type().Field(i).Tag.Get("encoding") if encoding == omit { continue } if value.Field(i).CanInterface() { size += wireSize(value.Field(i).Interface()) } else { size += int(value.Field(i).Type().Size()) // nolint:gosec // Size comes from type system and is bounded } } default: size = int(value.Type().Size()) // nolint:gosec // Size comes from type system and is bounde } return size } ================================================ FILE: packet_buffer_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) func TestWrite(t *testing.T) { type Subtree struct { SubA uint32 SubB []uint8 } structure := struct { A uint8 Z uint32 `encoding:"omit"` B uint16 C uint32 D uint64 _ uint8 E []uint16 F Subtree G []Subtree }{ 0xf8, 0x01234567, 0x1234, 0x56789ABC, 0x0102030405060708, 0x12, []uint16{0x0E, 0x02FF}, Subtree{0x11223344, []uint8{9, 8, 7, 6, 5, 4, 3, 2, 1}}, []Subtree{{0x01, []uint8{1, 2, 3, 4}}, {0x02, []uint8{5, 6, 7, 8}}}, } expected := []byte{ 0xf8, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x0E, 0x02, 0xFF, 0x11, 0x22, 0x33, 0x44, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0x00, 0x00, 0x00, 0x01, 1, 2, 3, 4, 0x00, 0x00, 0x00, 0x02, 5, 6, 7, 8, } assert.Equal(t, len(expected), wireSize(structure)) raw := make([]byte, len(expected)) buffer := packetBuffer{bytes: raw} err := buffer.write(structure) assert.NoError(t, err) assert.Equal(t, expected, raw) // Check for overflow raw = make([]byte, len(expected)-1) buffer = packetBuffer{bytes: raw} err = buffer.write(structure) assert.ErrorIs(t, err, errWrongMarshalSize) } func TestReadUint8(t *testing.T) { const expected = 0x01 raw := []byte{expected} output := uint8(0) buffer := packetBuffer{bytes: raw} err := buffer.read(&output) assert.NoError(t, err) assert.Equal(t, uint8(expected), output) } func TestReadUint16(t *testing.T) { const expected = 0x0102 raw := []byte{0x01, 0x02} output := uint16(0) buffer := packetBuffer{bytes: raw} err := buffer.read(&output) assert.NoError(t, err) assert.Equal(t, uint16(expected), output) } func TestReadUint32(t *testing.T) { const expected = 0x01020304 raw := []byte{0x01, 0x02, 0x03, 0x04} output := uint32(0) buffer := packetBuffer{bytes: raw} err := buffer.read(&output) assert.NoError(t, err) assert.Equal(t, uint32(expected), output) } func TestReadUint64(t *testing.T) { expected := uint64(0x0102030405060708) raw := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} output := uint64(0) buffer := packetBuffer{bytes: raw} err := buffer.read(&output) assert.NoError(t, err) assert.Equal(t, expected, output) } func TestReadStruct(t *testing.T) { type S struct { A uint8 B uint16 C uint32 D uint64 } expected := S{0x01, 0x0203, 0x04050607, 0x08090A0B0C0D0E0F} raw := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F} var output S buffer := packetBuffer{bytes: raw} err := buffer.read(&output) assert.NoError(t, err) assert.Equal(t, expected, output) } func TestReadSlice(t *testing.T) { expected := []uint16{0x0102, 0x0304, 0x0506, 0x0708} raw := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} var output []uint16 buffer := packetBuffer{bytes: raw} err := buffer.read(&output) assert.NoError(t, err) assert.Equal(t, expected, output) } func TestReadComplex(t *testing.T) { raw := []byte{ 0xf8, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x12, 0x11, 0x22, 0x33, 0x44, 9, 0x00, 0x00, 0x00, 0x01, 1, 0x00, 0x00, 0x00, 0x02, 5, } type Subtree struct { SubA uint32 SubB uint8 } type Tree struct { A uint8 B uint16 C uint32 D uint64 _ uint8 F Subtree G []Subtree } expected := Tree{ 0xf8, 0x1234, 0x56789ABC, 0x0102030405060708, 0x00, Subtree{0x11223344, 9}, []Subtree{{0x01, 1}, {0x02, 5}}, } var output Tree buffer := packetBuffer{bytes: raw} err := buffer.read(&output) assert.NoError(t, err) assert.Equal(t, expected, output) } ================================================ FILE: packet_stringifier.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "fmt" "reflect" ) /* Converts an RTCP Packet into a human-readable format. The Packets themselves can control the presentation as follows: - Fields of a type that have a String() method will be formatted with that String method (which should not emit '\n' characters) - Otherwise, fields with a tag containing a "fmt" string will use that format when serializing the value. For example, to format an SSRC value as base 16 insted of base 10: type ExamplePacket struct { LocalSSRC uint32 `fmt:"0x%X"` RemotsSSRCs []uint32 `fmt:"%X"` } - If no fmt string is present, "%+v" is used by default The intention of this stringify() function is to simplify creation of String() methods on new packet types, as it provides a simple baseline implementation that works well in the majority of cases. */ func stringify(p Packet) string { value := reflect.Indirect(reflect.ValueOf(p)) return formatField(value.Type().String(), "", p, "") } //nolint:gocognit,cyclop func formatField(name string, format string, f any, indent string) string { out := indent value := reflect.ValueOf(f) if !value.IsValid() { return fmt.Sprintf("%s%s: \n", out, name) } isPacket := reflect.TypeOf(f).Implements(reflect.TypeFor[Packet]()) // Resolve pointers to their underlying values if value.Type().Kind() == reflect.Ptr && !value.IsNil() { underlying := reflect.Indirect(value) if underlying.IsValid() { value = underlying } } // If the field type has a custom String method, use that // (unless we're a packet, since we want to avoid recursing // back into this function if the Packet's String() method // uses it) if stringMethod := value.MethodByName("String"); !isPacket && stringMethod.IsValid() { out += fmt.Sprintf("%s: %s\n", name, stringMethod.Call([]reflect.Value{})) return out } switch value.Kind() { case reflect.Struct: out += fmt.Sprintf("%s:\n", name) for i := 0; i < value.NumField(); i++ { if value.Field(i).CanInterface() { format = value.Type().Field(i).Tag.Get("fmt") if format == "" { format = "%+v" } out += formatField(value.Type().Field(i).Name, format, value.Field(i).Interface(), indent+"\t") } } case reflect.Slice: childKind := value.Type().Elem().Kind() _, hasStringMethod := value.Type().Elem().MethodByName("String") if hasStringMethod || childKind == reflect.Struct || childKind == reflect.Ptr || childKind == reflect.Interface || childKind == reflect.Slice { out += fmt.Sprintf("%s:\n", name) for i := 0; i < value.Len(); i++ { childName := fmt.Sprint(i) // Since interfaces can hold different types of things, we add the // most specific type name to the name to make it clear what the // subsequent fields represent. if value.Index(i).Kind() == reflect.Interface { childName += fmt.Sprintf(" (%s)", reflect.Indirect(reflect.ValueOf(value.Index(i).Interface())).Type()) } if value.Index(i).CanInterface() { out += formatField(childName, format, value.Index(i).Interface(), indent+"\t") } } return out } // If we didn't take care of stringing the value already, we fall through to the // generic case. This will print slices of basic types on a single line. fallthrough default: if value.CanInterface() { out += fmt.Sprintf("%s: "+format+"\n", name, value.Interface()) } } return out } ================================================ FILE: packet_stringifier_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) //nolint:maintidx func TestPrint(t *testing.T) { type Tests struct { packet Packet expected string } tests := []Tests{ { &ExtendedReport{ SenderSSRC: 0x01020304, Reports: []ReportBlock{ &LossRLEReportBlock{ XRHeader: XRHeader{ BlockType: LossRLEReportBlockType, }, SSRC: 0x12345689, BeginSeq: 5, EndSeq: 12, Chunks: []Chunk{ Chunk(0x4006), Chunk(0x0006), Chunk(0x8765), Chunk(0x0000), }, }, &DuplicateRLEReportBlock{ XRHeader: XRHeader{ BlockType: DuplicateRLEReportBlockType, }, SSRC: 0x12345689, BeginSeq: 5, EndSeq: 12, Chunks: []Chunk{ Chunk(0x4123), Chunk(0x3FFF), Chunk(0xFFFF), Chunk(0x0000), }, }, &PacketReceiptTimesReportBlock{ XRHeader: XRHeader{ BlockType: PacketReceiptTimesReportBlockType, }, SSRC: 0x98765432, BeginSeq: 15432, EndSeq: 15577, ReceiptTime: []uint32{ 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, }, }, &ReceiverReferenceTimeReportBlock{ XRHeader: XRHeader{ BlockType: ReceiverReferenceTimeReportBlockType, }, NTPTimestamp: 0x0102030405060708, }, &DLRRReportBlock{ XRHeader: XRHeader{ BlockType: DLRRReportBlockType, }, Reports: []DLRRReport{ { SSRC: 0x88888888, LastRR: 0x12345678, DLRR: 0x99999999, }, { SSRC: 0x09090909, LastRR: 0x12345678, DLRR: 0x99999999, }, { SSRC: 0x11223344, LastRR: 0x12345678, DLRR: 0x99999999, }, }, }, &StatisticsSummaryReportBlock{ XRHeader{ BlockType: StatisticsSummaryReportBlockType, }, true, true, true, ToHIPv4, 0xFEDCBA98, 0x1234, 0x5678, 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x01, 0x02, 0x03, 0x04, }, &VoIPMetricsReportBlock{ XRHeader{ BlockType: VoIPMetricsReportBlockType, }, 0x89ABCDEF, 0x05, 0x06, 0x07, 0x08, 0x1111, 0x2222, 0x3333, 0x4444, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x1122, 0x3344, 0x5566, }, }, }, // nolint "rtcp.ExtendedReport:\n" + "\tSenderSSRC: 0x1020304\n" + "\tReports:\n" + "\t\t0 (rtcp.LossRLEReportBlock):\n" + "\t\t\tXRHeader:\n" + "\t\t\t\tBlockType: [LossRLEReportBlockType]\n" + "\t\t\t\tTypeSpecific: 0x0\n" + "\t\t\t\tBlockLength: 0\n" + "\t\t\tT: 0\n" + "\t\t\tSSRC: 0x12345689\n" + "\t\t\tBeginSeq: 5\n" + "\t\t\tEndSeq: 12\n" + "\t\t\tChunks:\n" + "\t\t\t\t0: [[RunLength type=1, length=6]]\n" + "\t\t\t\t1: [[RunLength type=0, length=6]]\n" + "\t\t\t\t2: [[BitVector 0b000011101100101]]\n" + "\t\t\t\t3: [[TerminatingNull]]\n" + "\t\t1 (rtcp.DuplicateRLEReportBlock):\n" + "\t\t\tXRHeader:\n" + "\t\t\t\tBlockType: [DuplicateRLEReportBlockType]\n" + "\t\t\t\tTypeSpecific: 0x0\n" + "\t\t\t\tBlockLength: 0\n" + "\t\t\tT: 0\n" + "\t\t\tSSRC: 0x12345689\n" + "\t\t\tBeginSeq: 5\n" + "\t\t\tEndSeq: 12\n" + "\t\t\tChunks:\n" + "\t\t\t\t0: [[RunLength type=1, length=291]]\n" + "\t\t\t\t1: [[RunLength type=0, length=16383]]\n" + "\t\t\t\t2: [[BitVector 0b111111111111111]]\n" + "\t\t\t\t3: [[TerminatingNull]]\n" + "\t\t2 (rtcp.PacketReceiptTimesReportBlock):\n" + "\t\t\tXRHeader:\n" + "\t\t\t\tBlockType: [PacketReceiptTimesReportBlockType]\n" + "\t\t\t\tTypeSpecific: 0x0\n" + "\t\t\t\tBlockLength: 0\n" + "\t\t\tT: 0\n" + "\t\t\tSSRC: 0x98765432\n" + "\t\t\tBeginSeq: 15432\n" + "\t\t\tEndSeq: 15577\n" + "\t\t\tReceiptTime: [286331153 572662306 858993459 1145324612 1431655765]\n" + "\t\t3 (rtcp.ReceiverReferenceTimeReportBlock):\n" + "\t\t\tXRHeader:\n" + "\t\t\t\tBlockType: [ReceiverReferenceTimeReportBlockType]\n" + "\t\t\t\tTypeSpecific: 0x0\n" + "\t\t\t\tBlockLength: 0\n" + "\t\t\tNTPTimestamp: 72623859790382856\n" + "\t\t4 (rtcp.DLRRReportBlock):\n" + "\t\t\tXRHeader:\n" + "\t\t\t\tBlockType: [DLRRReportBlockType]\n" + "\t\t\t\tTypeSpecific: 0x0\n" + "\t\t\t\tBlockLength: 0\n" + "\t\t\tReports:\n" + "\t\t\t\t0:\n" + "\t\t\t\t\tSSRC: 0x88888888\n" + "\t\t\t\t\tLastRR: 305419896\n" + "\t\t\t\t\tDLRR: 2576980377\n" + "\t\t\t\t1:\n" + "\t\t\t\t\tSSRC: 0x9090909\n" + "\t\t\t\t\tLastRR: 305419896\n" + "\t\t\t\t\tDLRR: 2576980377\n" + "\t\t\t\t2:\n" + "\t\t\t\t\tSSRC: 0x11223344\n" + "\t\t\t\t\tLastRR: 305419896\n" + "\t\t\t\t\tDLRR: 2576980377\n" + "\t\t5 (rtcp.StatisticsSummaryReportBlock):\n" + "\t\t\tXRHeader:\n" + "\t\t\t\tBlockType: [StatisticsSummaryReportBlockType]\n" + "\t\t\t\tTypeSpecific: 0x0\n" + "\t\t\t\tBlockLength: 0\n" + "\t\t\tLossReports: true\n" + "\t\t\tDuplicateReports: true\n" + "\t\t\tJitterReports: true\n" + "\t\t\tTTLorHopLimit: [[ToH = IPv4]]\n" + "\t\t\tSSRC: 0xFEDCBA98\n" + "\t\t\tBeginSeq: 4660\n" + "\t\t\tEndSeq: 22136\n" + "\t\t\tLostPackets: 286331153\n" + "\t\t\tDupPackets: 572662306\n" + "\t\t\tMinJitter: 858993459\n" + "\t\t\tMaxJitter: 1145324612\n" + "\t\t\tMeanJitter: 1431655765\n" + "\t\t\tDevJitter: 1717986918\n" + "\t\t\tMinTTLOrHL: 1\n" + "\t\t\tMaxTTLOrHL: 2\n" + "\t\t\tMeanTTLOrHL: 3\n" + "\t\t\tDevTTLOrHL: 4\n" + "\t\t6 (rtcp.VoIPMetricsReportBlock):\n" + "\t\t\tXRHeader:\n" + "\t\t\t\tBlockType: [VoIPMetricsReportBlockType]\n" + "\t\t\t\tTypeSpecific: 0x0\n" + "\t\t\t\tBlockLength: 0\n" + "\t\t\tSSRC: 0x89ABCDEF\n" + "\t\t\tLossRate: 5\n" + "\t\t\tDiscardRate: 6\n" + "\t\t\tBurstDensity: 7\n" + "\t\t\tGapDensity: 8\n" + "\t\t\tBurstDuration: 4369\n" + "\t\t\tGapDuration: 8738\n" + "\t\t\tRoundTripDelay: 13107\n" + "\t\t\tEndSystemDelay: 17476\n" + "\t\t\tSignalLevel: 17\n" + "\t\t\tNoiseLevel: 34\n" + "\t\t\tRERL: 51\n" + "\t\t\tGmin: 68\n" + "\t\t\tRFactor: 85\n" + "\t\t\tExtRFactor: 102\n" + "\t\t\tMOSLQ: 119\n" + "\t\t\tMOSCQ: 136\n" + "\t\t\tRXConfig: 153\n" + "\t\t\tJBNominal: 4386\n" + "\t\t\tJBMaximum: 13124\n" + "\t\t\tJBAbsMax: 21862\n", }, { &FullIntraRequest{ SenderSSRC: 0x0, MediaSSRC: 0x4bc4fcb4, FIR: []FIREntry{ { SSRC: 0x12345678, SequenceNumber: 0x42, }, { SSRC: 0x98765432, SequenceNumber: 0x57, }, }, }, // nolint "rtcp.FullIntraRequest:\n" + "\tSenderSSRC: 0\n" + "\tMediaSSRC: 1271200948\n" + "\tFIR:\n" + "\t\t0:\n" + "\t\t\tSSRC: 305419896\n" + "\t\t\tSequenceNumber: 66\n" + "\t\t1:\n" + "\t\t\tSSRC: 2557891634\n" + "\t\t\tSequenceNumber: 87\n", }, { &Goodbye{ Sources: []uint32{ 0x01020304, 0x05060708, }, Reason: "because", }, "rtcp.Goodbye:\n" + "\tSources: [16909060 84281096]\n" + "\tReason: because\n", }, { &ReceiverReport{ SSRC: 0x902f9e2e, Reports: []ReceptionReport{{ SSRC: 0xbc5e9a40, FractionLost: 0, TotalLost: 0, LastSequenceNumber: 0x46e1, Jitter: 273, LastSenderReport: 0x9f36432, Delay: 150137, }}, ProfileExtensions: []byte{}, }, "rtcp.ReceiverReport:\n" + "\tSSRC: 2419039790\n" + "\tReports:\n" + "\t\t0:\n" + "\t\t\tSSRC: 3160316480\n" + "\t\t\tFractionLost: 0\n" + "\t\t\tTotalLost: 0\n" + "\t\t\tLastSequenceNumber: 18145\n" + "\t\t\tJitter: 273\n" + "\t\t\tLastSenderReport: 166945842\n" + "\t\t\tDelay: 150137\n" + "\tProfileExtensions: []\n", }, { NewCNAMESourceDescription(0x902f9e2e, "{9c00eb92-1afb-9d49-a47d-91f64eee69f5}"), "rtcp.SourceDescription:\n" + "\tChunks:\n" + "\t\t0:\n" + "\t\t\tSource: 2419039790\n" + "\t\t\tItems:\n" + "\t\t\t\t0:\n" + "\t\t\t\t\tType: [CNAME]\n" + "\t\t\t\t\tText: {9c00eb92-1afb-9d49-a47d-91f64eee69f5}\n", }, { &PictureLossIndication{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, }, // nolint "rtcp.PictureLossIndication:\n" + "\tSenderSSRC: 2419039790\n" + "\tMediaSSRC: 2419039790\n", }, { &RapidResynchronizationRequest{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, }, "rtcp.RapidResynchronizationRequest:\n" + "\tSenderSSRC: 2419039790\n" + "\tMediaSSRC: 2419039790\n", }, { &ReceiverEstimatedMaximumBitrate{ SenderSSRC: 1, Bitrate: 8927168, SSRCs: []uint32{1215622422}, }, "rtcp.ReceiverEstimatedMaximumBitrate:\n" + "\tSenderSSRC: 1\n" + "\tBitrate: 8.927168e+06\n" + "\tSSRCs: [1215622422]\n", }, { &SenderReport{ SSRC: 0x902f9e2e, NTPTime: 0xda8bd1fcdddda05a, RTPTime: 0xaaf4edd5, PacketCount: 1, OctetCount: 2, Reports: []ReceptionReport{{ SSRC: 0xbc5e9a40, FractionLost: 0, TotalLost: 0, LastSequenceNumber: 0x46e1, Jitter: 273, LastSenderReport: 0x9f36432, Delay: 150137, }}, ProfileExtensions: []byte{ 0x81, 0xca, 0x0, 0x6, 0x2b, 0x7e, 0xc0, 0xc5, 0x1, 0x10, 0x4c, 0x63, 0x49, 0x66, 0x7a, 0x58, 0x6f, 0x6e, 0x44, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x57, 0x36, 0x0, 0x0, }, }, "rtcp.SenderReport:\n" + "\tSSRC: 2419039790\n" + "\tNTPTime: 15747911406015324250\n" + "\tRTPTime: 2868178389\n" + "\tPacketCount: 1\n" + "\tOctetCount: 2\n" + "\tReports:\n" + "\t\t0:\n" + "\t\t\tSSRC: 3160316480\n" + "\t\t\tFractionLost: 0\n" + "\t\t\tTotalLost: 0\n" + "\t\t\tLastSequenceNumber: 18145\n" + "\t\t\tJitter: 273\n" + "\t\t\tLastSenderReport: 166945842\n" + "\t\t\tDelay: 150137\n" + "\tProfileExtensions: " + "[129 202 0 6 43 126 192 197 1 16 76 99 73 102 122 88 111 110 68 111 114 100 83 101 87 54 0 0]\n", }, { &SliceLossIndication{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, SLI: []SLIEntry{{0xaaa, 0, 0x2C}}, }, "rtcp.SliceLossIndication:\n" + "\tSenderSSRC: 2419039790\n" + "\tMediaSSRC: 2419039790\n" + "\tSLI:\n" + "\t\t0:\n" + "\t\t\tFirst: 2730\n" + "\t\t\tNumber: 0\n" + "\t\t\tPicture: 44\n", }, { &SourceDescription{ Chunks: []SourceDescriptionChunk{ { Source: 0x10000000, Items: []SourceDescriptionItem{ { Type: SDESCNAME, Text: "A", }, { Type: SDESPhone, Text: "B", }, }, }, }, }, "rtcp.SourceDescription:\n" + "\tChunks:\n" + "\t\t0:\n" + "\t\t\tSource: 268435456\n" + "\t\t\tItems:\n" + "\t\t\t\t0:\n" + "\t\t\t\t\tType: [CNAME]\n" + "\t\t\t\t\tText: A\n" + "\t\t\t\t1:\n" + "\t\t\t\t\tType: [PHONE]\n" + "\t\t\t\t\tText: B\n", }, { &TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 5, }, SenderSSRC: 4195875351, MediaSSRC: 1124282272, BaseSequenceNumber: 153, PacketStatusCount: 1, ReferenceTime: 4057090, FbPktCount: 23, // 0b00100000, 0b00000001 PacketChunks: []PacketStatusChunk{ &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedSmallDelta, RunLength: 1, }, }, // 0b10010100 RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 37000, }, }, }, "rtcp.TransportLayerCC:\n" + "\tHeader:\n" + "\t\tPadding: true\n" + "\t\tCount: 15\n" + "\t\tType: [TSFB]\n" + "\t\tLength: 5\n" + "\tSenderSSRC: 4195875351\n" + "\tMediaSSRC: 1124282272\n" + "\tBaseSequenceNumber: 153\n" + "\tPacketStatusCount: 1\n" + "\tReferenceTime: 4057090\n" + "\tFbPktCount: 23\n" + "\tPacketChunks:\n" + "\t\t0 (rtcp.RunLengthChunk):\n" + "\t\t\tPacketStatusChunk: \n" + "\t\t\tType: 0\n" + "\t\t\tPacketStatusSymbol: 1\n" + "\t\t\tRunLength: 1\n" + "\tRecvDeltas:\n" + "\t\t0:\n" + "\t\t\tType: 1\n" + "\t\t\tDelta: 37000\n", }, { &TransportLayerNack{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, Nacks: []NackPair{{1, 0xAA}, {1034, 0x05}}, }, "rtcp.TransportLayerNack:\n" + "\tSenderSSRC: 2419039790\n" + "\tMediaSSRC: 2419039790\n" + "\tNacks:\n" + "\t\t0:\n" + "\t\t\tPacketID: 1\n" + "\t\t\tLostPackets: 170\n" + "\t\t1:\n" + "\t\t\tPacketID: 1034\n" + "\t\t\tLostPackets: 5\n", }, } for i, test := range tests { assert.Equalf(t, test.expected, stringify(test.packet), "Error stringifying test %d", i) } } ================================================ FILE: packet_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) // An RTCP packet from a packet dump. func realPacket() []byte { return []byte{ // Receiver Report (offset=0) // v=2, p=0, count=1, RR, len=7 0x81, 0xc9, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, // Source Description (offset=32) // v=2, p=0, count=1, SDES, len=12 0x81, 0xca, 0x0, 0xc, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // CNAME, len=38 0x1, 0x26, // text="{9c00eb92-1afb-9d49-a47d-91f64eee69f5}" 0x7b, 0x39, 0x63, 0x30, 0x30, 0x65, 0x62, 0x39, 0x32, 0x2d, 0x31, 0x61, 0x66, 0x62, 0x2d, 0x39, 0x64, 0x34, 0x39, 0x2d, 0x61, 0x34, 0x37, 0x64, 0x2d, 0x39, 0x31, 0x66, 0x36, 0x34, 0x65, 0x65, 0x65, 0x36, 0x39, 0x66, 0x35, 0x7d, // END + padding 0x0, 0x0, 0x0, 0x0, // Goodbye (offset=84) // v=2, p=0, count=1, BYE, len=1 0x81, 0xcb, 0x0, 0x1, // source=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // Picture Loss Indication (offset=92) 0x81, 0xce, 0x0, 0x2, // sender=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // RapidResynchronizationRequest (offset=104) 0x85, 0xcd, 0x0, 0x2, // sender=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ApplicationDefined (offset=116) 0x80, 0xcc, 0x00, 0x03, // sender=0x4baae1ab 0x4b, 0xaa, 0xe1, 0xab, // name='NAME' 0x4E, 0x41, 0x4D, 0x45, // data='ABCD' 0x41, 0x42, 0x43, 0x44, } } func TestUnmarshal(t *testing.T) { packet, err := Unmarshal(realPacket()) assert.NoError(t, err) expected := []Packet{ &ReceiverReport{ SSRC: 0x902f9e2e, Reports: []ReceptionReport{{ SSRC: 0xbc5e9a40, FractionLost: 0, TotalLost: 0, LastSequenceNumber: 0x46e1, Jitter: 273, LastSenderReport: 0x9f36432, Delay: 150137, }}, ProfileExtensions: []byte{}, }, NewCNAMESourceDescription(0x902f9e2e, "{9c00eb92-1afb-9d49-a47d-91f64eee69f5}"), &Goodbye{ Sources: []uint32{0x902f9e2e}, }, &PictureLossIndication{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, }, &RapidResynchronizationRequest{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, }, &ApplicationDefined{ SSRC: 0x4baae1ab, Name: "NAME", Data: []byte{0x41, 0x42, 0x43, 0x44}, }, } assert.Equal(t, expected, packet) } func TestUnmarshalNil(t *testing.T) { _, err := Unmarshal(nil) assert.ErrorIs(t, err, errInvalidHeader) } func TestInvalidHeaderLength(t *testing.T) { invalidPacket := []byte{ // Receiver Report (offset=0) // v=2, p=0, count=1, RR, len=100 0x81, 0xc9, 0x0, 0x64, } _, err := Unmarshal(invalidPacket) assert.ErrorIs(t, err, errPacketTooShort) } ================================================ FILE: picture_loss_indication.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" ) // The PictureLossIndication packet informs the encoder about the loss of an undefined amount of // coded video data belonging to one or more pictures. type PictureLossIndication struct { // SSRC of sender SenderSSRC uint32 // SSRC where the loss was experienced MediaSSRC uint32 } const ( pliLength = 2 ) // Marshal encodes the PictureLossIndication in binary. func (p PictureLossIndication) Marshal() ([]byte, error) { /* * PLI does not require parameters. Therefore, the length field MUST be * 2, and there MUST NOT be any Feedback Control Information. * * The semantics of this FB message is independent of the payload type. */ rawPacket := make([]byte, p.MarshalSize()) packetBody := rawPacket[headerLength:] binary.BigEndian.PutUint32(packetBody, p.SenderSSRC) binary.BigEndian.PutUint32(packetBody[4:], p.MediaSSRC) h := Header{ Count: FormatPLI, Type: TypePayloadSpecificFeedback, Length: pliLength, } hData, err := h.Marshal() if err != nil { return nil, err } copy(rawPacket, hData) return rawPacket, nil } // Unmarshal decodes the PictureLossIndication from binary. func (p *PictureLossIndication) Unmarshal(rawPacket []byte) error { if len(rawPacket) < (headerLength + (ssrcLength * 2)) { return errPacketTooShort } var h Header if err := h.Unmarshal(rawPacket); err != nil { return err } if h.Type != TypePayloadSpecificFeedback || h.Count != FormatPLI { return errWrongType } p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:]) p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:]) return nil } // Header returns the Header associated with this packet. func (p *PictureLossIndication) Header() Header { return Header{ Count: FormatPLI, Type: TypePayloadSpecificFeedback, Length: pliLength, } } // MarshalSize returns the size of the packet once marshaled. func (p *PictureLossIndication) MarshalSize() int { return headerLength + ssrcLength*2 } func (p *PictureLossIndication) String() string { return fmt.Sprintf("PictureLossIndication %x %x", p.SenderSSRC, p.MediaSSRC) } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (p *PictureLossIndication) DestinationSSRC() []uint32 { return []uint32{p.MediaSSRC} } ================================================ FILE: picture_loss_indication_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*PictureLossIndication)(nil) // assert is a Packet func TestPictureLossIndicationUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want PictureLossIndication WantError error }{ { Name: "valid", Data: []byte{ // v=2, p=0, FMT=1, PSFB, len=1 0x81, 0xce, 0x00, 0x02, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, }, Want: PictureLossIndication{ SenderSSRC: 0x0, MediaSSRC: 0x4bc4fcb4, }, }, { Name: "packet too short", Data: []byte{ 0x00, 0x00, 0x00, 0x00, }, WantError: errPacketTooShort, }, { Name: "invalid header", Data: []byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, WantError: errBadVersion, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, FMT=1, RR, len=1 0x81, 0xc9, 0x00, 0x02, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, }, WantError: errWrongType, }, { Name: "wrong fmt", Data: []byte{ // v=2, p=0, FMT=2, RR, len=1 0x82, 0xc9, 0x00, 0x02, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, }, WantError: errWrongType, }, } { var pli PictureLossIndication err := pli.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, pli, "Unmarshal %q", test.Name) } } func TestPictureLossIndicationRoundTrip(t *testing.T) { for _, test := range []struct { Name string Packet PictureLossIndication WantError error }{ { Name: "valid", Packet: PictureLossIndication{ SenderSSRC: 1, MediaSSRC: 2, }, }, { Name: "also valid", Packet: PictureLossIndication{ SenderSSRC: 5000, MediaSSRC: 6000, }, }, } { data, err := test.Packet.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded PictureLossIndication assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Packet, decoded, "%q rr round trip mismatch", test.Name) } } func TestPictureLossIndicationUnmarshalHeader(t *testing.T) { for _, test := range []struct { Name string Data []byte Want Header WantError error }{ { Name: "valid header", Data: []byte{ // v=2, p=0, FMT=1, PSFB, len=1 0x81, 0xce, 0x00, 0x02, // ssrc=0x0 0x00, 0x00, 0x00, 0x00, // ssrc=0x4bc4fcb4 0x4b, 0xc4, 0xfc, 0xb4, }, Want: Header{ Count: FormatPLI, Type: TypePayloadSpecificFeedback, Length: pliLength, }, }, } { var pli PictureLossIndication err := pli.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal header %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, pli.Header(), "Unmarshal header %q", test.Name) } } ================================================ FILE: rapid_resynchronization_request.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" ) // The RapidResynchronizationRequest packet informs the encoder about the loss of // an undefined amount of coded video data belonging to one or more pictures. type RapidResynchronizationRequest struct { // SSRC of sender SenderSSRC uint32 // SSRC of the media source MediaSSRC uint32 } // RapidResynchronisationRequest is provided as RFC 6051 spells resynchronization with an s. // We provide both names to be consistent with other RFCs which spell resynchronization with a z. type RapidResynchronisationRequest = RapidResynchronizationRequest const ( rrrLength = 2 rrrHeaderLength = ssrcLength * 2 rrrMediaOffset = 4 ) // Marshal encodes the RapidResynchronizationRequest in binary. func (p RapidResynchronizationRequest) Marshal() ([]byte, error) { /* * RRR does not require parameters. Therefore, the length field MUST be * 2, and there MUST NOT be any Feedback Control Information. * * The semantics of this FB message is independent of the payload type. */ rawPacket := make([]byte, p.MarshalSize()) packetBody := rawPacket[headerLength:] binary.BigEndian.PutUint32(packetBody, p.SenderSSRC) binary.BigEndian.PutUint32(packetBody[rrrMediaOffset:], p.MediaSSRC) hData, err := p.Header().Marshal() if err != nil { return nil, err } copy(rawPacket, hData) return rawPacket, nil } // Unmarshal decodes the RapidResynchronizationRequest from binary. func (p *RapidResynchronizationRequest) Unmarshal(rawPacket []byte) error { if len(rawPacket) < (headerLength + (ssrcLength * 2)) { return errPacketTooShort } var h Header if err := h.Unmarshal(rawPacket); err != nil { return err } if h.Type != TypeTransportSpecificFeedback || h.Count != FormatRRR { return errWrongType } p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:]) p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:]) return nil } // MarshalSize returns the size of the packet once marshaled. func (p *RapidResynchronizationRequest) MarshalSize() int { return headerLength + rrrHeaderLength } // Header returns the Header associated with this packet. func (p *RapidResynchronizationRequest) Header() Header { return Header{ Count: FormatRRR, Type: TypeTransportSpecificFeedback, Length: rrrLength, } } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (p *RapidResynchronizationRequest) DestinationSSRC() []uint32 { return []uint32{p.MediaSSRC} } func (p *RapidResynchronizationRequest) String() string { return fmt.Sprintf("RapidResynchronizationRequest %x %x", p.SenderSSRC, p.MediaSSRC) } ================================================ FILE: rapid_resynchronization_request_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*RapidResynchronizationRequest)(nil) // assert is a Packet func TestRapidResynchronizationRequestUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want RapidResynchronizationRequest WantError error }{ { Name: "valid", Data: []byte{ // RapidResynchronizationRequest 0x85, 0xcd, 0x0, 0x2, // sender=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, }, Want: RapidResynchronizationRequest{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, }, }, { Name: "short report", Data: []byte{ 0x85, 0xcd, 0x0, 0x2, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // report ends early }, WantError: errPacketTooShort, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, count=1, SR, len=7 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, WantError: errWrongType, }, { Name: "nil", Data: nil, WantError: errPacketTooShort, }, } { var rrr RapidResynchronizationRequest err := rrr.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, rrr, "Unmarshal %q", test.Name) } } func TestRapidResynchronizationRequestRoundTrip(t *testing.T) { for _, test := range []struct { Name string Report RapidResynchronizationRequest WantError error }{ { Name: "valid", Report: RapidResynchronizationRequest{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, }, }, } { data, err := test.Report.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded RapidResynchronizationRequest assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Report, decoded, "%q round trip", test.Name) } } ================================================ FILE: raw_packet.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import "fmt" // RawPacket represents an unparsed RTCP packet. It's returned by Unmarshal when // a packet with an unknown type is encountered. type RawPacket []byte // Marshal encodes the packet in binary. func (r RawPacket) Marshal() ([]byte, error) { return r, nil } // Unmarshal decodes the packet from binary. func (r *RawPacket) Unmarshal(b []byte) error { if len(b) < (headerLength) { return errPacketTooShort } *r = b var h Header return h.Unmarshal(b) } // Header returns the Header associated with this packet. func (r RawPacket) Header() Header { var h Header if err := h.Unmarshal(r); err != nil { return Header{} } return h } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (r *RawPacket) DestinationSSRC() []uint32 { return []uint32{} } func (r RawPacket) String() string { out := fmt.Sprintf("RawPacket: %v", ([]byte)(r)) return out } // MarshalSize returns the size of the packet once marshaled. func (r RawPacket) MarshalSize() int { return len(r) } ================================================ FILE: raw_packet_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*RawPacket)(nil) // assert is a Packet func TestRawPacketRoundTrip(t *testing.T) { for _, test := range []struct { Name string Packet RawPacket WantMarshalError error WantUnmarshalError error }{ { Name: "valid", Packet: RawPacket([]byte{ // v=2, p=0, count=1, BYE, len=12 0x81, 0xcb, 0x00, 0x0c, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // len=3, text=FOO 0x03, 0x46, 0x4f, 0x4f, }), }, { Name: "short header", Packet: RawPacket([]byte{0x00}), WantUnmarshalError: errPacketTooShort, }, { Name: "invalid header", Packet: RawPacket([]byte{ // v=0, p=0, count=0, RR, len=4 0x00, 0xc9, 0x00, 0x04, }), WantUnmarshalError: errBadVersion, }, } { data, err := test.Packet.Marshal() assert.ErrorIsf(t, err, test.WantMarshalError, "Marshal %q", test.Name) if err != nil { continue } var decoded RawPacket err = decoded.Unmarshal(data) assert.ErrorIsf(t, err, test.WantUnmarshalError, "Unmarshal %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Packet, decoded, "Unmarshal %q", test.Name) } } ================================================ FILE: receiver_estimated_maximum_bitrate.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "bytes" "encoding/binary" "fmt" "math" ) // ReceiverEstimatedMaximumBitrate contains the receiver's estimated maximum bitrate. // see: https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03 type ReceiverEstimatedMaximumBitrate struct { // SSRC of sender SenderSSRC uint32 // Estimated maximum bitrate Bitrate float32 // SSRC entries which this packet applies to SSRCs []uint32 } // Marshal serializes the packet and returns a byte slice. func (p ReceiverEstimatedMaximumBitrate) Marshal() (buf []byte, err error) { // Allocate a buffer of the exact output size. buf = make([]byte, p.MarshalSize()) // Write to our buffer. n, err := p.MarshalTo(buf) if err != nil { return nil, err } // This will always be true but just to be safe. if n != len(buf) { return nil, errWrongMarshalSize } return buf, nil } // MarshalSize returns the size of the packet once marshaled. func (p ReceiverEstimatedMaximumBitrate) MarshalSize() int { return 20 + 4*len(p.SSRCs) } // MarshalTo serializes the packet to the given byte slice. func (p ReceiverEstimatedMaximumBitrate) MarshalTo(buf []byte) (n int, err error) { const bitratemax = 0x3FFFFp+63 /* 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P| FMT=15 | PT=206 | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of packet sender | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of media source | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Unique identifier 'R' 'E' 'M' 'B' | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Num SSRC | BR Exp | BR Mantissa | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC feedback | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ... | */ size := p.MarshalSize() if len(buf) < size { return 0, errPacketTooShort } buf[0] = 143 // v=2, p=0, fmt=15 buf[1] = 206 // Length of this packet in 32-bit words minus one. length := uint16((p.MarshalSize() / 4) - 1) //nolint:gosec // G115 binary.BigEndian.PutUint16(buf[2:4], length) binary.BigEndian.PutUint32(buf[4:8], p.SenderSSRC) binary.BigEndian.PutUint32(buf[8:12], 0) // always zero // ALL HAIL REMB buf[12] = 'R' buf[13] = 'E' buf[14] = 'M' buf[15] = 'B' // Write the length of the ssrcs to follow at the end if len(p.SSRCs) > math.MaxUint8 { return 0, errTooManySSRCs } buf[16] = byte(len(p.SSRCs)) //nolint:gosec // length validated above exp := 0 bitrate := p.Bitrate if bitrate >= bitratemax { bitrate = bitratemax } if bitrate < 0 { return 0, errInvalidBitrate } for bitrate >= (1 << 18) { bitrate /= 2.0 exp++ } if exp >= (1 << 6) { return 0, errInvalidBitrate } mantissa := uint(math.Floor(float64(bitrate))) // We can't quite use the binary package because // a) it's a uint24 and b) the exponent is only 6-bits // Just trust me; this is big-endian encoding. buf[17] = byte(exp<<2) | byte(mantissa>>16) //nolint: gosec // mantissa is limited to 18 bits buf[18] = byte(mantissa >> 8) //nolint: gosec // mantissa is limited to 18 bits buf[19] = byte(mantissa) // nolint: gosec // mantissa is limited to 18 bits // Write the SSRCs at the very end. n = 20 for _, ssrc := range p.SSRCs { binary.BigEndian.PutUint32(buf[n:n+4], ssrc) n += 4 } return n, nil } // Unmarshal reads a REMB packet from the given byte slice. // //nolint:cyclop func (p *ReceiverEstimatedMaximumBitrate) Unmarshal(buf []byte) (err error) { const mantissamax = 0x7FFFFF /* 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P| FMT=15 | PT=206 | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of packet sender | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of media source | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Unique identifier 'R' 'E' 'M' 'B' | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Num SSRC | BR Exp | BR Mantissa | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC feedback | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ... | */ // 20 bytes is the size of the packet with no SSRCs if len(buf) < 20 { return errPacketTooShort } // version must be 2 version := buf[0] >> 6 if version != 2 { return fmt.Errorf("%w expected(2) actual(%d)", errBadVersion, version) } // padding must be unset padding := (buf[0] >> 5) & 1 if padding != 0 { return fmt.Errorf("%w expected(0) actual(%d)", errWrongPadding, padding) } // fmt must be 15 fmtVal := buf[0] & 31 if fmtVal != 15 { return fmt.Errorf("%w expected(15) actual(%d)", errWrongFeedbackType, fmtVal) } // Must be payload specific feedback if buf[1] != 206 { return fmt.Errorf("%w expected(206) actual(%d)", errWrongPayloadType, buf[1]) } // length is the number of 32-bit words, minus 1 length := binary.BigEndian.Uint16(buf[2:4]) size := int((length + 1) * 4) // There's not way this could be legit if size < 20 { return errHeaderTooSmall } // Make sure the buffer is large enough. if len(buf) < size { return errPacketTooShort } // The sender SSRC is 32-bits p.SenderSSRC = binary.BigEndian.Uint32(buf[4:8]) // The destination SSRC must be 0 media := binary.BigEndian.Uint32(buf[8:12]) if media != 0 { return errSSRCMustBeZero } // REMB rules all around me if !bytes.Equal(buf[12:16], []byte{'R', 'E', 'M', 'B'}) { return errMissingREMBidentifier } // The next byte is the number of SSRC entries at the end. num := int(buf[16]) // Now we know the expected size, make sure they match. if size != 20+4*num { return errSSRCNumAndLengthMismatch } // Get the 6-bit exponent value. exp := buf[17] >> 2 exp += 127 // bias for IEEE754 exp += 23 // IEEE754 biases the decimal to the left, abs-send-time biases it to the right // The remaining 2-bits plus the next 16-bits are the mantissa. mantissa := uint32(buf[17]&3)<<16 | uint32(buf[18])<<8 | uint32(buf[19]) if mantissa != 0 { // ieee754 requires an implicit leading bit for (mantissa & (mantissamax + 1)) == 0 { exp-- mantissa *= 2 } } // bitrate = mantissa * 2^exp p.Bitrate = math.Float32frombits((uint32(exp) << 23) | (mantissa & mantissamax)) // Clear any existing SSRCs p.SSRCs = nil // Loop over and parse the SSRC entires at the end. // We already verified that size == num * 4 for n := 20; n < size; n += 4 { ssrc := binary.BigEndian.Uint32(buf[n : n+4]) p.SSRCs = append(p.SSRCs, ssrc) } return nil } // Header returns the Header associated with this packet. func (p *ReceiverEstimatedMaximumBitrate) Header() Header { return Header{ Count: FormatREMB, Type: TypePayloadSpecificFeedback, Length: uint16((p.MarshalSize() / 4) - 1), //nolint:gosec // G115 } } // String prints the REMB packet in a human-readable format. func (p *ReceiverEstimatedMaximumBitrate) String() string { // Keep a table of powers to units for fast conversion. bitUnits := []string{"b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"} // Do some unit conversions because b/s is far too difficult to read. bitrate := p.Bitrate powers := 0 // Keep dividing the bitrate until it's under 1000 for bitrate >= 1000.0 && powers < len(bitUnits) { bitrate /= 1000.0 powers++ } unit := bitUnits[powers] //nolint:gosec // powers is bounded by loop condition return fmt.Sprintf("ReceiverEstimatedMaximumBitrate %x %.2f %s/s", p.SenderSSRC, bitrate, unit) } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (p *ReceiverEstimatedMaximumBitrate) DestinationSSRC() []uint32 { return p.SSRCs } ================================================ FILE: receiver_estimated_maximum_bitrate_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "math" "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*ReceiverEstimatedMaximumBitrate)(nil) // assert is a Packet func TestReceiverEstimatedMaximumBitrateMarshal(t *testing.T) { assert := assert.New(t) input := ReceiverEstimatedMaximumBitrate{ SenderSSRC: 1, Bitrate: 8927168.0, SSRCs: []uint32{1215622422}, } expected := []byte{143, 206, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 82, 69, 77, 66, 1, 26, 32, 223, 72, 116, 237, 22} output, err := input.Marshal() assert.NoError(err) assert.Equal(expected, output) } func TestReceiverEstimatedMaximumBitrateUnmarshal(t *testing.T) { assert := assert.New(t) // Real data sent by Chrome while watching a 6Mb/s stream input := []byte{143, 206, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 82, 69, 77, 66, 1, 26, 32, 223, 72, 116, 237, 22} // mantissa = []byte{26 & 3, 32, 223} = []byte{2, 32, 223} = 139487 // exp = 26 >> 2 = 6 // bitrate = 139487 * 2^6 = 139487 * 64 = 8927168 = 8.9 Mb/s expected := ReceiverEstimatedMaximumBitrate{ SenderSSRC: 1, Bitrate: 8927168, SSRCs: []uint32{1215622422}, } packet := ReceiverEstimatedMaximumBitrate{} err := packet.Unmarshal(input) assert.NoError(err) assert.Equal(expected, packet) } func TestReceiverEstimatedMaximumBitrateTruncate(t *testing.T) { assert := assert.New(t) input := []byte{143, 206, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0, 82, 69, 77, 66, 1, 26, 32, 223, 72, 116, 237, 22} // Make sure that we're interpreting the bitrate correctly. // For the above example, we have: // mantissa = 139487 // exp = 6 // bitrate = 8927168 packet := ReceiverEstimatedMaximumBitrate{} err := packet.Unmarshal(input) assert.NoError(err) assert.Equal(float32(8927168), packet.Bitrate) // Just verify marshal produces the same input. output, err := packet.Marshal() assert.NoError(err) assert.Equal(input, output) // If we subtract the bitrate by 1, we'll round down a lower mantissa packet.Bitrate-- // bitrate = 8927167 // mantissa = 139486 // exp = 6 output, err = packet.Marshal() assert.NoError(err) assert.NotEqual(input, output) // Which if we actually unmarshal again, we'll find that it's actually decreased by 63 (which is exp) // mantissa = 139486 // exp = 6 // bitrate = 8927104 err = packet.Unmarshal(output) assert.NoError(err) assert.Equal(float32(8927104), packet.Bitrate) } func TestReceiverEstimatedMaximumBitrateOverflow(t *testing.T) { assert := assert.New(t) // Marshal a packet with the maximum possible bitrate. packet := ReceiverEstimatedMaximumBitrate{ Bitrate: math.MaxFloat32, } // mantissa = 262143 = 0x3FFFF // exp = 63 expected := []byte{143, 206, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 82, 69, 77, 66, 0, 255, 255, 255} output, err := packet.Marshal() assert.NoError(err) assert.Equal(expected, output) // mantissa = 262143 // exp = 63 // bitrate = 0xFFFFC00000000000 err = packet.Unmarshal(output) assert.NoError(err) assert.Equal(math.Float32frombits(0x67FFFFC0), packet.Bitrate) // Make sure we marshal to the same result again. output, err = packet.Marshal() assert.NoError(err) assert.Equal(expected, output) // Finally, try unmarshalling one number higher than we used to be able to handle. input := []byte{143, 206, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 82, 69, 77, 66, 0, 188, 0, 0} err = packet.Unmarshal(input) assert.NoError(err) assert.Equal(math.Float32frombits(0x62800000), packet.Bitrate) } ================================================ FILE: receiver_report.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" "strings" ) // A ReceiverReport (RR) packet provides reception quality feedback for an RTP stream. type ReceiverReport struct { // The synchronization source identifier for the originator of this RR packet. SSRC uint32 // Zero or more reception report blocks depending on the number of other // sources heard by this sender since the last report. Each reception report // block conveys statistics on the reception of RTP packets from a // single synchronization source. Reports []ReceptionReport // Extension contains additional, payload-specific information that needs to // be reported regularly about the receiver. ProfileExtensions []byte } const ( ssrcLength = 4 rrSSRCOffset = headerLength rrReportOffset = rrSSRCOffset + ssrcLength ) // Marshal encodes the ReceiverReport in binary. func (r ReceiverReport) Marshal() ([]byte, error) { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * header |V=2|P| RC | PT=RR=201 | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SSRC of packet sender | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_1 (SSRC of first source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 1 | fraction lost | cumulative number of packets lost | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | extended highest sequence number received | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | interarrival jitter | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | last SR (LSR) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | delay since last SR (DLSR) | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_2 (SSRC of second source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 2 : ... : * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | profile-specific extensions | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ rawPacket := make([]byte, r.MarshalSize()) packetBody := rawPacket[headerLength:] binary.BigEndian.PutUint32(packetBody, r.SSRC) for i, rp := range r.Reports { data, err := rp.Marshal() if err != nil { return nil, err } offset := ssrcLength + receptionReportLength*i copy(packetBody[offset:], data) } if len(r.Reports) > countMax { return nil, errTooManyReports } pe := make([]byte, len(r.ProfileExtensions)) copy(pe, r.ProfileExtensions) // if the length of the profile extensions isn't devisible // by 4, we need to pad the end. for (len(pe) & 0x3) != 0 { pe = append(pe, 0) //nolint:makezero } rawPacket = append(rawPacket, pe...) //nolint:makezero hData, err := r.Header().Marshal() if err != nil { return nil, err } copy(rawPacket, hData) return rawPacket, nil } // Unmarshal decodes the ReceiverReport from binary. func (r *ReceiverReport) Unmarshal(rawPacket []byte) error { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * header |V=2|P| RC | PT=RR=201 | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SSRC of packet sender | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_1 (SSRC of first source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 1 | fraction lost | cumulative number of packets lost | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | extended highest sequence number received | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | interarrival jitter | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | last SR (LSR) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | delay since last SR (DLSR) | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_2 (SSRC of second source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 2 : ... : * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | profile-specific extensions | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ if len(rawPacket) < (headerLength + ssrcLength) { return errPacketTooShort } var header Header if err := header.Unmarshal(rawPacket); err != nil { return err } if header.Type != TypeReceiverReport { return errWrongType } r.SSRC = binary.BigEndian.Uint32(rawPacket[rrSSRCOffset:]) for i := rrReportOffset; i < len(rawPacket) && len(r.Reports) < int(header.Count); i += receptionReportLength { var rr ReceptionReport if err := rr.Unmarshal(rawPacket[i:]); err != nil { return err } r.Reports = append(r.Reports, rr) } r.ProfileExtensions = rawPacket[rrReportOffset+(len(r.Reports)*receptionReportLength):] //nolint:gosec // G115 if uint8(len(r.Reports)) != header.Count { return errInvalidHeader } return nil } // MarshalSize returns the size of the packet once marshaled. func (r *ReceiverReport) MarshalSize() int { repsLength := 0 for _, rep := range r.Reports { repsLength += rep.len() } return headerLength + ssrcLength + repsLength } // Header returns the Header associated with this packet. func (r *ReceiverReport) Header() Header { return Header{ Count: uint8(len(r.Reports)), //nolint:gosec // G115 Type: TypeReceiverReport, Length: uint16((r.MarshalSize()/4)-1) + uint16(getPadding(len(r.ProfileExtensions))), //nolint:gosec // G115 } } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (r *ReceiverReport) DestinationSSRC() []uint32 { out := make([]uint32, len(r.Reports)) for i, v := range r.Reports { out[i] = v.SSRC } return out } func (r ReceiverReport) String() string { var out strings.Builder fmt.Fprintf(&out, "ReceiverReport from %x\n", r.SSRC) out.WriteString("\tSSRC \tLost\tLastSequence\n") for _, i := range r.Reports { fmt.Fprintf(&out, "\t%x\t%d/%d\t%d\n", i.SSRC, i.FractionLost, i.TotalLost, i.LastSequenceNumber) } fmt.Fprintf(&out, "\tProfile Extension Data: %v\n", r.ProfileExtensions) return out.String() } ================================================ FILE: receiver_report_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*ReceiverReport)(nil) // assert is a Packet func TestReceiverReportUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want ReceiverReport WantError error }{ { Name: "valid", Data: []byte{ // v=2, p=0, count=1, RR, len=7 0x81, 0xc9, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, Want: ReceiverReport{ SSRC: 0x902f9e2e, Reports: []ReceptionReport{{ SSRC: 0xbc5e9a40, FractionLost: 0, TotalLost: 0, LastSequenceNumber: 0x46e1, Jitter: 273, LastSenderReport: 0x9f36432, Delay: 150137, }}, ProfileExtensions: []byte{}, }, }, { Name: "valid with extension data", Data: []byte{ // v=2, p=0, count=1, RR, len=9 0x81, 0xc9, 0x0, 0x9, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, // profile-specific extension data 0x54, 0x45, 0x53, 0x54, 0x44, 0x41, 0x54, 0x41, }, Want: ReceiverReport{ SSRC: 0x902f9e2e, Reports: []ReceptionReport{{ SSRC: 0xbc5e9a40, FractionLost: 0, TotalLost: 0, LastSequenceNumber: 0x46e1, Jitter: 273, LastSenderReport: 0x9f36432, Delay: 150137, }}, ProfileExtensions: []byte{ 0x54, 0x45, 0x53, 0x54, 0x44, 0x41, 0x54, 0x41, }, }, }, { Name: "short report", Data: []byte{ // v=2, p=0, count=1, RR, len=7 0x81, 0xc9, 0x00, 0x0c, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // fracLost=0, totalLost=0 0x00, 0x00, 0x00, 0x00, // report ends early }, WantError: errPacketTooShort, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, count=1, SR, len=7 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, WantError: errWrongType, }, { Name: "bad count in header", Data: []byte{ // v=2, p=0, count=2, RR, len=7 0x82, 0xc9, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, WantError: errInvalidHeader, }, { Name: "nil", Data: nil, WantError: errPacketTooShort, }, } { var rr ReceiverReport err := rr.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, rr, "Unmarshal %q", test.Name) } } func tooManyReports() []ReceptionReport { // a slice with enough ReceptionReports to overflow an 5-bit int var tooManyReports []ReceptionReport for range 1 << 5 { tooManyReports = append(tooManyReports, ReceptionReport{ SSRC: 2, FractionLost: 2, TotalLost: 3, LastSequenceNumber: 4, Jitter: 5, LastSenderReport: 6, Delay: 7, }) } return tooManyReports } func TestReceiverReportRoundTrip(t *testing.T) { for _, test := range []struct { Name string Report ReceiverReport WantError error }{ { Name: "valid", Report: ReceiverReport{ SSRC: 1, Reports: []ReceptionReport{ { SSRC: 2, FractionLost: 2, TotalLost: 3, LastSequenceNumber: 4, Jitter: 5, LastSenderReport: 6, Delay: 7, }, { SSRC: 0, }, }, ProfileExtensions: []byte{}, }, }, { Name: "also valid", Report: ReceiverReport{ SSRC: 2, Reports: []ReceptionReport{ { SSRC: 999, FractionLost: 30, TotalLost: 12345, LastSequenceNumber: 99, Jitter: 22, LastSenderReport: 92, Delay: 46, }, }, ProfileExtensions: []byte{}, }, }, { Name: "totallost overflow", Report: ReceiverReport{ SSRC: 1, Reports: []ReceptionReport{{ TotalLost: 1 << 25, }}, }, WantError: errInvalidTotalLost, }, { Name: "count overflow", Report: ReceiverReport{ SSRC: 1, Reports: tooManyReports(), }, WantError: errTooManyReports, }, } { data, err := test.Report.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded ReceiverReport assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Report, decoded, "%s rr round trip mismatch", test.Name) } } ================================================ FILE: reception_report.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import "encoding/binary" // A ReceptionReport block conveys statistics on the reception of RTP packets // from a single synchronization source. type ReceptionReport struct { // The SSRC identifier of the source to which the information in this // reception report block pertains. SSRC uint32 // The fraction of RTP data packets from source SSRC lost since the // previous SR or RR packet was sent, expressed as a fixed point // number with the binary point at the left edge of the field. FractionLost uint8 // The total number of RTP data packets from source SSRC that have // been lost since the beginning of reception. TotalLost uint32 // The low 16 bits contain the highest sequence number received in an // RTP data packet from source SSRC, and the most significant 16 // bits extend that sequence number with the corresponding count of // sequence number cycles. LastSequenceNumber uint32 // An estimate of the statistical variance of the RTP data packet // interarrival time, measured in timestamp units and expressed as an // unsigned integer. Jitter uint32 // The middle 32 bits out of 64 in the NTP timestamp received as part of // the most recent RTCP sender report (SR) packet from source SSRC. If no // SR has been received yet, the field is set to zero. LastSenderReport uint32 // The delay, expressed in units of 1/65536 seconds, between receiving the // last SR packet from source SSRC and sending this reception report block. // If no SR packet has been received yet from SSRC, the field is set to zero. Delay uint32 } const ( receptionReportLength = 24 fractionLostOffset = 4 totalLostOffset = 5 lastSeqOffset = 8 jitterOffset = 12 lastSROffset = 16 delayOffset = 20 ) // Marshal encodes the ReceptionReport in binary. func (r ReceptionReport) Marshal() ([]byte, error) { /* * 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 * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | SSRC | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | fraction lost | cumulative number of packets lost | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | extended highest sequence number received | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | interarrival jitter | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | last SR (LSR) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | delay since last SR (DLSR) | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ */ rawPacket := make([]byte, receptionReportLength) binary.BigEndian.PutUint32(rawPacket, r.SSRC) rawPacket[fractionLostOffset] = r.FractionLost // pack TotalLost into 24 bits if r.TotalLost >= (1 << 25) { return nil, errInvalidTotalLost } tlBytes := rawPacket[totalLostOffset:] tlBytes[0] = byte(r.TotalLost >> 16) //nolint:gosec // rawPacket is created with length receptionReportLength (24) tlBytes[1] = byte(r.TotalLost >> 8) //nolint:gosec // rawPacket is created with length receptionReportLength (24) tlBytes[2] = byte(r.TotalLost) //nolint:gosec // rawPacket is created with length receptionReportLength (24) binary.BigEndian.PutUint32(rawPacket[lastSeqOffset:], r.LastSequenceNumber) binary.BigEndian.PutUint32(rawPacket[jitterOffset:], r.Jitter) binary.BigEndian.PutUint32(rawPacket[lastSROffset:], r.LastSenderReport) binary.BigEndian.PutUint32(rawPacket[delayOffset:], r.Delay) return rawPacket, nil } // Unmarshal decodes the ReceptionReport from binary. func (r *ReceptionReport) Unmarshal(rawPacket []byte) error { if len(rawPacket) < receptionReportLength { return errPacketTooShort } /* * 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 * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | SSRC | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | fraction lost | cumulative number of packets lost | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | extended highest sequence number received | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | interarrival jitter | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | last SR (LSR) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | delay since last SR (DLSR) | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ */ r.SSRC = binary.BigEndian.Uint32(rawPacket) r.FractionLost = rawPacket[fractionLostOffset] tlBytes := rawPacket[totalLostOffset:] r.TotalLost = uint32(tlBytes[2]) | uint32(tlBytes[1])<<8 | uint32(tlBytes[0])<<16 r.LastSequenceNumber = binary.BigEndian.Uint32(rawPacket[lastSeqOffset:]) r.Jitter = binary.BigEndian.Uint32(rawPacket[jitterOffset:]) r.LastSenderReport = binary.BigEndian.Uint32(rawPacket[lastSROffset:]) r.Delay = binary.BigEndian.Uint32(rawPacket[delayOffset:]) return nil } func (r *ReceptionReport) len() int { return receptionReportLength } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>pion/renovate-config" ] } ================================================ FILE: rfc8888.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "errors" "fmt" "math" "strings" ) // https://www.rfc-editor.org/rfc/rfc8888.html#name-rtcp-congestion-control-fee // 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 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |V=2|P| FMT=11 | PT = 205 | length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of RTCP packet sender | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of 1st RTP Stream | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | begin_seq | num_reports | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |R|ECN| Arrival time offset | ... . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . . // . . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of nth RTP Stream | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | begin_seq | num_reports | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |R|ECN| Arrival time offset | ... | // . . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Report Timestamp (32 bits) | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ var ( errReportBlockLength = errors.New("feedback report blocks must be at least 8 bytes") errIncorrectNumReports = errors.New("feedback report block contains less reports than num_reports") errMetricBlockLength = errors.New("feedback report metric blocks must be exactly 2 bytes") ) // ECN represents the two ECN bits. type ECN uint8 const ( //nolint:misspell // ECNNonECT signals Non ECN-Capable Transport, Non-ECT. ECNNonECT ECN = iota // 00 //nolint:misspell // ECNECT1 signals ECN Capable Transport, ECT(0). ECNECT1 // 01 //nolint:misspell // ECNECT0 signals ECN Capable Transport, ECT(1). ECNECT0 // 10 // ECNCE signals ECN Congestion Encountered, CE. ECNCE // 11 ) func (e ECN) String() string { switch e { case ECNNonECT: //nolint:misspell return "Non-ECT (00)" case ECNECT0: //nolint:misspell return "ECT(0) (01)" case ECNECT1: //nolint:misspell return "ECT(1) (10)" case ECNCE: //nolint:misspell return "CE (11)" } return "invalid ECN value" } const ( reportTimestampLength = 4 reportBlockOffset = 8 ) // CCFeedbackReport is a Congestion Control Feedback Report as defined in // https://www.rfc-editor.org/rfc/rfc8888.html#name-rtcp-congestion-control-fee type CCFeedbackReport struct { // SSRC of sender SenderSSRC uint32 // Report Blocks ReportBlocks []CCFeedbackReportBlock // Basetime ReportTimestamp uint32 } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (b CCFeedbackReport) DestinationSSRC() []uint32 { ssrcs := make([]uint32, len(b.ReportBlocks)) for i, block := range b.ReportBlocks { ssrcs[i] = block.MediaSSRC } return ssrcs } // Len returns the length of the report in bytes. func (b *CCFeedbackReport) Len() int { return b.MarshalSize() } // MarshalSize returns the size of the packet once marshaled. func (b *CCFeedbackReport) MarshalSize() int { n := 0 for _, block := range b.ReportBlocks { n += block.len() } return reportBlockOffset + n + reportTimestampLength } // Header returns the Header associated with this packet. func (b *CCFeedbackReport) Header() Header { return Header{ Padding: false, Count: FormatCCFB, Type: TypeTransportSpecificFeedback, Length: uint16(b.MarshalSize()/4 - 1), //nolint:gosec // G115 } } // Marshal encodes the Congestion Control Feedback Report in binary. func (b CCFeedbackReport) Marshal() ([]byte, error) { header := b.Header() headerBuf, err := header.Marshal() if err != nil { return nil, err } length := 4 * (header.Length + 1) buf := make([]byte, length) copy(buf[:headerLength], headerBuf) binary.BigEndian.PutUint32(buf[headerLength:], b.SenderSSRC) offset := reportBlockOffset for _, block := range b.ReportBlocks { b, err := block.marshal() if err != nil { return nil, err } copy(buf[offset:], b) offset += block.len() } binary.BigEndian.PutUint32(buf[offset:], b.ReportTimestamp) return buf, nil } func (b CCFeedbackReport) String() string { var out strings.Builder fmt.Fprintf(&out, "CCFB:\n\tHeader %v\n", b.Header()) fmt.Fprintf(&out, "CCFB:\n\tSender SSRC %d\n", b.SenderSSRC) fmt.Fprintf(&out, "\tReport Timestamp %d\n", b.ReportTimestamp) out.WriteString("\tFeedback Reports \n") for _, report := range b.ReportBlocks { fmt.Fprintf(&out, "%v ", report) } out.WriteString("\n") return out.String() } // Unmarshal decodes the Congestion Control Feedback Report from binary. func (b *CCFeedbackReport) Unmarshal(rawPacket []byte) error { if len(rawPacket) < headerLength+ssrcLength+reportTimestampLength { return errPacketTooShort } var h Header if err := h.Unmarshal(rawPacket); err != nil { return err } if h.Type != TypeTransportSpecificFeedback { return errWrongType } b.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:]) reportTimestampOffset := len(rawPacket) - reportTimestampLength b.ReportTimestamp = binary.BigEndian.Uint32(rawPacket[reportTimestampOffset:]) offset := reportBlockOffset b.ReportBlocks = []CCFeedbackReportBlock{} for offset < reportTimestampOffset { var block CCFeedbackReportBlock if err := block.unmarshal(rawPacket[offset:]); err != nil { return err } b.ReportBlocks = append(b.ReportBlocks, block) offset += block.len() } return nil } const ( ssrcOffset = 0 beginSequenceOffset = 4 numReportsOffset = 6 reportsOffset = 8 maxMetricBlocks = 16384 ) // CCFeedbackReportBlock is a Feedback Report Block. type CCFeedbackReportBlock struct { // SSRC of the RTP stream on which this block is reporting MediaSSRC uint32 BeginSequence uint16 MetricBlocks []CCFeedbackMetricBlock } // len returns the length of the report block in bytes. func (b *CCFeedbackReportBlock) len() int { n := len(b.MetricBlocks) if n%2 != 0 { n++ } return reportsOffset + 2*n } func (b CCFeedbackReportBlock) String() string { var out strings.Builder fmt.Fprintf(&out, "\tReport Block Media SSRC %d\n", b.MediaSSRC) fmt.Fprintf(&out, "\tReport Begin Sequence Nr %d\n", b.BeginSequence) fmt.Fprintf(&out, "\tReport length %d\n\t", len(b.MetricBlocks)) for i, block := range b.MetricBlocks { //nolint:gosec // G115 fmt.Fprintf(&out, "{nr: %d, rx: %v, ts: %v, ecn: %v} ", b.BeginSequence+uint16(i), block.Received, block.ArrivalTimeOffset, block.ECN) } out.WriteString("\n") return out.String() } // marshal encodes the Congestion Control Feedback Report Block in binary. func (b CCFeedbackReportBlock) marshal() ([]byte, error) { if len(b.MetricBlocks) > maxMetricBlocks { return nil, errTooManyReports } buf := make([]byte, b.len()) binary.BigEndian.PutUint32(buf[ssrcOffset:], b.MediaSSRC) binary.BigEndian.PutUint16(buf[beginSequenceOffset:], b.BeginSequence) length := uint16(len(b.MetricBlocks)) //nolint:gosec // G115 binary.BigEndian.PutUint16(buf[numReportsOffset:], length) for i, block := range b.MetricBlocks { b, err := block.marshal() if err != nil { return nil, err } copy(buf[reportsOffset+i*2:], b) } return buf, nil } // Unmarshal decodes the Congestion Control Feedback Report Block from binary. func (b *CCFeedbackReportBlock) unmarshal(rawPacket []byte) error { if len(rawPacket) < reportsOffset { return errReportBlockLength } b.MediaSSRC = binary.BigEndian.Uint32(rawPacket[:beginSequenceOffset]) b.BeginSequence = binary.BigEndian.Uint16(rawPacket[beginSequenceOffset:numReportsOffset]) numReports := int(binary.BigEndian.Uint16(rawPacket[numReportsOffset:])) if numReports == 0 { return nil } if numReports > math.MaxUint16 { return errIncorrectNumReports } if len(rawPacket) < reportsOffset+numReports*2 { return errIncorrectNumReports } b.MetricBlocks = make([]CCFeedbackMetricBlock, numReports) for i := range numReports { var mb CCFeedbackMetricBlock offset := reportsOffset + 2*i if err := mb.unmarshal(rawPacket[offset : offset+2]); err != nil { return err } b.MetricBlocks[i] = mb } return nil } const ( metricBlockLength = 2 ) // CCFeedbackMetricBlock is a Feedback Metric Block. type CCFeedbackMetricBlock struct { Received bool ECN ECN // Offset in 1/1024 seconds before Report Timestamp ArrivalTimeOffset uint16 } // Marshal encodes the Congestion Control Feedback Metric Block in binary. func (b CCFeedbackMetricBlock) marshal() ([]byte, error) { buf := make([]byte, 2) r := uint16(0) if b.Received { r = 1 } dst, err := setNBitsOfUint16(0, 1, 0, r) if err != nil { return nil, err } dst, err = setNBitsOfUint16(dst, 2, 1, uint16(b.ECN)) if err != nil { return nil, err } dst, err = setNBitsOfUint16(dst, 13, 3, b.ArrivalTimeOffset) if err != nil { return nil, err } binary.BigEndian.PutUint16(buf, dst) return buf, nil } // Unmarshal decodes the Congestion Control Feedback Metric Block from binary. func (b *CCFeedbackMetricBlock) unmarshal(rawPacket []byte) error { if len(rawPacket) != metricBlockLength { return errMetricBlockLength } b.Received = rawPacket[0]&0x80 != 0 if !b.Received { b.ECN = ECNNonECT b.ArrivalTimeOffset = 0 return nil } b.ECN = ECN(rawPacket[0] >> 5 & 0x03) b.ArrivalTimeOffset = binary.BigEndian.Uint16(rawPacket) & 0x1FFF return nil } ================================================ FILE: rfc8888_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "bytes" "fmt" "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*CCFeedbackReport)(nil) // assert is a Packet func TestCCFeedbackMetricBlockUnmarshalMarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want CCFeedbackMetricBlock }{ { Name: "NotReceived", Data: []byte{0x00, 0x00}, Want: CCFeedbackMetricBlock{ Received: false, ECN: 0, ArrivalTimeOffset: 0, }, }, { Name: "ReceivedNoOffset", Data: []byte{0x80, 0x00}, Want: CCFeedbackMetricBlock{ Received: true, ECN: 0, ArrivalTimeOffset: 0, }, }, { Name: "ReceivedOffset", Data: []byte{0x9F, 0xFD}, Want: CCFeedbackMetricBlock{ Received: true, ECN: 0, ArrivalTimeOffset: 8189, }, }, { Name: "ReceivedOverRangeOffset", Data: []byte{0x9F, 0xFE}, Want: CCFeedbackMetricBlock{ Received: true, ECN: 0, ArrivalTimeOffset: 8190, }, }, { Name: "ReceivedAfterReportTimestamp", Data: []byte{0x9F, 0xFF}, Want: CCFeedbackMetricBlock{ Received: true, ECN: 0, ArrivalTimeOffset: 8191, }, }, { Name: "ReceivedECNCE", Data: []byte{0xFF, 0xF8}, Want: CCFeedbackMetricBlock{ Received: true, ECN: ECNCE, ArrivalTimeOffset: 8184, }, }, } { t.Run(fmt.Sprintf("Unmarshal-%v", test.Name), func(t *testing.T) { var block CCFeedbackMetricBlock err := block.unmarshal(test.Data) assert.NoError(t, err) assert.Equal(t, test.Want, block) }) t.Run(fmt.Sprintf("Marshal-%v", test.Name), func(t *testing.T) { buf, err := test.Want.marshal() assert.NoError(t, err) assert.Equal(t, test.Data, buf) }) } for _, test := range []struct { Name string Data []byte Want CCFeedbackMetricBlock }{ { Name: "NotReceivedECNCE", // Not received must ignore 15 other bits Data: []byte{0x62, 0x00}, Want: CCFeedbackMetricBlock{ Received: false, ECN: ECNNonECT, ArrivalTimeOffset: 0, }, }, { Name: "NotReceivedECNECT1", // Not received must ignore 15 other bits Data: []byte{0x22, 0x00}, Want: CCFeedbackMetricBlock{ Received: false, ECN: ECNNonECT, ArrivalTimeOffset: 0, }, }, } { t.Run(fmt.Sprintf("Unmarshal-%v", test.Name), func(t *testing.T) { var block CCFeedbackMetricBlock err := block.unmarshal(test.Data) assert.NoError(t, err) assert.Equal(t, test.Want, block) }) } for _, l := range []int{0, 1, 3} { t.Run(fmt.Sprintf("shortMetricBlock-%v", l), func(t *testing.T) { var block CCFeedbackMetricBlock data := make([]byte, l) err := block.unmarshal(data) assert.Error(t, err) assert.ErrorIs(t, err, errMetricBlockLength) }) } } func TestCCFeedbackReportBlockUnmarshalMarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want CCFeedbackReportBlock }{ { Name: "ZeroLengthBlock", Data: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, Want: CCFeedbackReportBlock{ MediaSSRC: 0, BeginSequence: 0, }, }, { Name: "ReceivedTwoOFFourBlocks", Data: []byte{ 0x00, 0x00, 0x00, 0x01, // SSRC 0x00, 0x02, 0x00, 0x04, // begin_seq, num_reports 0x9F, 0xFD, 0x9F, 0xFC, // reports[0], reports[1] 0x00, 0x00, 0x00, 0x00, // reports[2], reports[3] }, Want: CCFeedbackReportBlock{ MediaSSRC: 1, BeginSequence: 2, MetricBlocks: []CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 8189, }, { Received: true, ECN: 0, ArrivalTimeOffset: 8188, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, }, }, }, { Name: "ReceivedTwoOFThreeBlocksPadding", Data: []byte{ 0x00, 0x00, 0x00, 0x01, // SSRC 0x00, 0x02, 0x00, 0x03, // begin_seq, num_reports 0x9F, 0xFD, 0x9F, 0xFC, // reports[0], reports[1] 0x00, 0x00, 0x00, 0x00, // reports[2], Padding }, Want: CCFeedbackReportBlock{ MediaSSRC: 1, BeginSequence: 2, MetricBlocks: []CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 8189, }, { Received: true, ECN: 0, ArrivalTimeOffset: 8188, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, }, }, }, { Name: "WrapAroundSequenceNumber", Data: []byte{ 0x00, 0x00, 0x00, 0x01, // SSRC 0xff, 0xfe, 0x00, 0x04, // begin_seq, num_reports 0x9F, 0xFD, 0x9F, 0xFC, // reports[0], reports[1] 0x00, 0x00, 0x00, 0x00, // reports[2], reports[3] }, Want: CCFeedbackReportBlock{ MediaSSRC: 1, BeginSequence: 65534, MetricBlocks: []CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 8189, }, { Received: true, ECN: 0, ArrivalTimeOffset: 8188, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, }, }, }, } { t.Run(fmt.Sprintf("Unmarshal-%v", test.Name), func(t *testing.T) { var block CCFeedbackReportBlock err := block.unmarshal(test.Data) assert.NoError(t, err) assert.Equal(t, test.Want, block) }) t.Run(fmt.Sprintf("Marshal-%v", test.Name), func(t *testing.T) { buf, err := test.Want.marshal() assert.NoError(t, err) assert.Equal(t, test.Data, buf) }) } t.Run("MarshalTooManyMetricBlocks", func(t *testing.T) { block := CCFeedbackReportBlock{ MediaSSRC: 0, BeginSequence: 0, MetricBlocks: make([]CCFeedbackMetricBlock, 16385), } _, err := block.marshal() assert.Error(t, err) assert.ErrorIs(t, err, errTooManyReports) }) t.Run("emptyRawPacket", func(t *testing.T) { var block CCFeedbackReportBlock data := []byte{} err := block.unmarshal(data) assert.Error(t, err) assert.ErrorIs(t, err, errReportBlockLength) }) t.Run("shortRawPacket", func(t *testing.T) { var block CCFeedbackReportBlock data := []byte{ 0x00, 0x00, 0x00, 0x01, // SSRC 0x00, 0x02, // begin_seq } err := block.unmarshal(data) assert.Error(t, err) assert.ErrorIs(t, err, errReportBlockLength) }) t.Run("incorrectNumReports", func(t *testing.T) { var block CCFeedbackReportBlock data := []byte{ 0x00, 0x00, 0x00, 0x01, // SSRC 0x00, 0x02, 0x00, 0x05, // begin_seq, num_reports 0x9F, 0xFD, 0x9F, 0xFC, // reports[0], reports[1] 0x00, 0x00, 0x00, 0x00, // reports[2], reports[3] } err := block.unmarshal(data) assert.Error(t, err) assert.ErrorIs(t, err, errIncorrectNumReports) }) t.Run("overflowNumReports", func(t *testing.T) { var block CCFeedbackReportBlock data := append([]byte{ 0, 0, 0, 0, // SSRC 0, 0, 0x7F, 0xFB, // begin_seq, num_reports }, bytes.Repeat([]byte{0, 0}, 0x7FFF)...) err := block.unmarshal(data) assert.NoError(t, err) }) } func TestCCFeedbackReportUnmarshalMarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want CCFeedbackReport }{ { Name: "EmtpyReport", Data: []byte{ 0x8B, 0xCD, 0x00, 0x02, // V=2, P=0, FMT=11, PT=205, Length=2 0x00, 0x00, 0x00, 0x01, // Sender SSRC=1 0x00, 0x00, 0x00, 0x01, // Report Timestamp=1 }, Want: CCFeedbackReport{ SenderSSRC: 1, ReportBlocks: []CCFeedbackReportBlock{}, ReportTimestamp: 1, }, }, { Name: "Report", Data: []byte{ 0x8B, 0xCD, 0x00, 0x0A, // V=2, P=0, FMT=11, PT=205, Length=10 0x00, 0x00, 0x00, 0x01, // Sender SSRC=1 0x00, 0x00, 0x00, 0x01, // SSRC=1 0x00, 0x02, 0x00, 0x04, // begin_seq, num_reports 0x9F, 0xFD, 0x9F, 0xFC, // reports[0], reports[1] 0x00, 0x00, 0x00, 0x00, // reports[2], reports[3] 0x00, 0x00, 0x00, 0x02, // Media SSRC=2 0x00, 0x02, 0x00, 0x03, // begin_seq=2, num_reports=3 0x9F, 0xFD, 0x9F, 0xFC, // reports[0], reports[1] 0x00, 0x00, 0x00, 0x00, // reports[2], Padding 0x00, 0x00, 0x00, 0x01, }, Want: CCFeedbackReport{ SenderSSRC: 1, ReportBlocks: []CCFeedbackReportBlock{ { MediaSSRC: 1, BeginSequence: 2, MetricBlocks: []CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 8189, }, { Received: true, ECN: 0, ArrivalTimeOffset: 8188, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, }, }, { MediaSSRC: 2, BeginSequence: 2, MetricBlocks: []CCFeedbackMetricBlock{ { Received: true, ECN: 0, ArrivalTimeOffset: 8189, }, { Received: true, ECN: 0, ArrivalTimeOffset: 8188, }, { Received: false, ECN: 0, ArrivalTimeOffset: 0, }, }, }, }, ReportTimestamp: 1, }, }, } { t.Run(fmt.Sprintf("Unmarshal-%v", test.Name), func(t *testing.T) { pkts, err := Unmarshal(test.Data) assert.NoError(t, err) assert.Len(t, pkts, 1) var ok bool var report *CCFeedbackReport report, ok = pkts[0].(*CCFeedbackReport) assert.True(t, ok) assert.Equal(t, test.Want, *report) }) t.Run(fmt.Sprintf("Marshal-%v", test.Name), func(t *testing.T) { buf, err := test.Want.Marshal() assert.NoError(t, err) assert.Equal(t, test.Data, buf) }) } } func TestCCFeedbackOverflow(t *testing.T) { p := &CCFeedbackReport{} err := p.Unmarshal(append([]byte{ // Header 0b10000000, // V = 2 205, // h.Type = TypeTransportSpecificFeedback 0, 0, // h.Length (unused) // SSRC 0, 0, 0, 0, // CCFeedbackReportBlock 0, 0, 0, 0, 0, 0, 0x7F, 0xFB, // numReportsField }, bytes.Repeat([]byte{0, 0}, 0x7FFF)...)) assert.ErrorIs(t, err, errReportBlockLength) } ================================================ FILE: sender_report.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" "strings" ) // A SenderReport (SR) packet provides reception quality feedback for an RTP stream. type SenderReport struct { // The synchronization source identifier for the originator of this SR packet. SSRC uint32 // The wallclock time when this report was sent so that it may be used in // combination with timestamps returned in reception reports from other // receivers to measure round-trip propagation to those receivers. NTPTime uint64 // Corresponds to the same time as the NTP timestamp (above), but in // the same units and with the same random offset as the RTP // timestamps in data packets. This correspondence may be used for // intra- and inter-media synchronization for sources whose NTP // timestamps are synchronized, and may be used by media-independent // receivers to estimate the nominal RTP clock frequency. RTPTime uint32 // The total number of RTP data packets transmitted by the sender // since starting transmission up until the time this SR packet was // generated. PacketCount uint32 // The total number of payload octets (i.e., not including header or // padding) transmitted in RTP data packets by the sender since // starting transmission up until the time this SR packet was // generated. OctetCount uint32 // Zero or more reception report blocks depending on the number of other // sources heard by this sender since the last report. Each reception report // block conveys statistics on the reception of RTP packets from a // single synchronization source. Reports []ReceptionReport // ProfileExtensions contains additional, payload-specific information that needs to // be reported regularly about the sender. ProfileExtensions []byte } const ( srHeaderLength = 24 srSSRCOffset = 0 srNTPOffset = srSSRCOffset + ssrcLength ntpTimeLength = 8 srRTPOffset = srNTPOffset + ntpTimeLength rtpTimeLength = 4 srPacketCountOffset = srRTPOffset + rtpTimeLength srPacketCountLength = 4 srOctetCountOffset = srPacketCountOffset + srPacketCountLength srOctetCountLength = 4 srReportOffset = srOctetCountOffset + srOctetCountLength ) // Marshal encodes the SenderReport in binary. func (r SenderReport) Marshal() ([]byte, error) { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * header |V=2|P| RC | PT=SR=200 | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SSRC of sender | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * sender | NTP timestamp, most significant word | * info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | NTP timestamp, least significant word | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | RTP timestamp | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | sender's packet count | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | sender's octet count | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_1 (SSRC of first source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 1 | fraction lost | cumulative number of packets lost | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | extended highest sequence number received | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | interarrival jitter | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | last SR (LSR) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | delay since last SR (DLSR) | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_2 (SSRC of second source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 2 : ... : * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | profile-specific extensions | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ rawPacket := make([]byte, r.MarshalSize()) packetBody := rawPacket[headerLength:] binary.BigEndian.PutUint32(packetBody[srSSRCOffset:], r.SSRC) binary.BigEndian.PutUint64(packetBody[srNTPOffset:], r.NTPTime) binary.BigEndian.PutUint32(packetBody[srRTPOffset:], r.RTPTime) binary.BigEndian.PutUint32(packetBody[srPacketCountOffset:], r.PacketCount) binary.BigEndian.PutUint32(packetBody[srOctetCountOffset:], r.OctetCount) offset := srHeaderLength for _, rp := range r.Reports { data, err := rp.Marshal() if err != nil { return nil, err } copy(packetBody[offset:], data) offset += receptionReportLength } if len(r.Reports) > countMax { return nil, errTooManyReports } copy(packetBody[offset:], r.ProfileExtensions) hData, err := r.Header().Marshal() if err != nil { return nil, err } copy(rawPacket, hData) return rawPacket, nil } // Unmarshal decodes the SenderReport from binary. func (r *SenderReport) Unmarshal(rawPacket []byte) error { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * header |V=2|P| RC | PT=SR=200 | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SSRC of sender | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * sender | NTP timestamp, most significant word | * info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | NTP timestamp, least significant word | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | RTP timestamp | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | sender's packet count | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | sender's octet count | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_1 (SSRC of first source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 1 | fraction lost | cumulative number of packets lost | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | extended highest sequence number received | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | interarrival jitter | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | last SR (LSR) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | delay since last SR (DLSR) | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * report | SSRC_2 (SSRC of second source) | * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * 2 : ... : * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | profile-specific extensions | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ if len(rawPacket) < (headerLength + srHeaderLength) { return errPacketTooShort } var header Header if err := header.Unmarshal(rawPacket); err != nil { return err } if header.Type != TypeSenderReport { return errWrongType } packetBody := rawPacket[headerLength:] r.SSRC = binary.BigEndian.Uint32(packetBody[srSSRCOffset:]) r.NTPTime = binary.BigEndian.Uint64(packetBody[srNTPOffset:]) r.RTPTime = binary.BigEndian.Uint32(packetBody[srRTPOffset:]) r.PacketCount = binary.BigEndian.Uint32(packetBody[srPacketCountOffset:]) r.OctetCount = binary.BigEndian.Uint32(packetBody[srOctetCountOffset:]) offset := srReportOffset for i := 0; i < int(header.Count); i++ { rrEnd := offset + receptionReportLength if rrEnd > len(packetBody) { return errPacketTooShort } rrBody := packetBody[offset : offset+receptionReportLength] offset = rrEnd var rr ReceptionReport if err := rr.Unmarshal(rrBody); err != nil { return err } r.Reports = append(r.Reports, rr) } if offset < len(packetBody) { r.ProfileExtensions = packetBody[offset:] } if uint8(len(r.Reports)) != header.Count { //nolint:gosec // G115 return errInvalidHeader } return nil } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (r *SenderReport) DestinationSSRC() []uint32 { out := make([]uint32, len(r.Reports)+1) for i, v := range r.Reports { out[i] = v.SSRC } out[len(r.Reports)] = r.SSRC return out } // MarshalSize returns the size of the packet once marshaled. func (r *SenderReport) MarshalSize() int { repsLength := 0 for _, rep := range r.Reports { repsLength += rep.len() } return headerLength + srHeaderLength + repsLength + len(r.ProfileExtensions) } // Header returns the Header associated with this packet. func (r *SenderReport) Header() Header { return Header{ Count: uint8(len(r.Reports)), //nolint:gosec // G115 Type: TypeSenderReport, Length: uint16((r.MarshalSize() / 4) - 1), //nolint:gosec // G115 } } func (r SenderReport) String() string { var out strings.Builder fmt.Fprintf(&out, "SenderReport from %x\n", r.SSRC) fmt.Fprintf(&out, "\tNTPTime:\t%d\n", r.NTPTime) fmt.Fprintf(&out, "\tRTPTIme:\t%d\n", r.RTPTime) fmt.Fprintf(&out, "\tPacketCount:\t%d\n", r.PacketCount) fmt.Fprintf(&out, "\tOctetCount:\t%d\n", r.OctetCount) out.WriteString("\tSSRC \tLost\tLastSequence\n") for _, i := range r.Reports { fmt.Fprintf(&out, "\t%x\t%d/%d\t%d\n", i.SSRC, i.FractionLost, i.TotalLost, i.LastSequenceNumber) } fmt.Fprintf(&out, "\tProfile Extension Data: %v\n", r.ProfileExtensions) return out.String() } ================================================ FILE: sender_report_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "slices" "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*SenderReport)(nil) // assert is a Packet func TestSenderReportUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want SenderReport WantError error }{ { Name: "nil", Data: nil, WantError: errPacketTooShort, }, { Name: "valid", Data: []byte{ // v=2, p=0, count=1, SR, len=7 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ntp=0xda8bd1fcdddda05a 0xda, 0x8b, 0xd1, 0xfc, 0xdd, 0xdd, 0xa0, 0x5a, // rtp=0xaaf4edd5 0xaa, 0xf4, 0xed, 0xd5, // packetCount=1 0x00, 0x00, 0x00, 0x01, // octetCount=2 0x00, 0x00, 0x00, 0x02, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, Want: SenderReport{ SSRC: 0x902f9e2e, NTPTime: 0xda8bd1fcdddda05a, RTPTime: 0xaaf4edd5, PacketCount: 1, OctetCount: 2, Reports: []ReceptionReport{{ SSRC: 0xbc5e9a40, FractionLost: 0, TotalLost: 0, LastSequenceNumber: 0x46e1, Jitter: 273, LastSenderReport: 0x9f36432, Delay: 150137, }}, }, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, count=1, RR, len=7 0x81, 0xc9, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ntp=0xda8bd1fcdddda05a 0xda, 0x8b, 0xd1, 0xfc, 0xdd, 0xdd, 0xa0, 0x5a, // rtp=0xaaf4edd5 0xaa, 0xf4, 0xed, 0xd5, // packetCount=1 0x00, 0x00, 0x00, 0x01, // octetCount=2 0x00, 0x00, 0x00, 0x02, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, WantError: errWrongType, }, { Name: "bad count in header", Data: []byte{ // v=2, p=0, count=1, SR, len=7 0x82, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ntp=0xda8bd1fcdddda05a 0xda, 0x8b, 0xd1, 0xfc, 0xdd, 0xdd, 0xa0, 0x5a, // rtp=0xaaf4edd5 0xaa, 0xf4, 0xed, 0xd5, // packetCount=1 0x00, 0x00, 0x00, 0x01, // octetCount=2 0x00, 0x00, 0x00, 0x02, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, WantError: errPacketTooShort, }, { Name: "with extension", // issue #447 Data: []byte{ // v=2, p=0, count=0, SR, len=6 0x80, 0xc8, 0x0, 0x6, // ssrc=0x2b7ec0c5 0x2b, 0x7e, 0xc0, 0xc5, // ntp=0xe020a2a952a53fc0 0xe0, 0x20, 0xa2, 0xa9, 0x52, 0xa5, 0x3f, 0xc0, // rtp=0x2e48a552 0x2e, 0x48, 0xa5, 0x52, // packetCount=70 0x0, 0x0, 0x0, 0x46, // octetCount=4637 0x0, 0x0, 0x12, 0x1d, // profile-specific extension 0x81, 0xca, 0x0, 0x6, 0x2b, 0x7e, 0xc0, 0xc5, 0x1, 0x10, 0x4c, 0x63, 0x49, 0x66, 0x7a, 0x58, 0x6f, 0x6e, 0x44, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x57, 0x36, 0x0, 0x0, }, Want: SenderReport{ SSRC: 0x2b7ec0c5, NTPTime: 0xe020a2a952a53fc0, RTPTime: 0x2e48a552, PacketCount: 70, OctetCount: 4637, ProfileExtensions: []byte{ 0x81, 0xca, 0x0, 0x6, 0x2b, 0x7e, 0xc0, 0xc5, 0x1, 0x10, 0x4c, 0x63, 0x49, 0x66, 0x7a, 0x58, 0x6f, 0x6e, 0x44, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x57, 0x36, 0x0, 0x0, }, }, WantError: nil, }, } { var sr SenderReport err := sr.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q sr", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, sr, "Unmarshal %q sr", test.Name) var ssrcFound bool dstSsrc := sr.DestinationSSRC() if slices.Contains(dstSsrc, sr.SSRC) { ssrcFound = true } assert.Truef(t, ssrcFound, "Unmarshal %q sr: sr's DestinationSSRC should include it's SSRC field", test.Name) } } func TestSenderReportRoundTrip(t *testing.T) { for _, test := range []struct { Name string Report SenderReport WantError error }{ { Name: "valid", Report: SenderReport{ SSRC: 1, NTPTime: 999, RTPTime: 555, PacketCount: 32, OctetCount: 11, Reports: []ReceptionReport{ { SSRC: 2, FractionLost: 2, TotalLost: 3, LastSequenceNumber: 4, Jitter: 5, LastSenderReport: 6, Delay: 7, }, { SSRC: 0, }, }, }, }, { Name: "also valid", Report: SenderReport{ SSRC: 2, Reports: []ReceptionReport{ { SSRC: 999, FractionLost: 30, TotalLost: 12345, LastSequenceNumber: 99, Jitter: 22, LastSenderReport: 92, Delay: 46, }, }, }, }, { Name: "extension", Report: SenderReport{ SSRC: 2, Reports: []ReceptionReport{ { SSRC: 999, FractionLost: 30, TotalLost: 12345, LastSequenceNumber: 99, Jitter: 22, LastSenderReport: 92, Delay: 46, }, }, ProfileExtensions: []byte{1, 2, 3, 4}, }, }, { Name: "count overflow", Report: SenderReport{ SSRC: 1, Reports: tooManyReports(), }, WantError: errTooManyReports, }, } { data, err := test.Report.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded SenderReport assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Report, decoded, "%q sr round trip", test.Name) } } ================================================ FILE: slice_loss_indication.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" "math" ) // SLIEntry represents a single entry to the SLI packet's // list of lost slices. type SLIEntry struct { // ID of first lost slice First uint16 // Number of lost slices Number uint16 // ID of related picture Picture uint8 } // The SliceLossIndication packet informs the encoder about the loss of a picture slice. type SliceLossIndication struct { // SSRC of sender SenderSSRC uint32 // SSRC of the media source MediaSSRC uint32 SLI []SLIEntry } const ( sliLength = 2 sliOffset = 8 ) // Marshal encodes the SliceLossIndication in binary. func (p SliceLossIndication) Marshal() ([]byte, error) { if len(p.SLI)+sliLength > math.MaxUint8 { return nil, errTooManyReports } rawPacket := make([]byte, sliOffset+(len(p.SLI)*4)) binary.BigEndian.PutUint32(rawPacket, p.SenderSSRC) binary.BigEndian.PutUint32(rawPacket[4:], p.MediaSSRC) for i, s := range p.SLI { sli := ((uint32(s.First) & 0x1FFF) << 19) | ((uint32(s.Number) & 0x1FFF) << 6) | (uint32(s.Picture) & 0x3F) binary.BigEndian.PutUint32(rawPacket[sliOffset+(4*i):], sli) } hData, err := p.Header().Marshal() if err != nil { return nil, err } return append(hData, rawPacket...), nil } // Unmarshal decodes the SliceLossIndication from binary. func (p *SliceLossIndication) Unmarshal(rawPacket []byte) error { if len(rawPacket) < (headerLength + ssrcLength) { return errPacketTooShort } var header Header if err := header.Unmarshal(rawPacket); err != nil { return err } if len(rawPacket) < (headerLength + int(4*header.Length)) { return errPacketTooShort } if header.Type != TypeTransportSpecificFeedback || header.Count != FormatSLI { return errWrongType } p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:]) p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:]) for i := headerLength + sliOffset; i < (headerLength + int(header.Length*4)); i += 4 { sli := binary.BigEndian.Uint32(rawPacket[i:]) p.SLI = append(p.SLI, SLIEntry{ First: uint16((sli >> 19) & 0x1FFF), //nolint:gosec // G115 Number: uint16((sli >> 6) & 0x1FFF), //nolint:gosec // G115 Picture: uint8(sli & 0x3F), //nolint:gosec // G115 }) } return nil } // MarshalSize returns the size of the packet once marshaled. func (p *SliceLossIndication) MarshalSize() int { return headerLength + sliOffset + (len(p.SLI) * 4) } // Header returns the Header associated with this packet. func (p *SliceLossIndication) Header() Header { return Header{ Count: FormatSLI, Type: TypeTransportSpecificFeedback, Length: uint16((p.MarshalSize() / 4) - 1), //nolint:gosec // G115 } } func (p *SliceLossIndication) String() string { return fmt.Sprintf("SliceLossIndication %x %x %+v", p.SenderSSRC, p.MediaSSRC, p.SLI) } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (p *SliceLossIndication) DestinationSSRC() []uint32 { return []uint32{p.MediaSSRC} } ================================================ FILE: slice_loss_indication_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*SliceLossIndication)(nil) // assert is a Packet func TestSliceLossIndicationUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want SliceLossIndication WantError error }{ { Name: "valid", Data: []byte{ // SliceLossIndication 0x82, 0xcd, 0x0, 0x3, // sender=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // nack 0xAAAA, 0x5555 0x55, 0x50, 0x00, 0x2C, }, Want: SliceLossIndication{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, SLI: []SLIEntry{{0xaaa, 0, 0x2C}}, }, }, { Name: "short report", Data: []byte{ 0x81, 0xcd, 0x0, 0x2, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // report ends early }, WantError: errPacketTooShort, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, count=1, SR, len=7 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, WantError: errWrongType, }, { Name: "nil", Data: nil, WantError: errPacketTooShort, }, } { var sli SliceLossIndication err := sli.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) if err != nil { continue } assert.Equalf(t, test.Want, sli, "Unmarshal %q rr", test.Name) } } func TestSliceLossIndicationRoundTrip(t *testing.T) { for _, test := range []struct { Name string Report SliceLossIndication WantError error }{ { Name: "valid", Report: SliceLossIndication{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, SLI: []SLIEntry{{1, 0xAA, 0x1F}, {1034, 0x05, 0x6}}, }, }, } { data, err := test.Report.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded SliceLossIndication assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Report, decoded, "%q sli round trip mismatch", test.Name) } } ================================================ FILE: source_description.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" "strings" ) // SDESType is the item type used in the RTCP SDES control packet. type SDESType uint8 // RTP SDES item types registered with IANA. // See: https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-5 // . const ( SDESEnd SDESType = iota // end of SDES list RFC 3550, 6.5 SDESCNAME // canonical name RFC 3550, 6.5.1 SDESName // user name RFC 3550, 6.5.2 SDESEmail // user's electronic mail address RFC 3550, 6.5.3 SDESPhone // user's phone number RFC 3550, 6.5.4 SDESLocation // geographic user location RFC 3550, 6.5.5 SDESTool // name of application or tool RFC 3550, 6.5.6 SDESNote // notice about the source RFC 3550, 6.5.7 SDESPrivate // private extensions RFC 3550, 6.5.8 (not implemented) ) //nolint:cyclop func (s SDESType) String() string { switch s { case SDESEnd: return "END" case SDESCNAME: return "CNAME" case SDESName: return "NAME" case SDESEmail: return "EMAIL" case SDESPhone: return "PHONE" case SDESLocation: return "LOC" case SDESTool: return "TOOL" case SDESNote: return "NOTE" case SDESPrivate: return "PRIV" default: return string(s) } } const ( sdesSourceLen = 4 sdesTypeLen = 1 sdesTypeOffset = 0 sdesOctetCountLen = 1 sdesOctetCountOffset = 1 sdesMaxOctetCount = (1 << 8) - 1 sdesTextOffset = 2 ) // A SourceDescription (SDES) packet describes the sources in an RTP stream. type SourceDescription struct { Chunks []SourceDescriptionChunk } // NewCNAMESourceDescription creates a new SourceDescription with a single CNAME item. func NewCNAMESourceDescription(ssrc uint32, cname string) *SourceDescription { return &SourceDescription{ Chunks: []SourceDescriptionChunk{{ Source: ssrc, Items: []SourceDescriptionItem{{ Type: SDESCNAME, Text: cname, }}, }}, } } // Marshal encodes the SourceDescription in binary. func (s SourceDescription) Marshal() ([]byte, error) { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * header |V=2|P| SC | PT=SDES=202 | length | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * chunk | SSRC/CSRC_1 | * 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SDES items | * | ... | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * chunk | SSRC/CSRC_2 | * 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SDES items | * | ... | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ */ rawPacket := make([]byte, s.MarshalSize()) packetBody := rawPacket[headerLength:] chunkOffset := 0 for _, c := range s.Chunks { data, err := c.Marshal() if err != nil { return nil, err } copy(packetBody[chunkOffset:], data) chunkOffset += len(data) } if len(s.Chunks) > countMax { return nil, errTooManyChunks } hData, err := s.Header().Marshal() if err != nil { return nil, err } copy(rawPacket, hData) return rawPacket, nil } // Unmarshal decodes the SourceDescription from binary. func (s *SourceDescription) Unmarshal(rawPacket []byte) error { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * header |V=2|P| SC | PT=SDES=202 | length | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * chunk | SSRC/CSRC_1 | * 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SDES items | * | ... | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * chunk | SSRC/CSRC_2 | * 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SDES items | * | ... | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ */ var header Header if err := header.Unmarshal(rawPacket); err != nil { return err } if header.Type != TypeSourceDescription { return errWrongType } for i := headerLength; i < len(rawPacket); { var chunk SourceDescriptionChunk if err := chunk.Unmarshal(rawPacket[i:]); err != nil { return err } s.Chunks = append(s.Chunks, chunk) i += chunk.len() } if len(s.Chunks) != int(header.Count) { return errInvalidHeader } return nil } // MarshalSize returns the size of the packet once marshaled. func (s *SourceDescription) MarshalSize() int { chunksLength := 0 for _, c := range s.Chunks { chunksLength += c.len() } return headerLength + chunksLength } // Header returns the Header associated with this packet. func (s *SourceDescription) Header() Header { return Header{ Count: uint8(len(s.Chunks)), //nolint:gosec // G115 Type: TypeSourceDescription, Length: uint16((s.MarshalSize() / 4) - 1), //nolint:gosec // G115 } } // A SourceDescriptionChunk contains items describing a single RTP source. type SourceDescriptionChunk struct { // The source (ssrc) or contributing source (csrc) identifier this packet describes Source uint32 Items []SourceDescriptionItem } // Marshal encodes the SourceDescriptionChunk in binary. func (s SourceDescriptionChunk) Marshal() ([]byte, error) { /* * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | SSRC/CSRC_1 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SDES items | * | ... | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ */ rawPacket := make([]byte, sdesSourceLen) binary.BigEndian.PutUint32(rawPacket, s.Source) for _, it := range s.Items { data, err := it.Marshal() if err != nil { return nil, err } rawPacket = append(rawPacket, data...) //nolint:makezero } // The list of items in each chunk MUST be terminated by one or more null octets rawPacket = append(rawPacket, uint8(SDESEnd)) //nolint:makezero // additional null octets MUST be included if needed to pad until the next 32-bit boundary rawPacket = append(rawPacket, make([]byte, getPadding(len(rawPacket)))...) //nolint:makezero return rawPacket, nil } // Unmarshal decodes the SourceDescriptionChunk from binary. func (s *SourceDescriptionChunk) Unmarshal(rawPacket []byte) error { /* * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | SSRC/CSRC_1 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | SDES items | * | ... | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ */ if len(rawPacket) < (sdesSourceLen + sdesTypeLen) { return errPacketTooShort } s.Source = binary.BigEndian.Uint32(rawPacket) for i := 4; i < len(rawPacket); { if pktType := SDESType(rawPacket[i]); pktType == SDESEnd { return nil } var it SourceDescriptionItem if err := it.Unmarshal(rawPacket[i:]); err != nil { return err } s.Items = append(s.Items, it) i += it.Len() } return errPacketTooShort } func (s SourceDescriptionChunk) len() int { chunkLen := sdesSourceLen for _, it := range s.Items { chunkLen += it.Len() } chunkLen += sdesTypeLen // for terminating null octet // align to 32-bit boundary chunkLen += getPadding(chunkLen) return chunkLen } // A SourceDescriptionItem is a part of a SourceDescription that describes a stream. type SourceDescriptionItem struct { // The type identifier for this item. eg, SDESCNAME for canonical name description. // // Type zero or SDESEnd is interpreted as the end of an item list and cannot be used. Type SDESType // Text is a unicode text blob associated with the item. Its meaning varies based on the item's Type. Text string } // Len returns the length of the SourceDescriptionItem when encoded as binary. func (s SourceDescriptionItem) Len() int { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | CNAME=1 | length | user and domain name ... * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ return sdesTypeLen + sdesOctetCountLen + len([]byte(s.Text)) } // Marshal encodes the SourceDescriptionItem in binary. func (s SourceDescriptionItem) Marshal() ([]byte, error) { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | CNAME=1 | length | user and domain name ... * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ if s.Type == SDESEnd { return nil, errSDESMissingType } rawPacket := make([]byte, sdesTypeLen+sdesOctetCountLen) rawPacket[sdesTypeOffset] = uint8(s.Type) //nolint:gosec // rawPacket is created with length 2 txtBytes := []byte(s.Text) octetCount := len(txtBytes) if octetCount > sdesMaxOctetCount { return nil, errSDESTextTooLong } rawPacket[sdesOctetCountOffset] = uint8(octetCount) //nolint:gosec // rawPacket is created with length 2 rawPacket = append(rawPacket, txtBytes...) //nolint:makezero return rawPacket, nil } // Unmarshal decodes the SourceDescriptionItem from binary. func (s *SourceDescriptionItem) Unmarshal(rawPacket []byte) error { /* * 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | CNAME=1 | length | user and domain name ... * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ if len(rawPacket) < (sdesTypeLen + sdesOctetCountLen) { return errPacketTooShort } s.Type = SDESType(rawPacket[sdesTypeOffset]) octetCount := int(rawPacket[sdesOctetCountOffset]) if sdesTextOffset+octetCount > len(rawPacket) { return errPacketTooShort } txtBytes := rawPacket[sdesTextOffset : sdesTextOffset+octetCount] s.Text = string(txtBytes) return nil } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (s *SourceDescription) DestinationSSRC() []uint32 { out := make([]uint32, len(s.Chunks)) for i, v := range s.Chunks { out[i] = v.Source } return out } func (s *SourceDescription) String() string { var out strings.Builder out.WriteString("Source Description:\n") for _, c := range s.Chunks { fmt.Fprintf(&out, "\t%x: %s\n", c.Source, c.Items) } return out.String() } ================================================ FILE: source_description_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "strings" "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*SourceDescription)(nil) // assert is a Packet func TestSourceDescriptionUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want SourceDescription WantError error }{ { Name: "nil", Data: nil, WantError: errPacketTooShort, }, { Name: "no chunks", Data: []byte{ // v=2, p=0, count=1, SDES, len=8 0x80, 0xca, 0x00, 0x04, }, Want: SourceDescription{ Chunks: nil, }, }, { Name: "missing type", Data: []byte{ // v=2, p=0, count=1, SDES, len=8 0x81, 0xca, 0x00, 0x08, // ssrc=0x00000000 0x00, 0x00, 0x00, 0x00, }, WantError: errPacketTooShort, }, { Name: "bad cname length", Data: []byte{ // v=2, p=0, count=1, SDES, len=10 0x81, 0xca, 0x00, 0x0a, // ssrc=0x00000000 0x00, 0x00, 0x00, 0x00, // CNAME, len = 1 0x01, 0x01, }, WantError: errPacketTooShort, }, { Name: "short cname", Data: []byte{ // v=2, p=0, count=1, SDES, len=9 0x81, 0xca, 0x00, 0x09, // ssrc=0x00000000 0x00, 0x00, 0x00, 0x00, // CNAME, Missing length 0x01, }, WantError: errPacketTooShort, }, { Name: "no end", Data: []byte{ // v=2, p=0, count=1, SDES, len=11 0x81, 0xca, 0x00, 0x0b, // ssrc=0x00000000 0x00, 0x00, 0x00, 0x00, // CNAME, len=1, content=A 0x01, 0x02, 0x41, // Missing END }, WantError: errPacketTooShort, }, { Name: "bad octet count", Data: []byte{ // v=2, p=0, count=1, SDES, len=10 0x81, 0xca, 0x00, 0x0a, // ssrc=0x00000000 0x00, 0x00, 0x00, 0x00, // CNAME, len=1 0x01, 0x01, }, WantError: errPacketTooShort, }, { Name: "zero item chunk", Data: []byte{ // v=2, p=0, count=1, SDES, len=12 0x81, 0xca, 0x00, 0x0c, // ssrc=0x01020304 0x01, 0x02, 0x03, 0x04, // END + padding 0x00, 0x00, 0x00, 0x00, }, Want: SourceDescription{ Chunks: []SourceDescriptionChunk{{ Source: 0x01020304, Items: nil, }}, }, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, count=1, SR, len=12 0x81, 0xc8, 0x00, 0x0c, // ssrc=0x01020304 0x01, 0x02, 0x03, 0x04, // END + padding 0x00, 0x00, 0x00, 0x00, }, WantError: errWrongType, }, { Name: "bad count in header", Data: []byte{ // v=2, p=0, count=1, SDES, len=12 0x81, 0xca, 0x00, 0x0c, }, WantError: errInvalidHeader, }, { Name: "empty string", Data: []byte{ // v=2, p=0, count=1, SDES, len=12 0x81, 0xca, 0x00, 0x0c, // ssrc=0x01020304 0x01, 0x02, 0x03, 0x04, // CNAME, len=0 0x01, 0x00, // END + padding 0x00, 0x00, }, Want: *NewCNAMESourceDescription(0x01020304, ""), }, { Name: "two items", Data: []byte{ // v=2, p=0, count=1, SDES, len=16 0x81, 0xca, 0x00, 0x10, // ssrc=0x10000000 0x10, 0x00, 0x00, 0x00, // CNAME, len=1, content=A 0x01, 0x01, 0x41, // PHONE, len=1, content=B 0x04, 0x01, 0x42, // END + padding 0x00, 0x00, }, Want: SourceDescription{ Chunks: []SourceDescriptionChunk{ { Source: 0x10000000, Items: []SourceDescriptionItem{ { Type: SDESCNAME, Text: "A", }, { Type: SDESPhone, Text: "B", }, }, }, }, }, }, { Name: "two chunks", Data: []byte{ // v=2, p=0, count=2, SDES, len=24 0x82, 0xca, 0x00, 0x18, // ssrc=0x01020304 0x01, 0x02, 0x03, 0x04, // Chunk 1 // CNAME, len=1, content=A 0x01, 0x01, 0x41, // END 0x00, // Chunk 2 // SSRC 0x05060708 0x05, 0x06, 0x07, 0x08, // CNAME, len=3, content=BCD 0x01, 0x03, 0x42, 0x43, 0x44, // END 0x00, 0x00, 0x00, }, Want: SourceDescription{ Chunks: []SourceDescriptionChunk{ { Source: 0x01020304, Items: []SourceDescriptionItem{ { Type: SDESCNAME, Text: "A", }, }, }, { Source: 0x05060708, Items: []SourceDescriptionItem{ { Type: SDESCNAME, Text: "BCD", }, }, }, }, }, }, } { var sdes SourceDescription err := sdes.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) assert.Equalf(t, test.Want, sdes, "Unmarshal %q", test.Name) } } func TestSourceDescriptionRoundTrip(t *testing.T) { // a slice with enough SourceDescriptionChunks to overflow an 5-bit int var tooManyChunks []SourceDescriptionChunk var tooLongText strings.Builder for range 1 << 5 { tooManyChunks = append(tooManyChunks, SourceDescriptionChunk{}) } for range 1 << 8 { tooLongText.WriteString("x") } for _, test := range []struct { Name string Desc SourceDescription WantError error }{ { Name: "valid", Desc: SourceDescription{ Chunks: []SourceDescriptionChunk{ { Source: 1, Items: []SourceDescriptionItem{ { Type: SDESCNAME, Text: "test@example.com", }, }, }, { Source: 2, Items: []SourceDescriptionItem{ { Type: SDESNote, Text: "some note", }, { Type: SDESNote, Text: "another note", }, }, }, }, }, }, { Name: "item without type", Desc: SourceDescription{ Chunks: []SourceDescriptionChunk{{ Source: 1, Items: []SourceDescriptionItem{{ Text: "test@example.com", }}, }}, }, WantError: errSDESMissingType, }, { Name: "zero items", Desc: SourceDescription{ Chunks: []SourceDescriptionChunk{{ Source: 1, }}, }, }, { Name: "email item", Desc: SourceDescription{ Chunks: []SourceDescriptionChunk{{ Source: 1, Items: []SourceDescriptionItem{{ Type: SDESEmail, Text: "test@example.com", }}, }}, }, }, { Name: "empty text", Desc: *NewCNAMESourceDescription(1, ""), }, { Name: "text too long", Desc: SourceDescription{ Chunks: []SourceDescriptionChunk{{ Items: []SourceDescriptionItem{{ Type: SDESCNAME, Text: tooLongText.String(), }}, }}, }, WantError: errSDESTextTooLong, }, { Name: "count overflow", Desc: SourceDescription{ Chunks: tooManyChunks, }, WantError: errTooManyChunks, }, } { data, err := test.Desc.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal %q", test.Name) if err != nil { continue } var decoded SourceDescription assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Desc, decoded, "%s sdes round trip mismatch", test.Name) } } ================================================ FILE: testdata/fuzz/FuzzUnmarshal/0b954a73147600a3 ================================================ go test fuzz v1 []byte("\x8f\xcd\x00 0000000000000000A000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") ================================================ FILE: testdata/fuzz/FuzzUnmarshal/16c369bd58290097 ================================================ go test fuzz v1 []byte("\x8f\xcd\x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") ================================================ FILE: testdata/fuzz/FuzzUnmarshal/5eaf215c68e1ddb3 ================================================ go test fuzz v1 []byte("\x81\xcd\x00\x010000") ================================================ FILE: testdata/fuzz/FuzzUnmarshal/60753346a105d3c3 ================================================ go test fuzz v1 []byte("\x8b\xcd\x00\x150000000000\x80 000000000000000000000000000000000000000000000000000000000000000000000000") ================================================ FILE: testdata/fuzz/FuzzUnmarshal/6366fbb9980fa33a ================================================ go test fuzz v1 []byte("\xa4\xce\x00\x010000") ================================================ FILE: testdata/fuzz/FuzzUnmarshal/e1a48af9f8e7db71 ================================================ go test fuzz v1 []byte("\xaf\xcd\x00\a0000000000000000\xf70A000000000") ================================================ FILE: transport_layer_cc.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp // Author: adwpc import ( "encoding/binary" "errors" "fmt" "math" "strings" ) // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-5 // 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 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |V=2|P| FMT=15 | PT=205 | length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of packet sender | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of media source | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | base sequence number | packet status count | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | reference time | fb pkt. count | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | packet chunk | packet chunk | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | packet chunk | recv delta | recv delta | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | recv delta | recv delta | zero padding | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // for packet status chunk. const ( // type of packet status chunk. TypeTCCRunLengthChunk = 0 TypeTCCStatusVectorChunk = 1 // len of packet status chunk. packetStatusChunkLength = 2 ) // type of packet status symbol and recv delta. const ( // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.1 TypeTCCPacketNotReceived = uint16(iota) TypeTCCPacketReceivedSmallDelta TypeTCCPacketReceivedLargeDelta // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 // see Example 2: "packet received, w/o recv delta". TypeTCCPacketReceivedWithoutDelta ) // for status vector chunk. const ( // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.4 TypeTCCSymbolSizeOneBit = 0 TypeTCCSymbolSizeTwoBit = 1 // Notice: RFC is wrong: "packet received" (0) and "packet not received" (1) // if S == TypeTCCSymbolSizeOneBit, symbol list will be: TypeTCCPacketNotReceived TypeTCCPacketReceivedSmallDelta // if S == TypeTCCSymbolSizeTwoBit, symbol list will be same as above: //. ) func numOfBitsOfSymbolSize() map[uint16]uint16 { return map[uint16]uint16{ TypeTCCSymbolSizeOneBit: 1, TypeTCCSymbolSizeTwoBit: 2, } } var ( errPacketStatusChunkLength = errors.New("packet status chunk must be 2 bytes") errDeltaExceedLimit = errors.New("delta exceed limit") ) // PacketStatusChunk has two kinds: // RunLengthChunk and StatusVectorChunk. type PacketStatusChunk interface { Marshal() ([]byte, error) Unmarshal(rawPacket []byte) error } // RunLengthChunk T=TypeTCCRunLengthChunk // 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |T| S | Run Length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type RunLengthChunk struct { PacketStatusChunk // T = TypeTCCRunLengthChunk Type uint16 // S: type of packet status // kind: TypeTCCPacketNotReceived or... PacketStatusSymbol uint16 // RunLength: count of S RunLength uint16 } // Marshal .. func (r RunLengthChunk) Marshal() ([]byte, error) { chunk := make([]byte, 2) // append 1 bit '0' dst, err := setNBitsOfUint16(0, 1, 0, 0) if err != nil { return nil, err } // append 2 bit PacketStatusSymbol dst, err = setNBitsOfUint16(dst, 2, 1, r.PacketStatusSymbol) if err != nil { return nil, err } // append 13 bit RunLength dst, err = setNBitsOfUint16(dst, 13, 3, r.RunLength) if err != nil { return nil, err } binary.BigEndian.PutUint16(chunk, dst) return chunk, nil } // Unmarshal .. func (r *RunLengthChunk) Unmarshal(rawPacket []byte) error { if len(rawPacket) != packetStatusChunkLength { return errPacketStatusChunkLength } // record type r.Type = TypeTCCRunLengthChunk // get PacketStatusSymbol // r.PacketStatusSymbol = uint16(rawPacket[0] >> 5 & 0x03) r.PacketStatusSymbol = getNBitsFromByte(rawPacket[0], 1, 2) // get RunLength // r.RunLength = uint16(rawPacket[0]&0x1F)*256 + uint16(rawPacket[1]) r.RunLength = getNBitsFromByte(rawPacket[0], 3, 5)<<8 + uint16(rawPacket[1]) return nil } // StatusVectorChunk T=typeStatusVecotrChunk // 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |T|S| symbol list | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . type StatusVectorChunk struct { PacketStatusChunk // T = TypeTCCRunLengthChunk Type uint16 // TypeTCCSymbolSizeOneBit or TypeTCCSymbolSizeTwoBit SymbolSize uint16 // when SymbolSize = TypeTCCSymbolSizeOneBit, SymbolList is 14*1bit: // TypeTCCSymbolListPacketReceived or TypeTCCSymbolListPacketNotReceived // when SymbolSize = TypeTCCSymbolSizeTwoBit, SymbolList is 7*2bit: // TypeTCCPacketNotReceived TypeTCCPacketReceivedSmallDelta TypeTCCPacketReceivedLargeDelta or typePacketReserved SymbolList []uint16 } // Marshal .. func (r StatusVectorChunk) Marshal() ([]byte, error) { chunk := make([]byte, 2) // set first bit '1' dst, err := setNBitsOfUint16(0, 1, 0, 1) if err != nil { return nil, err } // set second bit SymbolSize dst, err = setNBitsOfUint16(dst, 1, 1, r.SymbolSize) if err != nil { return nil, err } numOfBits := numOfBitsOfSymbolSize()[r.SymbolSize] // append 14 bit SymbolList for i, s := range r.SymbolList { index := numOfBits*uint16(i) + 2 //nolint:gosec // G115 dst, err = setNBitsOfUint16(dst, numOfBits, index, s) if err != nil { return nil, err } } binary.BigEndian.PutUint16(chunk, dst) // set SymbolList(bit8-15) // chunk[1] = uint8(r.SymbolList) & 0x0f return chunk, nil } // Unmarshal .. func (r *StatusVectorChunk) Unmarshal(rawPacket []byte) error { if len(rawPacket) != packetStatusChunkLength { return errPacketStatusChunkLength } r.Type = TypeTCCStatusVectorChunk r.SymbolSize = getNBitsFromByte(rawPacket[0], 1, 1) if r.SymbolSize == TypeTCCSymbolSizeOneBit { for i := range uint16(6) { r.SymbolList = append(r.SymbolList, getNBitsFromByte(rawPacket[0], 2+i, 1)) } for i := range uint16(8) { r.SymbolList = append(r.SymbolList, getNBitsFromByte(rawPacket[1], i, 1)) } return nil } if r.SymbolSize == TypeTCCSymbolSizeTwoBit { for i := range uint16(3) { r.SymbolList = append(r.SymbolList, getNBitsFromByte(rawPacket[0], 2+i*2, 2)) } for i := range uint16(4) { r.SymbolList = append(r.SymbolList, getNBitsFromByte(rawPacket[1], i*2, 2)) } return nil } r.SymbolSize = getNBitsFromByte(rawPacket[0], 2, 6)<<8 + uint16(rawPacket[1]) return nil } const ( // TypeTCCDeltaScaleFactor https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.5 TypeTCCDeltaScaleFactor = 250 ) // RecvDelta are represented as multiples of 250us // small delta is 1 byte: [0,63.75]ms = [0, 63750]us = [0, 255]*250us // big delta is 2 bytes: [-8192.0, 8191.75]ms = [-8192000, 8191750]us = [-32768, 32767]*250us // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1.5 type RecvDelta struct { Type uint16 // us Delta int64 } // Marshal .. func (r RecvDelta) Marshal() ([]byte, error) { delta := r.Delta / TypeTCCDeltaScaleFactor // small delta if r.Type == TypeTCCPacketReceivedSmallDelta && delta >= 0 && delta <= math.MaxUint8 { deltaChunk := make([]byte, 1) deltaChunk[0] = byte(delta) //nolint:gosec // deltaChunk is created with length 1 return deltaChunk, nil } // big delta if r.Type == TypeTCCPacketReceivedLargeDelta && delta >= math.MinInt16 && delta <= math.MaxInt16 { deltaChunk := make([]byte, 2) binary.BigEndian.PutUint16(deltaChunk, uint16(delta)) //nolint:gosec //delta is validated to fit in uint16 return deltaChunk, nil } // overflow return nil, errDeltaExceedLimit } // Unmarshal .. func (r *RecvDelta) Unmarshal(rawPacket []byte) error { chunkLen := len(rawPacket) // must be 1 or 2 bytes if chunkLen != 1 && chunkLen != 2 { return errDeltaExceedLimit } if chunkLen == 1 { r.Type = TypeTCCPacketReceivedSmallDelta r.Delta = TypeTCCDeltaScaleFactor * int64(rawPacket[0]) return nil } r.Type = TypeTCCPacketReceivedLargeDelta r.Delta = TypeTCCDeltaScaleFactor * int64(int16(binary.BigEndian.Uint16(rawPacket))) //nolint:gosec // G115 return nil } const ( // the offset after header. baseSequenceNumberOffset = 8 packetStatusCountOffset = 10 referenceTimeOffset = 12 fbPktCountOffset = 15 packetChunkOffset = 16 ) // TransportLayerCC for sender-BWE // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-5 type TransportLayerCC struct { // header Header Header // SSRC of sender SenderSSRC uint32 // SSRC of the media source MediaSSRC uint32 // Transport wide sequence of rtp extension BaseSequenceNumber uint16 // PacketStatusCount PacketStatusCount uint16 // ReferenceTime ReferenceTime uint32 // FbPktCount FbPktCount uint8 // PacketChunks PacketChunks []PacketStatusChunk // RecvDeltas RecvDeltas []*RecvDelta } // Header returns the Header associated with this packet. // func (t *TransportLayerCC) Header() Header { // return t.Header // return Header{ // Padding: true, // Count: FormatTCC, // Type: TypeTCCTransportSpecificFeedback, // // https://tools.ietf.org/html/rfc4585#page-33 // Length: uint16((t.len() / 4) - 1), // } // } func (t *TransportLayerCC) packetLen() uint16 { //nolint:gocognit,cyclop n := uint16(headerLength + packetChunkOffset + len(t.PacketChunks)*2) //nolint:gosec // G115 for _, d := range t.RecvDeltas { if d.Type == TypeTCCPacketReceivedSmallDelta { n++ } else { n += 2 } } return n } // Len return total bytes with padding. func (t *TransportLayerCC) Len() uint16 { return uint16(t.MarshalSize()) //nolint:gosec // G115 } // MarshalSize returns the size of the packet once marshaled. func (t *TransportLayerCC) MarshalSize() int { n := t.packetLen() // has padding if n%4 != 0 { n = (n/4 + 1) * 4 } return int(n) } func (t TransportLayerCC) String() string { var out strings.Builder fmt.Fprintf(&out, "TransportLayerCC:\n\tHeader %v\n", t.Header) fmt.Fprintf(&out, "TransportLayerCC:\n\tSender Ssrc %d\n", t.SenderSSRC) fmt.Fprintf(&out, "\tMedia Ssrc %d\n", t.MediaSSRC) fmt.Fprintf(&out, "\tBase Sequence Number %d\n", t.BaseSequenceNumber) fmt.Fprintf(&out, "\tStatus Count %d\n", t.PacketStatusCount) fmt.Fprintf(&out, "\tReference Time %d\n", t.ReferenceTime) fmt.Fprintf(&out, "\tFeedback Packet Count %d\n", t.FbPktCount) out.WriteString("\tPacketChunks ") for _, chunk := range t.PacketChunks { fmt.Fprintf(&out, "%+v ", chunk) } out.WriteString("\n\tRecvDeltas ") for _, delta := range t.RecvDeltas { fmt.Fprintf(&out, "%+v ", delta) } out.WriteString("\n") return out.String() } // Marshal encodes the TransportLayerCC in binary. func (t TransportLayerCC) Marshal() ([]byte, error) { header, err := t.Header.Marshal() if err != nil { return nil, err } payload := make([]byte, t.MarshalSize()-headerLength) binary.BigEndian.PutUint32(payload, t.SenderSSRC) binary.BigEndian.PutUint32(payload[4:], t.MediaSSRC) binary.BigEndian.PutUint16(payload[baseSequenceNumberOffset:], t.BaseSequenceNumber) binary.BigEndian.PutUint16(payload[packetStatusCountOffset:], t.PacketStatusCount) ReferenceTimeAndFbPktCount := appendNBitsToUint32(0, 24, t.ReferenceTime) ReferenceTimeAndFbPktCount = appendNBitsToUint32(ReferenceTimeAndFbPktCount, 8, uint32(t.FbPktCount)) binary.BigEndian.PutUint32(payload[referenceTimeOffset:], ReferenceTimeAndFbPktCount) for i, chunk := range t.PacketChunks { b, err := chunk.Marshal() if err != nil { return nil, err } copy(payload[packetChunkOffset+i*2:], b) } recvDeltaOffset := packetChunkOffset + len(t.PacketChunks)*2 var i int for _, delta := range t.RecvDeltas { b, err := delta.Marshal() if err == nil { copy(payload[recvDeltaOffset+i:], b) i++ if delta.Type == TypeTCCPacketReceivedLargeDelta { i++ } } } if t.Header.Padding { payload[len(payload)-1] = uint8(t.MarshalSize() - int(t.packetLen())) //nolint:gosec // G115 } return append(header, payload...), nil } // Unmarshal .. // //nolint:gocognit,cyclop func (t *TransportLayerCC) Unmarshal(rawPacket []byte) error { if len(rawPacket) < (headerLength + ssrcLength) { return errPacketTooShort } if err := t.Header.Unmarshal(rawPacket); err != nil { return err } // https://tools.ietf.org/html/rfc4585#page-33 // header's length + payload's length totalLength := 4 * (t.Header.Length + 1) if totalLength < headerLength+packetChunkOffset { return errPacketTooShort } if len(rawPacket) < int(totalLength) { return errPacketTooShort } if t.Header.Type != TypeTransportSpecificFeedback || t.Header.Count != FormatTCC { return errWrongType } t.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:]) t.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:]) t.BaseSequenceNumber = binary.BigEndian.Uint16(rawPacket[headerLength+baseSequenceNumberOffset:]) t.PacketStatusCount = binary.BigEndian.Uint16(rawPacket[headerLength+packetStatusCountOffset:]) t.ReferenceTime = get24BitsFromBytes(rawPacket[headerLength+referenceTimeOffset : headerLength+referenceTimeOffset+3]) t.FbPktCount = rawPacket[headerLength+fbPktCountOffset] packetStatusPos := uint16(headerLength + packetChunkOffset) var processedPacketNum uint16 for processedPacketNum < t.PacketStatusCount { if packetStatusPos+packetStatusChunkLength >= totalLength { return errPacketTooShort } typ := getNBitsFromByte(rawPacket[packetStatusPos : packetStatusPos+1][0], 0, 1) var iPacketStatus PacketStatusChunk switch typ { case TypeTCCRunLengthChunk: packetStatus := &RunLengthChunk{Type: typ} iPacketStatus = packetStatus err := packetStatus.Unmarshal(rawPacket[packetStatusPos : packetStatusPos+2]) if err != nil { return err } packetNumberToProcess := localMin(t.PacketStatusCount-processedPacketNum, packetStatus.RunLength) if packetStatus.PacketStatusSymbol == TypeTCCPacketReceivedSmallDelta || packetStatus.PacketStatusSymbol == TypeTCCPacketReceivedLargeDelta { for range packetNumberToProcess { t.RecvDeltas = append(t.RecvDeltas, &RecvDelta{Type: packetStatus.PacketStatusSymbol}) } } processedPacketNum += packetNumberToProcess case TypeTCCStatusVectorChunk: packetStatus := &StatusVectorChunk{Type: typ} iPacketStatus = packetStatus err := packetStatus.Unmarshal(rawPacket[packetStatusPos : packetStatusPos+2]) if err != nil { return err } if packetStatus.SymbolSize == TypeTCCSymbolSizeOneBit { for j := 0; j < len(packetStatus.SymbolList); j++ { if packetStatus.SymbolList[j] == TypeTCCPacketReceivedSmallDelta { t.RecvDeltas = append(t.RecvDeltas, &RecvDelta{Type: TypeTCCPacketReceivedSmallDelta}) } } } if packetStatus.SymbolSize == TypeTCCSymbolSizeTwoBit { for j := 0; j < len(packetStatus.SymbolList); j++ { if packetStatus.SymbolList[j] == TypeTCCPacketReceivedSmallDelta || packetStatus.SymbolList[j] == TypeTCCPacketReceivedLargeDelta { t.RecvDeltas = append(t.RecvDeltas, &RecvDelta{Type: packetStatus.SymbolList[j]}) } } } processedPacketNum += uint16(len(packetStatus.SymbolList)) //nolint:gosec // G115 } packetStatusPos += packetStatusChunkLength t.PacketChunks = append(t.PacketChunks, iPacketStatus) } recvDeltasPos := packetStatusPos for _, delta := range t.RecvDeltas { if delta.Type == TypeTCCPacketReceivedSmallDelta { if recvDeltasPos+1 > totalLength { return errPacketTooShort } err := delta.Unmarshal(rawPacket[recvDeltasPos : recvDeltasPos+1]) if err != nil { return err } recvDeltasPos++ } if delta.Type == TypeTCCPacketReceivedLargeDelta { if recvDeltasPos+2 > totalLength { return errPacketTooShort } err := delta.Unmarshal(rawPacket[recvDeltasPos : recvDeltasPos+2]) if err != nil { return err } recvDeltasPos += 2 } } return nil } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (t TransportLayerCC) DestinationSSRC() []uint32 { return []uint32{t.MediaSSRC} } func localMin(x, y uint16) uint16 { if x < y { return x } return y } ================================================ FILE: transport_layer_cc_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*TransportLayerCC)(nil) // assert is a Packet func TestTransportLayerCC_RunLengthChunkUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want RunLengthChunk WantError error }{ { // 3.1.3 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example1", Data: []byte{0, 0xDD}, Want: RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketNotReceived, RunLength: 221, }, WantError: nil, }, { // 3.1.3 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example2", Data: []byte{0x60, 0x18}, Want: RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedWithoutDelta, RunLength: 24, }, WantError: nil, }, } { var chunk RunLengthChunk assert.NoError(t, chunk.Unmarshal(test.Data)) assert.Equalf(t, test.Want, chunk, "Unmarshal %q", test.Name) } } func TestTransportLayerCC_RunLengthChunkMarshal(t *testing.T) { for _, test := range []struct { Name string Data RunLengthChunk Want []byte WantError error }{ { // 3.1.3 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example1", Data: RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketNotReceived, RunLength: 221, }, Want: []byte{0, 0xDD}, WantError: nil, }, { // 3.1.3 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example2", Data: RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedWithoutDelta, RunLength: 24, }, Want: []byte{0x60, 0x18}, WantError: nil, }, } { chunk := test.Data data, _ := chunk.Marshal() assert.Equalf(t, test.Want, data, "Marshal %q", test.Name) } } func TestTransportLayerCC_StatusVectorChunkUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want StatusVectorChunk WantError error }{ { // 3.1.4 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example1", Data: []byte{0x9F, 0x1C}, Want: StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeOneBit, SymbolList: []uint16{ TypeTCCPacketNotReceived, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, WantError: nil, }, { // 3.1.4 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example2", Data: []byte{0xCD, 0x50}, Want: StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ TypeTCCPacketNotReceived, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, WantError: nil, }, } { var chunk StatusVectorChunk assert.NoErrorf(t, chunk.Unmarshal(test.Data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Want.Type, chunk.Type, "Unmarshal %q", test.Name) assert.Equalf(t, test.Want.SymbolSize, chunk.SymbolSize, "Unmarshal %q", test.Name) assert.Equalf(t, test.Want.SymbolList, chunk.SymbolList, "Unmarshal %q", test.Name) } } func TestTransportLayerCC_StatusVectorChunkMarshal(t *testing.T) { for _, test := range []struct { Name string Data StatusVectorChunk Want []byte WantError error }{ { // 3.1.4 example1: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example1", Data: StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeOneBit, SymbolList: []uint16{ TypeTCCPacketNotReceived, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, Want: []byte{0x9F, 0x1C}, WantError: nil, }, { // 3.1.4 example2: https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-7 Name: "example2", Data: StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ TypeTCCPacketNotReceived, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, Want: []byte{0xCD, 0x50}, WantError: nil, }, } { chunk := test.Data data, _ := chunk.Marshal() assert.Equal(t, test.Want, data, "Unmarshal %q", test.Name) } } func TestTransportLayerCC_RecvDeltaUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want RecvDelta WantError error }{ { Name: "small delta 63.75ms", Data: []byte{0xFF}, Want: RecvDelta{ Type: TypeTCCPacketReceivedSmallDelta, // 255 * 250 Delta: 63750, }, WantError: nil, }, { Name: "big delta 8191.75ms", Data: []byte{0x7F, 0xFF}, Want: RecvDelta{ Type: TypeTCCPacketReceivedLargeDelta, // 32767 * 250 Delta: 8191750, }, WantError: nil, }, { Name: "big delta -8192ms", Data: []byte{0x80, 0x00}, Want: RecvDelta{ Type: TypeTCCPacketReceivedLargeDelta, // -32768 * 250 Delta: -8192000, }, WantError: nil, }, } { var chunk RecvDelta assert.NoErrorf(t, chunk.Unmarshal(test.Data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Want, chunk, "Unmarshal %q", test.Name) } } func TestTransportLayerCC_RecvDeltaMarshal(t *testing.T) { for _, test := range []struct { Name string Data RecvDelta Want []byte WantError error }{ { Name: "small delta 63.75ms", Data: RecvDelta{ Type: TypeTCCPacketReceivedSmallDelta, // 255 * 250 Delta: 63750, }, Want: []byte{0xFF}, WantError: nil, }, { Name: "big delta 8191.75ms", Data: RecvDelta{ Type: TypeTCCPacketReceivedLargeDelta, // 32767 * 250 Delta: 8191750, }, Want: []byte{0x7F, 0xFF}, WantError: nil, }, { Name: "big delta -8192ms", Data: RecvDelta{ Type: TypeTCCPacketReceivedLargeDelta, // -32768 * 250 Delta: -8192000, }, Want: []byte{0x80, 0x00}, WantError: nil, }, } { chunk := test.Data data, _ := chunk.Marshal() assert.Equalf(t, test.Want, data, "Unmarshal %q", test.Name) } } // 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 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |V=2|P| FMT=15 | PT=205 | length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of packet sender | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | SSRC of media source | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | base sequence number | packet status count | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | reference time | fb pkt. count | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | packet chunk | recv delta | recv delta | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0b10101111,0b11001101,0b00000000,0b00000101, // 0b11111010,0b00010111,0b11111010,0b00010111, // 0b01000011,0b00000011,0b00101111,0b10100000, // 0b00000000,0b10011001,0b00000000,0b00000001, // 0b00111101,0b11101000,0b00000010,0b00010111, // 0b00100000,0b00000001,0b10010100,0b00000001, // . // //nolint:maintidx func TestTransportLayerCC_Unmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want TransportLayerCC WantError error }{ { Name: "example1", Data: []byte{ 0xaf, 0xcd, 0x0, 0x5, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x0, 0x99, 0x0, 0x1, 0x3d, 0xe8, 0x2, 0x17, 0x20, 0x1, 0x94, 0x1, }, Want: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 5, }, SenderSSRC: 4195875351, MediaSSRC: 1124282272, BaseSequenceNumber: 153, PacketStatusCount: 1, ReferenceTime: 4057090, FbPktCount: 23, // 0b00100000, 0b00000001 PacketChunks: []PacketStatusChunk{ &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedSmallDelta, RunLength: 1, }, }, // 0b10010100 RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 37000, }, }, }, WantError: nil, }, { Name: "example2", Data: []byte{ 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, 0x0, 0xe, 0x45, 0xb1, 0x5a, 0x40, 0xd8, 0x0, 0xf0, 0xff, 0xd0, 0x0, 0x0, 0x3, }, Want: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 6, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 372, PacketStatusCount: 14, ReferenceTime: 4567386, FbPktCount: 64, PacketChunks: []PacketStatusChunk{ &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedLargeDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedWithoutDelta, }, }, }, // 0b10010100 RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 52000, }, { Type: TypeTCCPacketReceivedLargeDelta, Delta: 0, }, }, }, WantError: nil, }, { Name: "example3", Data: []byte{ 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, 0x0, 0x6, 0x45, 0xb1, 0x5a, 0x40, 0x40, 0x2, 0x20, 0x04, 0x1f, 0xfe, 0x1f, 0x9a, 0xd0, 0x0, 0xd0, 0x0, }, Want: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 7, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 372, PacketStatusCount: 6, ReferenceTime: 4567386, FbPktCount: 64, PacketChunks: []PacketStatusChunk{ &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedLargeDelta, RunLength: 2, }, &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedSmallDelta, RunLength: 4, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedLargeDelta, Delta: 2047500, }, { Type: TypeTCCPacketReceivedLargeDelta, Delta: 2022500, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 52000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 0, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 52000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 0, }, }, }, WantError: nil, }, { Name: "example4", Data: []byte{ 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x4, 0x0, 0x7, 0x10, 0x63, 0x6e, 0x1, 0x20, 0x7, 0x4c, 0x24, 0x24, 0x10, 0xc, 0xc, 0x10, 0x0, 0x0, 0x3, }, Want: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 7, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 4, PacketStatusCount: 7, ReferenceTime: 1074030, FbPktCount: 1, PacketChunks: []PacketStatusChunk{ &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedSmallDelta, RunLength: 7, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 19000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 9000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 9000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, }, }, WantError: nil, }, { Name: "example5", Data: []byte{ 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x1, 0x0, 0xe, 0x10, 0x63, 0x6d, 0x0, 0xba, 0x0, 0x10, 0xc, 0xc, 0x10, 0x0, 0x3, }, Want: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 6, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 1, PacketStatusCount: 14, ReferenceTime: 1074029, FbPktCount: 0, PacketChunks: []PacketStatusChunk{ &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: 0, SymbolList: []uint16{ TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, }, }, WantError: nil, }, { Name: "example6", Data: []byte{ 0xaf, 0xcd, 0x0, 0x7, 0x9b, 0x74, 0xf6, 0x1f, 0x93, 0x71, 0xdc, 0xbc, 0x85, 0x3c, 0x0, 0x9, 0x63, 0xf9, 0x16, 0xb3, 0xd5, 0x52, 0x0, 0x30, 0x9b, 0xaa, 0x6a, 0xaa, 0x7b, 0x1, 0x9, 0x1, }, Want: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 7, }, SenderSSRC: 2608133663, MediaSSRC: 2473712828, BaseSequenceNumber: 34108, PacketStatusCount: 9, ReferenceTime: 6551830, FbPktCount: 179, PacketChunks: []PacketStatusChunk{ &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketReceivedLargeDelta, }, }, &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketNotReceived, RunLength: 48, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 38750, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 42500, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 26500, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 42500, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 30750, }, { Type: TypeTCCPacketReceivedLargeDelta, Delta: 66250, }, }, }, WantError: nil, }, { Name: "example7", Data: []byte{ 0x8f, 0xcd, 0x0, 0x4, 0x9a, 0xcb, 0x4, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, }, Want: TransportLayerCC{ Header: Header{ Padding: false, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 4, }, SenderSSRC: 2596996162, MediaSSRC: 0, BaseSequenceNumber: 0, PacketStatusCount: 0, ReferenceTime: 0, FbPktCount: 0, }, WantError: nil, }, { Name: "example8", Data: []byte{ 0xaf, 0xcd, 0x0, 0x5, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x0, 0x99, 0x0, 0x3, 0x3d, 0xe8, 0x2, 0x17, 0x20, 0x3, 0x94, 0x1, }, Want: TransportLayerCC{}, WantError: errPacketTooShort, }, { Name: "example9", Data: []byte{ 0xaf, 0xcd, 0x0, 0x5, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x0, 0x99, 0x0, 0x2, 0x3d, 0xe8, 0x2, 0x17, 0x40, 0x2, 0x94, 0x1, }, Want: TransportLayerCC{}, WantError: errPacketTooShort, }, } { t.Run(test.Name, func(t *testing.T) { var chunk TransportLayerCC err := chunk.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) if err != nil { return } assert.Equalf(t, test.Want, chunk, "Unmarshal %q", test.Name) }) } } //nolint:maintidx func TestTransportLayerCC_Marshal(t *testing.T) { for _, test := range []struct { Name string Data TransportLayerCC Want []byte WantError error }{ { Name: "example1", Data: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 5, }, SenderSSRC: 4195875351, MediaSSRC: 1124282272, BaseSequenceNumber: 153, PacketStatusCount: 1, ReferenceTime: 4057090, FbPktCount: 23, // 0b00100000, 0b00000001 PacketChunks: []PacketStatusChunk{ &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedSmallDelta, RunLength: 1, }, }, // 0b10010100 RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 37000, }, }, }, Want: []byte{ 0xaf, 0xcd, 0x0, 0x5, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x0, 0x99, 0x0, 0x1, 0x3d, 0xe8, 0x2, 0x17, 0x20, 0x1, 0x94, 0x1, }, WantError: nil, }, { Name: "example2", Data: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 6, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 372, PacketStatusCount: 2, ReferenceTime: 4567386, FbPktCount: 64, PacketChunks: []PacketStatusChunk{ &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedLargeDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeTwoBit, SymbolList: []uint16{ TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedWithoutDelta, TypeTCCPacketReceivedWithoutDelta, }, }, }, // 0b10010100 RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 52000, }, { Type: TypeTCCPacketReceivedLargeDelta, Delta: 0, }, }, }, Want: []byte{ 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, 0x0, 0x2, 0x45, 0xb1, 0x5a, 0x40, 0xd8, 0x0, 0xf0, 0xff, 0xd0, 0x0, 0x0, 0x1, }, WantError: nil, }, { Name: "example3", Data: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 7, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 372, PacketStatusCount: 6, ReferenceTime: 4567386, FbPktCount: 64, PacketChunks: []PacketStatusChunk{ &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedLargeDelta, RunLength: 2, }, &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedSmallDelta, RunLength: 4, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedLargeDelta, Delta: 2047500, }, { Type: TypeTCCPacketReceivedLargeDelta, Delta: 2022500, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 52000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 0, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 52000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 0, }, }, }, Want: []byte{ 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x1, 0x74, 0x0, 0x6, 0x45, 0xb1, 0x5a, 0x40, 0x40, 0x2, 0x20, 0x04, 0x1f, 0xfe, 0x1f, 0x9a, 0xd0, 0x0, 0xd0, 0x0, }, WantError: nil, }, { Name: "example4", Data: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 7, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 4, PacketStatusCount: 7, ReferenceTime: 1074030, FbPktCount: 1, PacketChunks: []PacketStatusChunk{ &RunLengthChunk{ Type: TypeTCCRunLengthChunk, PacketStatusSymbol: TypeTCCPacketReceivedSmallDelta, RunLength: 7, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 19000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 9000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 9000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, }, }, Want: []byte{ 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x4, 0x0, 0x7, 0x10, 0x63, 0x6e, 0x1, 0x20, 0x7, 0x4c, 0x24, 0x24, 0x10, 0xc, 0xc, 0x10, 0x0, 0x0, 0x3, }, WantError: nil, }, { Name: "example5", Data: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 6, }, SenderSSRC: 4195875351, MediaSSRC: 423483579, BaseSequenceNumber: 1, PacketStatusCount: 14, ReferenceTime: 1074029, FbPktCount: 0, PacketChunks: []PacketStatusChunk{ &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeOneBit, SymbolList: []uint16{ TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 3000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 4000, }, }, }, Want: []byte{ 0xaf, 0xcd, 0x0, 0x6, 0xfa, 0x17, 0xfa, 0x17, 0x19, 0x3d, 0xd8, 0xbb, 0x0, 0x1, 0x0, 0xe, 0x10, 0x63, 0x6d, 0x0, 0xba, 0x0, 0x10, 0xc, 0xc, 0x10, 0x0, 0x2, }, WantError: nil, }, { Name: "example6", Data: TransportLayerCC{ Header: Header{ Padding: true, Count: FormatTCC, Type: TypeTransportSpecificFeedback, Length: 7, }, SenderSSRC: 4195875351, MediaSSRC: 1124282272, BaseSequenceNumber: 39956, PacketStatusCount: 12, ReferenceTime: 7701536, FbPktCount: 0, PacketChunks: []PacketStatusChunk{ &StatusVectorChunk{ Type: TypeTCCStatusVectorChunk, SymbolSize: TypeTCCSymbolSizeOneBit, SymbolList: []uint16{ TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketReceivedSmallDelta, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, TypeTCCPacketNotReceived, }, }, }, RecvDeltas: []*RecvDelta{ { Type: TypeTCCPacketReceivedSmallDelta, Delta: 48250, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 15750, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 14750, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 15750, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 20750, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 36000, }, { Type: TypeTCCPacketReceivedSmallDelta, Delta: 14750, }, }, }, Want: []byte{ 0xaf, 0xcd, 0x0, 0x7, 0xfa, 0x17, 0xfa, 0x17, 0x43, 0x3, 0x2f, 0xa0, 0x9c, 0x14, 0x0, 0xc, 0x75, 0x84, 0x20, 0x0, 0xbe, 0xc0, 0xc1, 0x3f, 0x3b, 0x3f, 0x53, 0x90, 0x3b, 0x0, 0x0, 0x3, }, WantError: nil, }, } { t.Run(test.Name, func(t *testing.T) { bin, err := test.Data.Marshal() assert.NoError(t, err) assert.Equalf(t, test.Want, bin, "Marshal %q", test.Name) }) } } ================================================ FILE: transport_layer_nack.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "encoding/binary" "fmt" "math" "strings" ) // PacketBitmap shouldn't be used like a normal integral, // so it's type is masked here. Access it with PacketList(). type PacketBitmap uint16 // NackPair is a wire-representation of a collection of // Lost RTP packets. type NackPair struct { // ID of lost packets PacketID uint16 // Bitmask of following lost packets LostPackets PacketBitmap } // The TransportLayerNack packet informs the encoder about the loss of a transport packet // IETF RFC 4585, Section 6.2.1 // https://tools.ietf.org/html/rfc4585#section-6.2.1 type TransportLayerNack struct { // SSRC of sender SenderSSRC uint32 // SSRC of the media source MediaSSRC uint32 Nacks []NackPair } // NackPairsFromSequenceNumbers generates a slice of NackPair from a list of SequenceNumbers // This handles generating the proper values for PacketID/LostPackets. func NackPairsFromSequenceNumbers(sequenceNumbers []uint16) (pairs []NackPair) { if len(sequenceNumbers) == 0 { return []NackPair{} } nackPair := &NackPair{PacketID: sequenceNumbers[0]} for i := 1; i < len(sequenceNumbers); i++ { m := sequenceNumbers[i] if m-nackPair.PacketID > 16 { pairs = append(pairs, *nackPair) nackPair = &NackPair{PacketID: m} continue } nackPair.LostPackets |= 1 << (m - nackPair.PacketID - 1) } pairs = append(pairs, *nackPair) return } // Range calls f sequentially for each sequence number covered by n. // If f returns false, Range stops the iteration. func (n *NackPair) Range(f func(seqno uint16) bool) { more := f(n.PacketID) if !more { return } b := n.LostPackets for i := uint16(0); b != 0; i++ { if (b & (1 << i)) != 0 { b &^= (1 << i) more = f(n.PacketID + i + 1) if !more { return } } } } // PacketList returns a list of Nack'd packets that's referenced by a NackPair. func (n *NackPair) PacketList() []uint16 { out := make([]uint16, 0, 17) n.Range(func(seqno uint16) bool { out = append(out, seqno) return true }) return out } const ( tlnLength = 2 nackOffset = 8 ) // Marshal encodes the TransportLayerNack in binary. func (p TransportLayerNack) Marshal() ([]byte, error) { if len(p.Nacks)+tlnLength > math.MaxUint8 { return nil, errTooManyReports } rawPacket := make([]byte, nackOffset+(len(p.Nacks)*4)) binary.BigEndian.PutUint32(rawPacket, p.SenderSSRC) binary.BigEndian.PutUint32(rawPacket[4:], p.MediaSSRC) for i := 0; i < len(p.Nacks); i++ { binary.BigEndian.PutUint16(rawPacket[nackOffset+(4*i):], p.Nacks[i].PacketID) binary.BigEndian.PutUint16(rawPacket[nackOffset+(4*i)+2:], uint16(p.Nacks[i].LostPackets)) } h := p.Header() hData, err := h.Marshal() if err != nil { return nil, err } return append(hData, rawPacket...), nil } // Unmarshal decodes the TransportLayerNack from binary. func (p *TransportLayerNack) Unmarshal(rawPacket []byte) error { if len(rawPacket) < (headerLength + ssrcLength) { return errPacketTooShort } var header Header if err := header.Unmarshal(rawPacket); err != nil { return err } if len(rawPacket) < (headerLength + int(4*header.Length)) { return errPacketTooShort } if header.Type != TypeTransportSpecificFeedback || header.Count != FormatTLN { return errWrongType } // The FCI field MUST contain at least one and MAY contain more than one Generic NACK if 4*header.Length <= nackOffset { return errBadLength } p.SenderSSRC = binary.BigEndian.Uint32(rawPacket[headerLength:]) p.MediaSSRC = binary.BigEndian.Uint32(rawPacket[headerLength+ssrcLength:]) for i := headerLength + nackOffset; i < (headerLength + int(header.Length*4)); i += 4 { p.Nacks = append(p.Nacks, NackPair{ binary.BigEndian.Uint16(rawPacket[i:]), PacketBitmap(binary.BigEndian.Uint16(rawPacket[i+2:])), }) } return nil } // MarshalSize returns the size of the packet once marshaled. func (p *TransportLayerNack) MarshalSize() int { return headerLength + nackOffset + (len(p.Nacks) * 4) } // Header returns the Header associated with this packet. func (p *TransportLayerNack) Header() Header { return Header{ Count: FormatTLN, Type: TypeTransportSpecificFeedback, Length: uint16((p.MarshalSize() / 4) - 1), //nolint:gosec // G115 } } func (p TransportLayerNack) String() string { var out strings.Builder fmt.Fprintf(&out, "TransportLayerNack from %x\n", p.SenderSSRC) fmt.Fprintf(&out, "\tMedia Ssrc %x\n", p.MediaSSRC) out.WriteString("\tID\tLostPackets\n") for _, i := range p.Nacks { fmt.Fprintf(&out, "\t%d\t%b\n", i.PacketID, i.LostPackets) } return out.String() } // DestinationSSRC returns an array of SSRC values that this packet refers to. func (p *TransportLayerNack) DestinationSSRC() []uint32 { return []uint32{p.MediaSSRC} } ================================================ FILE: transport_layer_nack_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) var _ Packet = (*TransportLayerNack)(nil) // assert is a Packet func TestTransportLayerNackUnmarshal(t *testing.T) { for _, test := range []struct { Name string Data []byte Want TransportLayerNack WantError error }{ { Name: "valid", Data: []byte{ // TransportLayerNack 0x81, 0xcd, 0x0, 0x3, // sender=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // nack 0xAAAA, 0x5555 0xaa, 0xaa, 0x55, 0x55, }, Want: TransportLayerNack{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, Nacks: []NackPair{{0xaaaa, 0x5555}}, }, }, { Name: "short report", Data: []byte{ 0x81, 0xcd, 0x0, 0x2, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // report ends early }, WantError: errPacketTooShort, }, { Name: "bad length", Data: []byte{ // TransportLayerNack 0x81, 0xcd, 0x0, 0x2, // sender=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, }, WantError: errBadLength, }, { Name: "wrong type", Data: []byte{ // v=2, p=0, count=1, SR, len=7 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 0x0, 0x0, 0x46, 0xe1, // jitter=273 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 0x9, 0xf3, 0x64, 0x32, // delay=150137 0x0, 0x2, 0x4a, 0x79, }, WantError: errWrongType, }, { Name: "nil", Data: nil, WantError: errPacketTooShort, }, } { var tln TransportLayerNack err := tln.Unmarshal(test.Data) assert.ErrorIsf(t, err, test.WantError, "Unmarshal %q", test.Name) assert.Equalf(t, test.Want, tln, "Unmarshal %q", test.Name) } } func TestTransportLayerNackRoundTrip(t *testing.T) { for _, test := range []struct { Name string Report TransportLayerNack WantError error }{ { Name: "valid", Report: TransportLayerNack{ SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, Nacks: []NackPair{{1, 0xAA}, {1034, 0x05}}, }, }, } { data, err := test.Report.Marshal() assert.ErrorIsf(t, err, test.WantError, "Marshal err on %q", test.Name) var decoded TransportLayerNack assert.NoErrorf(t, decoded.Unmarshal(data), "Unmarshal %q", test.Name) assert.Equalf(t, test.Report, decoded, "Unmarshal %q: decoded mismatch", test.Name) } } func testNackPair(t *testing.T, s []uint16, n NackPair) { t.Helper() l := n.PacketList() assert.Equalf(t, s, l, "NackPair %v mismatch", n) } func TestNackPair(t *testing.T) { testNackPair(t, []uint16{42}, NackPair{42, 0}) testNackPair(t, []uint16{42, 43}, NackPair{42, 1}) testNackPair(t, []uint16{42, 44}, NackPair{42, 2}) testNackPair(t, []uint16{42, 43, 44}, NackPair{42, 3}) testNackPair(t, []uint16{42, 42 + 16}, NackPair{42, 0x8000}) } func TestNackPairRange(t *testing.T) { pair := NackPair{42, 2} out := make([]uint16, 0) pair.Range(func(s uint16) bool { out = append(out, s) return true }) assert.Equal(t, []uint16{42, 44}, out) out = make([]uint16, 0) pair.Range(func(s uint16) bool { out = append(out, s) return false }) assert.Equal(t, []uint16{42}, out) } func TestTransportLayerNackPairGeneration(t *testing.T) { for _, test := range []struct { Name string SequenceNumbers []uint16 Expected []NackPair }{ { "No Sequence Numbers", []uint16{}, []NackPair{}, }, { "Single Sequence Number", []uint16{100}, []NackPair{ {PacketID: 100, LostPackets: 0x0}, }, }, { "Multiple in range, Single NACKPair", []uint16{100, 101, 105, 115}, []NackPair{ {PacketID: 100, LostPackets: 0x4011}, }, }, { "Multiple Ranges, Multiple NACKPair", []uint16{100, 117, 500, 501, 502}, []NackPair{ {PacketID: 100, LostPackets: 0}, {PacketID: 117, LostPackets: 0}, {PacketID: 500, LostPackets: 0x3}, }, }, } { actual := NackPairsFromSequenceNumbers(test.SequenceNumbers) assert.Equalf(t, test.Expected, actual, "%q NackPair generation mismatch", test.Name) } } ================================================ FILE: util.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp // getPadding Returns the padding required to make the length a multiple of 4. func getPadding(packetLen int) int { if packetLen%4 == 0 { return 0 } return 4 - (packetLen % 4) } // setNBitsOfUint16 will truncate the value to size, left-shift to startIndex position and set. func setNBitsOfUint16(src, size, startIndex, val uint16) (uint16, error) { if startIndex+size > 16 { return 0, errInvalidSizeOrStartIndex } // truncate val to size bits val &= (1 << size) - 1 return src | (val << (16 - size - startIndex)), nil } // appendBit32 will left-shift and append n bits of val. func appendNBitsToUint32(src, n, val uint32) uint32 { return (src << n) | (val & (0xFFFFFFFF >> (32 - n))) } // getNBit get n bits from 1 byte, begin with a position. func getNBitsFromByte(b byte, begin, n uint16) uint16 { endShift := 8 - (begin + n) mask := (0xFF >> begin) & uint8(0xFF<> endShift } // get24BitFromBytes get 24bits from `[3]byte` slice. func get24BitsFromBytes(b []byte) uint32 { return uint32(b[0])<<16 + uint32(b[1])<<8 + uint32(b[2]) } ================================================ FILE: util_test.go ================================================ // SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package rtcp import ( "testing" "github.com/stretchr/testify/assert" ) func TestGetPadding(t *testing.T) { assert := assert.New(t) type testCase struct { input int result int } cases := []testCase{ {input: 0, result: 0}, {input: 1, result: 3}, {input: 2, result: 2}, {input: 3, result: 1}, {input: 4, result: 0}, {input: 100, result: 0}, {input: 500, result: 0}, } for _, testCase := range cases { assert.Equalf( getPadding(testCase.input), testCase.result, "Test case returned wrong value for input %d", testCase.input, ) } } func TestSetNBitsOfUint16(t *testing.T) { for _, test := range []struct { name string source uint16 size uint16 index uint16 value uint16 result uint16 expectedErr error }{ { "setOneBit", 0, 1, 8, 1, 128, nil, }, { "setStatusVectorBit", 0, 1, 0, 1, 32768, nil, }, { "setStatusVectorSecondBit", 32768, 1, 1, 1, 49152, nil, }, { "setStatusVectorInnerBitsAndCutValue", 49152, 2, 6, 11111, 49920, nil, }, { "setRunLengthSecondTwoBit", 32768, 2, 1, 1, 40960, nil, }, { "setOneBitOutOfBounds", 32768, 2, 15, 1, 0, errInvalidSizeOrStartIndex, }, } { t.Run(test.name, func(t *testing.T) { got, err := setNBitsOfUint16(test.source, test.size, test.index, test.value) if test.expectedErr != nil { assert.ErrorIs(t, err, test.expectedErr) } else { assert.NoError(t, err) } assert.Equalf(t, test.result, got, "setNBitsOfUint16 %q", test.name) }) } }