diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 00000000..c3421a1a --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +.goassets diff --git a/.github/fetch-scripts.sh b/.github/fetch-scripts.sh new file mode 100755 index 00000000..f333841e --- /dev/null +++ b/.github/fetch-scripts.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# +# If you want to update the shared CI config, send a PR to +# https://github.com/pion/.goassets instead of this repository. +# +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +set -eu + +SCRIPT_PATH="$(realpath "$(dirname "$0")")" +GOASSETS_PATH="${SCRIPT_PATH}/.goassets" + +GOASSETS_REF=${GOASSETS_REF:-master} + +if [ -d "${GOASSETS_PATH}" ]; then + if ! git -C "${GOASSETS_PATH}" diff --exit-code; then + echo "${GOASSETS_PATH} has uncommitted changes" >&2 + exit 1 + fi + git -C "${GOASSETS_PATH}" fetch origin + git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} + git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} +else + git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" +fi diff --git a/.github/generate-authors.sh b/.github/generate-authors.sh deleted file mode 100755 index 182e4f5e..00000000 --- a/.github/generate-authors.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash - -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# - -set -e - -SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) -AUTHORS_PATH="$GITHUB_WORKSPACE/AUTHORS.txt" - -if [ -f ${SCRIPT_PATH}/.ci.conf ] -then - . ${SCRIPT_PATH}/.ci.conf -fi - -# -# DO NOT EDIT THIS -# -EXCLUDED_CONTRIBUTORS+=('John R. Bradley' 'renovate[bot]' 'Renovate Bot' 'Pion Bot' 'pionbot') -# If you want to exclude a name from all repositories, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# If you want to exclude a name only from this repository, -# add EXCLUDED_CONTRIBUTORS=('name') to .github/.ci.conf - -CONTRIBUTORS=() - -shouldBeIncluded () { - for i in "${EXCLUDED_CONTRIBUTORS[@]}" - do - if [[ $1 =~ "$i" ]]; then - return 1 - fi - done - return 0 -} - - -IFS=$'\n' #Only split on newline -for contributor in $(git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf) -do - if shouldBeIncluded $contributor; then - CONTRIBUTORS+=("$contributor") - fi -done -unset IFS - -if [ ${#CONTRIBUTORS[@]} -ne 0 ]; then - cat >$AUTHORS_PATH <<-'EOH' -# Thank you to everyone that made Pion possible. If you are interested in contributing -# we would love to have you https://github.com/pion/webrtc/wiki/Contributing -# -# This file is auto generated, using git to list all individuals contributors. -# see `.github/generate-authors.sh` for the scripting -EOH - for i in "${CONTRIBUTORS[@]}" - do - echo "$i" >> $AUTHORS_PATH - done - exit 0 -fi diff --git a/.github/hooks/commit-msg.sh b/.github/hooks/commit-msg.sh deleted file mode 100755 index 8213dc20..00000000 --- a/.github/hooks/commit-msg.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# -# DO NOT EDIT THIS FILE DIRECTLY -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# - -set -e - -.github/lint-commit-message.sh $1 diff --git a/.github/hooks/pre-commit.sh b/.github/hooks/pre-commit.sh deleted file mode 100755 index d5a1ce5c..00000000 --- a/.github/hooks/pre-commit.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -# -# DO NOT EDIT THIS FILE DIRECTLY -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# - -# Redirect output to stderr. -exec 1>&2 - -.github/lint-disallowed-functions-in-library.sh -.github/lint-no-trailing-newline-in-log-messages.sh diff --git a/.github/hooks/pre-push.sh b/.github/hooks/pre-push.sh deleted file mode 100755 index bfe65bc5..00000000 --- a/.github/hooks/pre-push.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -# -# DO NOT EDIT THIS FILE DIRECTLY -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# - -set -e - -.github/generate-authors.sh - -exit 0 diff --git a/.github/install-hooks.sh b/.github/install-hooks.sh index 73d20a4e..8aa34be9 100755 --- a/.github/install-hooks.sh +++ b/.github/install-hooks.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # # DO NOT EDIT THIS FILE @@ -8,9 +8,13 @@ # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT -SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +SCRIPT_PATH="$(realpath "$(dirname "$0")")" -cp "$SCRIPT_PATH/hooks/commit-msg.sh" "$SCRIPT_PATH/../.git/hooks/commit-msg" -cp "$SCRIPT_PATH/hooks/pre-commit.sh" "$SCRIPT_PATH/../.git/hooks/pre-commit" -cp "$SCRIPT_PATH/hooks/pre-push.sh" "$SCRIPT_PATH/../.git/hooks/pre-push" +. ${SCRIPT_PATH}/fetch-scripts.sh + +cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" +cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" +cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" diff --git a/.github/lint-commit-message.sh b/.github/lint-commit-message.sh deleted file mode 100755 index 010a3328..00000000 --- a/.github/lint-commit-message.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash - -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# - -set -e - -display_commit_message_error() { -cat << EndOfMessage -$1 - -------------------------------------------------- -The preceding commit message is invalid -it failed '$2' of the following checks - -* Separate subject from body with a blank line -* Limit the subject line to 50 characters -* Capitalize the subject line -* Do not end the subject line with a period -* Wrap the body at 72 characters -EndOfMessage - - exit 1 -} - -lint_commit_message() { - if [[ "$(echo "$1" | awk 'NR == 2 {print $1;}' | wc -c)" -ne 1 ]]; then - display_commit_message_error "$1" 'Separate subject from body with a blank line' - fi - - if [[ "$(echo "$1" | head -n1 | awk '{print length}')" -gt 50 ]]; then - display_commit_message_error "$1" 'Limit the subject line to 50 characters' - fi - - if [[ ! $1 =~ ^[A-Z] ]]; then - display_commit_message_error "$1" 'Capitalize the subject line' - fi - - if [[ "$(echo "$1" | awk 'NR == 1 {print substr($0,length($0),1)}')" == "." ]]; then - display_commit_message_error "$1" 'Do not end the subject line with a period' - fi - - if [[ "$(echo "$1" | awk '{print length}' | sort -nr | head -1)" -gt 72 ]]; then - display_commit_message_error "$1" 'Wrap the body at 72 characters' - fi -} - -if [ "$#" -eq 1 ]; then - if [ ! -f "$1" ]; then - echo "$0 was passed one argument, but was not a valid file" - exit 1 - fi - lint_commit_message "$(sed -n '/# Please enter the commit message for your changes. Lines starting/q;p' "$1")" -else - for commit in $(git rev-list --no-merges origin/master..); do - lint_commit_message "$(git log --format="%B" -n 1 $commit)" - done -fi diff --git a/.github/lint-disallowed-functions-in-library.sh b/.github/lint-disallowed-functions-in-library.sh deleted file mode 100755 index 8ce5d096..00000000 --- a/.github/lint-disallowed-functions-in-library.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash - -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# - -set -e - -# Disallow usages of functions that cause the program to exit in the library code -SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) -if [ -f ${SCRIPT_PATH}/.ci.conf ] -then - . ${SCRIPT_PATH}/.ci.conf -fi - -EXCLUDE_DIRECTORIES=${DISALLOWED_FUNCTIONS_EXCLUDED_DIRECTORIES:-"examples"} -DISALLOWED_FUNCTIONS=('os.Exit(' 'panic(' 'Fatal(' 'Fatalf(' 'Fatalln(' 'fmt.Println(' 'fmt.Printf(' 'log.Print(' 'log.Println(' 'log.Printf(' 'print(' 'println(') - -files=$( - find "$SCRIPT_PATH/.." -name "*.go" \ - | grep -v -e '^.*_test.go$' \ - | while read file - do - excluded=false - for ex in $EXCLUDE_DIRECTORIES - do - if [[ $file == */$ex/* ]] - then - excluded=true - break - fi - done - $excluded || echo "$file" - done -) - -for disallowedFunction in "${DISALLOWED_FUNCTIONS[@]}" -do - if grep -e "\s$disallowedFunction" $files | grep -v -e 'nolint'; then - echo "$disallowedFunction may only be used in example code" - exit 1 - fi -done diff --git a/.github/lint-filename.sh b/.github/lint-filename.sh deleted file mode 100755 index 81b3f14f..00000000 --- a/.github/lint-filename.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# - -set -e - -SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) -GO_REGEX="^[a-zA-Z][a-zA-Z0-9_]*\.go$" - -find "$SCRIPT_PATH/.." -name "*.go" | while read fullpath; do - filename=$(basename -- "$fullpath") - - if ! [[ $filename =~ $GO_REGEX ]]; then - echo "$filename is not a valid filename for Go code, only alpha, numbers and underscores are supported" - exit 1 - fi -done diff --git a/.github/lint-no-trailing-newline-in-log-messages.sh b/.github/lint-no-trailing-newline-in-log-messages.sh deleted file mode 100755 index 29cd4a28..00000000 --- a/.github/lint-no-trailing-newline-in-log-messages.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# - -set -e - -# Disallow usages of functions that cause the program to exit in the library code -SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) -if [ -f ${SCRIPT_PATH}/.ci.conf ] -then - . ${SCRIPT_PATH}/.ci.conf -fi - -files=$( - find "$SCRIPT_PATH/.." -name "*.go" \ - | while read file - do - excluded=false - for ex in $EXCLUDE_DIRECTORIES - do - if [[ $file == */$ex/* ]] - then - excluded=true - break - fi - done - $excluded || echo "$file" - done -) - -if grep -E '\.(Trace|Debug|Info|Warn|Error)f?\("[^"]*\\n"\)?' $files | grep -v -e 'nolint'; then - echo "Log format strings should have trailing new-line" - exit 1 -fi \ No newline at end of file diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml new file mode 100644 index 00000000..1032179e --- /dev/null +++ b/.github/workflows/api.yaml @@ -0,0 +1,20 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# If this repository should have package specific CI config, +# remove the repository name from .goassets/.github/workflows/assets-sync.yml. +# +# If you want to update the shared CI config, send a PR to +# https://github.com/pion/.goassets instead of this repository. +# +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +name: API +on: + pull_request: + +jobs: + check: + uses: pion/.goassets/.github/workflows/api.reusable.yml@master diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cec0d7c6..ea9b825e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,17 @@ -name: "CodeQL" +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# If this repository should have package specific CI config, +# remove the repository name from .goassets/.github/workflows/assets-sync.yml. +# +# If you want to update the shared CI config, send a PR to +# https://github.com/pion/.goassets instead of this repository. +# +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +name: CodeQL on: workflow_dispatch: @@ -12,29 +25,4 @@ on: jobs: analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - # The code in examples/ might intentionally do things like log credentials - # in order to show how the library is used, aid in debugging etc. We - # should ignore those for CodeQL scanning, and only focus on the package - # itself. - - name: Remove example code - run: | - rm -rf examples/ - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: 'go' - - - name: CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master diff --git a/.github/workflows/generate-authors.yml b/.github/workflows/generate-authors.yml deleted file mode 100644 index c7a8404d..00000000 --- a/.github/workflows/generate-authors.yml +++ /dev/null @@ -1,77 +0,0 @@ -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# If this repository should have package specific CI config, -# remove the repository name from .goassets/.github/workflows/assets-sync.yml. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# - -name: generate-authors - -on: - pull_request: - -jobs: - checksecret: - permissions: - contents: none - runs-on: ubuntu-latest - outputs: - is_PIONBOT_PRIVATE_KEY_set: ${{ steps.checksecret_job.outputs.is_PIONBOT_PRIVATE_KEY_set }} - steps: - - id: checksecret_job - env: - PIONBOT_PRIVATE_KEY: ${{ secrets.PIONBOT_PRIVATE_KEY }} - run: | - echo "is_PIONBOT_PRIVATE_KEY_set: ${{ env.PIONBOT_PRIVATE_KEY != '' }}" - echo "::set-output name=is_PIONBOT_PRIVATE_KEY_set::${{ env.PIONBOT_PRIVATE_KEY != '' }}" - - generate-authors: - permissions: - contents: write - needs: [checksecret] - if: needs.checksecret.outputs.is_PIONBOT_PRIVATE_KEY_set == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - token: ${{ secrets.PIONBOT_PRIVATE_KEY }} - - - name: Generate the authors file - run: .github/generate-authors.sh - - - name: Add the authors file to git - run: git add AUTHORS.txt - - - name: Get last commit message - id: last-commit-message - run: | - COMMIT_MSG=$(git log -1 --pretty=%B) - COMMIT_MSG="${COMMIT_MSG//'%'/'%25'}" - COMMIT_MSG="${COMMIT_MSG//$'\n'/'%0A'}" - COMMIT_MSG="${COMMIT_MSG//$'\r'/'%0D'}" - echo "::set-output name=msg::$COMMIT_MSG" - - - name: Get last commit author - id: last-commit-author - run: | - echo "::set-output name=msg::$(git log -1 --pretty='%aN <%ae>')" - - - name: Check if AUTHORS.txt file has changed - id: git-status-output - run: | - echo "::set-output name=msg::$(git status -s | wc -l)" - - - name: Commit and push - if: ${{ steps.git-status-output.outputs.msg != '0' }} - run: | - git config user.email $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\2/') - git config user.name $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\1/') - git add AUTHORS.txt - git commit --amend --no-edit - git push --force https://github.com/${GITHUB_REPOSITORY} $(git symbolic-ref -q --short HEAD) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 11b63360..5dd3a993 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,54 +8,13 @@ # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT name: Lint on: pull_request: - types: - - opened - - edited - - synchronize - -permissions: - contents: read jobs: - lint-commit-message: - name: Metadata - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Commit Message - run: .github/lint-commit-message.sh - - - name: File names - run: .github/lint-filename.sh - - - name: Functions - run: .github/lint-disallowed-functions-in-library.sh - - - name: Logging messages should not have trailing newlines - run: .github/lint-no-trailing-newline-in-log-messages.sh - - lint-go: - name: Go - permissions: - contents: read - pull-requests: read - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v3 - - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: v1.45.2 - args: $GOLANGCI_LINT_EXRA_ARGS + lint: + uses: pion/.goassets/.github/workflows/lint.reusable.yml@master diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..0e72ea4d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,24 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# If this repository should have package specific CI config, +# remove the repository name from .goassets/.github/workflows/assets-sync.yml. +# +# If you want to update the shared CI config, send a PR to +# https://github.com/pion/.goassets instead of this repository. +# +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +name: Release +on: + push: + tags: + - 'v*' + +jobs: + release: + uses: pion/.goassets/.github/workflows/release.reusable.yml@master + with: + go-version: "1.22" # auto-update/latest-go-version diff --git a/.github/workflows/renovate-go-mod-fix.yaml b/.github/workflows/renovate-go-mod-fix.yaml deleted file mode 100644 index 0804642e..00000000 --- a/.github/workflows/renovate-go-mod-fix.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# If this repository should have package specific CI config, -# remove the repository name from .goassets/.github/workflows/assets-sync.yml. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# - -name: go-mod-fix -on: - push: - branches: - - renovate/* - -permissions: - contents: write - -jobs: - go-mod-fix: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - name: fix - uses: at-wat/go-sum-fix-action@v0 - with: - git_user: Pion Bot - git_email: 59523206+pionbot@users.noreply.github.com - github_token: ${{ secrets.PIONBOT_PRIVATE_KEY }} - commit_style: squash - push: force diff --git a/.github/workflows/renovate-go-sum-fix.yaml b/.github/workflows/renovate-go-sum-fix.yaml new file mode 100644 index 00000000..b7bb1b4f --- /dev/null +++ b/.github/workflows/renovate-go-sum-fix.yaml @@ -0,0 +1,24 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# If this repository should have package specific CI config, +# remove the repository name from .goassets/.github/workflows/assets-sync.yml. +# +# If you want to update the shared CI config, send a PR to +# https://github.com/pion/.goassets instead of this repository. +# +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +name: Fix go.sum +on: + push: + branches: + - renovate/* + +jobs: + fix: + uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master + secrets: + token: ${{ secrets.PIONBOT_PRIVATE_KEY }} diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml new file mode 100644 index 00000000..8633a12a --- /dev/null +++ b/.github/workflows/reuse.yml @@ -0,0 +1,22 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# If this repository should have package specific CI config, +# remove the repository name from .goassets/.github/workflows/assets-sync.yml. +# +# If you want to update the shared CI config, send a PR to +# https://github.com/pion/.goassets instead of this repository. +# +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +name: REUSE Compliance Check + +on: + push: + pull_request: + +jobs: + lint: + uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 300fac6a..b0242893 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,167 +8,38 @@ # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT name: Test on: push: branches: - - master + - master pull_request: - branches: - - master - -permissions: - contents: read jobs: test: - runs-on: ubuntu-latest + uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: - go: ["1.17", "1.18"] + go: ["1.23", "1.22"] # auto-update/supported-go-version-list fail-fast: false - name: Go ${{ matrix.go }} - steps: - - uses: actions/checkout@v3 - - - uses: actions/cache@v3 - with: - path: | - ~/go/pkg/mod - ~/go/bin - ~/.cache - key: ${{ runner.os }}-amd64-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-amd64-go- - - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: ${{ matrix.go }} - - - name: Setup go-acc - run: go install github.com/ory/go-acc@latest - - - name: Set up gotestfmt - uses: haveyoudebuggedit/gotestfmt-action@v2 - with: - token: ${{ secrets.GITHUB_TOKEN }} # Avoid getting rate limited - - - name: Run test - run: | - TEST_BENCH_OPTION="-bench=." - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi - - set -euo pipefail - go-acc -o cover.out ./... -- \ - ${TEST_BENCH_OPTION} \ - -json \ - -v -race 2>&1 | grep -v '^go: downloading' | tee /tmp/gotest.log | gotestfmt - - - name: Upload test log - uses: actions/upload-artifact@v2 - if: always() - with: - name: test-log-${{ matrix.go }} - path: /tmp/gotest.log - if-no-files-found: error - - - name: Run TEST_HOOK - run: | - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi - if [ -n "${TEST_HOOK}" ]; then ${TEST_HOOK}; fi - - - uses: codecov/codecov-action@v2 - with: - name: codecov-umbrella - fail_ci_if_error: true - flags: go + with: + go-version: ${{ matrix.go }} + secrets: inherit test-i386: - runs-on: ubuntu-latest + uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: - go: ["1.17", "1.18"] + go: ["1.23", "1.22"] # auto-update/supported-go-version-list fail-fast: false - name: Go i386 ${{ matrix.go }} - steps: - - uses: actions/checkout@v3 - - - uses: actions/cache@v3 - with: - path: | - ~/go/pkg/mod - ~/.cache - key: ${{ runner.os }}-i386-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-i386-go- - - - name: Run test - run: | - mkdir -p $HOME/go/pkg/mod $HOME/.cache - docker run \ - -u $(id -u):$(id -g) \ - -e "GO111MODULE=on" \ - -e "CGO_ENABLED=0" \ - -v $GITHUB_WORKSPACE:/go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \ - -v $HOME/go/pkg/mod:/go/pkg/mod \ - -v $HOME/.cache:/.cache \ - -w /go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \ - i386/golang:${{matrix.go}}-alpine \ - /usr/local/go/bin/go test \ - ${TEST_EXTRA_ARGS:-} \ - -v ./... + with: + go-version: ${{ matrix.go }} test-wasm: - runs-on: ubuntu-latest - strategy: - fail-fast: false - name: WASM - steps: - - uses: actions/checkout@v3 - - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: '16.x' - - - uses: actions/cache@v3 - with: - path: | - ~/go/pkg/mod - ~/.cache - key: ${{ runner.os }}-wasm-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-wasm-go- - - - name: Download Go - run: curl -sSfL https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar -C ~ -xzf - - env: - GO_VERSION: 1.17 - - - name: Set Go Root - run: echo "GOROOT=${HOME}/go" >> $GITHUB_ENV - - - name: Set Go Path - run: echo "GOPATH=${HOME}/go" >> $GITHUB_ENV - - - name: Set Go Path - run: echo "GO_JS_WASM_EXEC=${GOROOT}/misc/wasm/go_js_wasm_exec" >> $GITHUB_ENV - - - name: Insall NPM modules - run: yarn install - - - name: Run Tests - run: | - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi - GOOS=js GOARCH=wasm $GOPATH/bin/go test \ - -coverprofile=cover.out -covermode=atomic \ - -exec="${GO_JS_WASM_EXEC}" \ - -v ./... - - - uses: codecov/codecov-action@v2 - with: - name: codecov-umbrella - fail_ci_if_error: true - flags: wasm + uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master + with: + go-version: "1.23" # auto-update/latest-go-version + secrets: inherit diff --git a/.github/workflows/tidy-check.yaml b/.github/workflows/tidy-check.yaml index fa52ce94..417e730a 100644 --- a/.github/workflows/tidy-check.yaml +++ b/.github/workflows/tidy-check.yaml @@ -8,33 +8,18 @@ # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT name: Go mod tidy on: pull_request: - branches: - - master push: branches: - master -permissions: - contents: read - jobs: - Check: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - - name: Setup Go - uses: actions/setup-go@v3 - - name: check - run: | - go mod download - go mod tidy - if ! git diff --exit-code - then - echo "Not go mod tidied" - exit 1 - fi + tidy: + uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master + with: + go-version: "1.22" # auto-update/latest-go-version diff --git a/.gitignore b/.gitignore index f977e748..6e2f206a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + ### JetBrains IDE ### ##################### .idea/ diff --git a/.golangci.yml b/.golangci.yml index d7a88eca..a3235bec 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,13 @@ +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +run: + timeout: 5m + linters-settings: govet: - check-shadowing: true + enable: + - shadow misspell: locale: US exhaustive: @@ -10,7 +17,14 @@ linters-settings: modules: - github.com/pkg/errors: recommendations: - - errors + - errors + forbidigo: + forbid: + - ^fmt.Print(f|ln)?$ + - ^log.(Panic|Fatal|Print)(f|ln)?$ + - ^os.Exit$ + - ^panic$ + - ^print(ln)?$ linters: enable: @@ -18,9 +32,7 @@ linters: - bidichk # Checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - contextcheck # check the function whether use a non-inherited context - - deadcode # Finds unused code - decorder # check declaration order and count of types, constants, variables and functions - - depguard # Go linter that checks if package imports are in a list of acceptable packages - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dupl # Tool for code clone detection - durationcheck # check for two durations multiplied together @@ -30,6 +42,7 @@ linters: - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. - exhaustive # check exhaustiveness of enum switch statements - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # Forbids identifiers - forcetypeassert # finds forced type assertions - gci # Gci control golang package import order and make it always deterministic. - gochecknoglobals # Checks that no globals are present in Go code @@ -38,7 +51,7 @@ linters: - goconst # Finds repeated strings that could be replaced by a constant - gocritic # The most opinionated Go source code linter - godox # Tool for detection of FIXME, TODO and other comment keywords - - goerr113 # Golang linter to check the errors handling expressions + - err113 # Golang linter to check the errors handling expressions - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification - gofumpt # Gofumpt checks whether code was gofumpt-ed. - goheader # Checks is file header matches to pattern @@ -53,14 +66,12 @@ linters: - importas # Enforces consistent import aliases - ineffassign # Detects when assignments to existing variables are not used - misspell # Finds commonly misspelled English words in comments - - nakedret # Finds naked returns in functions greater than a specified function length - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. - noctx # noctx finds sending http request without context.Context - predeclared # find code that shadows one of Go's predeclared identifiers - revive # golint replacement, finds style mistakes - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks - - structcheck # Finds unused struct fields - stylecheck # Stylecheck is a replacement for golint - tagliatelle # Checks the struct tags. - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 @@ -69,24 +80,21 @@ linters: - unconvert # Remove unnecessary type conversions - unparam # Reports unused function parameters - unused # Checks Go code for unused constants, variables, functions and types - - varcheck # Finds unused global variables and constants - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: + - depguard # Go linter that checks if package imports are in a list of acceptable packages - containedctx # containedctx is a linter that detects struct contained context.Context field - cyclop # checks function and package cyclomatic complexity - - exhaustivestruct # Checks if all struct's fields are initialized - - forbidigo # Forbids identifiers - funlen # Tool for detection of long functions - gocyclo # Computes and checks the cyclomatic complexity of functions - godot # Check if comments end in a period - gomnd # An analyzer to detect magic numbers. - - ifshort # Checks that your code uses short syntax for if-statements whenever possible - ireturn # Accept Interfaces, Return Concrete Types - lll # Reports long lines - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - - maligned # Tool to detect Go structs that would take less memory if their fields were sorted + - nakedret # Finds naked returns in functions greater than a specified function length - nestif # Reports deeply nested if statements - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - nolintlint # Reports ill-formed or insufficient nolint directives @@ -103,17 +111,15 @@ linters: issues: exclude-use-default: false + exclude-dirs-use-default: false exclude-rules: - # Allow complex tests, better to be self contained - - path: _test\.go + # Allow complex tests and examples, better to be self contained + - path: (examples|main\.go|_test\.go) linters: + - forbidigo - gocognit - # Allow complex main function in examples - - path: examples - text: "of func `main` is high" + # Allow forbidden identifiers in CLI commands + - path: cmd linters: - - gocognit - -run: - skip-dirs-use-default: false + - forbidigo diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..30093e9d --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +builds: +- skip: true diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 00000000..eb7fac2f --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,11 @@ +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 +Copyright: 2023 The Pion community +License: MIT + +Files: testdata/fuzz/* **/testdata/fuzz/* api/*.txt +Copyright: 2023 The Pion community +License: CC0-1.0 diff --git a/AUTHORS.txt b/AUTHORS.txt deleted file mode 100644 index b5c111f9..00000000 --- a/AUTHORS.txt +++ /dev/null @@ -1,38 +0,0 @@ -# Thank you to everyone that made Pion possible. If you are interested in contributing -# we would love to have you https://github.com/pion/webrtc/wiki/Contributing -# -# This file is auto generated, using git to list all individuals contributors. -# see `.github/generate-authors.sh` for the scripting -Aaron Boushley -adwpc -aler9 <46489434+aler9@users.noreply.github.com> -Antoine Baché -Antoine Baché -Atsushi Watanabe -baiyufei -Bao Nguyen -boks1971 -debiandebiandebian -ffmiyo -Guilherme -Haiyang Wang -Hugo Arregui -John Bradley -Juliusz Chroboczek -Kazuyuki Honda -Kevin Wang -Luke Curley -lxb -Michael MacDonald -Michael MacDonald -Michael Uti -Raphael Derosso Pereira -Rob Lofthouse -Robin Raymond -Sean DuBois -Sean DuBois -Sean DuBois -Simone Gotti -Tarrence van As -wangzixiang -Woodrow Douglass diff --git a/LICENSE b/LICENSE index ab602974..2071b23b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,9 @@ MIT License -Copyright (c) 2018 +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: +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 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. +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. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 00000000..2071b23b --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +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. diff --git a/README.md b/README.md index ce04599a..c84621c0 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Sourcegraph Widget Slack Widget
- Build Status - GoDoc + GitHub Workflow Status + Go Reference Coverage Status Go Report Card License: MIT @@ -21,14 +21,15 @@ The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. ### Community -Pion has an active community on the [Golang Slack](https://invite.slack.golangbridge.org/). Sign up and join the **#pion** channel for discussions and support. You can also use [Pion mailing list](https://groups.google.com/forum/#!forum/pion). +Pion has an active community on the [Slack](https://pion.ly/slack). -We are always looking to support **your projects**. Please reach out if you have something to build! +Follow the [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: +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 diff --git a/abscapturetimeextension.go b/abscapturetimeextension.go new file mode 100644 index 00000000..5e96cffb --- /dev/null +++ b/abscapturetimeextension.go @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "time" +) + +const ( + absCaptureTimeExtensionSize = 8 + absCaptureTimeExtendedExtensionSize = 16 +) + +// AbsCaptureTimeExtension is a extension payload format in +// http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ID | len=7 | absolute capture timestamp (bit 0-23) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | absolute capture timestamp (bit 24-55) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ... (56-63) | +// +-+-+-+-+-+-+-+-+ +type AbsCaptureTimeExtension struct { + Timestamp uint64 + EstimatedCaptureClockOffset *int64 +} + +// Marshal serializes the members to buffer. +func (t AbsCaptureTimeExtension) Marshal() ([]byte, error) { + if t.EstimatedCaptureClockOffset != nil { + buf := make([]byte, 16) + binary.BigEndian.PutUint64(buf[0:8], t.Timestamp) + binary.BigEndian.PutUint64(buf[8:16], uint64(*t.EstimatedCaptureClockOffset)) + return buf, nil + } + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf[0:8], t.Timestamp) + return buf, nil +} + +// Unmarshal parses the passed byte slice and stores the result in the members. +func (t *AbsCaptureTimeExtension) Unmarshal(rawData []byte) error { + if len(rawData) < absCaptureTimeExtensionSize { + return errTooSmall + } + t.Timestamp = binary.BigEndian.Uint64(rawData[0:8]) + if len(rawData) >= absCaptureTimeExtendedExtensionSize { + offset := int64(binary.BigEndian.Uint64(rawData[8:16])) + t.EstimatedCaptureClockOffset = &offset + } + return nil +} + +// CaptureTime produces the estimated time.Time represented by this extension. +func (t AbsCaptureTimeExtension) CaptureTime() time.Time { + return toTime(t.Timestamp) +} + +// EstimatedCaptureClockOffsetDuration produces the estimated time.Duration represented by this extension. +func (t AbsCaptureTimeExtension) EstimatedCaptureClockOffsetDuration() *time.Duration { + if t.EstimatedCaptureClockOffset == nil { + return nil + } + offset := *t.EstimatedCaptureClockOffset + negative := false + if offset < 0 { + offset = -offset + negative = true + } + duration := time.Duration(offset/(1<<32))*time.Second + time.Duration((offset&0xFFFFFFFF)*1e9/(1<<32))*time.Nanosecond + if negative { + duration = -duration + } + return &duration +} + +// NewAbsCaptureTimeExtension makes new AbsCaptureTimeExtension from time.Time. +func NewAbsCaptureTimeExtension(captureTime time.Time) *AbsCaptureTimeExtension { + return &AbsCaptureTimeExtension{ + Timestamp: toNtpTime(captureTime), + } +} + +// NewAbsCaptureTimeExtensionWithCaptureClockOffset makes new AbsCaptureTimeExtension from time.Time and a clock offset. +func NewAbsCaptureTimeExtensionWithCaptureClockOffset(captureTime time.Time, captureClockOffset time.Duration) *AbsCaptureTimeExtension { + ns := captureClockOffset.Nanoseconds() + negative := false + if ns < 0 { + ns = -ns + negative = true + } + lsb := (ns / 1e9) & 0xFFFFFFFF + msb := (((ns % 1e9) * (1 << 32)) / 1e9) & 0xFFFFFFFF + offset := (lsb << 32) | msb + if negative { + offset = -offset + } + return &AbsCaptureTimeExtension{ + Timestamp: toNtpTime(captureTime), + EstimatedCaptureClockOffset: &offset, + } +} diff --git a/abscapturetimeextension_test.go b/abscapturetimeextension_test.go new file mode 100644 index 00000000..038e677a --- /dev/null +++ b/abscapturetimeextension_test.go @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "testing" + "time" +) + +func TestAbsCaptureTimeExtension_Roundtrip(t *testing.T) { + t.Run("positive captureClockOffset", func(t *testing.T) { + t0 := time.Now() + e1 := NewAbsCaptureTimeExtension(t0) + b1, err1 := e1.Marshal() + if err1 != nil { + t.Fatal(err1) + } + var o1 AbsCaptureTimeExtension + if err := o1.Unmarshal(b1); err != nil { + t.Fatal(err) + } + dt1 := o1.CaptureTime().Sub(t0).Seconds() + if dt1 < -0.001 || dt1 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o1.CaptureTime(), dt1) + } + if o1.EstimatedCaptureClockOffsetDuration() != nil { + t.Fatalf("duration differs, want nil got %d", o1.EstimatedCaptureClockOffsetDuration()) + } + + e2 := NewAbsCaptureTimeExtensionWithCaptureClockOffset(t0, 1250*time.Millisecond) + b2, err2 := e2.Marshal() + if err2 != nil { + t.Fatal(err2) + } + var o2 AbsCaptureTimeExtension + if err := o2.Unmarshal(b2); err != nil { + t.Fatal(err) + } + dt2 := o1.CaptureTime().Sub(t0).Seconds() + if dt2 < -0.001 || dt2 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o2.CaptureTime(), dt2) + } + if *o2.EstimatedCaptureClockOffsetDuration() != 1250*time.Millisecond { + t.Fatalf("duration differs, want 250ms got %d", *o2.EstimatedCaptureClockOffsetDuration()) + } + }) + + // This test can verify the for for the issue 247 + t.Run("negative captureClockOffset", func(t *testing.T) { + t0 := time.Now() + e1 := NewAbsCaptureTimeExtension(t0) + b1, err1 := e1.Marshal() + if err1 != nil { + t.Fatal(err1) + } + var o1 AbsCaptureTimeExtension + if err := o1.Unmarshal(b1); err != nil { + t.Fatal(err) + } + dt1 := o1.CaptureTime().Sub(t0).Seconds() + if dt1 < -0.001 || dt1 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o1.CaptureTime(), dt1) + } + if o1.EstimatedCaptureClockOffsetDuration() != nil { + t.Fatalf("duration differs, want nil got %d", o1.EstimatedCaptureClockOffsetDuration()) + } + + e2 := NewAbsCaptureTimeExtensionWithCaptureClockOffset(t0, -250*time.Millisecond) + b2, err2 := e2.Marshal() + if err2 != nil { + t.Fatal(err2) + } + var o2 AbsCaptureTimeExtension + if err := o2.Unmarshal(b2); err != nil { + t.Fatal(err) + } + dt2 := o1.CaptureTime().Sub(t0).Seconds() + if dt2 < -0.001 || dt2 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o2.CaptureTime(), dt2) + } + if *o2.EstimatedCaptureClockOffsetDuration() != -250*time.Millisecond { + t.Fatalf("duration differs, want -250ms got %v", *o2.EstimatedCaptureClockOffsetDuration()) + } + }) +} diff --git a/abssendtimeextension.go b/abssendtimeextension.go index f0c6de37..fff38e98 100644 --- a/abssendtimeextension.go +++ b/abssendtimeextension.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/abssendtimeextension_test.go b/abssendtimeextension_test.go index 16243fd1..16834d9a 100644 --- a/abssendtimeextension_test.go +++ b/abssendtimeextension_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/audiolevelextension.go b/audiolevelextension.go index ca44f287..f180c8d6 100644 --- a/audiolevelextension.go +++ b/audiolevelextension.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/audiolevelextension_test.go b/audiolevelextension_test.go index b1ad89fc..9c651a0b 100644 --- a/audiolevelextension_test.go +++ b/audiolevelextension_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/codecov.yml b/codecov.yml index 085200a4..263e4d45 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,6 +3,8 @@ # # It is automatically copied from https://github.com/pion/.goassets repository. # +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT coverage: status: diff --git a/codecs/av1/frame/av1.go b/codecs/av1/frame/av1.go new file mode 100644 index 00000000..1e001a30 --- /dev/null +++ b/codecs/av1/frame/av1.go @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +// Package frame provides code to construct complete media frames from packetized media. +package frame + +import "github.com/pion/rtp/codecs" + +// AV1 represents a collection of OBUs given a stream of AV1 Packets. +// Each AV1 RTP Packet is a collection of OBU Elements. Each OBU Element may be a full OBU, or just a fragment of one. +// AV1 provides the tools to construct a collection of OBUs from a collection of OBU Elements. This structure +// contains an internal cache and should be used for the entire RTP Stream. +type AV1 struct { + // Buffer for fragmented OBU. If ReadFrames is called on a RTP Packet + // that doesn't contain a fully formed OBU + obuBuffer []byte +} + +func (f *AV1) pushOBUElement(isFirstOBUFragment *bool, obuElement []byte, obuList [][]byte) [][]byte { + if *isFirstOBUFragment { + *isFirstOBUFragment = false + // Discard pushed because we don't have a fragment to combine it with + if f.obuBuffer == nil { + return obuList + } + obuElement = append(f.obuBuffer, obuElement...) + f.obuBuffer = nil + } + return append(obuList, obuElement) +} + +// ReadFrames processes the codecs.AV1Packet and returns fully constructed frames +func (f *AV1) ReadFrames(pkt *codecs.AV1Packet) ([][]byte, error) { + OBUs := [][]byte{} + isFirstOBUFragment := pkt.Z + + for i := range pkt.OBUElements { + OBUs = f.pushOBUElement(&isFirstOBUFragment, pkt.OBUElements[i], OBUs) + } + + if pkt.Y && len(OBUs) > 0 { + // Take copy of OBUElement that is being cached + f.obuBuffer = append(f.obuBuffer, append([]byte{}, OBUs[len(OBUs)-1]...)...) + OBUs = OBUs[:len(OBUs)-1] + } + return OBUs, nil +} diff --git a/pkg/frame/av1_test.go b/codecs/av1/frame/av1_test.go similarity index 95% rename from pkg/frame/av1_test.go rename to codecs/av1/frame/av1_test.go index 11538cf7..aa6c73cf 100644 --- a/pkg/frame/av1_test.go +++ b/codecs/av1/frame/av1_test.go @@ -1,10 +1,13 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package frame import ( "reflect" "testing" - "github.com/pion/rtp/v2/codecs" + "github.com/pion/rtp/codecs" ) // First is Fragment (and no buffer) diff --git a/codecs/av1/obu/leb128.go b/codecs/av1/obu/leb128.go new file mode 100644 index 00000000..f5fcbf65 --- /dev/null +++ b/codecs/av1/obu/leb128.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +// Package obu implements tools for working with the Open Bitstream Unit. +package obu + +import "errors" + +const ( + sevenLsbBitmask = uint(0b01111111) + msbBitmask = uint(0b10000000) +) + +// ErrFailedToReadLEB128 indicates that a buffer ended before a LEB128 value could be successfully read +var ErrFailedToReadLEB128 = errors.New("payload ended before LEB128 was finished") + +// EncodeLEB128 encodes a uint as LEB128 +func EncodeLEB128(in uint) (out uint) { + for { + // Copy seven bits from in and discard + // what we have copied from in + out |= (in & sevenLsbBitmask) + in >>= 7 + + // If we have more bits to encode set MSB + // otherwise we are done + if in != 0 { + out |= msbBitmask + out <<= 8 + } else { + return out + } + } +} + +func decodeLEB128(in uint) (out uint) { + for { + // Take 7 LSB from in + out |= (in & sevenLsbBitmask) + + // Discard the MSB + in >>= 8 + if in == 0 { + return out + } + + out <<= 7 + } +} + +// ReadLeb128 scans an buffer and decodes a Leb128 value. +// If the end of the buffer is reached and all MSB are set +// an error is returned +func ReadLeb128(in []byte) (uint, uint, error) { + var encodedLength uint + + for i := range in { + encodedLength |= uint(in[i]) + + if in[i]&byte(msbBitmask) == 0 { + return decodeLEB128(encodedLength), uint(i + 1), nil + } + + // Make more room for next read + encodedLength <<= 8 + } + + return 0, 0, ErrFailedToReadLEB128 +} + +// WriteToLeb128 writes a uint to a LEB128 encoded byte slice. +func WriteToLeb128(in uint) []byte { + b := make([]byte, 10) + + for i := 0; i < len(b); i++ { + b[i] = byte(in & 0x7f) + in >>= 7 + if in == 0 { + return b[:i+1] + } + b[i] |= 0x80 + } + + return b // unreachable +} diff --git a/pkg/obu/leb128_test.go b/codecs/av1/obu/leb128_test.go similarity index 53% rename from pkg/obu/leb128_test.go rename to codecs/av1/obu/leb128_test.go index 42cf7773..2b2336a4 100644 --- a/pkg/obu/leb128_test.go +++ b/codecs/av1/obu/leb128_test.go @@ -1,7 +1,13 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package obu import ( + "encoding/hex" "errors" + "fmt" + "math" "testing" ) @@ -37,3 +43,33 @@ func TestReadLeb128(t *testing.T) { t.Fatal("ReadLeb128 on a buffer with all MSB set should fail") } } + +func TestWriteToLeb128(t *testing.T) { + type testVector struct { + value uint + leb128 string + } + testVectors := []testVector{ + {150, "9601"}, + {240, "f001"}, + {400, "9003"}, + {720, "d005"}, + {1200, "b009"}, + {999999, "bf843d"}, + {0, "00"}, + {math.MaxUint32, "ffffffff0f"}, + } + + runTest := func(t *testing.T, v testVector) { + b := WriteToLeb128(v.value) + if v.leb128 != hex.EncodeToString(b) { + t.Errorf("Expected %s, got %s", v.leb128, hex.EncodeToString(b)) + } + } + + for _, v := range testVectors { + t.Run(fmt.Sprintf("encode %d", v.value), func(t *testing.T) { + runTest(t, v) + }) + } +} diff --git a/codecs/av1_packet.go b/codecs/av1_packet.go index 7aa3a55a..84c6c99b 100644 --- a/codecs/av1_packet.go +++ b/codecs/av1_packet.go @@ -1,7 +1,10 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( - "github.com/pion/rtp/v2/pkg/obu" + "github.com/pion/rtp/codecs/av1/obu" ) const ( @@ -17,64 +20,91 @@ const ( nMask = byte(0b00001000) nBitshift = 3 + obuFrameTypeMask = byte(0b01111000) + obuFrameTypeBitshift = 3 + + obuFameTypeSequenceHeader = 1 + av1PayloaderHeadersize = 1 + + leb128Size = 1 ) // AV1Payloader payloads AV1 packets -type AV1Payloader struct{} +type AV1Payloader struct { + sequenceHeader []byte +} // Payload fragments a AV1 packet across one or more byte arrays // See AV1Packet for description of AV1 Payload Header func (p *AV1Payloader) Payload(mtu uint16, payload []byte) (payloads [][]byte) { - maxFragmentSize := int(mtu) - av1PayloaderHeadersize - 2 - payloadDataRemaining := len(payload) payloadDataIndex := 0 + payloadDataRemaining := len(payload) - // Make sure the fragment/payload size is correct - if min(maxFragmentSize, payloadDataRemaining) <= 0 { + // Payload Data and MTU is non-zero + if mtu <= 0 || payloadDataRemaining <= 0 { return payloads } + // Cache Sequence Header and packetize with next payload + frameType := (payload[0] & obuFrameTypeMask) >> obuFrameTypeBitshift + if frameType == obuFameTypeSequenceHeader { + p.sequenceHeader = payload + return + } + for payloadDataRemaining > 0 { - currentFragmentSize := min(maxFragmentSize, payloadDataRemaining) - leb128Size := 1 - if currentFragmentSize >= 127 { - leb128Size = 2 + obuCount := byte(1) + metadataSize := av1PayloaderHeadersize + if len(p.sequenceHeader) != 0 { + obuCount++ + metadataSize += leb128Size + len(p.sequenceHeader) } - out := make([]byte, av1PayloaderHeadersize+leb128Size+currentFragmentSize) - leb128Value := obu.EncodeLEB128(uint(currentFragmentSize)) - if leb128Size == 1 { - out[1] = byte(leb128Value) - } else { - out[1] = byte(leb128Value >> 8) - out[2] = byte(leb128Value) - } + out := make([]byte, minInt(int(mtu), payloadDataRemaining+metadataSize)) + outOffset := av1PayloaderHeadersize + out[0] = obuCount << wBitshift - copy(out[av1PayloaderHeadersize+leb128Size:], payload[payloadDataIndex:payloadDataIndex+currentFragmentSize]) - payloads = append(payloads, out) + if obuCount == 2 { + // This Payload contain the start of a Coded Video Sequence + out[0] ^= nMask - payloadDataRemaining -= currentFragmentSize - payloadDataIndex += currentFragmentSize + out[1] = byte(obu.EncodeLEB128(uint(len(p.sequenceHeader)))) + copy(out[2:], p.sequenceHeader) + + outOffset += leb128Size + len(p.sequenceHeader) + + p.sequenceHeader = nil + } - if len(payloads) > 1 { + outBufferRemaining := len(out) - outOffset + copy(out[outOffset:], payload[payloadDataIndex:payloadDataIndex+outBufferRemaining]) + payloadDataRemaining -= outBufferRemaining + payloadDataIndex += outBufferRemaining + + // Does this Fragment contain an OBU that started in a previous payload + if len(payloads) > 0 { out[0] ^= zMask } + + // This OBU will be continued in next Payload if payloadDataRemaining != 0 { out[0] ^= yMask } + + payloads = append(payloads, out) } return payloads } // AV1Packet represents a depacketized AV1 RTP Packet -// -// 0 1 2 3 4 5 6 7 -// +-+-+-+-+-+-+-+-+ -// |Z|Y| W |N|-|-|-| -// +-+-+-+-+-+-+-+-+ -// +/* +* 0 1 2 3 4 5 6 7 +* +-+-+-+-+-+-+-+-+ +* |Z|Y| W |N|-|-|-| +* +-+-+-+-+-+-+-+-+ +**/ // https://aomediacodec.github.io/av1-rtp-spec/#44-av1-aggregation-header type AV1Packet struct { // Z: MUST be set to 1 if the first OBU element is an @@ -104,6 +134,8 @@ type AV1Packet struct { // Each AV1 RTP Packet is a collection of OBU Elements. Each OBU Element may be a full OBU, or just a fragment of one. // AV1Frame provides the tools to construct a collection of OBUs from a collection of OBU Elements OBUElements [][]byte + + videoDepacketizer } // Unmarshal parses the passed byte slice and stores the result in the AV1Packet this method is called upon @@ -123,13 +155,26 @@ func (p *AV1Packet) Unmarshal(payload []byte) ([]byte, error) { return nil, errIsKeyframeAndFragment } - currentIndex := uint(1) - p.OBUElements = [][]byte{} + if !p.zeroAllocation { + obuElements, err := p.parseBody(payload[1:]) + if err != nil { + return nil, err + } + p.OBUElements = obuElements + } + + return payload[1:], nil +} - var ( - obuElementLength, bytesRead uint - err error - ) +func (p *AV1Packet) parseBody(payload []byte) ([][]byte, error) { + if p.OBUElements != nil { + return p.OBUElements, nil + } + + obuElements := [][]byte{} + + var obuElementLength, bytesRead uint + currentIndex := uint(0) for i := 1; ; i++ { if currentIndex == uint(len(payload)) { break @@ -140,6 +185,7 @@ func (p *AV1Packet) Unmarshal(payload []byte) ([]byte, error) { bytesRead = 0 obuElementLength = uint(len(payload)) - currentIndex } else { + var err error obuElementLength, bytesRead, err = obu.ReadLeb128(payload[currentIndex:]) if err != nil { return nil, err @@ -150,9 +196,9 @@ func (p *AV1Packet) Unmarshal(payload []byte) ([]byte, error) { if uint(len(payload)) < currentIndex+obuElementLength { return nil, errShortPacket } - p.OBUElements = append(p.OBUElements, payload[currentIndex:currentIndex+obuElementLength]) + obuElements = append(obuElements, payload[currentIndex:currentIndex+obuElementLength]) currentIndex += obuElementLength } - return payload[1:], nil + return obuElements, nil } diff --git a/codecs/av1_packet_test.go b/codecs/av1_packet_test.go index 27eeb47b..7df5f551 100644 --- a/codecs/av1_packet_test.go +++ b/codecs/av1_packet_test.go @@ -1,37 +1,84 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( + "bytes" "errors" - "fmt" "reflect" "testing" - "github.com/pion/rtp/v2/pkg/obu" + "github.com/pion/rtp/codecs/av1/obu" ) func TestAV1_Marshal(t *testing.T) { - const mtu = 5 + p := &AV1Payloader{} - for _, test := range []struct { - input []byte - output [][]byte - }{ - {[]byte{0x01}, [][]byte{{0x00, 0x01, 0x01}}}, - {[]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x04, 0x05}, [][]byte{{0x40, 0x02, 0x00, 0x01}, {0xc0, 0x02, 0x02, 0x03}, {0xc0, 0x02, 0x04, 0x04}, {0x80, 0x01, 0x05}}}, - } { - test := test + t.Run("Unfragmented OBU", func(t *testing.T) { + OBU := []byte{0x00, 0x01, 0x2, 0x3, 0x4, 0x5} + payloads := p.Payload(100, OBU) - p := &AV1Payloader{} - if payloads := p.Payload(mtu, test.input); !reflect.DeepEqual(payloads, test.output) { - t.Fatalf("Expected(%02x) did not equal actual(%02x)", test.output, payloads) + if len(payloads) != 1 || len(payloads[0]) != 7 { + t.Fatal("Expected one unfragmented Payload") } - } - p := &AV1Payloader{} - zeroMtuPayload := p.Payload(0, []byte{0x0A, 0x0B, 0x0C}) - if zeroMtuPayload != nil { - t.Fatal("Unexpected output from zero MTU AV1 Payloader") - } + if payloads[0][0] != 0x10 { + t.Fatal("Only W bit should be set") + } + + if !bytes.Equal(OBU, payloads[0][1:]) { + t.Fatal("OBU modified during packetization") + } + }) + + t.Run("Fragmented OBU", func(t *testing.T) { + OBU := []byte{0x00, 0x01, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8} + payloads := p.Payload(4, OBU) + + if len(payloads) != 3 || len(payloads[0]) != 4 || len(payloads[1]) != 4 || len(payloads[2]) != 4 { + t.Fatal("Expected three fragmented Payload") + } + + if payloads[0][0] != 0x10|yMask { + t.Fatal("W and Y bit should be set") + } + + if payloads[1][0] != 0x10|yMask|zMask { + t.Fatal("W, Y and Z bit should be set") + } + + if payloads[2][0] != 0x10|zMask { + t.Fatal("W and Z bit should be set") + } + + if !bytes.Equal(OBU[0:3], payloads[0][1:]) || !bytes.Equal(OBU[3:6], payloads[1][1:]) || !bytes.Equal(OBU[6:9], payloads[2][1:]) { + t.Fatal("OBU modified during packetization") + } + }) + + t.Run("Sequence Header Caching", func(t *testing.T) { + sequenceHeaderFrame := []byte{0xb, 0xA, 0xB, 0xC} + normalFrame := []byte{0x0, 0x1, 0x2, 0x3} + + payloads := p.Payload(100, sequenceHeaderFrame) + if len(payloads) != 0 { + t.Fatal("Sequence Header was not properly cached") + } + + payloads = p.Payload(100, normalFrame) + if len(payloads) != 1 { + t.Fatal("Expected one payload") + } + + if payloads[0][0] != 0x20|nMask { + t.Fatal("W and N bit should be set") + } + + if !bytes.Equal(sequenceHeaderFrame, payloads[0][2:6]) || !bytes.Equal(normalFrame, payloads[0][6:10]) { + t.Fatal("OBU modified during packetization") + } + }) } func TestAV1_Unmarshal_Error(t *testing.T) { @@ -49,12 +96,13 @@ func TestAV1_Unmarshal_Error(t *testing.T) { av1Pkt := &AV1Packet{} if _, err := av1Pkt.Unmarshal(test.input); !errors.Is(err, test.expectedError) { - t.Fatal(fmt.Sprintf("Expected error(%s) but got (%s)", test.expectedError, err)) + t.Fatalf("Expected error(%s) but got (%s)", test.expectedError, err) } } } func TestAV1_Unmarshal(t *testing.T) { + // nolint: dupl av1Payload := []byte{ 0x68, 0x0c, 0x08, 0x00, 0x00, 0x00, 0x2c, 0xd6, 0xd3, 0x0c, 0xd5, 0x02, 0x00, 0x80, diff --git a/codecs/codecs.go b/codecs/codecs.go index 0e078974..cd1c8915 100644 --- a/codecs/codecs.go +++ b/codecs/codecs.go @@ -1,2 +1,5 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + // Package codecs implements codec specific RTP payloader/depayloaders package codecs diff --git a/codecs/common.go b/codecs/common.go index af5632ac..5b7dd14a 100644 --- a/codecs/common.go +++ b/codecs/common.go @@ -1,6 +1,9 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs -func min(a, b int) int { +func minInt(a, b int) int { if a < b { return a } @@ -10,17 +13,27 @@ func min(a, b int) int { // audioDepacketizer is a mixin for audio codec depacketizers type audioDepacketizer struct{} -func (d *audioDepacketizer) IsPartitionTail(marker bool, payload []byte) bool { +func (d *audioDepacketizer) IsPartitionTail(_ bool, _ []byte) bool { return true } -func (d *audioDepacketizer) IsPartitionHead(payload []byte) bool { +func (d *audioDepacketizer) IsPartitionHead(_ []byte) bool { return true } // videoDepacketizer is a mixin for video codec depacketizers -type videoDepacketizer struct{} +type videoDepacketizer struct { + zeroAllocation bool +} -func (d *videoDepacketizer) IsPartitionTail(marker bool, payload []byte) bool { +func (d *videoDepacketizer) IsPartitionTail(marker bool, _ []byte) bool { return marker } + +// SetZeroAllocation enables Zero Allocation mode for the depacketizer +// By default the Depacketizers will allocate as they parse. These allocations +// are needed for Metadata and other optional values. If you don't need this information +// enabling SetZeroAllocation gives you higher performance at a reduced feature set. +func (d *videoDepacketizer) SetZeroAllocation(zeroAllocation bool) { + d.zeroAllocation = zeroAllocation +} diff --git a/codecs/common_test.go b/codecs/common_test.go index 5cbbd3c5..4c1f393e 100644 --- a/codecs/common_test.go +++ b/codecs/common_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( @@ -5,18 +8,245 @@ import ( ) func TestCommon_Min(t *testing.T) { - res := min(1, -1) + res := minInt(1, -1) if res != -1 { t.Fatal("Error: -1 < 1") } - res = min(1, 2) + res = minInt(1, 2) if res != 1 { t.Fatal("Error: 1 < 2") } - res = min(3, 3) + res = minInt(3, 3) if res != 3 { t.Fatal("Error: 3 == 3") } } + +func TestZeroAllocations(t *testing.T) { + type unmarshaller interface { + Unmarshal(data []byte) ([]byte, error) + } + type tst struct { + packet unmarshaller + data []byte + } + tests := []tst{ + { + packet: &VP8Packet{}, + data: []byte{ + 0x00, 0x11, 0x22, 0x33, + 0x44, 0x55, 0x66, 0x90, + }, + }, { + packet: &VP9Packet{}, + data: []byte{0xA0, 0x02, 0x23, 0x01, 0xAA}, + }, { + packet: &H264Packet{}, + data: []byte{ + 0x78, 0x00, 0x0f, 0x67, + 0x42, 0xc0, 0x1f, 0x1a, + 0x32, 0x35, 0x01, 0x40, + 0x7a, 0x40, 0x3c, 0x22, + 0x11, 0xa8, 0x00, 0x05, + 0x68, 0x1a, 0x34, 0xe3, + 0xc8, + }, + }, { + packet: &AV1Packet{}, + // nolint: dupl + data: []byte{ + 0x68, 0x0c, 0x08, 0x00, 0x00, 0x00, 0x2c, + 0xd6, 0xd3, 0x0c, 0xd5, 0x02, 0x00, 0x80, + 0x30, 0x10, 0xc3, 0xc0, 0x07, 0xff, 0xff, + 0xf8, 0xb7, 0x30, 0xc0, 0x00, 0x00, 0x88, + 0x17, 0xf9, 0x0c, 0xcf, 0xc6, 0x7b, 0x9c, + 0x0d, 0xda, 0x55, 0x82, 0x82, 0x67, 0x2f, + 0xf0, 0x07, 0x26, 0x5d, 0xf6, 0xc6, 0xe3, + 0x12, 0xdd, 0xf9, 0x71, 0x77, 0x43, 0xe6, + 0xba, 0xf2, 0xce, 0x36, 0x08, 0x63, 0x92, + 0xac, 0xbb, 0xbd, 0x26, 0x4c, 0x05, 0x52, + 0x91, 0x09, 0xf5, 0x37, 0xb5, 0x18, 0xbe, + 0x5c, 0x95, 0xb1, 0x2c, 0x13, 0x27, 0x81, + 0xc2, 0x52, 0x8c, 0xaf, 0x27, 0xca, 0xf2, + 0x93, 0xd6, 0x2e, 0x46, 0x32, 0xed, 0x71, + 0x87, 0x90, 0x1d, 0x0b, 0x84, 0x46, 0x7f, + 0xd1, 0x57, 0xc1, 0x0d, 0xc7, 0x5b, 0x41, + 0xbb, 0x8a, 0x7d, 0xe9, 0x2c, 0xae, 0x36, + 0x98, 0x13, 0x39, 0xb9, 0x0c, 0x66, 0x47, + 0x05, 0xa2, 0xdf, 0x55, 0xc4, 0x09, 0xab, + 0xe4, 0xfb, 0x11, 0x52, 0x36, 0x27, 0x88, + 0x86, 0xf3, 0x4a, 0xbb, 0xef, 0x40, 0xa7, + 0x85, 0x2a, 0xfe, 0x92, 0x28, 0xe4, 0xce, + 0xce, 0xdc, 0x4b, 0xd0, 0xaa, 0x3c, 0xd5, + 0x16, 0x76, 0x74, 0xe2, 0xfa, 0x34, 0x91, + 0x4f, 0xdc, 0x2b, 0xea, 0xae, 0x71, 0x36, + 0x74, 0xe1, 0x2a, 0xf3, 0xd3, 0x53, 0xe8, + 0xec, 0xd6, 0x63, 0xf6, 0x6a, 0x75, 0x95, + 0x68, 0xcc, 0x99, 0xbe, 0x17, 0xd8, 0x3b, + 0x87, 0x5b, 0x94, 0xdc, 0xec, 0x32, 0x09, + 0x18, 0x4b, 0x37, 0x58, 0xb5, 0x67, 0xfb, + 0xdf, 0x66, 0x6c, 0x16, 0x9e, 0xba, 0x72, + 0xc6, 0x21, 0xac, 0x02, 0x6d, 0x6b, 0x17, + 0xf9, 0x68, 0x22, 0x2e, 0x10, 0xd7, 0xdf, + 0xfb, 0x24, 0x69, 0x7c, 0xaf, 0x11, 0x64, + 0x80, 0x7a, 0x9d, 0x09, 0xc4, 0x1f, 0xf1, + 0xd7, 0x3c, 0x5a, 0xc2, 0x2c, 0x8e, 0xf5, + 0xff, 0xee, 0xc2, 0x7c, 0xa1, 0xe4, 0xcb, + 0x1c, 0x6d, 0xd8, 0x15, 0x0e, 0x40, 0x36, + 0x85, 0xe7, 0x04, 0xbb, 0x64, 0xca, 0x6a, + 0xd9, 0x21, 0x8e, 0x95, 0xa0, 0x83, 0x95, + 0x10, 0x48, 0xfa, 0x00, 0x54, 0x90, 0xe9, + 0x81, 0x86, 0xa0, 0x4a, 0x6e, 0xbe, 0x9b, + 0xf0, 0x73, 0x0a, 0x17, 0xbb, 0x57, 0x81, + 0x17, 0xaf, 0xd6, 0x70, 0x1f, 0xe8, 0x6d, + 0x32, 0x59, 0x14, 0x39, 0xd8, 0x1d, 0xec, + 0x59, 0xe4, 0x98, 0x4d, 0x44, 0xf3, 0x4f, + 0x7b, 0x47, 0xd9, 0x92, 0x3b, 0xd9, 0x5c, + 0x98, 0xd5, 0xf1, 0xc9, 0x8b, 0x9d, 0xb1, + 0x65, 0xb3, 0xe1, 0x87, 0xa4, 0x6a, 0xcc, + 0x42, 0x96, 0x66, 0xdb, 0x5f, 0xf9, 0xe1, + 0xa1, 0x72, 0xb6, 0x05, 0x02, 0x1f, 0xa3, + 0x14, 0x3e, 0xfe, 0x99, 0x7f, 0xeb, 0x42, + 0xcf, 0x76, 0x09, 0x19, 0xd2, 0xd2, 0x99, + 0x75, 0x1c, 0x67, 0xda, 0x4d, 0xf4, 0x87, + 0xe5, 0x55, 0x8b, 0xed, 0x01, 0x82, 0xf6, + 0xd6, 0x1c, 0x5c, 0x05, 0x96, 0x96, 0x79, + 0xc1, 0x61, 0x87, 0x74, 0xcd, 0x29, 0x83, + 0x27, 0xae, 0x47, 0x87, 0x36, 0x34, 0xab, + 0xc4, 0x73, 0x76, 0x58, 0x1b, 0x4a, 0xec, + 0x0e, 0x4c, 0x2f, 0xb1, 0x76, 0x08, 0x7f, + 0xaf, 0xfa, 0x6d, 0x8c, 0xde, 0xe4, 0xae, + 0x58, 0x87, 0xe7, 0xa0, 0x27, 0x05, 0x0d, + 0xf5, 0xa7, 0xfb, 0x2a, 0x75, 0x33, 0xd9, + 0x3b, 0x65, 0x60, 0xa4, 0x13, 0x27, 0xa5, + 0xe5, 0x1b, 0x83, 0x78, 0x7a, 0xd7, 0xec, + 0x0c, 0xed, 0x8b, 0xe6, 0x4e, 0x8f, 0xfe, + 0x6b, 0x5d, 0xbb, 0xa8, 0xee, 0x38, 0x81, + 0x6f, 0x09, 0x23, 0x08, 0x8f, 0x07, 0x21, + 0x09, 0x39, 0xf0, 0xf8, 0x03, 0x17, 0x24, + 0x2a, 0x22, 0x44, 0x84, 0xe1, 0x5c, 0xf3, + 0x4f, 0x20, 0xdc, 0xc1, 0xe7, 0xeb, 0xbc, + 0x0b, 0xfb, 0x7b, 0x20, 0x66, 0xa4, 0x27, + 0xe2, 0x01, 0xb3, 0x5f, 0xb7, 0x47, 0xa1, + 0x88, 0x4b, 0x8c, 0x47, 0xda, 0x36, 0x98, + 0x60, 0xd7, 0x46, 0x92, 0x0b, 0x7e, 0x5b, + 0x4e, 0x34, 0x50, 0x12, 0x67, 0x50, 0x8d, + 0xe7, 0xc9, 0xe4, 0x96, 0xef, 0xae, 0x2b, + 0xc7, 0xfa, 0x36, 0x29, 0x05, 0xf5, 0x92, + 0xbd, 0x62, 0xb7, 0xbb, 0x90, 0x66, 0xe0, + 0xad, 0x14, 0x3e, 0xe7, 0xb4, 0x24, 0xf3, + 0x04, 0xcf, 0x22, 0x14, 0x86, 0xa4, 0xb8, + 0xfb, 0x83, 0x56, 0xce, 0xaa, 0xb4, 0x87, + 0x5a, 0x9e, 0xf2, 0x0b, 0xaf, 0xad, 0x40, + 0xe1, 0xb5, 0x5c, 0x6b, 0xa7, 0xee, 0x9f, + 0xbb, 0x1a, 0x68, 0x4d, 0xc3, 0xbf, 0x22, + 0x4d, 0xbe, 0x58, 0x52, 0xc9, 0xcc, 0x0d, + 0x88, 0x04, 0xf1, 0xf8, 0xd4, 0xfb, 0xd6, + 0xad, 0xcf, 0x13, 0x84, 0xd6, 0x2f, 0x90, + 0x0c, 0x5f, 0xb4, 0xe2, 0xd8, 0x29, 0x26, + 0x8d, 0x7c, 0x6b, 0xab, 0x91, 0x91, 0x3c, + 0x25, 0x39, 0x9c, 0x86, 0x08, 0x39, 0x54, + 0x59, 0x0d, 0xa4, 0xa8, 0x31, 0x9f, 0xa3, + 0xbc, 0xc2, 0xcb, 0xf9, 0x30, 0x49, 0xc3, + 0x68, 0x0e, 0xfc, 0x2b, 0x9f, 0xce, 0x59, + 0x02, 0xfa, 0xd4, 0x4e, 0x11, 0x49, 0x0d, + 0x93, 0x0c, 0xae, 0x57, 0xd7, 0x74, 0xdd, + 0x13, 0x1a, 0x15, 0x79, 0x10, 0xcc, 0x99, + 0x32, 0x9b, 0x57, 0x6d, 0x53, 0x75, 0x1f, + 0x6d, 0xbb, 0xe4, 0xbc, 0xa9, 0xd4, 0xdb, + 0x06, 0xe7, 0x09, 0xb0, 0x6f, 0xca, 0xb3, + 0xb1, 0xed, 0xc5, 0x0b, 0x8d, 0x8e, 0x70, + 0xb0, 0xbf, 0x8b, 0xad, 0x2f, 0x29, 0x92, + 0xdd, 0x5a, 0x19, 0x3d, 0xca, 0xca, 0xed, + 0x05, 0x26, 0x25, 0xee, 0xee, 0xa9, 0xdd, + 0xa0, 0xe3, 0x78, 0xe0, 0x56, 0x99, 0x2f, + 0xa1, 0x3f, 0x07, 0x5e, 0x91, 0xfb, 0xc4, + 0xb3, 0xac, 0xee, 0x07, 0xa4, 0x6a, 0xcb, + 0x42, 0xae, 0xdf, 0x09, 0xe7, 0xd0, 0xbb, + 0xc6, 0xd4, 0x38, 0x58, 0x7d, 0xb4, 0x45, + 0x98, 0x38, 0x21, 0xc8, 0xc1, 0x3c, 0x81, + 0x12, 0x7e, 0x37, 0x03, 0xa8, 0xcc, 0xf3, + 0xf9, 0xd9, 0x9d, 0x8f, 0xc1, 0xa1, 0xcc, + 0xc1, 0x1b, 0xe3, 0xa8, 0x93, 0x91, 0x2c, + 0x0a, 0xe8, 0x1f, 0x28, 0x13, 0x44, 0x07, + 0x68, 0x5a, 0x8f, 0x27, 0x41, 0x18, 0xc9, + 0x31, 0xc4, 0xc1, 0x71, 0xe2, 0xf0, 0xc4, + 0xf4, 0x1e, 0xac, 0x29, 0x49, 0x2f, 0xd0, + 0xc0, 0x98, 0x13, 0xa6, 0xbc, 0x5e, 0x34, + 0x28, 0xa7, 0x30, 0x13, 0x8d, 0xb4, 0xca, + 0x91, 0x26, 0x6c, 0xda, 0x35, 0xb5, 0xf1, + 0xbf, 0x3f, 0x35, 0x3b, 0x87, 0x37, 0x63, + 0x40, 0x59, 0x73, 0x49, 0x06, 0x59, 0x04, + 0xe0, 0x84, 0x16, 0x3a, 0xe8, 0xc4, 0x28, + 0xd1, 0xf5, 0x11, 0x9c, 0x34, 0xf4, 0x5a, + 0xc0, 0xf8, 0x67, 0x47, 0x1c, 0x90, 0x63, + 0xbc, 0x06, 0x39, 0x2e, 0x8a, 0xa5, 0xa0, + 0xf1, 0x6b, 0x41, 0xb1, 0x16, 0xbd, 0xb9, + 0x50, 0x78, 0x72, 0x91, 0x8e, 0x8c, 0x99, + 0x0f, 0x7d, 0x99, 0x7e, 0x77, 0x36, 0x85, + 0x87, 0x1f, 0x2e, 0x47, 0x13, 0x55, 0xf8, + 0x07, 0xba, 0x7b, 0x1c, 0xaa, 0xbf, 0x20, + 0xd0, 0xfa, 0xc4, 0xe1, 0xd0, 0xb3, 0xe4, + 0xf4, 0xf9, 0x57, 0x8d, 0x56, 0x19, 0x4a, + 0xdc, 0x4c, 0x83, 0xc8, 0xf1, 0x30, 0xc0, + 0xb5, 0xdf, 0x67, 0x25, 0x58, 0xd8, 0x09, + 0x41, 0x37, 0x2e, 0x0b, 0x47, 0x2b, 0x86, + 0x4b, 0x73, 0x38, 0xf0, 0xa0, 0x6b, 0x83, + 0x30, 0x80, 0x3e, 0x46, 0xb5, 0x09, 0xc8, + 0x6d, 0x3e, 0x97, 0xaa, 0x70, 0x4e, 0x8c, + 0x75, 0x29, 0xec, 0x8a, 0x37, 0x4a, 0x81, + 0xfd, 0x92, 0xf1, 0x29, 0xf0, 0xe8, 0x9d, + 0x8c, 0xb4, 0x39, 0x2d, 0x67, 0x06, 0xcd, + 0x5f, 0x25, 0x02, 0x30, 0xbb, 0x6b, 0x41, + 0x93, 0x55, 0x1e, 0x0c, 0xc9, 0x6e, 0xb5, + 0xd5, 0x9f, 0x80, 0xf4, 0x7d, 0x9d, 0x8a, + 0x0d, 0x8d, 0x3b, 0x15, 0x14, 0xc9, 0xdf, + 0x03, 0x9c, 0x78, 0x39, 0x4e, 0xa0, 0xdc, + 0x3a, 0x1b, 0x8c, 0xdf, 0xaa, 0xed, 0x25, + 0xda, 0x60, 0xdd, 0x30, 0x64, 0x09, 0xcc, + 0x94, 0x53, 0xa1, 0xad, 0xfd, 0x9e, 0xe7, + 0x65, 0x15, 0xb8, 0xb1, 0xda, 0x9a, 0x28, + 0x80, 0x51, 0x88, 0x93, 0x92, 0xe3, 0x03, + 0xdf, 0x70, 0xba, 0x1b, 0x59, 0x3b, 0xb4, + 0x8a, 0xb6, 0x0b, 0x0a, 0xa8, 0x48, 0xdf, + 0xcc, 0x74, 0x4c, 0x71, 0x80, 0x08, 0xec, + 0xc8, 0x8a, 0x73, 0xf5, 0x0e, 0x3d, 0xec, + 0x16, 0xf6, 0x32, 0xfd, 0xf3, 0x6b, 0xba, + 0xa9, 0x65, 0xd1, 0x87, 0xe2, 0x56, 0xcd, + 0xde, 0x2c, 0xa4, 0x1b, 0x25, 0x81, 0xb2, + 0xed, 0xea, 0xe9, 0x11, 0x07, 0xf5, 0x17, + 0xd0, 0xca, 0x5d, 0x07, 0xb9, 0xb2, 0xa9, + 0xa9, 0xee, 0x42, 0x33, 0x93, 0x21, 0x30, + 0x5e, 0xd2, 0x58, 0xfd, 0xdd, 0x73, 0x0d, + 0xb2, 0x93, 0x58, 0x77, 0x78, 0x40, 0x69, + 0xba, 0x3c, 0x95, 0x1c, 0x61, 0xc6, 0xc6, + 0x97, 0x1c, 0xef, 0x4d, 0x91, 0x0a, 0x42, + 0x91, 0x1d, 0x14, 0x93, 0xf5, 0x78, 0x41, + 0x32, 0x8a, 0x0a, 0x43, 0xd4, 0x3e, 0x6b, + 0xb0, 0xd8, 0x0e, 0x04, + }, + }, + } + + type zeroAllocation interface { + SetZeroAllocation(zeroAllocation bool) + } + + for _, test := range tests { + allocs := testing.AllocsPerRun(10, func() { + if d, ok := test.packet.(zeroAllocation); ok { + d.SetZeroAllocation(true) + } + _, err := test.packet.Unmarshal(test.data) + if err != nil { + t.Errorf("Unmarshal failed: %v", err) + } + }) + + if allocs != 0 { + t.Errorf("%T: %v allocs", test.packet, allocs) + } + } +} diff --git a/codecs/error.go b/codecs/error.go index 7f72e7b8..2083ef4d 100644 --- a/codecs/error.go +++ b/codecs/error.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import "errors" diff --git a/codecs/g711_packet.go b/codecs/g711_packet.go index 7ab68b2c..4afda55e 100644 --- a/codecs/g711_packet.go +++ b/codecs/g711_packet.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs // G711Payloader payloads G711 packets diff --git a/codecs/g711_packet_test.go b/codecs/g711_packet_test.go index 36ee7299..75fa6a8a 100644 --- a/codecs/g711_packet_test.go +++ b/codecs/g711_packet_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs //nolint:dupl import ( diff --git a/codecs/g722_packet.go b/codecs/g722_packet.go index 13e17b67..ae6672d9 100644 --- a/codecs/g722_packet.go +++ b/codecs/g722_packet.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs // G722Payloader payloads G722 packets diff --git a/codecs/g722_packet_test.go b/codecs/g722_packet_test.go index cc75e6f8..f15f81ca 100644 --- a/codecs/g722_packet_test.go +++ b/codecs/g722_packet_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs //nolint:dupl import ( diff --git a/codecs/h264_packet.go b/codecs/h264_packet.go index 11a82fe4..227cb9d8 100644 --- a/codecs/h264_packet.go +++ b/codecs/h264_packet.go @@ -1,6 +1,10 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( + "bytes" "encoding/binary" "fmt" ) @@ -31,40 +35,32 @@ const ( outputStapAHeader = 0x78 ) -func annexbNALUStartCode() []byte { return []byte{0x00, 0x00, 0x00, 0x01} } +// nolint:gochecknoglobals +var ( + naluStartCode = []byte{0x00, 0x00, 0x01} + annexbNALUStartCode = []byte{0x00, 0x00, 0x00, 0x01} +) func emitNalus(nals []byte, emit func([]byte)) { - nextInd := func(nalu []byte, start int) (indStart int, indLen int) { - zeroCount := 0 - - for i, b := range nalu[start:] { - if b == 0 { - zeroCount++ - continue - } else if b == 1 { - if zeroCount >= 2 { - return start + i - zeroCount, zeroCount + 1 - } - } - zeroCount = 0 + start := 0 + length := len(nals) + + for start < length { + end := bytes.Index(nals[start:], annexbNALUStartCode) + offset := 4 + if end == -1 { + end = bytes.Index(nals[start:], naluStartCode) + offset = 3 } - return -1, -1 - } - - nextIndStart, nextIndLen := nextInd(nals, 0) - if nextIndStart == -1 { - emit(nals) - } else { - for nextIndStart != -1 { - prevStart := nextIndStart + nextIndLen - nextIndStart, nextIndLen = nextInd(nals, prevStart) - if nextIndStart != -1 { - emit(nals[prevStart:nextIndStart]) - } else { - // Emit until end of stream, no end indicator found - emit(nals[prevStart:]) - } + if end == -1 { + emit(nals[start:]) + break } + + emit(nals[start : start+end]) + + // next NAL start position + start += end + offset } } @@ -137,18 +133,17 @@ func (p *H264Payloader) Payload(mtu uint16, payload []byte) [][]byte { // the FU header. An FU payload MAY have any number of octets and MAY // be empty. - naluData := nalu // According to the RFC, the first octet is skipped due to redundant information - naluDataIndex := 1 - naluDataLength := len(nalu) - naluDataIndex - naluDataRemaining := naluDataLength + naluIndex := 1 + naluLength := len(nalu) - naluIndex + naluRemaining := naluLength - if min(maxFragmentSize, naluDataRemaining) <= 0 { + if minInt(maxFragmentSize, naluRemaining) <= 0 { return } - for naluDataRemaining > 0 { - currentFragmentSize := min(maxFragmentSize, naluDataRemaining) + for naluRemaining > 0 { + currentFragmentSize := minInt(maxFragmentSize, naluRemaining) out := make([]byte, fuaHeaderSize+currentFragmentSize) // +---------------+ @@ -166,19 +161,19 @@ func (p *H264Payloader) Payload(mtu uint16, payload []byte) [][]byte { // +---------------+ out[1] = naluType - if naluDataRemaining == naluDataLength { + if naluRemaining == naluLength { // Set start bit out[1] |= 1 << 7 - } else if naluDataRemaining-currentFragmentSize == 0 { + } else if naluRemaining-currentFragmentSize == 0 { // Set end bit out[1] |= 1 << 6 } - copy(out[fuaHeaderSize:], naluData[naluDataIndex:naluDataIndex+currentFragmentSize]) + copy(out[fuaHeaderSize:], nalu[naluIndex:naluIndex+currentFragmentSize]) payloads = append(payloads, out) - naluDataRemaining -= currentFragmentSize - naluDataIndex += currentFragmentSize + naluRemaining -= currentFragmentSize + naluIndex += currentFragmentSize } }) @@ -193,14 +188,16 @@ type H264Packet struct { videoDepacketizer } -func (p *H264Packet) doPackaging(nalu []byte) []byte { +func (p *H264Packet) doPackaging(buf, nalu []byte) []byte { if p.IsAVC { - naluLength := make([]byte, 4) - binary.BigEndian.PutUint32(naluLength, uint32(len(nalu))) - return append(naluLength, nalu...) + buf = binary.BigEndian.AppendUint32(buf, uint32(len(nalu))) + buf = append(buf, nalu...) + return buf } - return append(annexbNALUStartCode(), nalu...) + buf = append(buf, annexbNALUStartCode...) + buf = append(buf, nalu...) + return buf } // IsDetectedFinalPacketInSequence returns true of the packet passed in has the @@ -211,10 +208,16 @@ func (p *H264Packet) IsDetectedFinalPacketInSequence(rtpPacketMarketBit bool) bo // Unmarshal parses the passed byte slice and stores the result in the H264Packet this method is called upon func (p *H264Packet) Unmarshal(payload []byte) ([]byte, error) { - if payload == nil { - return nil, errNilPacket - } else if len(payload) <= 2 { - return nil, fmt.Errorf("%w: %d <= 2", errShortPacket, len(payload)) + if p.zeroAllocation { + return payload, nil + } + + return p.parseBody(payload) +} + +func (p *H264Packet) parseBody(payload []byte) ([]byte, error) { + if len(payload) == 0 { + return nil, fmt.Errorf("%w: %d <=0", errShortPacket, len(payload)) } // NALU Types @@ -222,20 +225,24 @@ func (p *H264Packet) Unmarshal(payload []byte) ([]byte, error) { naluType := payload[0] & naluTypeBitmask switch { case naluType > 0 && naluType < 24: - return p.doPackaging(payload), nil + return p.doPackaging(nil, payload), nil case naluType == stapaNALUType: currOffset := int(stapaHeaderSize) result := []byte{} for currOffset < len(payload) { - naluSize := int(binary.BigEndian.Uint16(payload[currOffset:])) + naluSizeBytes := payload[currOffset:] + if len(naluSizeBytes) < stapaNALULengthSize { + break + } + naluSize := int(binary.BigEndian.Uint16(naluSizeBytes)) currOffset += stapaNALULengthSize if len(payload) < currOffset+naluSize { return nil, fmt.Errorf("%w STAP-A declared size(%d) is larger than buffer(%d)", errShortPacket, naluSize, len(payload)-currOffset) } - result = append(result, p.doPackaging(payload[currOffset:currOffset+naluSize])...) + result = p.doPackaging(result, payload[currOffset:currOffset+naluSize]) currOffset += naluSize } return result, nil @@ -258,7 +265,7 @@ func (p *H264Packet) Unmarshal(payload []byte) ([]byte, error) { nalu := append([]byte{}, naluRefIdc|fragmentedNaluType) nalu = append(nalu, p.fuaBuffer...) p.fuaBuffer = nil - return p.doPackaging(nalu), nil + return p.doPackaging(nil, nalu), nil } return []byte{}, nil @@ -267,9 +274,18 @@ func (p *H264Packet) Unmarshal(payload []byte) ([]byte, error) { return nil, fmt.Errorf("%w: %d", errUnhandledNALUType, naluType) } -// H264PartitionHeadChecker is obsolete +// H264PartitionHeadChecker checks H264 partition head. +// +// Deprecated: replaced by H264Packet.IsPartitionHead() type H264PartitionHeadChecker struct{} +// IsPartitionHead checks if this is the head of a packetized nalu stream. +// +// Deprecated: replaced by H264Packet.IsPartitionHead() +func (*H264PartitionHeadChecker) IsPartitionHead(packet []byte) bool { + return (&H264Packet{}).IsPartitionHead(packet) +} + // IsPartitionHead checks if this is the head of a packetized nalu stream. func (*H264Packet) IsPartitionHead(payload []byte) bool { if len(payload) < 2 { diff --git a/codecs/h264_packet_test.go b/codecs/h264_packet_test.go index 47826d7c..ca8a92c0 100644 --- a/codecs/h264_packet_test.go +++ b/codecs/h264_packet_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( @@ -106,6 +109,9 @@ func TestH264Packet_Unmarshal(t *testing.T) { singlePayloadMultiNALU := []byte{0x78, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11, 0xa8, 0x00, 0x05, 0x68, 0x1a, 0x34, 0xe3, 0xc8} singlePayloadMultiNALUUnmarshaled := []byte{0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x68, 0x1a, 0x34, 0xe3, 0xc8} singlePayloadMultiNALUUnmarshaledAVC := []byte{0x00, 0x00, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11, 0xa8, 0x00, 0x00, 0x00, 0x05, 0x68, 0x1a, 0x34, 0xe3, 0xc8} + singlePayloadWithBrokenSecondNALU := []byte{0x78, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11, 0xa8, 0x00} + singlePayloadWithBrokenSecondNALUUnmarshaled := []byte{0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11, 0xa8} + singlePayloadWithBrokenSecondUnmarshaledAVC := []byte{0x00, 0x00, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11, 0xa8} incompleteSinglePayloadMultiNALU := []byte{0x78, 0x00, 0x0f, 0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11} @@ -115,8 +121,16 @@ func TestH264Packet_Unmarshal(t *testing.T) { t.Fatal("Unmarshal did not fail on nil payload") } - if _, err := pkt.Unmarshal([]byte{0x00, 0x00}); err == nil { - t.Fatal("Unmarshal accepted a packet that is too small for a payload and header") + if _, err := pkt.Unmarshal([]byte{}); err == nil { + t.Fatal("Unmarshal did not fail on []byte{}") + } + + if _, err := pkt.Unmarshal([]byte{0xFC}); err == nil { + t.Fatal("Unmarshal accepted a FU-A packet that is too small for a payload and header") + } + + if _, err := pkt.Unmarshal([]byte{0x0A}); err != nil { + t.Fatal("Unmarshaling end of sequence(NALU Type : 10) should succeed") } if _, err := pkt.Unmarshal([]byte{0xFF, 0x00, 0x00}); err == nil { @@ -178,6 +192,20 @@ func TestH264Packet_Unmarshal(t *testing.T) { } else if !reflect.DeepEqual(res, singlePayloadMultiNALUUnmarshaledAVC) { t.Fatal("Failed to unmarshal a single packet with multiple NALUs into avc stream") } + + res, err = pkt.Unmarshal(singlePayloadWithBrokenSecondNALU) + if err != nil { + t.Fatal(err) + } else if !reflect.DeepEqual(res, singlePayloadWithBrokenSecondNALUUnmarshaled) { + t.Fatal("Failed to unmarshal a single packet with broken second NALUs") + } + + res, err = avcPkt.Unmarshal(singlePayloadWithBrokenSecondNALU) + if err != nil { + t.Fatal(err) + } else if !reflect.DeepEqual(res, singlePayloadWithBrokenSecondUnmarshaledAVC) { + t.Fatal("Failed to unmarshal a single packet with broken second NALUs into avc stream") + } } func TestH264IsPartitionHead(t *testing.T) { diff --git a/codecs/h265_packet.go b/codecs/h265_packet.go index fe604347..3c8c7ca8 100644 --- a/codecs/h265_packet.go +++ b/codecs/h265_packet.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( @@ -34,11 +37,13 @@ const ( // H265NALUHeader is a H265 NAL Unit Header // https://datatracker.ietf.org/doc/html/rfc7798#section-1.1.4 -// +---------------+---------------+ -// |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// |F| Type | LayerID | TID | -// +-------------+-----------------+ +/* +* +---------------+---------------+ +* |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* |F| Type | LayerID | TID | +* +-------------+-----------------+ +**/ type H265NALUHeader uint16 func newH265NALUHeader(highByte, lowByte uint8) H265NALUHeader { @@ -97,18 +102,19 @@ func (h H265NALUHeader) IsPACIPacket() bool { // // H265SingleNALUnitPacket represents a NALU packet, containing exactly one NAL unit. -// 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 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | PayloadHdr | DONL (conditional) | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | | -// | NAL unit payload data | -// | | -// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | :...OPTIONAL RTP padding | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// +/* +* 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 +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | PayloadHdr | DONL (conditional) | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | | +* | NAL unit payload data | +* | | +* | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | :...OPTIONAL RTP padding | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +**/ // Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.1 type H265SingleNALUnitPacket struct { // payloadHeader is the header of the H265 packet. @@ -186,19 +192,19 @@ func (p *H265SingleNALUnitPacket) isH265Packet() {} // // H265AggregationUnitFirst represent the First Aggregation Unit in an AP. -// -// 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 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// : DONL (conditional) | NALU size | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | NALU size | | -// +-+-+-+-+-+-+-+-+ NAL unit | -// | | -// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | : -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// +/* +* 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 +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* : DONL (conditional) | NALU size | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | NALU size | | +* +-+-+-+-+-+-+-+-+ NAL unit | +* | | +* | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | : +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +**/ // Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2 type H265AggregationUnitFirst struct { donl *uint16 @@ -224,18 +230,18 @@ func (u H265AggregationUnitFirst) NalUnit() []byte { } // H265AggregationUnit represent the an Aggregation Unit in an AP, which is not the first one. -// -// 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 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// : DOND (cond) | NALU size | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | | -// | NAL unit | -// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | : -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// +/* +* 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 +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* : DOND (cond) | NALU size | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | | +* | NAL unit | +* | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | : +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +**/ // Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2 type H265AggregationUnit struct { dond *uint8 @@ -261,18 +267,19 @@ func (u H265AggregationUnit) NalUnit() []byte { } // H265AggregationPacket represents an Aggregation packet. -// 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 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | PayloadHdr (Type=48) | | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | -// | | -// | two or more aggregation units | -// | | -// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | :...OPTIONAL RTP padding | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// +/* +* 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 +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | PayloadHdr (Type=48) | | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +* | | +* | two or more aggregation units | +* | | +* | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | :...OPTIONAL RTP padding | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +**/ // Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2 type H265AggregationPacket struct { firstUnit *H265AggregationUnitFirst @@ -397,11 +404,13 @@ const ( ) // H265FragmentationUnitHeader is a H265 FU Header -// +---------------+ -// |0|1|2|3|4|5|6|7| -// +-+-+-+-+-+-+-+-+ -// |S|E| FuType | -// +---------------+ +/* +* +---------------+ +* |0|1|2|3|4|5|6|7| +* +-+-+-+-+-+-+-+-+ +* |S|E| FuType | +* +---------------+ +**/ type H265FragmentationUnitHeader uint8 // S represents the start of a fragmented NAL unit. @@ -423,20 +432,20 @@ func (h H265FragmentationUnitHeader) FuType() uint8 { } // H265FragmentationUnitPacket represents a single Fragmentation Unit packet. -// -// 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 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | PayloadHdr (Type=49) | FU header | DONL (cond) | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| -// | DONL (cond) | | -// |-+-+-+-+-+-+-+-+ | -// | FU payload | -// | | -// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | :...OPTIONAL RTP padding | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// +/* +* 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 +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | PayloadHdr (Type=49) | FU header | DONL (cond) | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| +* | DONL (cond) | | +* |-+-+-+-+-+-+-+-+ | +* | FU payload | +* | | +* | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | :...OPTIONAL RTP padding | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +**/ // Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.3 type H265FragmentationUnitPacket struct { // payloadHeader is the header of the H265 packet. @@ -523,22 +532,22 @@ func (p *H265FragmentationUnitPacket) isH265Packet() {} // // H265PACIPacket represents a single H265 PACI packet. -// -// 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 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | PayloadHdr (Type=50) |A| cType | PHSsize |F0..2|Y| -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | Payload Header Extension Structure (PHES) | -// |=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=| -// | | -// | PACI payload: NAL unit | -// | . . . | -// | | -// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | :...OPTIONAL RTP padding | -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// +/* +* 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 +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | PayloadHdr (Type=50) |A| cType | PHSsize |F0..2|Y| +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | Payload Header Extension Structure (PHES) | +* |=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=| +* | | +* | PACI payload: NAL unit | +* | . . . | +* | | +* | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +* | :...OPTIONAL RTP padding | +* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +**/ // Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.4 type H265PACIPacket struct { // payloadHeader is the header of the H265 packet. diff --git a/codecs/h265_packet_test.go b/codecs/h265_packet_test.go index bfbef224..0279d362 100644 --- a/codecs/h265_packet_test.go +++ b/codecs/h265_packet_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( diff --git a/codecs/opus_packet.go b/codecs/opus_packet.go index cd5ea332..00ee9035 100644 --- a/codecs/opus_packet.go +++ b/codecs/opus_packet.go @@ -1,10 +1,13 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs // OpusPayloader payloads Opus packets type OpusPayloader struct{} // Payload fragments an Opus packet across one or more byte arrays -func (p *OpusPayloader) Payload(mtu uint16, payload []byte) [][]byte { +func (p *OpusPayloader) Payload(_ uint16, payload []byte) [][]byte { if payload == nil { return [][]byte{} } @@ -33,5 +36,14 @@ func (p *OpusPacket) Unmarshal(packet []byte) ([]byte, error) { return packet, nil } -// OpusPartitionHeadChecker is obsolete +// OpusPartitionHeadChecker checks Opus partition head. +// +// Deprecated: replaced by OpusPacket.IsPartitionHead() type OpusPartitionHeadChecker struct{} + +// IsPartitionHead checks whether if this is a head of the Opus partition. +// +// Deprecated: replaced by OpusPacket.IsPartitionHead() +func (*OpusPartitionHeadChecker) IsPartitionHead(packet []byte) bool { + return (&OpusPacket{}).IsPartitionHead(packet) +} diff --git a/codecs/opus_packet_test.go b/codecs/opus_packet_test.go index e68ff57f..665d17b5 100644 --- a/codecs/opus_packet_test.go +++ b/codecs/opus_packet_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( diff --git a/codecs/vp8_packet.go b/codecs/vp8_packet.go index cd869290..87560abe 100644 --- a/codecs/vp8_packet.go +++ b/codecs/vp8_packet.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs // VP8Payloader payloads VP8 packets @@ -53,12 +56,12 @@ func (p *VP8Payloader) Payload(mtu uint16, payload []byte) [][]byte { var payloads [][]byte // Make sure the fragment/payload size is correct - if min(maxFragmentSize, payloadDataRemaining) <= 0 { + if minInt(maxFragmentSize, payloadDataRemaining) <= 0 { return payloads } first := true for payloadDataRemaining > 0 { - currentFragmentSize := min(maxFragmentSize, payloadDataRemaining) + currentFragmentSize := minInt(maxFragmentSize, payloadDataRemaining) out := make([]byte, usingHeaderSize+currentFragmentSize) if first { @@ -120,19 +123,18 @@ type VP8Packet struct { } // Unmarshal parses the passed byte slice and stores the result in the VP8Packet this method is called upon -func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) { +func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) { //nolint: gocognit if payload == nil { return nil, errNilPacket } payloadLen := len(payload) - if payloadLen < 4 { - return nil, errShortPacket - } - payloadIndex := 0 + if payloadIndex >= payloadLen { + return nil, errShortPacket + } p.X = (payload[payloadIndex] & 0x80) >> 7 p.N = (payload[payloadIndex] & 0x20) >> 5 p.S = (payload[payloadIndex] & 0x10) >> 4 @@ -141,57 +143,88 @@ func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) { payloadIndex++ if p.X == 1 { + if payloadIndex >= payloadLen { + return nil, errShortPacket + } p.I = (payload[payloadIndex] & 0x80) >> 7 p.L = (payload[payloadIndex] & 0x40) >> 6 p.T = (payload[payloadIndex] & 0x20) >> 5 p.K = (payload[payloadIndex] & 0x10) >> 4 payloadIndex++ + } else { + p.I = 0 + p.L = 0 + p.T = 0 + p.K = 0 } if p.I == 1 { // PID present? + if payloadIndex >= payloadLen { + return nil, errShortPacket + } if payload[payloadIndex]&0x80 > 0 { // M == 1, PID is 16bit + if payloadIndex+1 >= payloadLen { + return nil, errShortPacket + } p.PictureID = (uint16(payload[payloadIndex]&0x7F) << 8) | uint16(payload[payloadIndex+1]) payloadIndex += 2 } else { p.PictureID = uint16(payload[payloadIndex]) payloadIndex++ } - } - - if payloadIndex >= payloadLen { - return nil, errShortPacket + } else { + p.PictureID = 0 } if p.L == 1 { + if payloadIndex >= payloadLen { + return nil, errShortPacket + } p.TL0PICIDX = payload[payloadIndex] payloadIndex++ - } - - if payloadIndex >= payloadLen { - return nil, errShortPacket + } else { + p.TL0PICIDX = 0 } if p.T == 1 || p.K == 1 { + if payloadIndex >= payloadLen { + return nil, errShortPacket + } if p.T == 1 { p.TID = payload[payloadIndex] >> 6 p.Y = (payload[payloadIndex] >> 5) & 0x1 + } else { + p.TID = 0 + p.Y = 0 } if p.K == 1 { p.KEYIDX = payload[payloadIndex] & 0x1F + } else { + p.KEYIDX = 0 } payloadIndex++ + } else { + p.TID = 0 + p.Y = 0 + p.KEYIDX = 0 } - if payloadIndex >= payloadLen { - return nil, errShortPacket - } p.Payload = payload[payloadIndex:] return p.Payload, nil } -// VP8PartitionHeadChecker is obsolete +// VP8PartitionHeadChecker checks VP8 partition head +// +// Deprecated: replaced by VP8Packet.IsPartitionHead() type VP8PartitionHeadChecker struct{} +// IsPartitionHead checks whether if this is a head of the VP8 partition. +// +// Deprecated: replaced by VP8Packet.IsPartitionHead() +func (*VP8PartitionHeadChecker) IsPartitionHead(packet []byte) bool { + return (&VP8Packet{}).IsPartitionHead(packet) +} + // IsPartitionHead checks whether if this is a head of the VP8 partition func (*VP8Packet) IsPartitionHead(payload []byte) bool { if len(payload) < 1 { diff --git a/codecs/vp8_packet_test.go b/codecs/vp8_packet_test.go index b173cfb5..3abb5208 100644 --- a/codecs/vp8_packet_test.go +++ b/codecs/vp8_packet_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( @@ -27,15 +30,6 @@ func TestVP8Packet_Unmarshal(t *testing.T) { t.Fatal("Error should be:", errShortPacket) } - // Payload smaller than header size - raw, err = pck.Unmarshal([]byte{0x00, 0x11, 0x22}) - if raw != nil { - t.Fatal("Result should be nil in case of error") - } - if !errors.Is(err, errShortPacket) { - t.Fatal("Error should be:", errShortPacket) - } - // Normal payload raw, err = pck.Unmarshal([]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x90}) if raw == nil { @@ -65,11 +59,11 @@ func TestVP8Packet_Unmarshal(t *testing.T) { // Header size, X and I, PID 16bits raw, err = pck.Unmarshal([]byte{0x80, 0x80, 0x81, 0x00}) - if raw != nil { - t.Fatal("Result should be nil in case of error") + if raw == nil { + t.Fatal("Result shouldn't be nil in case of success") } - if !errors.Is(err, errShortPacket) { - t.Fatal("Error should be:", errShortPacket) + if err != nil { + t.Fatal("Error should be nil in case of success") } // Header size, X and L @@ -107,6 +101,37 @@ func TestVP8Packet_Unmarshal(t *testing.T) { if !errors.Is(err, errShortPacket) { t.Fatal("Error should be:", errShortPacket) } + + // According to RFC 7741 Section 4.4, the packetizer need not pay + // attention to partition boundaries. In that case, it may + // produce packets with minimal headers. + + // The next three have been witnessed in nature. + _, err = pck.Unmarshal([]byte{0x00}) + if err != nil { + t.Errorf("Empty packet with trivial header: %v", err) + } + _, err = pck.Unmarshal([]byte{0x00, 0x2a, 0x94}) + if err != nil { + t.Errorf("Non-empty packet with trivial header: %v", err) + } + raw, err = pck.Unmarshal([]byte{0x81, 0x81, 0x94}) + if raw != nil { + t.Fatal("Result should be nil in case of error") + } + if !errors.Is(err, errShortPacket) { + t.Fatal("Error should be:", errShortPacket) + } + + // The following two were invented. + _, err = pck.Unmarshal([]byte{0x80, 0x00}) + if err != nil { + t.Errorf("Empty packet with trivial extension: %v", err) + } + _, err = pck.Unmarshal([]byte{0x80, 0x80, 42}) + if err != nil { + t.Errorf("Header with PictureID: %v", err) + } } func TestVP8Payloader_Payload(t *testing.T) { diff --git a/codecs/vp9/bits.go b/codecs/vp9/bits.go new file mode 100644 index 00000000..a6a3c1f4 --- /dev/null +++ b/codecs/vp9/bits.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package vp9 + +import "errors" + +var errNotEnoughBits = errors.New("not enough bits") + +func hasSpace(buf []byte, pos int, n int) error { + if n > ((len(buf) * 8) - pos) { + return errNotEnoughBits + } + return nil +} + +func readFlag(buf []byte, pos *int) (bool, error) { + err := hasSpace(buf, *pos, 1) + if err != nil { + return false, err + } + + return readFlagUnsafe(buf, pos), nil +} + +func readFlagUnsafe(buf []byte, pos *int) bool { + b := (buf[*pos>>0x03] >> (7 - (*pos & 0x07))) & 0x01 + *pos++ + return b == 1 +} + +func readBits(buf []byte, pos *int, n int) (uint64, error) { + err := hasSpace(buf, *pos, n) + if err != nil { + return 0, err + } + + return readBitsUnsafe(buf, pos, n), nil +} + +func readBitsUnsafe(buf []byte, pos *int, n int) uint64 { + res := 8 - (*pos & 0x07) + if n < res { + v := uint64((buf[*pos>>0x03] >> (res - n)) & (1<>0x03] & (1<= 8 { + v = (v << 8) | uint64(buf[*pos>>0x03]) + *pos += 8 + n -= 8 + } + + if n > 0 { + v = (v << n) | uint64(buf[*pos>>0x03]>>(8-n)) + *pos += n + } + + return v +} diff --git a/codecs/vp9/header.go b/codecs/vp9/header.go new file mode 100644 index 00000000..30e15bcf --- /dev/null +++ b/codecs/vp9/header.go @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +// Package vp9 contains a VP9 header parser. +package vp9 + +import ( + "errors" +) + +var ( + errInvalidFrameMarker = errors.New("invalid frame marker") + errWrongFrameSyncByte0 = errors.New("wrong frame_sync_byte_0") + errWrongFrameSyncByte1 = errors.New("wrong frame_sync_byte_1") + errWrongFrameSyncByte2 = errors.New("wrong frame_sync_byte_2") +) + +// HeaderColorConfig is the color_config member of an header. +type HeaderColorConfig struct { + TenOrTwelveBit bool + BitDepth uint8 + ColorSpace uint8 + ColorRange bool + SubsamplingX bool + SubsamplingY bool +} + +func (c *HeaderColorConfig) unmarshal(profile uint8, buf []byte, pos *int) error { + if profile >= 2 { + var err error + c.TenOrTwelveBit, err = readFlag(buf, pos) + if err != nil { + return err + } + + if c.TenOrTwelveBit { + c.BitDepth = 12 + } else { + c.BitDepth = 10 + } + } else { + c.BitDepth = 8 + } + + tmp, err := readBits(buf, pos, 3) + if err != nil { + return err + } + c.ColorSpace = uint8(tmp) + + if c.ColorSpace != 7 { + var err error + c.ColorRange, err = readFlag(buf, pos) + if err != nil { + return err + } + + if profile == 1 || profile == 3 { + err := hasSpace(buf, *pos, 3) + if err != nil { + return err + } + + c.SubsamplingX = readFlagUnsafe(buf, pos) + c.SubsamplingY = readFlagUnsafe(buf, pos) + *pos++ + } else { + c.SubsamplingX = true + c.SubsamplingY = true + } + } else { + c.ColorRange = true + + if profile == 1 || profile == 3 { + c.SubsamplingX = false + c.SubsamplingY = false + + err := hasSpace(buf, *pos, 1) + if err != nil { + return err + } + *pos++ + } + } + + return nil +} + +// HeaderFrameSize is the frame_size member of an header. +type HeaderFrameSize struct { + FrameWidthMinus1 uint16 + FrameHeightMinus1 uint16 +} + +func (s *HeaderFrameSize) unmarshal(buf []byte, pos *int) error { + err := hasSpace(buf, *pos, 32) + if err != nil { + return err + } + + s.FrameWidthMinus1 = uint16(readBitsUnsafe(buf, pos, 16)) + s.FrameHeightMinus1 = uint16(readBitsUnsafe(buf, pos, 16)) + return nil +} + +// Header is a VP9 Frame header. +// Specification: +// https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf +type Header struct { + Profile uint8 + ShowExistingFrame bool + FrameToShowMapIdx uint8 + NonKeyFrame bool + ShowFrame bool + ErrorResilientMode bool + ColorConfig *HeaderColorConfig + FrameSize *HeaderFrameSize +} + +// Unmarshal decodes a Header. +func (h *Header) Unmarshal(buf []byte) error { + pos := 0 + + err := hasSpace(buf, pos, 4) + if err != nil { + return err + } + + frameMarker := readBitsUnsafe(buf, &pos, 2) + if frameMarker != 2 { + return errInvalidFrameMarker + } + + profileLowBit := uint8(readBitsUnsafe(buf, &pos, 1)) + profileHighBit := uint8(readBitsUnsafe(buf, &pos, 1)) + h.Profile = profileHighBit<<1 + profileLowBit + + if h.Profile == 3 { + err = hasSpace(buf, pos, 1) + if err != nil { + return err + } + pos++ + } + + h.ShowExistingFrame, err = readFlag(buf, &pos) + if err != nil { + return err + } + + if h.ShowExistingFrame { + var tmp uint64 + tmp, err = readBits(buf, &pos, 3) + if err != nil { + return err + } + h.FrameToShowMapIdx = uint8(tmp) + return nil + } + + err = hasSpace(buf, pos, 3) + if err != nil { + return err + } + + h.NonKeyFrame = readFlagUnsafe(buf, &pos) + h.ShowFrame = readFlagUnsafe(buf, &pos) + h.ErrorResilientMode = readFlagUnsafe(buf, &pos) + + if !h.NonKeyFrame { + err := hasSpace(buf, pos, 24) + if err != nil { + return err + } + + frameSyncByte0 := uint8(readBitsUnsafe(buf, &pos, 8)) + if frameSyncByte0 != 0x49 { + return errWrongFrameSyncByte0 + } + + frameSyncByte1 := uint8(readBitsUnsafe(buf, &pos, 8)) + if frameSyncByte1 != 0x83 { + return errWrongFrameSyncByte1 + } + + frameSyncByte2 := uint8(readBitsUnsafe(buf, &pos, 8)) + if frameSyncByte2 != 0x42 { + return errWrongFrameSyncByte2 + } + + h.ColorConfig = &HeaderColorConfig{} + err = h.ColorConfig.unmarshal(h.Profile, buf, &pos) + if err != nil { + return err + } + + h.FrameSize = &HeaderFrameSize{} + err = h.FrameSize.unmarshal(buf, &pos) + if err != nil { + return err + } + } + + return nil +} + +// Width returns the video width. +func (h Header) Width() uint16 { + if h.FrameSize == nil { + return 0 + } + return h.FrameSize.FrameWidthMinus1 + 1 +} + +// Height returns the video height. +func (h Header) Height() uint16 { + if h.FrameSize == nil { + return 0 + } + return h.FrameSize.FrameHeightMinus1 + 1 +} diff --git a/codecs/vp9/header_test.go b/codecs/vp9/header_test.go new file mode 100644 index 00000000..41fb46a9 --- /dev/null +++ b/codecs/vp9/header_test.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package vp9 + +import ( + "reflect" + "testing" +) + +func TestHeaderUnmarshal(t *testing.T) { + cases := []struct { + name string + byts []byte + sh Header + width uint16 + height uint16 + }{ + { + "chrome webrtc", + []byte{ + 0x82, 0x49, 0x83, 0x42, 0x00, 0x77, 0xf0, 0x32, + 0x34, 0x30, 0x38, 0x24, 0x1c, 0x19, 0x40, 0x18, + 0x03, 0x40, 0x5f, 0xb4, + }, + Header{ + ShowFrame: true, + ColorConfig: &HeaderColorConfig{ + BitDepth: 8, + SubsamplingX: true, + SubsamplingY: true, + }, + FrameSize: &HeaderFrameSize{ + FrameWidthMinus1: 1919, + FrameHeightMinus1: 803, + }, + }, + 1920, + 804, + }, + { + "vp9 sample", + []byte{ + 0x82, 0x49, 0x83, 0x42, 0x40, 0xef, 0xf0, 0x86, + 0xf4, 0x04, 0x21, 0xa0, 0xe0, 0x00, 0x30, 0x70, + 0x00, 0x00, 0x00, 0x01, + }, + Header{ + ShowFrame: true, + ColorConfig: &HeaderColorConfig{ + BitDepth: 8, + ColorSpace: 2, + SubsamplingX: true, + SubsamplingY: true, + }, + FrameSize: &HeaderFrameSize{ + FrameWidthMinus1: 3839, + FrameHeightMinus1: 2159, + }, + }, + 3840, + 2160, + }, + } + + for _, ca := range cases { + t.Run(ca.name, func(t *testing.T) { + var sh Header + err := sh.Unmarshal(ca.byts) + if err != nil { + t.Fatal("unexpected error") + } + + if !reflect.DeepEqual(ca.sh, sh) { + t.Fatalf("expected %#+v, got %#+v", ca.sh, sh) + } + if ca.width != sh.Width() { + t.Fatalf("unexpected width") + } + if ca.height != sh.Height() { + t.Fatalf("unexpected height") + } + }) + } +} diff --git a/codecs/vp9_packet.go b/codecs/vp9_packet.go index 917e630b..e2ffab79 100644 --- a/codecs/vp9_packet.go +++ b/codecs/vp9_packet.go @@ -1,7 +1,11 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( "github.com/pion/randutil" + "github.com/pion/rtp/codecs/vp9" ) // Use global random generator to properly seed by crypto grade random. @@ -9,24 +13,50 @@ var globalMathRandomGenerator = randutil.NewMathRandomGenerator() // nolint:goch // VP9Payloader payloads VP9 packets type VP9Payloader struct { - pictureID uint16 - initialized bool + // whether to use flexible mode or non-flexible mode. + FlexibleMode bool // InitialPictureIDFn is a function that returns random initial picture ID. InitialPictureIDFn func() uint16 + + pictureID uint16 + initialized bool } const ( - vp9HeaderSize = 3 // Flexible mode 15 bit picture ID maxSpatialLayers = 5 maxVP9RefPics = 3 ) // Payload fragments an VP9 packet across one or more byte arrays func (p *VP9Payloader) Payload(mtu uint16, payload []byte) [][]byte { + if !p.initialized { + if p.InitialPictureIDFn == nil { + p.InitialPictureIDFn = func() uint16 { + return uint16(globalMathRandomGenerator.Intn(0x7FFF)) + } + } + p.pictureID = p.InitialPictureIDFn() & 0x7FFF + p.initialized = true + } + + var payloads [][]byte + if p.FlexibleMode { + payloads = p.payloadFlexible(mtu, payload) + } else { + payloads = p.payloadNonFlexible(mtu, payload) + } + + p.pictureID++ + if p.pictureID >= 0x8000 { + p.pictureID = 0 + } + + return payloads +} + +func (p *VP9Payloader) payloadFlexible(mtu uint16, payload []byte) [][]byte { /* - * https://www.ietf.org/id/draft-ietf-payload-vp9-13.txt - * * Flexible mode (F=1) * 0 1 2 3 4 5 6 7 * +-+-+-+-+-+-+-+-+ @@ -43,7 +73,45 @@ func (p *VP9Payloader) Payload(mtu uint16, payload []byte) [][]byte { * V: | SS | * | .. | * +-+-+-+-+-+-+-+-+ - * + */ + + headerSize := 3 + maxFragmentSize := int(mtu) - headerSize + payloadDataRemaining := len(payload) + payloadDataIndex := 0 + var payloads [][]byte + + if minInt(maxFragmentSize, payloadDataRemaining) <= 0 { + return [][]byte{} + } + + for payloadDataRemaining > 0 { + currentFragmentSize := minInt(maxFragmentSize, payloadDataRemaining) + out := make([]byte, headerSize+currentFragmentSize) + + out[0] = 0x90 // F=1, I=1 + if payloadDataIndex == 0 { + out[0] |= 0x08 // B=1 + } + if payloadDataRemaining == currentFragmentSize { + out[0] |= 0x04 // E=1 + } + + out[1] = byte(p.pictureID>>8) | 0x80 + out[2] = byte(p.pictureID) + + copy(out[headerSize:], payload[payloadDataIndex:payloadDataIndex+currentFragmentSize]) + payloads = append(payloads, out) + + payloadDataRemaining -= currentFragmentSize + payloadDataIndex += currentFragmentSize + } + + return payloads +} + +func (p *VP9Payloader) payloadNonFlexible(mtu uint16, payload []byte) [][]byte { + /* * Non-flexible mode (F=0) * 0 1 2 3 4 5 6 7 * +-+-+-+-+-+-+-+-+ @@ -62,51 +130,80 @@ func (p *VP9Payloader) Payload(mtu uint16, payload []byte) [][]byte { * +-+-+-+-+-+-+-+-+ */ - if !p.initialized { - if p.InitialPictureIDFn == nil { - p.InitialPictureIDFn = func() uint16 { - return uint16(globalMathRandomGenerator.Intn(0x7FFF)) - } - } - p.pictureID = p.InitialPictureIDFn() & 0x7FFF - p.initialized = true - } - if payload == nil { + var h vp9.Header + err := h.Unmarshal(payload) + if err != nil { return [][]byte{} } - maxFragmentSize := int(mtu) - vp9HeaderSize payloadDataRemaining := len(payload) payloadDataIndex := 0 - - if min(maxFragmentSize, payloadDataRemaining) <= 0 { - return [][]byte{} - } - var payloads [][]byte + for payloadDataRemaining > 0 { - currentFragmentSize := min(maxFragmentSize, payloadDataRemaining) - out := make([]byte, vp9HeaderSize+currentFragmentSize) + var headerSize int + if !h.NonKeyFrame && payloadDataIndex == 0 { + headerSize = 3 + 8 + } else { + headerSize = 3 + } + + maxFragmentSize := int(mtu) - headerSize + currentFragmentSize := minInt(maxFragmentSize, payloadDataRemaining) + if currentFragmentSize <= 0 { + return [][]byte{} + } + + out := make([]byte, headerSize+currentFragmentSize) - out[0] = 0x90 // F=1 I=1 + out[0] = 0x80 | 0x01 // I=1, Z=1 + + if h.NonKeyFrame { + out[0] |= 0x40 // P=1 + } if payloadDataIndex == 0 { out[0] |= 0x08 // B=1 } if payloadDataRemaining == currentFragmentSize { out[0] |= 0x04 // E=1 } + out[1] = byte(p.pictureID>>8) | 0x80 out[2] = byte(p.pictureID) - copy(out[vp9HeaderSize:], payload[payloadDataIndex:payloadDataIndex+currentFragmentSize]) + off := 3 + + if !h.NonKeyFrame && payloadDataIndex == 0 { + out[0] |= 0x02 // V=1 + out[off] = 0x10 | 0x08 // N_S=0, Y=1, G=1 + off++ + + width := h.Width() + out[off] = byte(width >> 8) + off++ + out[off] = byte(width & 0xFF) + off++ + + height := h.Height() + out[off] = byte(height >> 8) + off++ + out[off] = byte(height & 0xFF) + off++ + + out[off] = 0x01 // N_G=1 + off++ + + out[off] = 1<<4 | 1<<2 // TID=0, U=1, R=1 + off++ + + out[off] = 0x01 // P_DIFF=1 + } + + copy(out[headerSize:], payload[payloadDataIndex:payloadDataIndex+currentFragmentSize]) payloads = append(payloads, out) payloadDataRemaining -= currentFragmentSize payloadDataIndex += currentFragmentSize } - p.pictureID++ - if p.pictureID >= 0x8000 { - p.pictureID = 0 - } return payloads } @@ -206,13 +303,13 @@ func (p *VP9Packet) Unmarshal(packet []byte) ([]byte, error) { } // Picture ID: -// -// +-+-+-+-+-+-+-+-+ -// I: |M| PICTURE ID | M:0 => picture id is 7 bits. -// +-+-+-+-+-+-+-+-+ M:1 => picture id is 15 bits. -// M: | EXTENDED PID | -// +-+-+-+-+-+-+-+-+ -// +/* +* +-+-+-+-+-+-+-+-+ +* I: |M| PICTURE ID | M:0 => picture id is 7 bits. +* +-+-+-+-+-+-+-+-+ M:1 => picture id is 15 bits. +* M: | EXTENDED PID | +* +-+-+-+-+-+-+-+-+ +**/ func (p *VP9Packet) parsePictureID(packet []byte, pos int) (int, error) { if len(packet) <= pos { return pos, errShortPacket @@ -244,11 +341,11 @@ func (p *VP9Packet) parseLayerInfo(packet []byte, pos int) (int, error) { } // Layer indices (flexible mode): -// -// +-+-+-+-+-+-+-+-+ -// L: | T |U| S |D| -// +-+-+-+-+-+-+-+-+ -// +/* +* +-+-+-+-+-+-+-+-+ +* L: | T |U| S |D| +* +-+-+-+-+-+-+-+-+ +**/ func (p *VP9Packet) parseLayerInfoCommon(packet []byte, pos int) (int, error) { if len(packet) <= pos { return pos, errShortPacket @@ -268,13 +365,13 @@ func (p *VP9Packet) parseLayerInfoCommon(packet []byte, pos int) (int, error) { } // Layer indices (non-flexible mode): -// -// +-+-+-+-+-+-+-+-+ -// L: | T |U| S |D| -// +-+-+-+-+-+-+-+-+ -// | TL0PICIDX | -// +-+-+-+-+-+-+-+-+ -// +/* +* +-+-+-+-+-+-+-+-+ +* L: | T |U| S |D| +* +-+-+-+-+-+-+-+-+ +* | TL0PICIDX | +* +-+-+-+-+-+-+-+-+ +**/ func (p *VP9Packet) parseLayerInfoNonFlexibleMode(packet []byte, pos int) (int, error) { if len(packet) <= pos { return pos, errShortPacket @@ -286,12 +383,12 @@ func (p *VP9Packet) parseLayerInfoNonFlexibleMode(packet []byte, pos int) (int, } // Reference indices: -// -// +-+-+-+-+-+-+-+-+ P=1,F=1: At least one reference index -// P,F: | P_DIFF |N| up to 3 times has to be specified. -// +-+-+-+-+-+-+-+-+ N=1: An additional P_DIFF follows -// current P_DIFF. -// +/* +* +-+-+-+-+-+-+-+-+ P=1,F=1: At least one reference index +* P,F: | P_DIFF |N| up to 3 times has to be specified. +* +-+-+-+-+-+-+-+-+ N=1: An additional P_DIFF follows +* current P_DIFF. +**/ func (p *VP9Packet) parseRefIndices(packet []byte, pos int) (int, error) { for { if len(packet) <= pos { @@ -312,25 +409,25 @@ func (p *VP9Packet) parseRefIndices(packet []byte, pos int) (int, error) { } // Scalability structure (SS): -// -// +-+-+-+-+-+-+-+-+ -// V: | N_S |Y|G|-|-|-| -// +-+-+-+-+-+-+-+-+ -| -// Y: | WIDTH | (OPTIONAL) . -// + + . -// | | (OPTIONAL) . -// +-+-+-+-+-+-+-+-+ . N_S + 1 times -// | HEIGHT | (OPTIONAL) . -// + + . -// | | (OPTIONAL) . -// +-+-+-+-+-+-+-+-+ -| -// G: | N_G | (OPTIONAL) -// +-+-+-+-+-+-+-+-+ -| -// N_G: | T |U| R |-|-| (OPTIONAL) . -// +-+-+-+-+-+-+-+-+ -| . N_G times -// | P_DIFF | (OPTIONAL) . R times . -// +-+-+-+-+-+-+-+-+ -| -| -// +/* +* +-+-+-+-+-+-+-+-+ +* V: | N_S |Y|G|-|-|-| +* +-+-+-+-+-+-+-+-+ -| +* Y: | WIDTH | (OPTIONAL) . +* + . +* | | (OPTIONAL) . +* +-+-+-+-+-+-+-+-+ . N_S + 1 times +* | HEIGHT | (OPTIONAL) . +* + . +* | | (OPTIONAL) . +* +-+-+-+-+-+-+-+-+ -| +* G: | N_G | (OPTIONAL) +* +-+-+-+-+-+-+-+-+ -| +* N_G: | T |U| R |-|-| (OPTIONAL) . +* +-+-+-+-+-+-+-+-+ -| . N_G times +* | P_DIFF | (OPTIONAL) . R times . +* +-+-+-+-+-+-+-+-+ -| -| +**/ func (p *VP9Packet) parseSSData(packet []byte, pos int) (int, error) { if len(packet) <= pos { return pos, errShortPacket @@ -348,6 +445,10 @@ func (p *VP9Packet) parseSSData(packet []byte, pos int) (int, error) { p.Width = make([]uint16, NS) p.Height = make([]uint16, NS) for i := 0; i < int(NS); i++ { + if len(packet) <= (pos + 3) { + return pos, errShortPacket + } + p.Width[i] = uint16(packet[pos])<<8 | uint16(packet[pos+1]) pos += 2 p.Height[i] = uint16(packet[pos])<<8 | uint16(packet[pos+1]) @@ -356,17 +457,30 @@ func (p *VP9Packet) parseSSData(packet []byte, pos int) (int, error) { } if p.G { + if len(packet) <= pos { + return pos, errShortPacket + } + p.NG = packet[pos] pos++ } for i := 0; i < int(p.NG); i++ { + if len(packet) <= pos { + return pos, errShortPacket + } + p.PGTID = append(p.PGTID, packet[pos]>>5) p.PGU = append(p.PGU, packet[pos]&0x10 != 0) R := (packet[pos] >> 2) & 0x3 pos++ p.PGPDiff = append(p.PGPDiff, []uint8{}) + + if len(packet) <= (pos + int(R) - 1) { + return pos, errShortPacket + } + for j := 0; j < int(R); j++ { p.PGPDiff[i] = append(p.PGPDiff[i], packet[pos]) pos++ @@ -376,9 +490,18 @@ func (p *VP9Packet) parseSSData(packet []byte, pos int) (int, error) { return pos, nil } -// VP9PartitionHeadChecker is obsolete +// VP9PartitionHeadChecker checks VP9 partition head. +// +// Deprecated: replaced by VP9Packet.IsPartitionHead() type VP9PartitionHeadChecker struct{} +// IsPartitionHead checks whether if this is a head of the VP9 partition. +// +// Deprecated: replaced by VP9Packet.IsPartitionHead() +func (*VP9PartitionHeadChecker) IsPartitionHead(packet []byte) bool { + return (&VP9Packet{}).IsPartitionHead(packet) +} + // IsPartitionHead checks whether if this is a head of the VP9 partition func (*VP9Packet) IsPartitionHead(payload []byte) bool { if len(payload) < 1 { diff --git a/codecs/vp9_packet_test.go b/codecs/vp9_packet_test.go index 65861c68..9591f22c 100644 --- a/codecs/vp9_packet_test.go +++ b/codecs/vp9_packet_test.go @@ -1,8 +1,10 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package codecs import ( "errors" - "fmt" "math/rand" "reflect" "testing" @@ -167,6 +169,22 @@ func TestVP9Packet_Unmarshal(t *testing.T) { Payload: []byte{}, }, }, + "ScalabilityMissingWidth": { + b: []byte("200"), + err: errShortPacket, + }, + "ScalabilityMissingNG": { + b: []byte("b00200000000"), + err: errShortPacket, + }, + "ScalabilityMissingTemporalLayerIDs": { + b: []byte("20B0"), + err: errShortPacket, + }, + "ScalabilityMissingReferenceIndices": { + b: []byte("20B007"), + err: errShortPacket, + }, } for name, c := range cases { c := c @@ -204,62 +222,134 @@ func TestVP9Payloader_Payload(t *testing.T) { } cases := map[string]struct { - b [][]byte - mtu uint16 - res [][]byte + b [][]byte + flexible bool + mtu uint16 + res [][]byte }{ - "NilPayload": { - b: [][]byte{nil}, - mtu: 100, - res: [][]byte{}, + "flexible NilPayload": { + b: [][]byte{nil}, + flexible: true, + mtu: 100, + res: [][]byte{}, }, - "SmallMTU": { - b: [][]byte{{0x00, 0x00}}, - mtu: 1, - res: [][]byte{}, + "flexible SmallMTU": { + b: [][]byte{{0x00, 0x00}}, + flexible: true, + mtu: 1, + res: [][]byte{}, }, - "OnePacket": { - b: [][]byte{{0x01, 0x02}}, - mtu: 10, + "flexible OnePacket": { + b: [][]byte{{0x01, 0x02}}, + flexible: true, + mtu: 10, res: [][]byte{ {0x9C, rands[0][0], rands[0][1], 0x01, 0x02}, }, }, - "TwoPackets": { - b: [][]byte{{0x01, 0x02}}, - mtu: 4, + "flexible TwoPackets": { + b: [][]byte{{0x01, 0x02}}, + flexible: true, + mtu: 4, res: [][]byte{ {0x98, rands[0][0], rands[0][1], 0x01}, {0x94, rands[0][0], rands[0][1], 0x02}, }, }, - "ThreePackets": { - b: [][]byte{{0x01, 0x02, 0x03}}, - mtu: 4, + "flexible ThreePackets": { + b: [][]byte{{0x01, 0x02, 0x03}}, + flexible: true, + mtu: 4, res: [][]byte{ {0x98, rands[0][0], rands[0][1], 0x01}, {0x90, rands[0][0], rands[0][1], 0x02}, {0x94, rands[0][0], rands[0][1], 0x03}, }, }, - "TwoFramesFourPackets": { - b: [][]byte{{0x01, 0x02, 0x03}, {0x04}}, - mtu: 5, + "flexible TwoFramesFourPackets": { + b: [][]byte{{0x01, 0x02, 0x03}, {0x04}}, + flexible: true, + mtu: 5, res: [][]byte{ {0x98, rands[0][0], rands[0][1], 0x01, 0x02}, {0x94, rands[0][0], rands[0][1], 0x03}, {0x9C, rands[1][0], rands[1][1], 0x04}, }, }, + "non-flexible NilPayload": { + b: [][]byte{nil}, + mtu: 100, + res: [][]byte{}, + }, + "non-flexible SmallMTU": { + b: [][]byte{{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34}}, + mtu: 1, + res: [][]byte{}, + }, + "non-flexible OnePacket key frame": { + b: [][]byte{{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34}}, + mtu: 20, + res: [][]byte{{ + 0x8f, 0xa1, 0xf4, 0x18, 0x07, 0x80, 0x03, 0x24, + 0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00, + 0x77, 0xf0, 0x32, 0x34, + }}, + }, + "non-flexible TwoPackets key frame": { + b: [][]byte{{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34}}, + mtu: 12, + res: [][]byte{ + { + 0x8b, 0xa1, 0xf4, 0x18, 0x07, 0x80, 0x03, 0x24, + 0x01, 0x14, 0x01, 0x82, + }, + { + 0x85, 0xa1, 0xf4, 0x49, 0x83, 0x42, 0x00, 0x77, + 0xf0, 0x32, 0x34, + }, + }, + }, + "non-flexible ThreePackets key frame": { + b: [][]byte{{ + 0x82, 0x49, 0x83, 0x42, 0x00, 0x77, 0xf0, 0x32, + 0x34, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, + }}, + mtu: 12, + res: [][]byte{ + { + 0x8b, 0xa1, 0xf4, 0x18, 0x07, 0x80, 0x03, 0x24, + 0x01, 0x14, 0x01, 0x82, + }, + { + 0x81, 0xa1, 0xf4, 0x49, 0x83, 0x42, 0x00, 0x77, + 0xf0, 0x32, 0x34, 0x01, + }, + { + 0x85, 0xa1, 0xf4, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, + }, + }, + }, + "non-flexible OnePacket non key frame": { + b: [][]byte{{0x86, 0x0, 0x40, 0x92, 0xe1, 0x31, 0x42, 0x8c, 0xc0, 0x40}}, + mtu: 20, + res: [][]byte{{ + 0xcd, 0xa1, 0xf4, 0x86, 0x00, 0x40, 0x92, 0xe1, + 0x31, 0x42, 0x8c, 0xc0, 0x40, + }}, + }, } + for name, c := range cases { - pck := VP9Payloader{ - InitialPictureIDFn: func() uint16 { - return uint16(rand.New(rand.NewSource(0)).Int31n(0x7FFF)) //nolint:gosec - }, - } - c := c - t.Run(fmt.Sprintf("%s_MTU%d", name, c.mtu), func(t *testing.T) { + t.Run(name, func(t *testing.T) { + pck := VP9Payloader{ + FlexibleMode: c.flexible, + InitialPictureIDFn: func() uint16 { + return uint16(rand.New(rand.NewSource(0)).Int31n(0x7FFF)) //nolint:gosec + }, + } + res := [][]byte{} for _, b := range c.b { res = append(res, pck.Payload(c.mtu, b)...) @@ -269,8 +359,10 @@ func TestVP9Payloader_Payload(t *testing.T) { } }) } + t.Run("PictureIDOverflow", func(t *testing.T) { pck := VP9Payloader{ + FlexibleMode: true, InitialPictureIDFn: func() uint16 { return uint16(rand.New(rand.NewSource(0)).Int31n(0x7FFF)) //nolint:gosec }, diff --git a/depacketizer.go b/depacketizer.go index c66d2e30..d10ad89b 100644 --- a/depacketizer.go +++ b/depacketizer.go @@ -1,12 +1,19 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp // Depacketizer depacketizes a RTP payload, removing any RTP specific data from the payload type Depacketizer interface { + // Unmarshal parses the RTP payload and returns media. + // Metadata may be stored on the Depacketizer itself Unmarshal(packet []byte) ([]byte, error) + // Checks if the packet is at the beginning of a partition. This // should return false if the result could not be determined, in // which case the caller will detect timestamp discontinuities. IsPartitionHead(payload []byte) bool + // Checks if the packet is at the end of a partition. This should // return false if the result could not be determined. IsPartitionTail(marker bool, payload []byte) bool diff --git a/error.go b/error.go index 5458c6fa..ac3ece45 100644 --- a/error.go +++ b/error.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( @@ -18,4 +21,6 @@ var ( errRFC8285TwoByteHeaderSize = errors.New("header extension payload must be 255bytes or less for RFC 5285 two byte extensions") errRFC3550HeaderIDRange = errors.New("header extension id must be 0 for non-RFC 5285 extensions") + + errInvalidRTPPadding = errors.New("invalid RTP padding") ) diff --git a/go.mod b/go.mod index 19a0cc01..3b37fe0f 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ -module github.com/pion/rtp/v2 +module github.com/pion/rtp -go 1.13 +go 1.20 require github.com/pion/randutil v0.1.0 diff --git a/header_extension.go b/header_extension.go index d010bb81..cdb16bd7 100644 --- a/header_extension.go +++ b/header_extension.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( @@ -160,7 +163,7 @@ type TwoByteHeaderExtension struct { // Set sets the extension payload for the specified ID. func (e *TwoByteHeaderExtension) Set(id uint8, buf []byte) error { - if id < 1 || id > 255 { + if id < 1 { return fmt.Errorf("%w actual(%d)", errRFC8285TwoByteHeaderIDRange, id) } if len(buf) > 255 { diff --git a/header_extension_test.go b/header_extension_test.go index 8aba9460..bac3bd0e 100644 --- a/header_extension_test.go +++ b/header_extension_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/packet.go b/packet.go index 6528b652..e74d48d2 100644 --- a/packet.go +++ b/packet.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( @@ -25,6 +28,9 @@ type Header struct { CSRC []uint32 ExtensionProfile uint16 Extensions []Extension + + // Deprecated: will be removed in a future version. + PayloadOffset int } // Packet represents an RTP Packet @@ -32,6 +38,9 @@ type Packet struct { Header Payload []byte PaddingSize byte + + // Deprecated: will be removed in a future version. + Raw []byte } const ( @@ -140,68 +149,60 @@ func (h *Header) Unmarshal(buf []byte) (n int, err error) { //nolint:gocognit n += 2 extensionLength := int(binary.BigEndian.Uint16(buf[n:])) * 4 n += 2 + extensionEnd := n + extensionLength - if expected := n + extensionLength; len(buf) < expected { - return n, fmt.Errorf("size %d < %d: %w", - len(buf), expected, - errHeaderSizeInsufficientForExtension, - ) + if len(buf) < extensionEnd { + return n, fmt.Errorf("size %d < %d: %w", len(buf), extensionEnd, errHeaderSizeInsufficientForExtension) } - switch h.ExtensionProfile { - // RFC 8285 RTP One Byte Header Extension - case extensionProfileOneByte: - end := n + extensionLength - for n < end { + if h.ExtensionProfile == extensionProfileOneByte || h.ExtensionProfile == extensionProfileTwoByte { + var ( + extid uint8 + payloadLen int + ) + + for n < extensionEnd { if buf[n] == 0x00 { // padding n++ continue } - extid := buf[n] >> 4 - payloadLen := int(buf[n]&^0xF0 + 1) - n++ + if h.ExtensionProfile == extensionProfileOneByte { + extid = buf[n] >> 4 + payloadLen = int(buf[n]&^0xF0 + 1) + n++ - if extid == extensionIDReserved { - break - } + if extid == extensionIDReserved { + break + } + } else { + extid = buf[n] + n++ - extension := Extension{id: extid, payload: buf[n : n+payloadLen]} - h.Extensions = append(h.Extensions, extension) - n += payloadLen - } + if len(buf) <= n { + return n, fmt.Errorf("size %d < %d: %w", len(buf), n, errHeaderSizeInsufficientForExtension) + } - // RFC 8285 RTP Two Byte Header Extension - case extensionProfileTwoByte: - end := n + extensionLength - for n < end { - if buf[n] == 0x00 { // padding + payloadLen = int(buf[n]) n++ - continue } - extid := buf[n] - n++ - - payloadLen := int(buf[n]) - n++ + if extensionPayloadEnd := n + payloadLen; len(buf) <= extensionPayloadEnd { + return n, fmt.Errorf("size %d < %d: %w", len(buf), extensionPayloadEnd, errHeaderSizeInsufficientForExtension) + } extension := Extension{id: extid, payload: buf[n : n+payloadLen]} h.Extensions = append(h.Extensions, extension) n += payloadLen } - - default: // RFC3550 Extension - if len(buf) < n+extensionLength { - return n, fmt.Errorf("%w: %d < %d", - errHeaderSizeInsufficientForExtension, len(buf), n+extensionLength) - } - - extension := Extension{id: 0, payload: buf[n : n+extensionLength]} + } else { + // RFC3550 Extension + extension := Extension{id: 0, payload: buf[n:extensionEnd]} h.Extensions = append(h.Extensions, extension) n += len(h.Extensions[0].payload) } } + return n, nil } @@ -211,15 +212,21 @@ func (p *Packet) Unmarshal(buf []byte) error { if err != nil { return err } + end := len(buf) if p.Header.Padding { + if end <= n { + return errTooSmall + } p.PaddingSize = buf[end-1] end -= int(p.PaddingSize) } if end < n { return errTooSmall } + p.Payload = buf[n:end] + return nil } @@ -231,6 +238,7 @@ func (h Header) Marshal() (buf []byte, err error) { if err != nil { return nil, err } + return buf[:n], nil } @@ -375,7 +383,7 @@ func (h *Header) SetExtension(id uint8, payload []byte) error { //nolint:gocogni } // RFC 8285 RTP Two Byte Header Extension case extensionProfileTwoByte: - if id < 1 || id > 255 { + if id < 1 { return fmt.Errorf("%w actual(%d)", errRFC8285TwoByteHeaderIDRange, id) } if len(payload) > 255 { @@ -469,7 +477,11 @@ func (p Packet) Marshal() (buf []byte, err error) { } // MarshalTo serializes the packet and writes to the buffer. -func (p Packet) MarshalTo(buf []byte) (n int, err error) { +func (p *Packet) MarshalTo(buf []byte) (n int, err error) { + if p.Header.Padding && p.PaddingSize == 0 { + return 0, errInvalidRTPPadding + } + n, err = p.Header.MarshalTo(buf) if err != nil { return 0, err @@ -481,6 +493,7 @@ func (p Packet) MarshalTo(buf []byte) (n int, err error) { } m := copy(buf[n:], p.Payload) + if p.Header.Padding { buf[n+m+int(p.PaddingSize-1)] = p.PaddingSize } diff --git a/packet_test.go b/packet_test.go index 6067c280..98dfb400 100644 --- a/packet_test.go +++ b/packet_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( @@ -1189,6 +1192,34 @@ func TestRFC8285TwoByteSetExtensionShouldErrorWhenPayloadTooLarge(t *testing.T) } } +func TestRFC8285Padding(t *testing.T) { + header := &Header{} + + for _, payload := range [][]byte{ + { + 0b00010000, // header.Extension = true + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // SequenceNumber, Timestamp, SSRC + 0xBE, 0xDE, // header.ExtensionProfile = extensionProfileOneByte + 0, 1, // extensionLength + 0, 0, 0, // padding + 1, // extid + }, + { + 0b00010000, // header.Extension = true + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // SequenceNumber, Timestamp, SSRC + 0x10, 0x00, // header.ExtensionProfile = extensionProfileOneByte + 0, 1, // extensionLength + 0, 0, 0, // padding + 1, // extid + }, + } { + _, err := header.Unmarshal(payload) + if !errors.Is(err, errHeaderSizeInsufficientForExtension) { + t.Fatal("Expected errHeaderSizeInsufficientForExtension") + } + } +} + func TestRFC3550SetExtensionShouldErrorWhenNonZero(t *testing.T) { payload := []byte{ // Payload diff --git a/packetizer.go b/packetizer.go index a4be143f..7a6a46db 100644 --- a/packetizer.go +++ b/packetizer.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( @@ -12,17 +15,22 @@ type Payloader interface { // Packetizer packetizes a payload type Packetizer interface { Packetize(payload []byte, samples uint32) []*Packet + GeneratePadding(samples uint32) []*Packet EnableAbsSendTime(value int) SkipSamples(skippedSamples uint32) } type packetizer struct { - MTU uint16 - PayloadType uint8 - SSRC uint32 - Payloader Payloader - Sequencer Sequencer - Timestamp uint32 + MTU uint16 + PayloadType uint8 + SSRC uint32 + Payloader Payloader + Sequencer Sequencer + Timestamp uint32 + + // Deprecated: will be removed in a future version. + ClockRate uint32 + extensionNumbers struct { // put extension numbers in here. If they're 0, the extension is disabled (0 is not a legal extension number) AbsSendTime int // http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time } @@ -30,7 +38,7 @@ type packetizer struct { } // NewPacketizer returns a new instance of a Packetizer for a specific payloader -func NewPacketizer(mtu uint16, pt uint8, ssrc uint32, payloader Payloader, sequencer Sequencer) Packetizer { +func NewPacketizer(mtu uint16, pt uint8, ssrc uint32, payloader Payloader, sequencer Sequencer, clockRate uint32) Packetizer { return &packetizer{ MTU: mtu, PayloadType: pt, @@ -38,6 +46,7 @@ func NewPacketizer(mtu uint16, pt uint8, ssrc uint32, payloader Payloader, seque Payloader: payloader, Sequencer: sequencer, Timestamp: globalMathRandomGenerator.Uint32(), + ClockRate: clockRate, timegen: time.Now, } } @@ -90,6 +99,38 @@ func (p *packetizer) Packetize(payload []byte, samples uint32) []*Packet { return packets } +// GeneratePadding returns required padding-only packages +func (p *packetizer) GeneratePadding(samples uint32) []*Packet { + // Guard against an empty payload + if samples == 0 { + return nil + } + + packets := make([]*Packet, samples) + + for i := 0; i < int(samples); i++ { + pp := make([]byte, 255) + pp[254] = 255 + + packets[i] = &Packet{ + Header: Header{ + Version: 2, + Padding: true, + Extension: false, + Marker: false, + PayloadType: p.PayloadType, + SequenceNumber: p.Sequencer.NextSequenceNumber(), + Timestamp: p.Timestamp, // Use latest timestamp + SSRC: p.SSRC, + CSRC: []uint32{}, + }, + Payload: pp, + } + } + + return packets +} + // SkipSamples causes a gap in sample count between Packetize requests so the // RTP payloads produced have a gap in timestamps func (p *packetizer) SkipSamples(skippedSamples uint32) { diff --git a/packetizer_test.go b/packetizer_test.go index 6beff6a7..78f82208 100644 --- a/packetizer_test.go +++ b/packetizer_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( @@ -6,13 +9,13 @@ import ( "testing" "time" - "github.com/pion/rtp/v2/codecs" + "github.com/pion/rtp/codecs" ) func TestPacketizer(t *testing.T) { multiplepayload := make([]byte, 128) // use the G722 payloader here, because it's very simple and all 0s is valid G722 data. - packetizer := NewPacketizer(100, 98, 0x1234ABCD, &codecs.G722Payloader{}, NewRandomSequencer()) + packetizer := NewPacketizer(100, 98, 0x1234ABCD, &codecs.G722Payloader{}, NewRandomSequencer(), 90000) packets := packetizer.Packetize(multiplepayload, 2000) if len(packets) != 2 { @@ -26,7 +29,7 @@ func TestPacketizer(t *testing.T) { func TestPacketizer_AbsSendTime(t *testing.T) { // use the G722 payloader here, because it's very simple and all 0s is valid G722 data. - pktizer := NewPacketizer(100, 98, 0x1234ABCD, &codecs.G722Payloader{}, NewFixedSequencer(1234)) + pktizer := NewPacketizer(100, 98, 0x1234ABCD, &codecs.G722Payloader{}, NewFixedSequencer(1234), 90000) p, ok := pktizer.(*packetizer) if !ok { t.Fatal("Failed to access packetizer") @@ -74,7 +77,7 @@ func TestPacketizer_AbsSendTime(t *testing.T) { func TestPacketizer_Roundtrip(t *testing.T) { multiplepayload := make([]byte, 128) - packetizer := NewPacketizer(100, 98, 0x1234ABCD, &codecs.G722Payloader{}, NewRandomSequencer()) + packetizer := NewPacketizer(100, 98, 0x1234ABCD, &codecs.G722Payloader{}, NewRandomSequencer(), 90000) packets := packetizer.Packetize(multiplepayload, 1000) rawPkts := make([][]byte, 0, 1400) @@ -140,6 +143,8 @@ func TestPacketizer_Roundtrip(t *testing.T) { t.Errorf("Packet versions don't match, expected %v but got %v", expectedPkt.Payload, pkt.Payload) } + pkt.PaddingSize = 0 + if !reflect.DeepEqual(expectedPkt, pkt) { t.Errorf("Packets don't match, expected %v but got %v", expectedPkt, pkt) } diff --git a/partitionheadchecker.go b/partitionheadchecker.go index 6ec2a763..6f05aa5a 100644 --- a/partitionheadchecker.go +++ b/partitionheadchecker.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp // PartitionHeadChecker is the interface that checks whether the packet is keyframe or not diff --git a/payload_types.go b/payload_types.go new file mode 100644 index 00000000..a9bf2272 --- /dev/null +++ b/payload_types.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2024 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +// https://en.wikipedia.org/wiki/RTP_payload_formats + +// Audio Payload Types as defined in https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +const ( + // PayloadTypePCMU is a payload type for ITU-T G.711 PCM μ-Law audio 64 kbit/s (RFC 3551). + PayloadTypePCMU = 0 + // PayloadTypeGSM is a payload type for European GSM Full Rate audio 13 kbit/s (GSM 06.10). + PayloadTypeGSM = 3 + // PayloadTypeG723 is a payload type for ITU-T G.723.1 audio (RFC 3551). + PayloadTypeG723 = 4 + // PayloadTypeDVI4_8000 is a payload type for IMA ADPCM audio 32 kbit/s (RFC 3551). + PayloadTypeDVI4_8000 = 5 + // PayloadTypeDVI4_16000 is a payload type for IMA ADPCM audio 64 kbit/s (RFC 3551). + PayloadTypeDVI4_16000 = 6 + // PayloadTypeLPC is a payload type for Experimental Linear Predictive Coding audio 5.6 kbit/s (RFC 3551). + PayloadTypeLPC = 7 + // PayloadTypePCMA is a payload type for ITU-T G.711 PCM A-Law audio 64 kbit/s (RFC 3551). + PayloadTypePCMA = 8 + // PayloadTypeG722 is a payload type for ITU-T G.722 audio 64 kbit/s (RFC 3551). + PayloadTypeG722 = 9 + // PayloadTypeL16Stereo is a payload type for Linear PCM 16-bit Stereo audio 1411.2 kbit/s, uncompressed (RFC 3551). + PayloadTypeL16Stereo = 10 + // PayloadTypeL16Mono is a payload type for Linear PCM 16-bit audio 705.6 kbit/s, uncompressed (RFC 3551). + PayloadTypeL16Mono = 11 + // PayloadTypeQCELP is a payload type for Qualcomm Code Excited Linear Prediction (RFC 2658, RFC 3551). + PayloadTypeQCELP = 12 + // PayloadTypeCN is a payload type for Comfort noise (RFC 3389). + PayloadTypeCN = 13 + // PayloadTypeMPA is a payload type for MPEG-1 or MPEG-2 audio only (RFC 3551, RFC 2250). + PayloadTypeMPA = 14 + // PayloadTypeG728 is a payload type for ITU-T G.728 audio 16 kbit/s (RFC 3551). + PayloadTypeG728 = 15 + // PayloadTypeDVI4_11025 is a payload type for IMA ADPCM audio 44.1 kbit/s (RFC 3551). + PayloadTypeDVI4_11025 = 16 + // PayloadTypeDVI4_22050 is a payload type for IMA ADPCM audio 88.2 kbit/s (RFC 3551). + PayloadTypeDVI4_22050 = 17 + // PayloadTypeG729 is a payload type for ITU-T G.729 and G.729a audio 8 kbit/s (RFC 3551, RFC 3555). + PayloadTypeG729 = 18 +) + +// Video Payload Types as defined in https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +const ( + // PayloadTypeCELLB is a payload type for Sun CellB video (RFC 2029). + PayloadTypeCELLB = 25 + // PayloadTypeJPEG is a payload type for JPEG video (RFC 2435). + PayloadTypeJPEG = 26 + // PayloadTypeNV is a payload type for Xerox PARC's Network Video (nv, RFC 3551). + PayloadTypeNV = 28 + // PayloadTypeH261 is a payload type for ITU-T H.261 video (RFC 4587). + PayloadTypeH261 = 31 + // PayloadTypeMPV is a payload type for MPEG-1 and MPEG-2 video (RFC 2250). + PayloadTypeMPV = 32 + // PayloadTypeMP2T is a payload type for MPEG-2 transport stream (RFC 2250). + PayloadTypeMP2T = 33 + // PayloadTypeH263 is a payload type for H.263 video, first version (1996, RFC 3551, RFC 2190). + PayloadTypeH263 = 34 +) + +const ( + // PayloadTypeFirstDynamic is a first non-static payload type. + PayloadTypeFirstDynamic = 35 +) diff --git a/pkg/frame/av1.go b/pkg/frame/av1.go index 46fb831b..73edea61 100644 --- a/pkg/frame/av1.go +++ b/pkg/frame/av1.go @@ -1,44 +1,17 @@ -// Package frame provides code to construct complete media frames from packetized media +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +// Package frame is deprecated. package frame -import "github.com/pion/rtp/v2/codecs" +import ( + "github.com/pion/rtp/codecs/av1/frame" +) // AV1 represents a collection of OBUs given a stream of AV1 Packets. // Each AV1 RTP Packet is a collection of OBU Elements. Each OBU Element may be a full OBU, or just a fragment of one. // AV1 provides the tools to construct a collection of OBUs from a collection of OBU Elements. This structure // contains an internal cache and should be used for the entire RTP Stream. -type AV1 struct { - // Buffer for fragmented OBU. If ReadFrames is called on a RTP Packet - // that doesn't contain a fully formed OBU - obuBuffer []byte -} - -func (f *AV1) pushOBUElement(isFirstOBUFragment *bool, obuElement []byte, obuList [][]byte) [][]byte { - if *isFirstOBUFragment { - *isFirstOBUFragment = false - // Discard pushed because we don't have a fragment to combine it with - if f.obuBuffer == nil { - return obuList - } - obuElement = append(f.obuBuffer, obuElement...) - f.obuBuffer = nil - } - return append(obuList, obuElement) -} - -// ReadFrames processes the codecs.AV1Packet and returns fully constructed frames -func (f *AV1) ReadFrames(pkt *codecs.AV1Packet) ([][]byte, error) { - OBUs := [][]byte{} - isFirstOBUFragment := pkt.Z - - for i := range pkt.OBUElements { - OBUs = f.pushOBUElement(&isFirstOBUFragment, pkt.OBUElements[i], OBUs) - } - - if pkt.Y && len(OBUs) > 0 { - // Take copy of OBUElement that is being cached - f.obuBuffer = append(f.obuBuffer, append([]byte{}, OBUs[len(OBUs)-1]...)...) - OBUs = OBUs[:len(OBUs)-1] - } - return OBUs, nil -} +// +// Deprecated: moved into codecs/av1/frame. +type AV1 = frame.AV1 diff --git a/pkg/obu/leb128.go b/pkg/obu/leb128.go index 988a8f44..e1d9c8cb 100644 --- a/pkg/obu/leb128.go +++ b/pkg/obu/leb128.go @@ -1,66 +1,30 @@ -// Package obu implements tools for working with the "Open Bitstream Unit" -package obu +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT -import "errors" +// Package obu is deprecated. +package obu -const ( - sevenLsbBitmask = uint(0b01111111) - msbBitmask = uint(0b10000000) +import ( + "github.com/pion/rtp/codecs/av1/obu" ) // ErrFailedToReadLEB128 indicates that a buffer ended before a LEB128 value could be successfully read -var ErrFailedToReadLEB128 = errors.New("payload ended before LEB128 was finished") +// +// Deprecated: moved into codecs/av1/obu. +var ErrFailedToReadLEB128 = obu.ErrFailedToReadLEB128 // EncodeLEB128 encodes a uint as LEB128 +// +// Deprecated: moved into codecs/av1/obu. func EncodeLEB128(in uint) (out uint) { - for { - // Copy seven bits from in and discard - // what we have copied from in - out |= (in & sevenLsbBitmask) - in >>= 7 - - // If we have more bits to encode set MSB - // otherwise we are done - if in != 0 { - out |= msbBitmask - out <<= 8 - } else { - return out - } - } -} - -func decodeLEB128(in uint) (out uint) { - for { - // Take 7 LSB from in - out |= (in & sevenLsbBitmask) - - // Discard the MSB - in >>= 8 - if in == 0 { - return out - } - - out <<= 7 - } + return obu.EncodeLEB128(in) } // ReadLeb128 scans an buffer and decodes a Leb128 value. // If the end of the buffer is reached and all MSB are set // an error is returned +// +// Deprecated: moved into codecs/av1/obu. func ReadLeb128(in []byte) (uint, uint, error) { - var encodedLength uint - - for i := range in { - encodedLength |= uint(in[i]) - - if in[i]&byte(msbBitmask) == 0 { - return decodeLEB128(encodedLength), uint(i + 1), nil - } - - // Make more room for next read - encodedLength <<= 8 - } - - return 0, 0, ErrFailedToReadLEB128 + return obu.ReadLeb128(in) } diff --git a/playoutdelayextension.go b/playoutdelayextension.go new file mode 100644 index 00000000..3882731a --- /dev/null +++ b/playoutdelayextension.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "errors" +) + +const ( + playoutDelayExtensionSize = 3 + playoutDelayMaxValue = (1 << 12) - 1 +) + +var errPlayoutDelayInvalidValue = errors.New("invalid playout delay value") + +// PlayoutDelayExtension is a extension payload format in +// http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ID | len=2 | MIN delay | MAX delay | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +type PlayoutDelayExtension struct { + minDelay, maxDelay uint16 +} + +// Marshal serializes the members to buffer +func (p PlayoutDelayExtension) Marshal() ([]byte, error) { + if p.minDelay > playoutDelayMaxValue || p.maxDelay > playoutDelayMaxValue { + return nil, errPlayoutDelayInvalidValue + } + + return []byte{ + byte(p.minDelay >> 4), + byte(p.minDelay<<4) | byte(p.maxDelay>>8), + byte(p.maxDelay), + }, nil +} + +// Unmarshal parses the passed byte slice and stores the result in the members +func (p *PlayoutDelayExtension) Unmarshal(rawData []byte) error { + if len(rawData) < playoutDelayExtensionSize { + return errTooSmall + } + p.minDelay = binary.BigEndian.Uint16(rawData[0:2]) >> 4 + p.maxDelay = binary.BigEndian.Uint16(rawData[1:3]) & 0x0FFF + return nil +} diff --git a/playoutdelayextension_test.go b/playoutdelayextension_test.go new file mode 100644 index 00000000..8810466a --- /dev/null +++ b/playoutdelayextension_test.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "bytes" + "errors" + "testing" +) + +func TestPlayoutDelayExtensionTooSmall(t *testing.T) { + t1 := PlayoutDelayExtension{} + + var rawData []byte + + if err := t1.Unmarshal(rawData); !errors.Is(err, errTooSmall) { + t.Fatal("err != errTooSmall") + } +} + +func TestPlayoutDelayExtensionTooLarge(t *testing.T) { + t1 := PlayoutDelayExtension{minDelay: 1 << 12, maxDelay: 1 << 12} + + if _, err := t1.Marshal(); !errors.Is(err, errPlayoutDelayInvalidValue) { + t.Fatal("err != errPlayoutDelayInvalidValue") + } +} + +func TestPlayoutDelayExtension(t *testing.T) { + t1 := PlayoutDelayExtension{} + + rawData := []byte{ + 0x01, 0x01, 0x00, + } + + if err := t1.Unmarshal(rawData); err != nil { + t.Fatal("Unmarshal error on extension data") + } + + t2 := PlayoutDelayExtension{ + minDelay: 1 << 4, maxDelay: 1 << 8, + } + + if t1 != t2 { + t.Error("Unmarshal failed") + } + + dstData, _ := t2.Marshal() + if !bytes.Equal(dstData, rawData) { + t.Error("Marshal failed") + } +} + +func TestPlayoutDelayExtensionExtraBytes(t *testing.T) { + t1 := PlayoutDelayExtension{} + + rawData := []byte{ + 0x01, 0x01, 0x00, 0xff, 0xff, + } + + if err := t1.Unmarshal(rawData); err != nil { + t.Fatal("Unmarshal error on extension data") + } + + t2 := PlayoutDelayExtension{ + minDelay: 1 << 4, maxDelay: 1 << 8, + } + + if t1 != t2 { + t.Error("Unmarshal failed") + } +} diff --git a/rand.go b/rand.go index ee855235..3ddddd1b 100644 --- a/rand.go +++ b/rand.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/renovate.json b/renovate.json index f1614058..f1bb98c6 100644 --- a/renovate.json +++ b/renovate.json @@ -1,27 +1,6 @@ { + "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base", - ":disableDependencyDashboard" - ], - "postUpdateOptions": [ - "gomodTidy" - ], - "commitBody": "Generated by renovateBot", - "packageRules": [ - { - "matchUpdateTypes": ["minor", "patch", "pin", "digest"], - "automerge": true - }, - { - "packagePatterns": ["^golang.org/x/"], - "schedule": ["on the first day of the month"] - } - ], - "ignorePaths": [ - ".github/workflows/generate-authors.yml", - ".github/workflows/lint.yaml", - ".github/workflows/renovate-go-mod-fix.yaml", - ".github/workflows/test.yaml", - ".github/workflows/tidy-check.yaml" + "github>pion/renovate-config" ] } diff --git a/rtp.go b/rtp.go index b66b2e4b..5487232e 100644 --- a/rtp.go +++ b/rtp.go @@ -1,2 +1,5 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + // Package rtp provides RTP packetizer and depacketizer package rtp diff --git a/sequencer.go b/sequencer.go index 2b4a5072..7aa7daae 100644 --- a/sequencer.go +++ b/sequencer.go @@ -1,7 +1,9 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( - "math" "sync" ) @@ -11,11 +13,18 @@ type Sequencer interface { RollOverCount() uint64 } +// maxInitialRandomSequenceNumber is the maximum value used for the initial sequence +// number when using NewRandomSequencer(). +// This uses only half the potential sequence number space to avoid issues decrypting +// SRTP when the sequence number starts near the rollover and there is packet loss. +// See https://webrtc-review.googlesource.com/c/src/+/358360 +const maxInitialRandomSequenceNumber = 1<<15 - 1 + // NewRandomSequencer returns a new sequencer starting from a random sequence // number func NewRandomSequencer() Sequencer { return &sequencer{ - sequenceNumber: uint16(globalMathRandomGenerator.Intn(math.MaxUint16)), + sequenceNumber: uint16(globalMathRandomGenerator.Intn(maxInitialRandomSequenceNumber)), } } diff --git a/transportccextension.go b/transportccextension.go index 236af056..c2a998c8 100644 --- a/transportccextension.go +++ b/transportccextension.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/transportccextension_test.go b/transportccextension_test.go index 5eb69679..5a06f2cd 100644 --- a/transportccextension_test.go +++ b/transportccextension_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + package rtp import ( diff --git a/vlaextension.go b/vlaextension.go new file mode 100644 index 00000000..e10820a3 --- /dev/null +++ b/vlaextension.go @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "errors" + "fmt" + "strings" + + "github.com/pion/rtp/codecs/av1/obu" +) + +var ( + ErrVLATooShort = errors.New("VLA payload too short") // ErrVLATooShort is returned when payload is too short + ErrVLAInvalidStreamCount = errors.New("invalid RTP stream count in VLA") // ErrVLAInvalidStreamCount is returned when RTP stream count is invalid + ErrVLAInvalidStreamID = errors.New("invalid RTP stream ID in VLA") // ErrVLAInvalidStreamID is returned when RTP stream ID is invalid + ErrVLAInvalidSpatialID = errors.New("invalid spatial ID in VLA") // ErrVLAInvalidSpatialID is returned when spatial ID is invalid + ErrVLADuplicateSpatialID = errors.New("duplicate spatial ID in VLA") // ErrVLADuplicateSpatialID is returned when spatial ID is invalid + ErrVLAInvalidTemporalLayer = errors.New("invalid temporal layer in VLA") // ErrVLAInvalidTemporalLayer is returned when temporal layer is invalid +) + +// SpatialLayer is a spatial layer in VLA. +type SpatialLayer struct { + RTPStreamID int + SpatialID int + TargetBitrates []int // target bitrates per temporal layer + + // Following members are valid only when HasResolutionAndFramerate is true + Width int + Height int + Framerate int +} + +// VLA is a Video Layer Allocation (VLA) extension. +// See https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/video-layers-allocation00 +type VLA struct { + RTPStreamID int // 0-origin RTP stream ID (RID) this allocation is sent on (0..3) + RTPStreamCount int // Number of RTP streams (1..4) + ActiveSpatialLayer []SpatialLayer + HasResolutionAndFramerate bool +} + +type vlaMarshalingContext struct { + slMBs [4]uint8 + sls [4][4]*SpatialLayer + commonSLBM uint8 + encodedTargetBitrates [][]byte + requiredLen int +} + +func (v VLA) preprocessForMashaling(ctx *vlaMarshalingContext) error { + for i := 0; i < len(v.ActiveSpatialLayer); i++ { + sl := v.ActiveSpatialLayer[i] + if sl.RTPStreamID < 0 || sl.RTPStreamID >= v.RTPStreamCount { + return fmt.Errorf("invalid RTP streamID %d:%w", sl.RTPStreamID, ErrVLAInvalidStreamID) + } + if sl.SpatialID < 0 || sl.SpatialID >= 4 { + return fmt.Errorf("invalid spatial ID %d: %w", sl.SpatialID, ErrVLAInvalidSpatialID) + } + if len(sl.TargetBitrates) == 0 || len(sl.TargetBitrates) > 4 { + return fmt.Errorf("invalid temporal layer count %d: %w", len(sl.TargetBitrates), ErrVLAInvalidTemporalLayer) + } + ctx.slMBs[sl.RTPStreamID] |= 1 << sl.SpatialID + if ctx.sls[sl.RTPStreamID][sl.SpatialID] != nil { + return fmt.Errorf("duplicate spatial layer: %w", ErrVLADuplicateSpatialID) + } + ctx.sls[sl.RTPStreamID][sl.SpatialID] = &sl + } + return nil +} + +func (v VLA) encodeTargetBitrates(ctx *vlaMarshalingContext) { + for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if sl := ctx.sls[rtpStreamID][spatialID]; sl != nil { + for _, kbps := range sl.TargetBitrates { + leb128 := obu.WriteToLeb128(uint(kbps)) + ctx.encodedTargetBitrates = append(ctx.encodedTargetBitrates, leb128) + ctx.requiredLen += len(leb128) + } + } + } + } +} + +func (v VLA) analyzeVLAForMarshaling() (*vlaMarshalingContext, error) { + // Validate RTPStreamCount + if v.RTPStreamCount <= 0 || v.RTPStreamCount > 4 { + return nil, ErrVLAInvalidStreamCount + } + // Validate RTPStreamID + if v.RTPStreamID < 0 || v.RTPStreamID >= v.RTPStreamCount { + return nil, ErrVLAInvalidStreamID + } + + ctx := &vlaMarshalingContext{} + err := v.preprocessForMashaling(ctx) + if err != nil { + return nil, err + } + + ctx.commonSLBM = commonSLBMValues(ctx.slMBs[:]) + + // RID, NS, sl_bm fields + if ctx.commonSLBM != 0 { + ctx.requiredLen = 1 + } else { + ctx.requiredLen = 3 + } + + // #tl fields + ctx.requiredLen += (len(v.ActiveSpatialLayer)-1)/4 + 1 + + v.encodeTargetBitrates(ctx) + + if v.HasResolutionAndFramerate { + ctx.requiredLen += len(v.ActiveSpatialLayer) * 5 + } + + return ctx, nil +} + +// Marshal encodes VLA into a byte slice. +func (v VLA) Marshal() ([]byte, error) { + ctx, err := v.analyzeVLAForMarshaling() + if err != nil { + return nil, err + } + + payload := make([]byte, ctx.requiredLen) + offset := 0 + + // RID, NS, sl_bm fields + payload[offset] = byte(v.RTPStreamID<<6) | byte(v.RTPStreamCount-1)<<4 | ctx.commonSLBM + + if ctx.commonSLBM == 0 { + offset++ + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + if streamID%2 == 0 { + payload[offset+streamID/2] |= ctx.slMBs[streamID] << 4 + } else { + payload[offset+streamID/2] |= ctx.slMBs[streamID] + } + } + offset += (v.RTPStreamCount - 1) / 2 + } + + // #tl fields + offset++ + var temporalLayerIndex int + for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if sl := ctx.sls[rtpStreamID][spatialID]; sl != nil { + if temporalLayerIndex >= 4 { + temporalLayerIndex = 0 + offset++ + } + payload[offset] |= byte(len(sl.TargetBitrates)-1) << (2 * (3 - temporalLayerIndex)) + temporalLayerIndex++ + } + } + } + + // Target bitrate fields + offset++ + for _, encodedKbps := range ctx.encodedTargetBitrates { + encodedSize := len(encodedKbps) + copy(payload[offset:], encodedKbps) + offset += encodedSize + } + + // Resolution & framerate fields + if v.HasResolutionAndFramerate { + for _, sl := range v.ActiveSpatialLayer { + binary.BigEndian.PutUint16(payload[offset+0:], uint16(sl.Width-1)) + binary.BigEndian.PutUint16(payload[offset+2:], uint16(sl.Height-1)) + payload[offset+4] = byte(sl.Framerate) + offset += 5 + } + } + + return payload, nil +} + +func commonSLBMValues(slMBs []uint8) uint8 { + var common uint8 + for i := 0; i < len(slMBs); i++ { + if slMBs[i] == 0 { + continue + } + if common == 0 { + common = slMBs[i] + continue + } + if slMBs[i] != common { + return 0 + } + } + return common +} + +type vlaUnmarshalingContext struct { + payload []byte + offset int + slBMField uint8 + slBMs [4]uint8 +} + +func (ctx *vlaUnmarshalingContext) checkRemainingLen(requiredLen int) bool { + return len(ctx.payload)-ctx.offset >= requiredLen +} + +func (v *VLA) unmarshalSpatialLayers(ctx *vlaUnmarshalingContext) error { + if !ctx.checkRemainingLen(1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + v.RTPStreamID = int(ctx.payload[ctx.offset] >> 6 & 0b11) + v.RTPStreamCount = int(ctx.payload[ctx.offset]>>4&0b11) + 1 + + // sl_bm fields + ctx.slBMField = ctx.payload[ctx.offset] & 0b1111 + ctx.offset++ + + if ctx.slBMField != 0 { + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + ctx.slBMs[streamID] = ctx.slBMField + } + } else { + if !ctx.checkRemainingLen((v.RTPStreamCount-1)/2 + 1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + // slX_bm fields + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + var bm uint8 + if streamID%2 == 0 { + bm = ctx.payload[ctx.offset+streamID/2] >> 4 & 0b1111 + } else { + bm = ctx.payload[ctx.offset+streamID/2] & 0b1111 + } + ctx.slBMs[streamID] = bm + } + ctx.offset += 1 + (v.RTPStreamCount-1)/2 + } + + return nil +} + +func (v *VLA) unmarshalTemporalLayers(ctx *vlaUnmarshalingContext) error { + if !ctx.checkRemainingLen(1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + + var temporalLayerIndex int + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if ctx.slBMs[streamID]&(1<= 4 { + temporalLayerIndex = 0 + ctx.offset++ + if !ctx.checkRemainingLen(1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + } + tlCount := int(ctx.payload[ctx.offset]>>(2*(3-temporalLayerIndex))&0b11) + 1 + temporalLayerIndex++ + sl := SpatialLayer{ + RTPStreamID: streamID, + SpatialID: spatialID, + TargetBitrates: make([]int, tlCount), + } + v.ActiveSpatialLayer = append(v.ActiveSpatialLayer, sl) + } + } + ctx.offset++ + + // target bitrates + for i, sl := range v.ActiveSpatialLayer { + for j := range sl.TargetBitrates { + kbps, n, err := obu.ReadLeb128(ctx.payload[ctx.offset:]) + if err != nil { + return err + } + if !ctx.checkRemainingLen(int(n)) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + v.ActiveSpatialLayer[i].TargetBitrates[j] = int(kbps) + ctx.offset += int(n) + } + } + + return nil +} + +func (v *VLA) unmarshalResolutionAndFramerate(ctx *vlaUnmarshalingContext) error { + if !ctx.checkRemainingLen(len(v.ActiveSpatialLayer) * 5) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + + v.HasResolutionAndFramerate = true + + for i := range v.ActiveSpatialLayer { + v.ActiveSpatialLayer[i].Width = int(binary.BigEndian.Uint16(ctx.payload[ctx.offset+0:])) + 1 + v.ActiveSpatialLayer[i].Height = int(binary.BigEndian.Uint16(ctx.payload[ctx.offset+2:])) + 1 + v.ActiveSpatialLayer[i].Framerate = int(ctx.payload[ctx.offset+4]) + ctx.offset += 5 + } + + return nil +} + +// Unmarshal decodes VLA from a byte slice. +func (v *VLA) Unmarshal(payload []byte) (int, error) { + ctx := &vlaUnmarshalingContext{ + payload: payload, + } + + err := v.unmarshalSpatialLayers(ctx) + if err != nil { + return ctx.offset, err + } + + // #tl fields (build the list ActiveSpatialLayer at the same time) + err = v.unmarshalTemporalLayers(ctx) + if err != nil { + return ctx.offset, err + } + + if len(ctx.payload) == ctx.offset { + return ctx.offset, nil + } + + // resolution & framerate (optional) + err = v.unmarshalResolutionAndFramerate(ctx) + if err != nil { + return ctx.offset, err + } + + return ctx.offset, nil +} + +// String makes VLA printable. +func (v VLA) String() string { + out := fmt.Sprintf("RID:%d,RTPStreamCount:%d", v.RTPStreamID, v.RTPStreamCount) + var slOut []string + for _, sl := range v.ActiveSpatialLayer { + out2 := fmt.Sprintf("RTPStreamID:%d", sl.RTPStreamID) + out2 += fmt.Sprintf(",TargetBitrates:%v", sl.TargetBitrates) + if v.HasResolutionAndFramerate { + out2 += fmt.Sprintf(",Resolution:(%d,%d)", sl.Width, sl.Height) + out2 += fmt.Sprintf(",Framerate:%d", sl.Framerate) + } + slOut = append(slOut, out2) + } + out += fmt.Sprintf(",ActiveSpatialLayers:{%s}", strings.Join(slOut, ",")) + return out +} diff --git a/vlaextension_test.go b/vlaextension_test.go new file mode 100644 index 00000000..b9b80669 --- /dev/null +++ b/vlaextension_test.go @@ -0,0 +1,532 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "bytes" + "encoding/hex" + "errors" + "reflect" + "testing" +) + +func TestVLAMarshal(t *testing.T) { + requireNoError := func(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } + } + + t.Run("3 streams no resolution and framerate", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 3, + ActiveSpatialLayer: []SpatialLayer{ + { + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{150}, + }, + { + RTPStreamID: 1, + SpatialID: 0, + TargetBitrates: []int{240, 400}, + }, + { + RTPStreamID: 2, + SpatialID: 0, + TargetBitrates: []int{720, 1200}, + }, + }, + } + + bytesActual, err := vla.Marshal() + requireNoError(t, err) + bytesExpected, err := hex.DecodeString("21149601f0019003d005b009") + requireNoError(t, err) + if !bytes.Equal(bytesExpected, bytesActual) { + t.Fatalf("expected %s, actual %s", hex.EncodeToString(bytesExpected), hex.EncodeToString(bytesActual)) + } + }) + + t.Run("3 streams with resolution and framerate", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 2, + RTPStreamCount: 3, + ActiveSpatialLayer: []SpatialLayer{ + { + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{150}, + Width: 320, + Height: 180, + Framerate: 30, + }, + { + RTPStreamID: 1, + SpatialID: 0, + TargetBitrates: []int{240, 400}, + Width: 640, + Height: 360, + Framerate: 30, + }, + { + RTPStreamID: 2, + SpatialID: 0, + TargetBitrates: []int{720, 1200}, + Width: 1280, + Height: 720, + Framerate: 30, + }, + }, + HasResolutionAndFramerate: true, + } + + bytesActual, err := vla.Marshal() + requireNoError(t, err) + bytesExpected, err := hex.DecodeString("a1149601f0019003d005b009013f00b31e027f01671e04ff02cf1e") + requireNoError(t, err) + if !bytes.Equal(bytesExpected, bytesActual) { + t.Fatalf("expected %s, actual %s", hex.EncodeToString(bytesExpected), hex.EncodeToString(bytesActual)) + } + }) + + t.Run("Negative RTPStreamCount", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: -1, + ActiveSpatialLayer: []SpatialLayer{}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamCount) { + t.Fatal("expected ErrVLAInvalidRTPStreamCount") + } + }) + + t.Run("RTPStreamCount too large", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 5, + ActiveSpatialLayer: []SpatialLayer{{}, {}, {}, {}, {}}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamCount) { + t.Fatal("expected ErrVLAInvalidRTPStreamCount") + } + }) + + t.Run("Negative RTPStreamID", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: -1, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{}}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidRTPStreamID, actual %v", err) + } + }) + + t.Run("RTPStreamID to large", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 1, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{}}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidRTPStreamID: %v", err) + } + }) + + t.Run("Invalid stream ID in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: -1, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidStreamID: %v", err) + } + vla = &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 1, + }}, + } + _, err = vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidStreamID: %v", err) + } + }) + + t.Run("Invalid spatial ID in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: -1, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidSpatialID) { + t.Fatalf("expected ErrVLAInvalidSpatialID: %v", err) + } + vla = &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 5, + }}, + } + _, err = vla.Marshal() + if !errors.Is(err, ErrVLAInvalidSpatialID) { + t.Fatalf("expected ErrVLAInvalidSpatialID: %v", err) + } + }) + + t.Run("Invalid temporal layer in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{}, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidTemporalLayer) { + t.Fatalf("expected ErrVLAInvalidTemporalLayer: %v", err) + } + vla = &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{100, 200, 300, 400, 500}, + }}, + } + _, err = vla.Marshal() + if !errors.Is(err, ErrVLAInvalidTemporalLayer) { + t.Fatalf("expected ErrVLAInvalidTemporalLayer: %v", err) + } + }) + + t.Run("Duplicate spatial ID in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{100}, + }, { + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{200}, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLADuplicateSpatialID) { + t.Fatalf("expected ErrVLADuplicateSpatialID: %v", err) + } + }) +} + +func TestVLAUnmarshal(t *testing.T) { + requireEqualInt := func(t *testing.T, expected, actual int) { + if expected != actual { + t.Fatalf("expected %d, actual %d", expected, actual) + } + } + requireNoError := func(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } + } + requireTrue := func(t *testing.T, val bool) { + if !val { + t.Fatal("expected true") + } + } + requireFalse := func(t *testing.T, val bool) { + if val { + t.Fatal("expected false") + } + } + + t.Run("3 streams no resolution and framerate", func(t *testing.T) { + // two layer ("low", "high") + b, err := hex.DecodeString("21149601f0019003d005b009") + requireNoError(t, err) + if err != nil { + t.Fatal("failed to decode input data") + } + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 0, vla.RTPStreamID) + requireEqualInt(t, 3, vla.RTPStreamCount) + requireEqualInt(t, 3, len(vla.ActiveSpatialLayer)) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 150, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 1, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 240, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 400, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireFalse(t, vla.HasResolutionAndFramerate) + + requireEqualInt(t, 2, vla.ActiveSpatialLayer[2].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[2].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[2].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[2].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[2].TargetBitrates[1]) + }) + + t.Run("3 streams with resolution and framerate", func(t *testing.T) { + b, err := hex.DecodeString("a1149601f0019003d005b009013f00b31e027f01671e04ff02cf1e") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 2, vla.RTPStreamID) + requireEqualInt(t, 3, vla.RTPStreamCount) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 150, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 1, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 240, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 400, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireEqualInt(t, 2, vla.ActiveSpatialLayer[2].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[2].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[2].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[2].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[2].TargetBitrates[1]) + + requireTrue(t, vla.HasResolutionAndFramerate) + + requireEqualInt(t, 320, vla.ActiveSpatialLayer[0].Width) + requireEqualInt(t, 180, vla.ActiveSpatialLayer[0].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[0].Framerate) + requireEqualInt(t, 640, vla.ActiveSpatialLayer[1].Width) + requireEqualInt(t, 360, vla.ActiveSpatialLayer[1].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[1].Framerate) + requireEqualInt(t, 1280, vla.ActiveSpatialLayer[2].Width) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[2].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[2].Framerate) + }) + + t.Run("2 streams", func(t *testing.T) { + // two layer ("low", "high") + b, err := hex.DecodeString("1110c801d005b009") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 0, vla.RTPStreamID) + requireEqualInt(t, 2, vla.RTPStreamCount) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer)) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 200, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 1, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireFalse(t, vla.HasResolutionAndFramerate) + }) + + t.Run("3 streams mid paused with resolution and framerate", func(t *testing.T) { + b, err := hex.DecodeString("601010109601d005b009013f00b31e04ff02cf1e") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 1, vla.RTPStreamID) + requireEqualInt(t, 3, vla.RTPStreamCount) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 150, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 2, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireTrue(t, vla.HasResolutionAndFramerate) + + requireEqualInt(t, 320, vla.ActiveSpatialLayer[0].Width) + requireEqualInt(t, 180, vla.ActiveSpatialLayer[0].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[0].Framerate) + requireEqualInt(t, 1280, vla.ActiveSpatialLayer[1].Width) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[1].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[1].Framerate) + }) + + t.Run("extra 1", func(t *testing.T) { + b, err := hex.DecodeString("a0001040ac02f403") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + }) + + t.Run("extra 2", func(t *testing.T) { + b, err := hex.DecodeString("a00010409405cc08") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + }) +} + +func TestVLAMarshalThenUnmarshal(t *testing.T) { + requireEqualInt := func(t *testing.T, expected, actual int) { + if expected != actual { + t.Fatalf("expected %d, actual %d", expected, actual) + } + } + requireNoError := func(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } + } + + t.Run("multiple spatial layers", func(t *testing.T) { + var spatialLayers []SpatialLayer + for streamID := 0; streamID < 3; streamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + spatialLayers = append(spatialLayers, SpatialLayer{ + RTPStreamID: streamID, + SpatialID: spatialID, + TargetBitrates: []int{150, 200}, + Width: 320, + Height: 180, + Framerate: 30, + }) + } + } + + vla0 := &VLA{ + RTPStreamID: 2, + RTPStreamCount: 3, + ActiveSpatialLayer: spatialLayers, + HasResolutionAndFramerate: true, + } + + b, err := vla0.Marshal() + requireNoError(t, err) + + vla1 := &VLA{} + n, err := vla1.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + if !reflect.DeepEqual(vla0, vla1) { + t.Fatalf("expected %v, actual %v", vla0, vla1) + } + }) + + t.Run("different spatial layer bitmasks", func(t *testing.T) { + var spatialLayers []SpatialLayer + for streamID := 0; streamID < 4; streamID++ { + for spatialID := 0; spatialID < streamID+1; spatialID++ { + spatialLayers = append(spatialLayers, SpatialLayer{ + RTPStreamID: streamID, + SpatialID: spatialID, + TargetBitrates: []int{150, 200}, + Width: 320, + Height: 180, + Framerate: 30, + }) + } + } + + vla0 := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 4, + ActiveSpatialLayer: spatialLayers, + HasResolutionAndFramerate: true, + } + + b, err := vla0.Marshal() + requireNoError(t, err) + if b[0]&0x0f != 0 { + t.Error("expects sl_bm to be 0") + } + if b[1] != 0x13 { + t.Error("expects sl0_bm,sl1_bm to be b0001,b0011") + } + if b[2] != 0x7f { + t.Error("expects sl1_bm,sl2_bm to be b0111,b1111") + } + t.Logf("b: %s", hex.EncodeToString(b)) + + vla1 := &VLA{} + n, err := vla1.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + if !reflect.DeepEqual(vla0, vla1) { + t.Fatalf("expected %v, actual %v", vla0, vla1) + } + }) +} + +func FuzzVLAUnmarshal(f *testing.F) { + f.Add([]byte{0}) + f.Add([]byte("70")) + + f.Fuzz(func(t *testing.T, data []byte) { + vla := &VLA{} + _, err := vla.Unmarshal(data) + if err != nil { + t.Skip() // If the function returns an error, we skip the test case + } + }) +}