diff --git a/core/webrtc/source.go b/core/webrtc/source.go new file mode 100644 index 0000000..fc61e44 --- /dev/null +++ b/core/webrtc/source.go @@ -0,0 +1,133 @@ +package webrtc + +import ( + "fmt" + "net" + "sync" + + "github.com/pion/rtp" +) + +// Source reads RTP packets from a local UDP socket and fans them out to +// subscribed peers via per-subscriber buffered channels. +type Source struct { + id string + conn *net.UDPConn + + mu sync.Mutex + subscribers map[chan *rtp.Packet]struct{} + started bool + closed bool + done chan struct{} +} + +// NewSource binds a UDP socket on 127.0.0.1:port. Pass port=0 to let the OS +// assign an ephemeral port (useful for tests). +func NewSource(streamID string, port int) (*Source, error) { + addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port} + conn, err := net.ListenUDP("udp4", addr) + if err != nil { + return nil, fmt.Errorf("webrtc: listen udp: %w", err) + } + return &Source{ + id: streamID, + conn: conn, + subscribers: make(map[chan *rtp.Packet]struct{}), + done: make(chan struct{}), + }, nil +} + +// ID returns the registered stream identifier. +func (s *Source) ID() string { return s.id } + +// LocalAddr returns the UDP address the source is listening on. +func (s *Source) LocalAddr() *net.UDPAddr { + return s.conn.LocalAddr().(*net.UDPAddr) +} + +// Subscribe returns a new buffered channel that receives every RTP packet +// read from the UDP socket. bufDepth is the channel buffer size; when full, +// packets are dropped (preventing a slow subscriber from back-pressuring +// the reader). +func (s *Source) Subscribe(bufDepth int) chan *rtp.Packet { + ch := make(chan *rtp.Packet, bufDepth) + s.mu.Lock() + s.subscribers[ch] = struct{}{} + s.mu.Unlock() + return ch +} + +// Unsubscribe removes ch from the subscriber set and closes it. +func (s *Source) Unsubscribe(ch chan *rtp.Packet) { + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.subscribers[ch]; ok { + delete(s.subscribers, ch) + close(ch) + } +} + +// Start begins the RTP reader goroutine. Safe to call once; subsequent calls +// are no-ops. +func (s *Source) Start() { + s.mu.Lock() + if s.started || s.closed { + s.mu.Unlock() + return + } + s.started = true + s.mu.Unlock() + + go s.readLoop() +} + +func (s *Source) readLoop() { + buf := make([]byte, 1500) // MTU-sized; RTP over UDP should fit + for { + select { + case <-s.done: + return + default: + } + + n, _, err := s.conn.ReadFromUDP(buf) + if err != nil { + // Socket closed or error — exit the loop. + return + } + + pkt := &rtp.Packet{} + if err := pkt.Unmarshal(buf[:n]); err != nil { + // Malformed packet; skip without crashing. + continue + } + + s.mu.Lock() + for ch := range s.subscribers { + select { + case ch <- pkt: + default: + // Subscriber full — drop to protect the reader. + } + } + s.mu.Unlock() + } +} + +// Close stops the reader goroutine, closes the UDP socket, and closes every +// subscriber channel. +func (s *Source) Close() error { + s.mu.Lock() + if s.closed { + s.mu.Unlock() + return nil + } + s.closed = true + close(s.done) + for ch := range s.subscribers { + delete(s.subscribers, ch) + close(ch) + } + s.mu.Unlock() + return s.conn.Close() +} diff --git a/core/webrtc/source_test.go b/core/webrtc/source_test.go new file mode 100644 index 0000000..c26def0 --- /dev/null +++ b/core/webrtc/source_test.go @@ -0,0 +1,129 @@ +package webrtc + +import ( + "net" + "testing" + "time" + + "github.com/pion/rtp" +) + +func TestSource_ID(t *testing.T) { + s, err := NewSource("streamA", 0) // 0 = ephemeral port + if err != nil { + t.Fatalf("NewSource: %v", err) + } + defer s.Close() + + if s.ID() != "streamA" { + t.Errorf("ID() = %q, want streamA", s.ID()) + } +} + +func TestSource_ReceiveAndFanout(t *testing.T) { + s, err := NewSource("streamA", 0) + if err != nil { + t.Fatalf("NewSource: %v", err) + } + defer s.Close() + + // Subscribe before sending. + sub := s.Subscribe(16) // buffer depth 16 + defer s.Unsubscribe(sub) + + s.Start() + + // Build and send a minimal RTP packet to the source's UDP port. + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + PayloadType: 96, + SequenceNumber: 1, + Timestamp: 1000, + SSRC: 0xDEADBEEF, + }, + Payload: []byte{0x01, 0x02, 0x03, 0x04}, + } + raw, err := pkt.Marshal() + if err != nil { + t.Fatalf("pkt.Marshal: %v", err) + } + + conn, err := net.Dial("udp", s.LocalAddr().String()) + if err != nil { + t.Fatalf("net.Dial: %v", err) + } + defer conn.Close() + + if _, err := conn.Write(raw); err != nil { + t.Fatalf("conn.Write: %v", err) + } + + select { + case got := <-sub: + if got.SSRC != 0xDEADBEEF { + t.Errorf("received SSRC = %x, want DEADBEEF", got.SSRC) + } + if got.SequenceNumber != 1 { + t.Errorf("received SeqNum = %d, want 1", got.SequenceNumber) + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for RTP packet on subscriber channel") + } +} + +func TestSource_MultipleSubscribers(t *testing.T) { + s, err := NewSource("streamA", 0) + if err != nil { + t.Fatalf("NewSource: %v", err) + } + defer s.Close() + + subs := []chan *rtp.Packet{ + s.Subscribe(8), + s.Subscribe(8), + s.Subscribe(8), + } + for _, sub := range subs { + defer s.Unsubscribe(sub) + } + + s.Start() + + raw, _ := (&rtp.Packet{ + Header: rtp.Header{Version: 2, PayloadType: 96, SequenceNumber: 42, SSRC: 1}, + Payload: []byte{0xAA}, + }).Marshal() + conn, _ := net.Dial("udp", s.LocalAddr().String()) + defer conn.Close() + _, _ = conn.Write(raw) + + for i, sub := range subs { + select { + case got := <-sub: + if got.SequenceNumber != 42 { + t.Errorf("sub %d got seq %d, want 42", i, got.SequenceNumber) + } + case <-time.After(2 * time.Second): + t.Errorf("sub %d timed out", i) + } + } +} + +func TestSource_UnsubscribeStopsDelivery(t *testing.T) { + s, _ := NewSource("streamA", 0) + defer s.Close() + sub := s.Subscribe(8) + s.Start() + s.Unsubscribe(sub) + + // After Unsubscribe, the channel should be closed. + select { + case _, ok := <-sub: + if ok { + t.Error("expected channel closed after Unsubscribe, got value") + } + case <-time.After(500 * time.Millisecond): + t.Error("timed out waiting for channel close") + } +} diff --git a/go.mod b/go.mod index c656b0e..fbd79ba 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/lithammer/shortuuid/v4 v4.0.0 github.com/mattn/go-isatty v0.0.20 github.com/minio/minio-go/v7 v7.0.70 + github.com/pion/rtp v1.10.1 github.com/prep/average v0.0.0-20200506183628-d26c465f48c3 github.com/prometheus/client_golang v1.19.1 github.com/puzpuzpuz/xsync/v3 v3.1.0 @@ -71,6 +72,7 @@ require ( github.com/miekg/dns v1.1.59 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pion/randutil v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/go.sum b/go.sum index c6085bc..fb52ee4 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,10 @@ github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJn github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA= +github.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/vendor/github.com/pion/randutil/.travis.yml b/vendor/github.com/pion/randutil/.travis.yml new file mode 100644 index 0000000..f04a896 --- /dev/null +++ b/vendor/github.com/pion/randutil/.travis.yml @@ -0,0 +1,142 @@ +# +# 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. +# + +dist: bionic +language: go + + +branches: + only: + - master + +env: + global: + - GO111MODULE=on + - GOLANGCI_LINT_VERSION=1.19.1 + +cache: + directories: + - ${HOME}/.cache/go-build + - ${GOPATH}/pkg/mod + npm: true + yarn: true + +_lint_job: &lint_job + env: CACHE_NAME=lint + before_install: + - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi + install: skip + before_script: + - | + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ + | bash -s - -b $GOPATH/bin v${GOLANGCI_LINT_VERSION} + script: + - bash .github/assert-contributors.sh + - bash .github/lint-disallowed-functions-in-library.sh + - bash .github/lint-commit-message.sh + - bash .github/lint-filename.sh + - golangci-lint run ./... +_test_job: &test_job + env: CACHE_NAME=test + before_install: + - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi + - go mod download + install: + - go build ./... + script: + # If you want to specify repository specific test packages rule, + # add `TEST_PACKAGES=$(command to list test target packages)` to .github/.ci.conf + - testpkgs=${TEST_PACKAGES:-$(go list ./... | grep -v examples)} + - coverpkgs=$(echo "${testpkgs}" | paste -s -d ',') + - | + go test \ + -coverpkg=${coverpkgs} -coverprofile=cover.out -covermode=atomic \ + ${TEST_EXTRA_ARGS:-} \ + -v -race ${testpkgs} + - if [ -n "${TEST_HOOK}" ]; then ${TEST_HOOK}; fi + after_success: + - travis_retry bash <(curl -s https://codecov.io/bash) -c -F go +_test_i386_job: &test_i386_job + env: CACHE_NAME=test386 + services: docker + before_install: + - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi + script: + - testpkgs=${TEST_PACKAGES:-$(go list ./... | grep -v examples)} + - | + docker run \ + -u $(id -u):$(id -g) \ + -e "GO111MODULE=on" \ + -e "CGO_ENABLED=0" \ + -v ${PWD}:/go/src/github.com/pion/$(basename ${PWD}) \ + -v ${HOME}/gopath/pkg/mod:/go/pkg/mod \ + -v ${HOME}/.cache/go-build:/.cache/go-build \ + -w /go/src/github.com/pion/$(basename ${PWD}) \ + -it i386/golang:${GO_VERSION}-alpine \ + /usr/local/go/bin/go test \ + ${TEST_EXTRA_ARGS:-} \ + -v ${testpkgs} +_test_wasm_job: &test_wasm_job + env: CACHE_NAME=wasm + language: node_js + node_js: 12 + before_install: + - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi + - if ${SKIP_WASM_TEST:-false}; then exit 0; fi + install: + # Manually download and install Go instead of using gimme. + # It looks like gimme Go causes some errors on go-test for Wasm. + - curl -sSfL https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar -C ~ -xzf - + - export GOROOT=${HOME}/go + - export PATH=${GOROOT}/bin:${PATH} + - yarn install + - export GO_JS_WASM_EXEC=${GO_JS_WASM_EXEC:-${GOROOT}/misc/wasm/go_js_wasm_exec} + script: + - testpkgs=${TEST_PACKAGES:-$(go list ./... | grep -v examples)} + - coverpkgs=$(echo "${testpkgs}" | paste -s -d ',') + - | + GOOS=js GOARCH=wasm go test \ + -coverpkg=${coverpkgs} -coverprofile=cover.out -covermode=atomic \ + -exec="${GO_JS_WASM_EXEC}" \ + -v ${testpkgs} + after_success: + - travis_retry bash <(curl -s https://codecov.io/bash) -c -F wasm + +jobs: + include: + - <<: *lint_job + name: Lint 1.14 + go: 1.14 + - <<: *test_job + name: Test 1.13 + go: 1.13 + - <<: *test_job + name: Test 1.14 + go: 1.14 + - <<: *test_i386_job + name: Test i386 1.13 + env: GO_VERSION=1.13 + go: 1.14 # Go version for host environment only for `go list`. + # All tests are done on the version specified by GO_VERSION. + - <<: *test_i386_job + name: Test i386 1.14 + env: GO_VERSION=1.14 + go: 1.14 # Go version for host environment only for `go list`. + # All tests are done on the version specified by GO_VERSION. + - <<: *test_wasm_job + name: Test WASM 1.13 + env: GO_VERSION=1.13 + - <<: *test_wasm_job + name: Test WASM 1.14 + env: GO_VERSION=1.14 + +notifications: + email: false diff --git a/vendor/github.com/pion/randutil/LICENSE b/vendor/github.com/pion/randutil/LICENSE new file mode 100644 index 0000000..5b5a394 --- /dev/null +++ b/vendor/github.com/pion/randutil/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Pion + +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/vendor/github.com/pion/randutil/README.md b/vendor/github.com/pion/randutil/README.md new file mode 100644 index 0000000..94baf77 --- /dev/null +++ b/vendor/github.com/pion/randutil/README.md @@ -0,0 +1,14 @@ +# randutil +Helper library for cryptographic and mathmatical randoms + +### Community +Pion has an active community on the [Golang Slack](https://invite.slack.golangbridge.org/). Sign up and join the **#pion** channel for discussions and support. You can also use [Pion mailing list](https://groups.google.com/forum/#!forum/pion). + +We are always looking to support **your projects**. Please reach out if you have something to build! + +If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) + +### Contributing +Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible: + +* [Atsushi Watanabe](https://github.com/at-wat) - *Original Author* diff --git a/vendor/github.com/pion/randutil/codecov.yml b/vendor/github.com/pion/randutil/codecov.yml new file mode 100644 index 0000000..085200a --- /dev/null +++ b/vendor/github.com/pion/randutil/codecov.yml @@ -0,0 +1,20 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# + +coverage: + status: + project: + default: + # Allow decreasing 2% of total coverage to avoid noise. + threshold: 2% + patch: + default: + target: 70% + only_pulls: true + +ignore: + - "examples/*" + - "examples/**/*" diff --git a/vendor/github.com/pion/randutil/crypto.go b/vendor/github.com/pion/randutil/crypto.go new file mode 100644 index 0000000..d10df98 --- /dev/null +++ b/vendor/github.com/pion/randutil/crypto.go @@ -0,0 +1,30 @@ +package randutil + +import ( + crand "crypto/rand" + "encoding/binary" + "math/big" +) + +// GenerateCryptoRandomString generates a random string for cryptographic usage. +func GenerateCryptoRandomString(n int, runes string) (string, error) { + letters := []rune(runes) + b := make([]rune, n) + for i := range b { + v, err := crand.Int(crand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + b[i] = letters[v.Int64()] + } + return string(b), nil +} + +// CryptoUint64 returns cryptographic random uint64. +func CryptoUint64() (uint64, error) { + var v uint64 + if err := binary.Read(crand.Reader, binary.LittleEndian, &v); err != nil { + return 0, err + } + return v, nil +} diff --git a/vendor/github.com/pion/randutil/math.go b/vendor/github.com/pion/randutil/math.go new file mode 100644 index 0000000..fbbf460 --- /dev/null +++ b/vendor/github.com/pion/randutil/math.go @@ -0,0 +1,72 @@ +package randutil + +import ( + mrand "math/rand" // used for non-crypto unique ID and random port selection + "sync" + "time" +) + +// MathRandomGenerator is a random generator for non-crypto usage. +type MathRandomGenerator interface { + // Intn returns random integer within [0:n). + Intn(n int) int + + // Uint32 returns random 32-bit unsigned integer. + Uint32() uint32 + + // Uint64 returns random 64-bit unsigned integer. + Uint64() uint64 + + // GenerateString returns ranom string using given set of runes. + // It can be used for generating unique ID to avoid name collision. + // + // Caution: DO NOT use this for cryptographic usage. + GenerateString(n int, runes string) string +} + +type mathRandomGenerator struct { + r *mrand.Rand + mu sync.Mutex +} + +// NewMathRandomGenerator creates new mathmatical random generator. +// Random generator is seeded by crypto random. +func NewMathRandomGenerator() MathRandomGenerator { + seed, err := CryptoUint64() + if err != nil { + // crypto/rand is unavailable. Fallback to seed by time. + seed = uint64(time.Now().UnixNano()) + } + + return &mathRandomGenerator{r: mrand.New(mrand.NewSource(int64(seed)))} +} + +func (g *mathRandomGenerator) Intn(n int) int { + g.mu.Lock() + v := g.r.Intn(n) + g.mu.Unlock() + return v +} + +func (g *mathRandomGenerator) Uint32() uint32 { + g.mu.Lock() + v := g.r.Uint32() + g.mu.Unlock() + return v +} + +func (g *mathRandomGenerator) Uint64() uint64 { + g.mu.Lock() + v := g.r.Uint64() + g.mu.Unlock() + return v +} + +func (g *mathRandomGenerator) GenerateString(n int, runes string) string { + letters := []rune(runes) + b := make([]rune, n) + for i := range b { + b[i] = letters[g.Intn(len(letters))] + } + return string(b) +} diff --git a/vendor/github.com/pion/randutil/renovate.json b/vendor/github.com/pion/randutil/renovate.json new file mode 100644 index 0000000..4400fd9 --- /dev/null +++ b/vendor/github.com/pion/randutil/renovate.json @@ -0,0 +1,15 @@ +{ + "extends": [ + "config:base" + ], + "postUpdateOptions": [ + "gomodTidy" + ], + "commitBody": "Generated by renovateBot", + "packageRules": [ + { + "packagePatterns": ["^golang.org/x/"], + "schedule": ["on the first day of the month"] + } + ] +} diff --git a/vendor/github.com/pion/rtp/.gitignore b/vendor/github.com/pion/rtp/.gitignore new file mode 100644 index 0000000..2394557 --- /dev/null +++ b/vendor/github.com/pion/rtp/.gitignore @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 The Pion community +# SPDX-License-Identifier: MIT + +### JetBrains IDE ### +##################### +.idea/ + +### Emacs Temporary Files ### +############################# +*~ + +### Folders ### +############### +bin/ +vendor/ +node_modules/ + +### Files ### +############# +*.ivf +*.ogg +tags +cover.out +*.sw[poe] +*.wasm +examples/sfu-ws/cert.pem +examples/sfu-ws/key.pem +wasm_exec.js diff --git a/vendor/github.com/pion/rtp/.golangci.yml b/vendor/github.com/pion/rtp/.golangci.yml new file mode 100644 index 0000000..43af4c3 --- /dev/null +++ b/vendor/github.com/pion/rtp/.golangci.yml @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: 2026 The Pion community +# SPDX-License-Identifier: MIT + +version: "2" +linters: + enable: + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers + - bidichk # Checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - containedctx # containedctx is a linter that detects struct contained context.Context field + - contextcheck # check the function whether use a non-inherited context + - cyclop # checks function and package cyclomatic complexity + - decorder # check declaration order and count of types, constants, variables and functions + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + - dupl # Tool for code clone detection + - durationcheck # check for two durations multiplied together + - err113 # Golang linter to check the errors handling expressions + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases + - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. + - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - exhaustive # check exhaustiveness of enum switch statements + - forbidigo # Forbids identifiers + - forcetypeassert # finds forced type assertions + - gochecknoglobals # Checks that no globals are present in Go code + - gocognit # Computes and checks the cognitive complexity of functions + - goconst # Finds repeated strings that could be replaced by a constant + - gocritic # The most opinionated Go source code linter + - gocyclo # Computes and checks the cyclomatic complexity of functions + - godot # Check if comments end in a period + - godox # Tool for detection of FIXME, TODO and other comment keywords + - goheader # Checks is file header matches to pattern + - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. + - goprintffuncname # Checks that printf-like functions are named with `f` at the end + - gosec # Inspects source code for security problems + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - grouper # An analyzer to analyze expression groups. + - importas # Enforces consistent import aliases + - ineffassign # Detects when assignments to existing variables are not used + - lll # Reports long lines + - maintidx # maintidx measures the maintainability index of each function. + - makezero # Finds slice declarations with non-zero initial length + - misspell # Finds commonly misspelled English words in comments + - nakedret # Finds naked returns in functions greater than a specified function length + - nestif # Reports deeply nested if statements + - nilerr # Finds the code that returns nil even if it checks that the error is not nil. + - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. + - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity + - noctx # noctx finds sending http request without context.Context + - predeclared # find code that shadows one of Go's predeclared identifiers + - revive # golint replacement, finds style mistakes + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks + - tagliatelle # Checks the struct tags. + - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - unconvert # Remove unnecessary type conversions + - unparam # Reports unused function parameters + - unused # Checks Go code for unused constants, variables, functions and types + - varnamelen # checks that the length of a variable's name matches its scope + - wastedassign # wastedassign finds wasted assignment statements + - whitespace # Tool for detection of leading and trailing whitespace + disable: + - depguard # Go linter that checks if package imports are in a list of acceptable packages + - funlen # Tool for detection of long functions + - gochecknoinits # Checks that no init functions are present in Go code + - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. + - interfacebloat # A linter that checks length of interface. + - ireturn # Accept Interfaces, Return Concrete Types + - mnd # An analyzer to detect magic numbers + - nolintlint # Reports ill-formed or insufficient nolint directives + - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test + - prealloc # Finds slice declarations that could potentially be preallocated + - promlinter # Check Prometheus metrics naming via promlint + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. + - testpackage # linter that makes you use a separate _test package + - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes + - wrapcheck # Checks that errors returned from external packages are wrapped + - wsl # Whitespace Linter - Forces you to use empty lines! + settings: + staticcheck: + checks: + - all + - -QF1008 # "could remove embedded field", to keep it explicit! + - -QF1003 # "could use tagged switch on enum", Cases conflicts with exhaustive! + exhaustive: + default-signifies-exhaustive: true + forbidigo: + forbid: + - pattern: ^fmt.Print(f|ln)?$ + - pattern: ^log.(Panic|Fatal|Print)(f|ln)?$ + - pattern: ^os.Exit$ + - pattern: ^panic$ + - pattern: ^print(ln)?$ + - pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$ + pkg: ^testing$ + msg: use testify/assert instead + analyze-types: true + gomodguard: + blocked: + modules: + - github.com/pkg/errors: + recommendations: + - errors + govet: + enable: + - shadow + revive: + rules: + # Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility + - name: use-any + severity: warning + disabled: false + misspell: + locale: US + varnamelen: + max-distance: 12 + min-name-length: 2 + ignore-type-assert-ok: true + ignore-map-index-ok: true + ignore-chan-recv-ok: true + ignore-decls: + - i int + - n int + - w io.Writer + - r io.Reader + - b []byte + exclusions: + generated: lax + rules: + - linters: + - forbidigo + - gocognit + path: (examples|main\.go) + - linters: + - gocognit + path: _test\.go + - linters: + - forbidigo + path: cmd +formatters: + enable: + - gci # Gci control golang package import order and make it always deterministic. + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification + - gofumpt # Gofumpt checks whether code was gofumpt-ed. + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports + exclusions: + generated: lax diff --git a/vendor/github.com/pion/rtp/.goreleaser.yml b/vendor/github.com/pion/rtp/.goreleaser.yml new file mode 100644 index 0000000..8577d86 --- /dev/null +++ b/vendor/github.com/pion/rtp/.goreleaser.yml @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 The Pion community +# SPDX-License-Identifier: MIT + +builds: +- skip: true diff --git a/vendor/github.com/pion/rtp/LICENSE b/vendor/github.com/pion/rtp/LICENSE new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/vendor/github.com/pion/rtp/LICENSE @@ -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/vendor/github.com/pion/rtp/README.md b/vendor/github.com/pion/rtp/README.md new file mode 100644 index 0000000..c193f3c --- /dev/null +++ b/vendor/github.com/pion/rtp/README.md @@ -0,0 +1,56 @@ +

+
+ Pion RTP +
+

+

A Go implementation of RTP

+

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

+
+ +### Implemented +- [RFC 3550](https://www.rfc-editor.org/rfc/rfc3550.html) — RTP: A Transport Protocol for Real-Time Applications +- [RFC 8285](https://www.rfc-editor.org/rfc/rfc8285.html) — A General Mechanism for RTP Header Extensions + +#### Header Extensions +- [RFC 6464](https://www.rfc-editor.org/rfc/rfc6464.html) — RTP Header Extension for Client-to-Mixer Audio Level Indication +- [draft-holmer-rmcat-transport-wide-cc-extensions-01](https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01) — Transport-Wide Congestion Control +- [Absolute Send Time](https://webrtc.googlesource.com/src/%2B/refs/heads/main/docs/native-code/rtp-hdrext/abs-send-time/README.md) (WebRTC extension, non-RFC) +- [Absolute Capture Time](https://webrtc.googlesource.com/src/%2B/refs/heads/main/docs/native-code/rtp-hdrext/abs-capture-time/README.md) (WebRTC extension, non-RFC) +- [Playout Delay](https://webrtc.googlesource.com/src/%2B/main/docs/native-code/rtp-hdrext/playout-delay/README.md) (WebRTC extension, non-RFC) +- [Video Layers Allocation](https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/video-layers-allocation00) (WebRTC extension, non-RFC) + +#### Codecs +- [RFC 3551](https://www.rfc-editor.org/rfc/rfc3551.html) — RTP Profile for PCMA/PCMU (G.711) and G.722 Audio +- [RFC 6184](https://www.rfc-editor.org/rfc/rfc6184.html) — RTP Payload Format for H.264 Video +- [RFC 7587](https://www.rfc-editor.org/rfc/rfc7587.html) — RTP Payload Format for the Opus Audio Codec +- [RFC 7741](https://www.rfc-editor.org/rfc/rfc7741.html) — RTP Payload Format for VP8 Video +- [draft-ietf-payload-vp9](https://datatracker.ietf.org/doc/draft-ietf-payload-vp9/) — RTP Payload Format for VP9 Video +- [draft-ietf-avtcore-rtp-hevc](https://datatracker.ietf.org/doc/draft-ietf-avtcore-rtp-hevc/) — RTP Payload Format for H.265 Video +- [AV1 RTP Payload Specification](https://aomediacodec.github.io/av1-rtp-spec/v1.0.0.html) — RTP Payload Format for AV1 Video + +### Roadmap +The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. + +### Community +Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). + +Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. + +We are always looking to support **your projects**. Please reach out if you have something to build! +If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) + +### Contributing +Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible + +### License +MIT License - see [LICENSE](LICENSE) for full text diff --git a/vendor/github.com/pion/rtp/abscapturetimeextension.go b/vendor/github.com/pion/rtp/abscapturetimeextension.go new file mode 100644 index 0000000..81dc615 --- /dev/null +++ b/vendor/github.com/pion/rtp/abscapturetimeextension.go @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "io" + "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 +} + +// MarshalSize returns the size of the AbsCaptureTimeExtension once marshaled. +func (t AbsCaptureTimeExtension) MarshalSize() int { + if t.EstimatedCaptureClockOffset != nil { + return absCaptureTimeExtendedExtensionSize + } + + return absCaptureTimeExtensionSize +} + +// MarshalTo marshals the extension to the given buffer. +// Returns io.ErrShortBuffer if buf is too small. +func (t AbsCaptureTimeExtension) MarshalTo(buf []byte) (int, error) { + if t.EstimatedCaptureClockOffset != nil { + if len(buf) < absCaptureTimeExtendedExtensionSize { + return 0, io.ErrShortBuffer + } + binary.BigEndian.PutUint64(buf[0:8], t.Timestamp) + binary.BigEndian.PutUint64(buf[8:16], uint64(*t.EstimatedCaptureClockOffset)) // nolint: gosec // G115 + + return absCaptureTimeExtendedExtensionSize, nil + } + if len(buf) < absCaptureTimeExtensionSize { + return 0, io.ErrShortBuffer + } + binary.BigEndian.PutUint64(buf[0:8], t.Timestamp) + + return absCaptureTimeExtensionSize, nil +} + +// Marshal serializes the members to buffer. +func (t AbsCaptureTimeExtension) Marshal() ([]byte, error) { + if t.EstimatedCaptureClockOffset != nil { + buf := make([]byte, absCaptureTimeExtendedExtensionSize) + binary.BigEndian.PutUint64(buf[0:8], t.Timestamp) + binary.BigEndian.PutUint64(buf[8:16], uint64(*t.EstimatedCaptureClockOffset)) // nolint: gosec // G115 + + return buf, nil + } + buf := make([]byte, absCaptureTimeExtensionSize) + 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])) // nolint: gosec // G115 false positive + 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/vendor/github.com/pion/rtp/abssendtimeextension.go b/vendor/github.com/pion/rtp/abssendtimeextension.go new file mode 100644 index 0000000..30bc7ea --- /dev/null +++ b/vendor/github.com/pion/rtp/abssendtimeextension.go @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "io" + "time" +) + +const ( + absSendTimeExtensionSize = 3 +) + +// AbsSendTimeExtension is a extension payload format in +// http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +type AbsSendTimeExtension struct { + Timestamp uint64 +} + +// MarshalSize returns the size of the AbsSendTimeExtension once marshaled. +func (t AbsSendTimeExtension) MarshalSize() int { + return absSendTimeExtensionSize +} + +// MarshalTo marshals the extension to the given buffer. +// Returns io.ErrShortBuffer if buf is too small. +func (t AbsSendTimeExtension) MarshalTo(buf []byte) (int, error) { + if len(buf) < absSendTimeExtensionSize { + return 0, io.ErrShortBuffer + } + buf[0] = byte(t.Timestamp & 0xFF0000 >> 16) + buf[1] = byte(t.Timestamp & 0xFF00 >> 8) + buf[2] = byte(t.Timestamp & 0xFF) + + return absSendTimeExtensionSize, nil +} + +// Marshal serializes the members to buffer. +func (t AbsSendTimeExtension) Marshal() ([]byte, error) { + return []byte{ + byte(t.Timestamp & 0xFF0000 >> 16), + byte(t.Timestamp & 0xFF00 >> 8), + byte(t.Timestamp & 0xFF), + }, nil +} + +// Unmarshal parses the passed byte slice and stores the result in the members. +func (t *AbsSendTimeExtension) Unmarshal(rawData []byte) error { + if len(rawData) < absSendTimeExtensionSize { + return errTooSmall + } + t.Timestamp = uint64(rawData[0])<<16 | uint64(rawData[1])<<8 | uint64(rawData[2]) + + return nil +} + +// Estimate absolute send time according to the receive time. +// Note that if the transmission delay is larger than 64 seconds, estimated time will be wrong. +func (t *AbsSendTimeExtension) Estimate(receive time.Time) time.Time { + receiveNTP := toNtpTime(receive) + ntp := receiveNTP&0xFFFFFFC000000000 | (t.Timestamp&0xFFFFFF)<<14 + if receiveNTP < ntp { + // Receive time must be always later than send time + ntp -= 0x1000000 << 14 + } + + return toTime(ntp) +} + +// NewAbsSendTimeExtension makes new AbsSendTimeExtension from time.Time. +func NewAbsSendTimeExtension(sendTime time.Time) *AbsSendTimeExtension { + return &AbsSendTimeExtension{ + Timestamp: toNtpTime(sendTime) >> 14, + } +} + +func toNtpTime(t time.Time) uint64 { + var s uint64 + var f uint64 + u := uint64(t.UnixNano()) // nolint: gosec // G115 false positive + s = u / 1e9 + s += 0x83AA7E80 // offset in seconds between unix epoch and ntp epoch + f = u % 1e9 + f <<= 32 + f /= 1e9 + s <<= 32 + + return s | f +} + +func toTime(t uint64) time.Time { + s := t >> 32 + f := t & 0xFFFFFFFF + f *= 1e9 + f >>= 32 + s -= 0x83AA7E80 + u := s*1e9 + f + + return time.Unix(0, int64(u)) // nolint: gosec // G115 false positive +} diff --git a/vendor/github.com/pion/rtp/audiolevelextension.go b/vendor/github.com/pion/rtp/audiolevelextension.go new file mode 100644 index 0000000..19ba50f --- /dev/null +++ b/vendor/github.com/pion/rtp/audiolevelextension.go @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "errors" + "io" +) + +const ( + // audioLevelExtensionSize One byte header size. + audioLevelExtensionSize = 1 +) + +var errAudioLevelOverflow = errors.New("audio level overflow") + +// AudioLevelExtension is a extension payload format described in +// https://tools.ietf.org/html/rfc6464 +// +// Implementation based on: +// https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#49 +// +// One byte format: +// 0 1 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ID | len=0 |V| level | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// Two byte format: +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ID | len=1 |V| level | 0 (pad) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +//nolint:lll +type AudioLevelExtension struct { + Level uint8 + Voice bool +} + +// MarshalSize returns the size of the AudioLevelExtension once marshaled. +func (a AudioLevelExtension) MarshalSize() int { + return audioLevelExtensionSize +} + +// MarshalTo marshals the extension to the given buffer. +// Returns io.ErrShortBuffer if buf is too small. +func (a AudioLevelExtension) MarshalTo(buf []byte) (int, error) { + if a.Level > 127 { + return 0, errAudioLevelOverflow + } + if len(buf) < audioLevelExtensionSize { + return 0, io.ErrShortBuffer + } + voice := uint8(0x00) + if a.Voice { + voice = 0x80 + } + buf[0] = voice | a.Level + + return audioLevelExtensionSize, nil +} + +// Marshal serializes the members to buffer. +func (a AudioLevelExtension) Marshal() ([]byte, error) { + if a.Level > 127 { + return nil, errAudioLevelOverflow + } + voice := uint8(0x00) + if a.Voice { + voice = 0x80 + } + buf := make([]byte, audioLevelExtensionSize) + buf[0] = voice | a.Level + + return buf, nil +} + +// Unmarshal parses the passed byte slice and stores the result in the members. +func (a *AudioLevelExtension) Unmarshal(rawData []byte) error { + if len(rawData) < audioLevelExtensionSize { + return errTooSmall + } + a.Level = rawData[0] & 0x7F + a.Voice = rawData[0]&0x80 != 0 + + return nil +} diff --git a/vendor/github.com/pion/rtp/codecov.yml b/vendor/github.com/pion/rtp/codecov.yml new file mode 100644 index 0000000..b9639c2 --- /dev/null +++ b/vendor/github.com/pion/rtp/codecov.yml @@ -0,0 +1,22 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# +# SPDX-FileCopyrightText: 2026 The Pion community +# SPDX-License-Identifier: MIT + +coverage: + status: + project: + default: + # Allow decreasing 2% of total coverage to avoid noise. + threshold: 2% + patch: + default: + target: 70% + only_pulls: true + +ignore: + - "examples/*" + - "examples/**/*" diff --git a/vendor/github.com/pion/rtp/codecs/av1/obu/errors.go b/vendor/github.com/pion/rtp/codecs/av1/obu/errors.go new file mode 100644 index 0000000..75106be --- /dev/null +++ b/vendor/github.com/pion/rtp/codecs/av1/obu/errors.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package obu + +import "errors" + +var ( + // ErrInvalidOBUHeader is returned when an OBU header has forbidden bits set. + ErrInvalidOBUHeader = errors.New("invalid OBU header") + // ErrShortHeader is returned when an OBU header is not large enough. + // This can happen when an extension header is expected but not present. + ErrShortHeader = errors.New("OBU header is not large enough") +) diff --git a/vendor/github.com/pion/rtp/codecs/av1/obu/leb128.go b/vendor/github.com/pion/rtp/codecs/av1/obu/leb128.go new file mode 100644 index 0000000..7662bc0 --- /dev/null +++ b/vendor/github.com/pion/rtp/codecs/av1/obu/leb128.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 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 // nolint: gosec // G115 + } + + // 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/vendor/github.com/pion/rtp/codecs/av1/obu/obu.go b/vendor/github.com/pion/rtp/codecs/av1/obu/obu.go new file mode 100644 index 0000000..7d7a3ff --- /dev/null +++ b/vendor/github.com/pion/rtp/codecs/av1/obu/obu.go @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package obu + +import ( + "fmt" +) + +// Type represents the type of an AV1 OBU. +type Type uint8 + +// OBU types as defined in the AV1 specification. +// 5.3.1: https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=39 +const ( + // OBUSequenceHeader av1 sequence_header_obu. + OBUSequenceHeader = Type(1) + // OBUTemporalDelimiter av1 temporal_delimiter_obu. + OBUTemporalDelimiter = Type(2) + // OBUFrameHeader av1 frame_header_obu. + OBUFrameHeader = Type(3) + // OBUTileGroup av1 tile_group_obu. + OBUTileGroup = Type(4) + // OBUMetadata av1 metadata_obu. + OBUMetadata = Type(5) + // OBUFrame av1 frame_obu. + OBUFrame = Type(6) + // OBURedundantFrameHeader av1 redundant_frame_header_obu. + OBURedundantFrameHeader = Type(7) + // OBUTileList av1 tile_list_obu. + OBUTileList = Type(8) + // OBUPadding av1 padding_obu. + OBUPadding = Type(15) +) + +//nolint:cyclop +func (o Type) String() string { + switch o { + case OBUSequenceHeader: + return "OBU_SEQUENCE_HEADER" + case OBUTemporalDelimiter: + return "OBU_TEMPORAL_DELIMITER" + case OBUFrameHeader: + return "OBU_FRAME_HEADER" + case OBUTileGroup: + return "OBU_TILE_GROUP" + case OBUMetadata: + return "OBU_METADATA" + case OBUFrame: + return "OBU_FRAME" + case OBURedundantFrameHeader: + return "OBU_REDUNDANT_FRAME_HEADER" + case OBUTileList: + return "OBU_TILE_LIST" + case OBUPadding: + return "OBU_PADDING" + default: + return "OBU_RESERVED" + } +} + +// Header represents the header of an OBU obu_header(). +// 5.3.2: https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=40 +type Header struct { + Type Type + ExtensionHeader *ExtensionHeader + HasSizeField bool + Reserved1Bit bool +} + +// ParseOBUHeader parses an OBU header from the given data. +// 5.3.2: https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=40 +/* + obu_header() { Type + obu_forbidden_bit f(1) + obu_type f(4) + obu_extension_flag f(1) + obu_has_size_field f(1) + obu_reserved_1bit f(1) + if ( obu_extension_flag == 1 ) + obu_extension_header() + } + } +*/ +func ParseOBUHeader(data []byte) (*Header, error) { + if len(data) < 1 { + return nil, fmt.Errorf("%w: data is too short", ErrShortHeader) + } + + forbiddenBit := data[0] & 0x80 + if forbiddenBit != 0 { + return nil, fmt.Errorf("%w: forbidden bit is set", ErrInvalidOBUHeader) + } + + obuType := Type((data[0] & 0x78) >> 3) + obuExtensionFlag := (data[0] & 0x04) != 0 + obuHasSizeField := (data[0] & 0x02) != 0 + obuReserved1Bit := (data[0] & 0x01) != 0 + + header := &Header{ + Type: obuType, + HasSizeField: obuHasSizeField, + Reserved1Bit: obuReserved1Bit, + } + + if obuExtensionFlag { + if len(data) < 2 { + return nil, fmt.Errorf("%w: Unexpected end of data, expected extension header", ErrShortHeader) + } + + extensionHeader := ParseOBUExtensionHeader(data[1]) + header.ExtensionHeader = &extensionHeader + } + + return header, nil +} + +// Marshal serializes the OBU header to a byte slice. +// If the OBU has an extension header, the extension header is serialized as well. +// 5.3.2: https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=40 +/* + obu_header() { Type + obu_forbidden_bit f(1) + obu_type f(4) + obu_extension_flag f(1) + obu_has_size_field f(1) + obu_reserved_1bit f(1) + if ( obu_extension_flag == 1 ) + obu_extension_header() + } + } +*/ +func (o *Header) Marshal() []byte { + header := make([]byte, o.Size()) + + header[0] = (byte(o.Type) & 0x0f) << 3 + + if o.ExtensionHeader != nil { + header[0] |= 0x04 + header[1] = o.ExtensionHeader.Marshal() + } + + if o.HasSizeField { + header[0] |= 0x02 + } + + if o.Reserved1Bit { + header[0] |= 0x01 + } + + return header +} + +// Size returns the size of the OBU header in bytes. +func (o *Header) Size() int { + size := 1 + if o.ExtensionHeader != nil { + size++ + } + + return size +} + +// ExtensionHeader represents an OBU extension header obu_extension_header(). +// 5.3.3 https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=40 +type ExtensionHeader struct { + TemporalID uint8 + SpatialID uint8 + Reserved3Bits uint8 +} + +// ParseOBUExtensionHeader parses an OBU extension header from the given data. +// 5.3.3 https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=40 +/* + obu_extension_header() { Type + temporal_id f(3) + spatial_id f(2) + extension_header_reserved_3bits f(3) + } +*/ +func ParseOBUExtensionHeader(headerData byte) ExtensionHeader { + return ExtensionHeader{ + TemporalID: headerData >> 5, + SpatialID: (headerData >> 3) & 0x03, + Reserved3Bits: headerData & 0x07, + } +} + +// Marshal serializes the OBU extension header to a byte slice. +// 5.3.3 https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=40 +/* + obu_extension_header() { Type + temporal_id f(3) + spatial_id f(2) + extension_header_reserved_3bits f(3) + } +*/ +func (o *ExtensionHeader) Marshal() byte { + return (o.TemporalID << 5) | ((o.SpatialID & 0x3) << 3) | (o.Reserved3Bits & 0x07) +} + +// OBU represents an AV1 OBU. +// 5.1 https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=39 +type OBU struct { + Header Header + Payload []byte +} + +// Marshal serializes the OBU to low-overhead bitstream format. +// 5.2 https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=40 +func (o *OBU) Marshal() []byte { + buffer := o.Header.Marshal() + + if o.Header.HasSizeField { + buffer = append(buffer, WriteToLeb128(uint(len(o.Payload)))...) + } + + return append(buffer, o.Payload...) +} diff --git a/vendor/github.com/pion/rtp/depacketizer.go b/vendor/github.com/pion/rtp/depacketizer.go new file mode 100644 index 0000000..9722da5 --- /dev/null +++ b/vendor/github.com/pion/rtp/depacketizer.go @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 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/vendor/github.com/pion/rtp/error.go b/vendor/github.com/pion/rtp/error.go new file mode 100644 index 0000000..c0777eb --- /dev/null +++ b/vendor/github.com/pion/rtp/error.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "errors" +) + +var ( + errHeaderSizeInsufficient = errors.New("RTP header size insufficient") + errHeaderSizeInsufficientForExtension = errors.New("RTP header size insufficient for extension") + errTooSmall = errors.New("buffer too small") + errHeaderExtensionsNotEnabled = errors.New("h.Extension not enabled") + errHeaderExtensionNotFound = errors.New("extension not found") + + errRFC8285OneByteHeaderIDRange = errors.New( + "header extension id must be between 1 and 14 for RFC 5285 one byte extensions", + ) + errRFC8285OneByteHeaderSize = errors.New( + "header extension payload must be 16bytes or less for RFC 5285 one byte extensions", + ) + + errRFC8285TwoByteHeaderIDRange = errors.New( + "header extension id must be between 1 and 255 for RFC 5285 two byte extensions", + ) + 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/vendor/github.com/pion/rtp/header_extension.go b/vendor/github.com/pion/rtp/header_extension.go new file mode 100644 index 0000000..174bd4e --- /dev/null +++ b/vendor/github.com/pion/rtp/header_extension.go @@ -0,0 +1,415 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" +) + +const ( + headerExtensionIDReserved = 0xF +) + +// HeaderExtension represents an RTP extension header. +type HeaderExtension interface { + Set(id uint8, payload []byte) error + GetIDs() []uint8 + Get(id uint8) []byte + Del(id uint8) error + + Unmarshal(buf []byte) (int, error) + Marshal() ([]byte, error) + MarshalTo(buf []byte) (int, error) + MarshalSize() int +} + +// OneByteHeaderExtension is an RFC8285 one-byte header extension. +type OneByteHeaderExtension struct { + payload []byte +} + +// Set sets the extension payload for the specified ID. +func (e *OneByteHeaderExtension) Set(id uint8, buf []byte) error { + if err := headerExtensionCheck(ExtensionProfileOneByte, id, buf); err != nil { + return err + } + + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] >> 4 + payloadLen := int(e.payload[n]&^0xF0 + 1) + n++ + + if extid == id { + e.payload = append(e.payload[:n], append(buf, e.payload[n+payloadLen:]...)...) + + return nil + } + n += payloadLen + } + + if len(e.payload) == 0 && !bytes.HasPrefix(buf, []byte{0xBE, 0xDE, 0x00, 0x00}) { + e.payload = []byte{0xBE, 0xDE, 0x00, 0x00} + } + + e.payload = append(e.payload, (id<<4 | uint8(len(buf)-1))) // nolint: gosec // G115 + e.payload = append(e.payload, buf...) + binary.BigEndian.PutUint16(e.payload[2:4], binary.BigEndian.Uint16(e.payload[2:4])+1) + + return nil +} + +// GetIDs returns the available IDs. +func (e *OneByteHeaderExtension) GetIDs() []uint8 { + ids := make([]uint8, 0, binary.BigEndian.Uint16(e.payload[2:4])) + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] >> 4 + payloadLen := int(e.payload[n]&^0xF0 + 1) + n++ + + if extid == headerExtensionIDReserved { + break + } + + ids = append(ids, extid) + n += payloadLen + } + + return ids +} + +// Get returns the payload of the extension with the given ID. +func (e *OneByteHeaderExtension) Get(id uint8) []byte { + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] >> 4 + payloadLen := int(e.payload[n]&^0xF0 + 1) + n++ + + if extid == id { + return e.payload[n : n+payloadLen] + } + n += payloadLen + } + + return nil +} + +// Del deletes the extension with the specified ID. +func (e *OneByteHeaderExtension) Del(id uint8) error { + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] >> 4 + payloadLen := int(e.payload[n]&^0xF0 + 1) + + if extid == id { + e.payload = append(e.payload[:n], e.payload[n+1+payloadLen:]...) + + return nil + } + n += payloadLen + 1 + } + + return errHeaderExtensionNotFound +} + +// Unmarshal parses the extension payload. +func (e *OneByteHeaderExtension) Unmarshal(buf []byte) (int, error) { + profile := binary.BigEndian.Uint16(buf[0:2]) + if profile != ExtensionProfileOneByte { + return 0, fmt.Errorf("%w actual(%x)", errHeaderExtensionNotFound, buf[0:2]) + } + e.payload = buf + + return len(buf), nil +} + +// Marshal returns the extension payload. +func (e OneByteHeaderExtension) Marshal() ([]byte, error) { + return e.payload, nil +} + +// MarshalTo writes the extension payload to the given buffer. +func (e OneByteHeaderExtension) MarshalTo(buf []byte) (int, error) { + size := e.MarshalSize() + if size > len(buf) { + return 0, io.ErrShortBuffer + } + + return copy(buf, e.payload), nil +} + +// MarshalSize returns the size of the extension payload. +func (e OneByteHeaderExtension) MarshalSize() int { + return len(e.payload) +} + +// TwoByteHeaderExtension is an RFC8285 two-byte header extension. +type TwoByteHeaderExtension struct { + payload []byte +} + +// Set sets the extension payload for the specified ID. +func (e *TwoByteHeaderExtension) Set(id uint8, buf []byte) error { + if err := headerExtensionCheck(ExtensionProfileTwoByte, id, buf); err != nil { + return err + } + + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] + n++ + + payloadLen := int(e.payload[n]) + n++ + + if extid == id { + e.payload = append(e.payload[:n], append(buf, e.payload[n+payloadLen:]...)...) + + return nil + } + n += payloadLen + } + + if len(e.payload) == 0 && !bytes.HasPrefix(buf, []byte{0x10, 0x00, 0x00, 0x00}) { + e.payload = []byte{0x10, 0x00, 0x00, 0x00} + } + + e.payload = append(e.payload, id, uint8(len(buf))) // nolint: gosec // G115 + e.payload = append(e.payload, buf...) + binary.BigEndian.PutUint16(e.payload[2:4], binary.BigEndian.Uint16(e.payload[2:4])+1) + + return nil +} + +// GetIDs returns the available IDs. +func (e *TwoByteHeaderExtension) GetIDs() []uint8 { + ids := make([]uint8, 0, binary.BigEndian.Uint16(e.payload[2:4])) + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] + n++ + + payloadLen := int(e.payload[n]) + n++ + + ids = append(ids, extid) + n += payloadLen + } + + return ids +} + +// Get returns the payload of the extension with the given ID. +func (e *TwoByteHeaderExtension) Get(id uint8) []byte { + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] + n++ + + payloadLen := int(e.payload[n]) + n++ + + if extid == id { + return e.payload[n : n+payloadLen] + } + n += payloadLen + } + + return nil +} + +// Del deletes the extension with the specified ID. +func (e *TwoByteHeaderExtension) Del(id uint8) error { + for n := 4; n < len(e.payload); { + if e.payload[n] == 0x00 { // padding + n++ + + continue + } + + extid := e.payload[n] + + payloadLen := int(e.payload[n+1]) + + if extid == id { + e.payload = append(e.payload[:n], e.payload[n+2+payloadLen:]...) + + return nil + } + n += payloadLen + 2 + } + + return errHeaderExtensionNotFound +} + +// Unmarshal parses the extension payload. +func (e *TwoByteHeaderExtension) Unmarshal(buf []byte) (int, error) { + profile := binary.BigEndian.Uint16(buf[0:2]) + if profile != ExtensionProfileTwoByte { + return 0, fmt.Errorf("%w actual(%x)", errHeaderExtensionNotFound, buf[0:2]) + } + e.payload = buf + + return len(buf), nil +} + +// Marshal returns the extension payload. +func (e TwoByteHeaderExtension) Marshal() ([]byte, error) { + return e.payload, nil +} + +// MarshalTo marshals the extension to the given buffer. +func (e TwoByteHeaderExtension) MarshalTo(buf []byte) (int, error) { + size := e.MarshalSize() + if size > len(buf) { + return 0, io.ErrShortBuffer + } + + return copy(buf, e.payload), nil +} + +// MarshalSize returns the size of the extension payload. +func (e TwoByteHeaderExtension) MarshalSize() int { + return len(e.payload) +} + +// RawExtension represents an RFC3550 header extension. +type RawExtension struct { + payload []byte +} + +// Set sets the extension payload for the specified ID. +func (e *RawExtension) Set(id uint8, payload []byte) error { + if err := headerExtensionCheck(0, id, payload); err != nil { + return err + } + + e.payload = payload + + return nil +} + +// GetIDs returns the available IDs. +func (e *RawExtension) GetIDs() []uint8 { + return []uint8{0} +} + +// Get returns the payload of the extension with the given ID. +func (e *RawExtension) Get(id uint8) []byte { + if id == 0 { + return e.payload + } + + return nil +} + +// Del deletes the extension with the specified ID. +func (e *RawExtension) Del(id uint8) error { + if id == 0 { + e.payload = nil + + return nil + } + + return fmt.Errorf("%w actual(%d)", errRFC3550HeaderIDRange, id) +} + +// Unmarshal parses the extension from the given buffer. +func (e *RawExtension) Unmarshal(buf []byte) (int, error) { + profile := binary.BigEndian.Uint16(buf[0:2]) + if profile == ExtensionProfileOneByte || profile == ExtensionProfileTwoByte { + return 0, fmt.Errorf("%w actual(%x)", errHeaderExtensionNotFound, buf[0:2]) + } + e.payload = buf + + return len(buf), nil +} + +// Marshal returns the raw extension payload. +func (e RawExtension) Marshal() ([]byte, error) { + return e.payload, nil +} + +// MarshalTo marshals the extension to the given buffer. +func (e RawExtension) MarshalTo(buf []byte) (int, error) { + size := e.MarshalSize() + if size > len(buf) { + return 0, io.ErrShortBuffer + } + + return copy(buf, e.payload), nil +} + +// MarshalSize returns the size of the extension when marshaled. +func (e RawExtension) MarshalSize() int { + return len(e.payload) +} + +// Assert that id + value is valid for give Header Extension Profile. +func headerExtensionCheck(extensionProfile uint16, id uint8, payload []byte) error { + switch extensionProfile { + // RFC 8285 RTP One Byte Header Extension + case ExtensionProfileOneByte: + if id < 1 || id > 14 { + return fmt.Errorf("%w actual(%d)", errRFC8285OneByteHeaderIDRange, id) + } + if len(payload) > 16 { + return fmt.Errorf("%w actual(%d)", errRFC8285OneByteHeaderSize, len(payload)) + } + // RFC 8285 RTP Two Byte Header Extension + case ExtensionProfileTwoByte: + if id < 1 { + return fmt.Errorf("%w actual(%d)", errRFC8285TwoByteHeaderIDRange, id) + } + if len(payload) > 255 { + return fmt.Errorf("%w actual(%d)", errRFC8285TwoByteHeaderSize, len(payload)) + } + default: // RFC3550 Extension + if id != 0 { + return fmt.Errorf("%w actual(%d)", errRFC3550HeaderIDRange, id) + } + } + + return nil +} diff --git a/vendor/github.com/pion/rtp/packet.go b/vendor/github.com/pion/rtp/packet.go new file mode 100644 index 0000000..d7a5e08 --- /dev/null +++ b/vendor/github.com/pion/rtp/packet.go @@ -0,0 +1,633 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "fmt" + "io" +) + +// Extension RTP Header extension. +type Extension struct { + id uint8 + payload []byte +} + +// Header represents an RTP packet header. +type Header struct { + Version uint8 + Padding bool + Extension bool + Marker bool + PayloadType uint8 + SequenceNumber uint16 + Timestamp uint32 + SSRC uint32 + CSRC []uint32 + ExtensionProfile uint16 + Extensions []Extension + + // PaddingLength is the length of the padding in bytes. It is not part of the RTP header + // (it is sent in the last byte of RTP packet padding), but logically it belongs here. + PaddingSize byte + + // Deprecated: will be removed in a future version. + PayloadOffset int +} + +// Packet represents an RTP Packet. +type Packet struct { + Header + Payload []byte + + PaddingSize byte // Deprecated: will be removed in a future version. Use Header.PaddingSize instead. + + // Deprecated: will be removed in a future version. + Raw []byte + + // Please do not add any new field directly to Packet struct unless you know that it is safe. + // pion internally passes Header and Payload separately, what causes bugs like + // https://github.com/pion/webrtc/issues/2403 . +} + +const ( + // ExtensionProfileOneByte is the RTP One Byte Header Extension Profile, defined in RFC 8285. + ExtensionProfileOneByte = 0xBEDE + // ExtensionProfileTwoByte is the RTP Two Byte Header Extension Profile, defined in RFC 8285. + ExtensionProfileTwoByte = 0x1000 + // CryptexProfileOneByte is the Cryptex One Byte Header Extension Profile, defined in RFC 9335. + CryptexProfileOneByte = 0xC0DE + // CryptexProfileTwoByte is the Cryptex Two Byte Header Extension Profile, defined in RFC 9335. + CryptexProfileTwoByte = 0xC2DE +) + +const ( + headerLength = 4 + versionShift = 6 + versionMask = 0x3 + paddingShift = 5 + paddingMask = 0x1 + extensionShift = 4 + extensionMask = 0x1 + extensionIDReserved = 0xF + extensionIDPadding = 0x0 + ccMask = 0xF + markerShift = 7 + markerMask = 0x1 + ptMask = 0x7F + seqNumOffset = 2 + seqNumLength = 2 + timestampOffset = 4 + timestampLength = 4 + ssrcOffset = 8 + ssrcLength = 4 + csrcOffset = 12 + csrcLength = 4 +) + +// String helps with debugging by printing packet information in a readable way. +func (p Packet) String() string { + out := "RTP PACKET:\n" + + out += fmt.Sprintf("\tVersion: %v\n", p.Version) + out += fmt.Sprintf("\tMarker: %v\n", p.Marker) + out += fmt.Sprintf("\tPayload Type: %d\n", p.PayloadType) + out += fmt.Sprintf("\tSequence Number: %d\n", p.SequenceNumber) + out += fmt.Sprintf("\tTimestamp: %d\n", p.Timestamp) + out += fmt.Sprintf("\tSSRC: %d (%x)\n", p.SSRC, p.SSRC) + out += fmt.Sprintf("\tPayload Length: %d\n", len(p.Payload)) + + return out +} + +// Unmarshal parses the passed byte slice and stores the result in the Header. +// It returns the number of bytes read n and any error. +func (h *Header) Unmarshal(buf []byte) (n int, err error) { //nolint:gocognit,cyclop + if len(buf) < headerLength { + return 0, fmt.Errorf("%w: %d < %d", errHeaderSizeInsufficient, len(buf), headerLength) + } + + /* + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |V=2|P|X| CC |M| PT | sequence number | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | timestamp | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | synchronization source (SSRC) identifier | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * | contributing source (CSRC) identifiers | + * | .... | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + h.Version = buf[0] >> versionShift & versionMask + h.Padding = (buf[0] >> paddingShift & paddingMask) > 0 + h.Extension = (buf[0] >> extensionShift & extensionMask) > 0 + nCSRC := int(buf[0] & ccMask) + if cap(h.CSRC) < nCSRC { + h.CSRC = make([]uint32, nCSRC) + } else { + h.CSRC = h.CSRC[:nCSRC] + } + + n = csrcOffset + (nCSRC * csrcLength) + if len(buf) < n { + return n, fmt.Errorf("size %d < %d: %w", len(buf), n, + errHeaderSizeInsufficient) + } + headerLength := n + + h.Marker = (buf[1] >> markerShift & markerMask) > 0 + h.PayloadType = buf[1] & ptMask + + h.SequenceNumber = binary.BigEndian.Uint16(buf[seqNumOffset : seqNumOffset+seqNumLength]) + h.Timestamp = binary.BigEndian.Uint32(buf[timestampOffset : timestampOffset+timestampLength]) + h.SSRC = binary.BigEndian.Uint32(buf[ssrcOffset : ssrcOffset+ssrcLength]) + + for i := range h.CSRC { + offset := csrcOffset + (i * csrcLength) + h.CSRC[i] = binary.BigEndian.Uint32(buf[offset:]) + } + + h.Extensions = h.Extensions[:0] + + if h.Extension { // nolint: nestif + if expected := n + 4; len(buf) < expected { + return n, fmt.Errorf("size %d < %d: %w", + len(buf), expected, + errHeaderSizeInsufficientForExtension, + ) + } + + h.ExtensionProfile = binary.BigEndian.Uint16(buf[n:]) + n += 2 + extensionLength := int(binary.BigEndian.Uint16(buf[n:])) * 4 + n += 2 + extensionEnd := n + extensionLength + headerLength = extensionEnd + + if len(buf) < extensionEnd { + return n, fmt.Errorf("size %d < %d: %w", len(buf), extensionEnd, errHeaderSizeInsufficientForExtension) + } + + if h.ExtensionProfile == ExtensionProfileOneByte || h.ExtensionProfile == ExtensionProfileTwoByte { + var ( + extid uint8 + payloadLen int + ) + + for n < extensionEnd { + if buf[n] == extensionIDPadding { // padding + n++ + + continue + } + + if h.ExtensionProfile == ExtensionProfileOneByte { + extid = buf[n] >> 4 // nolint:gosec // n is defined to be in bounds + payloadLen = int(buf[n]&^0xF0 + 1) //nolint:gosec // n is defined to be in bounds + n++ + + // Stop parsing extensions if we reach the reserved ID or padding with non-zero length + if extid == extensionIDReserved || extid == extensionIDPadding { + break + } + } else { + extid = buf[n] // nolint:gosec // n is defined to be in bounds + n++ + + if extensionEnd <= n { + return n, fmt.Errorf("size %d < %d: %w", extensionEnd, n, errHeaderSizeInsufficientForExtension) + } + + payloadLen = int(buf[n]) + n++ + } + + if extensionPayloadEnd := n + payloadLen; extensionEnd < extensionPayloadEnd { + return n, fmt.Errorf("size %d < %d: %w", extensionEnd, extensionPayloadEnd, errHeaderSizeInsufficientForExtension) + } + + extension := Extension{id: extid, payload: buf[n : n+payloadLen]} + h.Extensions = append(h.Extensions, extension) + n += payloadLen + } + } else { + // RFC3550 Extension + extension := Extension{id: 0, payload: buf[n:extensionEnd]} + h.Extensions = append(h.Extensions, extension) + } + } + + return headerLength, nil +} + +// Unmarshal parses the passed byte slice and stores the result in the Packet. +func (p *Packet) Unmarshal(buf []byte) error { + n, err := p.Header.Unmarshal(buf) + if err != nil { + return err + } + + end := len(buf) + if p.Header.Padding { + if end <= n { + return errTooSmall + } + p.Header.PaddingSize = buf[end-1] + end -= int(p.Header.PaddingSize) + } else { + p.Header.PaddingSize = 0 + } + p.PaddingSize = p.Header.PaddingSize + if end < n { + return errTooSmall + } + + p.Payload = buf[n:end] + + return nil +} + +// Marshal serializes the header into bytes. +func (h Header) Marshal() (buf []byte, err error) { + buf = make([]byte, h.MarshalSize()) + + n, err := h.MarshalTo(buf) + if err != nil { + return nil, err + } + + return buf[:n], nil +} + +// MarshalTo serializes the header and writes to the buffer. +func (h Header) MarshalTo(buf []byte) (n int, err error) { //nolint:cyclop + /* + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |V=2|P|X| CC |M| PT | sequence number | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | timestamp | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | synchronization source (SSRC) identifier | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * | contributing source (CSRC) identifiers | + * | .... | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + size := h.MarshalSize() + if size > len(buf) { + return 0, io.ErrShortBuffer + } + + // The first byte contains the version, padding bit, extension bit, + // and csrc size. + buf[0] = (h.Version << versionShift) | uint8(len(h.CSRC)) // nolint: gosec // G115 + if h.Padding { + buf[0] |= 1 << paddingShift + } + + if h.Extension { + buf[0] |= 1 << extensionShift + } + + // The second byte contains the marker bit and payload type. + buf[1] = h.PayloadType + if h.Marker { + buf[1] |= 1 << markerShift + } + + binary.BigEndian.PutUint16(buf[2:4], h.SequenceNumber) + binary.BigEndian.PutUint32(buf[4:8], h.Timestamp) + binary.BigEndian.PutUint32(buf[8:12], h.SSRC) + + n = 12 + for _, csrc := range h.CSRC { + binary.BigEndian.PutUint32(buf[n:n+4], csrc) + n += 4 + } + + if h.Extension { + extHeaderPos := n + binary.BigEndian.PutUint16(buf[n+0:n+2], h.ExtensionProfile) + n += 4 + startExtensionsPos := n + + switch h.ExtensionProfile { + // RFC 8285 RTP One Byte Header Extension + case ExtensionProfileOneByte: + for _, extension := range h.Extensions { + buf[n] = extension.id<<4 | (uint8(len(extension.payload)) - 1) // nolint: gosec // G115 + n++ + n += copy(buf[n:], extension.payload) + } + // RFC 8285 RTP Two Byte Header Extension + case ExtensionProfileTwoByte: + for _, extension := range h.Extensions { + buf[n] = extension.id + n++ + buf[n] = uint8(len(extension.payload)) // nolint: gosec // G115 + n++ + n += copy(buf[n:], extension.payload) + } + default: // RFC3550 Extension + // Zero length extension is valid per the RFC3550 spec + // https://www.rfc-editor.org/rfc/rfc3550#section-5.3.1 + if len(h.Extensions) > 0 { + extlen := len(h.Extensions[0].payload) + if extlen%4 != 0 { + // the payload must be in 32-bit words. + return 0, io.ErrShortBuffer + } + n += copy(buf[n:], h.Extensions[0].payload) + } + } + + // calculate extensions size and round to 4 bytes boundaries + extSize := n - startExtensionsPos + roundedExtSize := ((extSize + 3) / 4) * 4 + + // nolint: gosec // G115 false positive + binary.BigEndian.PutUint16(buf[extHeaderPos+2:extHeaderPos+4], uint16(roundedExtSize/4)) + + // add padding to reach 4 bytes boundaries + for i := 0; i < roundedExtSize-extSize; i++ { + buf[n] = 0 + n++ + } + } + + return n, nil +} + +// MarshalSize returns the size of the header once marshaled. +func (h Header) MarshalSize() int { + // NOTE: Be careful to match the MarshalTo() method. + size := 12 + (len(h.CSRC) * csrcLength) + + if h.Extension { + extSize := 4 + + switch h.ExtensionProfile { + // RFC 8285 RTP One Byte Header Extension + case ExtensionProfileOneByte: + for _, extension := range h.Extensions { + extSize += 1 + len(extension.payload) + } + // RFC 8285 RTP Two Byte Header Extension + case ExtensionProfileTwoByte: + for _, extension := range h.Extensions { + extSize += 2 + len(extension.payload) + } + default: + if len(h.Extensions) > 0 { + extSize += len(h.Extensions[0].payload) + } + } + + // extensions size must have 4 bytes boundaries + size += ((extSize + 3) / 4) * 4 + } + + return size +} + +// SetExtension sets an RTP header extension. +func (h *Header) SetExtension(id uint8, payload []byte) error { //nolint:gocognit, cyclop + if h.Extension { // nolint: nestif + if err := headerExtensionCheck(h.ExtensionProfile, id, payload); err != nil { + return err + } + + // Update existing if it exists else add new extension + for i, extension := range h.Extensions { + if extension.id == id { + h.Extensions[i].payload = payload + + return nil + } + } + + h.Extensions = append(h.Extensions, Extension{id: id, payload: payload}) + + return nil + } + + // No existing header extensions + h.Extension = true + + switch payloadLen := len(payload); { + case payloadLen <= 16: + h.ExtensionProfile = ExtensionProfileOneByte + case payloadLen > 16 && payloadLen < 256: + h.ExtensionProfile = ExtensionProfileTwoByte + } + + h.Extensions = append(h.Extensions, Extension{id: id, payload: payload}) + + return nil +} + +// SetExtensionWithProfile sets an RTP header extension and converts Header Extension Profile if needed. +func (h *Header) SetExtensionWithProfile(id uint8, payload []byte, intendedProfile uint16) error { + if !h.Extension || h.ExtensionProfile == intendedProfile { + return h.SetExtension(id, payload) + } + + // Don't mutate the packet if Set is going to fail anyway + if err := headerExtensionCheck(intendedProfile, id, payload); err != nil { + return err + } + + // If downgrading assert that existing Extensions will work + if intendedProfile == ExtensionProfileOneByte { + for i := range h.Extensions { + if err := headerExtensionCheck(intendedProfile, h.Extensions[i].id, h.Extensions[i].payload); err != nil { + return err + } + } + } + + h.ExtensionProfile = intendedProfile + + return h.SetExtension(id, payload) +} + +// GetExtensionIDs returns an extension id array. +func (h *Header) GetExtensionIDs() []uint8 { + if !h.Extension { + return nil + } + + if len(h.Extensions) == 0 { + return nil + } + + ids := make([]uint8, 0, len(h.Extensions)) + for _, extension := range h.Extensions { + ids = append(ids, extension.id) + } + + return ids +} + +// GetExtension returns an RTP header extension. +func (h *Header) GetExtension(id uint8) []byte { + if !h.Extension { + return nil + } + for _, extension := range h.Extensions { + if extension.id == id { + return extension.payload + } + } + + return nil +} + +// DelExtension Removes an RTP Header extension. +func (h *Header) DelExtension(id uint8) error { + if !h.Extension { + return errHeaderExtensionsNotEnabled + } + for i, extension := range h.Extensions { + if extension.id == id { + h.Extensions = append(h.Extensions[:i], h.Extensions[i+1:]...) + + return nil + } + } + + return errHeaderExtensionNotFound +} + +// Marshal serializes the packet into bytes. +func (p Packet) Marshal() (buf []byte, err error) { + buf = make([]byte, p.MarshalSize()) + + n, err := p.MarshalTo(buf) + if err != nil { + return nil, err + } + + return buf[:n], nil +} + +// MarshalTo serializes the packet and writes to the buffer. +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 + } + + return marshalPayloadAndPaddingTo(buf, n, &p.Header, p.Payload, p.paddingSize()) +} + +func marshalPayloadAndPaddingTo(buf []byte, offset int, header *Header, payload []byte, paddingSize byte, +) (n int, err error) { + // Make sure the buffer is large enough to hold the packet. + if offset+len(payload)+int(paddingSize) > len(buf) { + return 0, io.ErrShortBuffer + } + + m := copy(buf[offset:], payload) + + if header.Padding { + buf[offset+m+int(paddingSize-1)] = paddingSize + } + + return offset + m + int(paddingSize), nil +} + +// MarshalSize returns the size of the packet once marshaled. +func (p Packet) MarshalSize() int { + return p.Header.MarshalSize() + len(p.Payload) + int(p.paddingSize()) +} + +// Clone returns a deep copy of p. +func (p Packet) Clone() *Packet { + clone := &Packet{} + clone.Header = p.Header.Clone() + if p.Payload != nil { + clone.Payload = make([]byte, len(p.Payload)) + copy(clone.Payload, p.Payload) + } + clone.PaddingSize = p.PaddingSize + + return clone +} + +// Clone returns a deep copy h. +func (h Header) Clone() Header { + clone := h + if h.CSRC != nil { + clone.CSRC = make([]uint32, len(h.CSRC)) + copy(clone.CSRC, h.CSRC) + } + if h.Extensions != nil { + ext := make([]Extension, len(h.Extensions)) + for i, e := range h.Extensions { + ext[i] = e + if e.payload != nil { + ext[i].payload = make([]byte, len(e.payload)) + copy(ext[i].payload, e.payload) + } + } + clone.Extensions = ext + } + + return clone +} + +func (p *Packet) paddingSize() byte { + if p.Header.PaddingSize > 0 { + return p.Header.PaddingSize + } + + return p.PaddingSize +} + +// MarshalPacketTo serializes the header and payload into bytes. +// Parts of pion code passes RTP header and payload separately, so this function +// is provided to help with that. +// +// Deprecated: this function is a temporary workaround and will be removed in pion/webrtc v5. +func MarshalPacketTo(buf []byte, header *Header, payload []byte) (int, error) { + n, err := header.MarshalTo(buf) + if err != nil { + return 0, err + } + + return marshalPayloadAndPaddingTo(buf, n, header, payload, header.PaddingSize) +} + +// PacketMarshalSize returns the size of the header and payload once marshaled. +// Parts of pion code passes RTP header and payload separately, so this function +// is provided to help with that. +// +// Deprecated: this function is a temporary workaround and will be removed in pion/webrtc v5. +func PacketMarshalSize(header *Header, payload []byte) int { + return header.MarshalSize() + len(payload) + int(header.PaddingSize) +} + +// HeaderAndPacketMarshalSize returns the size of the header and full packet once marshaled. +// Parts of pion code passes RTP header and payload separately, so this function +// is provided to help with that. +// +// Deprecated: this function is a temporary workaround and will be removed in pion/webrtc v5. +func HeaderAndPacketMarshalSize(header *Header, payload []byte) (headerSize int, packetSize int) { + headerSize = header.MarshalSize() + + return headerSize, headerSize + len(payload) + int(header.PaddingSize) +} diff --git a/vendor/github.com/pion/rtp/packetizer.go b/vendor/github.com/pion/rtp/packetizer.go new file mode 100644 index 0000000..f5bfc15 --- /dev/null +++ b/vendor/github.com/pion/rtp/packetizer.go @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "time" +) + +// Payloader payloads a byte array for use as rtp.Packet payloads. +type Payloader interface { + Payload(mtu uint16, payload []byte) [][]byte +} + +// 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 + + // Deprecated: will be removed in a future version. + ClockRate uint32 + + // put extension numbers in here. If they're 0, the extension is disabled (0 is not a legal extension number) + extensionNumbers struct { + AbsSendTime int // http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time + } + timegen func() time.Time +} + +// NewPacketizer returns a new instance of a Packetizer for a specific payloader. +func NewPacketizer( + mtu uint16, + pt uint8, + ssrc uint32, + payloader Payloader, + sequencer Sequencer, + clockRate uint32, +) Packetizer { + return &packetizer{ + MTU: mtu, + PayloadType: pt, + SSRC: ssrc, + Payloader: payloader, + Sequencer: sequencer, + Timestamp: globalMathRandomGenerator.Uint32(), + ClockRate: clockRate, + timegen: time.Now, + } +} + +// WithSSRC sets the SSRC for the Packetizer. +func WithSSRC(ssrc uint32) func(*packetizer) { + return func(p *packetizer) { + p.SSRC = ssrc + } +} + +// WithPayloadType sets the PayloadType for the Packetizer. +func WithPayloadType(pt uint8) func(*packetizer) { + return func(p *packetizer) { + p.PayloadType = pt + } +} + +// WithTimestamp sets the initial Timestamp for the Packetizer. +func WithTimestamp(timestamp uint32) func(*packetizer) { + return func(p *packetizer) { + p.Timestamp = timestamp + } +} + +// PacketizerOption is a function that configures a RTP Packetizer. +type PacketizerOption func(*packetizer) + +// NewPacketizerWithOptions returns a new instance of a Packetizer with the given options. +func NewPacketizerWithOptions( + mtu uint16, + payloader Payloader, + sequencer Sequencer, + clockRate uint32, + options ...PacketizerOption, +) Packetizer { + packetizerInstance := &packetizer{ + MTU: mtu, + Payloader: payloader, + Sequencer: sequencer, + Timestamp: globalMathRandomGenerator.Uint32(), + ClockRate: clockRate, + timegen: time.Now, + } + + for _, option := range options { + option(packetizerInstance) + } + + return packetizerInstance +} + +func (p *packetizer) EnableAbsSendTime(value int) { + p.extensionNumbers.AbsSendTime = value +} + +// Packetize packetizes the payload of an RTP packet and returns one or more RTP packets. +func (p *packetizer) Packetize(payload []byte, samples uint32) []*Packet { + // Guard against an empty payload + if len(payload) == 0 { + p.SkipSamples(samples) + + return nil + } + + payloads := p.Payloader.Payload(p.MTU-12, payload) + packets := make([]*Packet, len(payloads)) + + for i, pp := range payloads { + packets[i] = &Packet{ + Header: Header{ + Version: 2, + Padding: false, + Extension: false, + Marker: i == len(payloads)-1, + PayloadType: p.PayloadType, + SequenceNumber: p.Sequencer.NextSequenceNumber(), + Timestamp: p.Timestamp, // Figure out how to do timestamps + SSRC: p.SSRC, + }, + Payload: pp, + } + } + p.Timestamp += samples + + if len(packets) != 0 && p.extensionNumbers.AbsSendTime != 0 { + sendTime := NewAbsSendTimeExtension(p.timegen()) + // apply http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time + b, err := sendTime.Marshal() + if err != nil { + return nil // never happens + } + err = packets[len(packets)-1].SetExtension(uint8(p.extensionNumbers.AbsSendTime), b) // nolint: gosec // G115 + if err != nil { + return nil // never happens + } + } + + 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++ { + 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, + PaddingSize: 255, + }, + } + } + + 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) { + p.Timestamp += skippedSamples +} diff --git a/vendor/github.com/pion/rtp/partitionheadchecker.go b/vendor/github.com/pion/rtp/partitionheadchecker.go new file mode 100644 index 0000000..10a0994 --- /dev/null +++ b/vendor/github.com/pion/rtp/partitionheadchecker.go @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +// PartitionHeadChecker is the interface that checks whether the packet is keyframe or not. +type PartitionHeadChecker interface { + IsPartitionHead([]byte) bool +} diff --git a/vendor/github.com/pion/rtp/payload_types.go b/vendor/github.com/pion/rtp/payload_types.go new file mode 100644 index 0000000..2cb4d3f --- /dev/null +++ b/vendor/github.com/pion/rtp/payload_types.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2026 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/vendor/github.com/pion/rtp/playoutdelayextension.go b/vendor/github.com/pion/rtp/playoutdelayextension.go new file mode 100644 index 0000000..bb728ff --- /dev/null +++ b/vendor/github.com/pion/rtp/playoutdelayextension.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "errors" + "io" +) + +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 +} + +// MarshalSize returns the size of the PlayoutDelayExtension once marshaled. +func (p PlayoutDelayExtension) MarshalSize() int { + return playoutDelayExtensionSize +} + +// MarshalTo marshals the extension to the given buffer. +// Returns io.ErrShortBuffer if buf is too small. +func (p PlayoutDelayExtension) MarshalTo(buf []byte) (int, error) { + if p.MinDelay > playoutDelayMaxValue || p.MaxDelay > playoutDelayMaxValue { + return 0, errPlayoutDelayInvalidValue + } + if len(buf) < playoutDelayExtensionSize { + return 0, io.ErrShortBuffer + } + buf[0] = byte(p.MinDelay >> 4) + buf[1] = byte(p.MinDelay<<4) | byte(p.MaxDelay>>8) + buf[2] = byte(p.MaxDelay) + + return playoutDelayExtensionSize, nil +} + +// 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/vendor/github.com/pion/rtp/rand.go b/vendor/github.com/pion/rtp/rand.go new file mode 100644 index 0000000..2ded2bb --- /dev/null +++ b/vendor/github.com/pion/rtp/rand.go @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "github.com/pion/randutil" +) + +// Use global random generator to properly seed by crypto grade random. +var globalMathRandomGenerator = randutil.NewMathRandomGenerator() // nolint:gochecknoglobals diff --git a/vendor/github.com/pion/rtp/renovate.json b/vendor/github.com/pion/rtp/renovate.json new file mode 100644 index 0000000..f1bb98c --- /dev/null +++ b/vendor/github.com/pion/rtp/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>pion/renovate-config" + ] +} diff --git a/vendor/github.com/pion/rtp/rtp.go b/vendor/github.com/pion/rtp/rtp.go new file mode 100644 index 0000000..4ac2318 --- /dev/null +++ b/vendor/github.com/pion/rtp/rtp.go @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +// Package rtp provides RTP packetizer and depacketizer +package rtp diff --git a/vendor/github.com/pion/rtp/sequencer.go b/vendor/github.com/pion/rtp/sequencer.go new file mode 100644 index 0000000..ab554cd --- /dev/null +++ b/vendor/github.com/pion/rtp/sequencer.go @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "sync/atomic" +) + +// Sequencer generates sequential sequence numbers for building RTP packets. +type Sequencer interface { + NextSequenceNumber() uint16 + 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 { + s := &sequencer{} + s.state.Store(uint64(globalMathRandomGenerator.Intn(maxInitialRandomSequenceNumber))) // nolint: gosec // G115 + + return s +} + +// NewFixedSequencer returns a new sequencer starting from a specific +// sequence number. +func NewFixedSequencer(s uint16) Sequencer { + seq := &sequencer{} + seq.state.Store(uint64(s - 1)) // -1 because the first sequence number prepends 1 + + return seq +} + +type sequencer struct { + // state packs both sequenceNumber (lower 16 bits) and rollOverCount (upper 48 bits) + // into a single atomic uint64 + state atomic.Uint64 +} + +// NextSequenceNumber increment and returns a new sequence number for +// building RTP packets. +func (s *sequencer) NextSequenceNumber() uint16 { + return uint16(s.state.Add(1)) // nolint: gosec // G115 +} + +// RollOverCount returns the amount of times the 16bit sequence number +// has wrapped. +func (s *sequencer) RollOverCount() uint64 { + return s.state.Load() >> 16 +} diff --git a/vendor/github.com/pion/rtp/transportccextension.go b/vendor/github.com/pion/rtp/transportccextension.go new file mode 100644 index 0000000..4f60661 --- /dev/null +++ b/vendor/github.com/pion/rtp/transportccextension.go @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "io" +) + +const ( + // transport-wide sequence. + transportCCExtensionSize = 2 +) + +// TransportCCExtension is a extension payload format in +// https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01 +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | 0xBE | 0xDE | length=1 | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ID | L=1 |transport-wide sequence number | zero padding | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// . +type TransportCCExtension struct { + TransportSequence uint16 +} + +// MarshalSize returns the size of the TransportCCExtension once marshaled. +func (t TransportCCExtension) MarshalSize() int { + return transportCCExtensionSize +} + +// MarshalTo marshals the extension to the given buffer. +// Returns io.ErrShortBuffer if buf is too small. +func (t TransportCCExtension) MarshalTo(buf []byte) (int, error) { + if len(buf) < transportCCExtensionSize { + return 0, io.ErrShortBuffer + } + binary.BigEndian.PutUint16(buf[0:2], t.TransportSequence) + + return transportCCExtensionSize, nil +} + +// Marshal serializes the members to buffer. +func (t TransportCCExtension) Marshal() ([]byte, error) { + buf := make([]byte, transportCCExtensionSize) + binary.BigEndian.PutUint16(buf[0:2], t.TransportSequence) + + return buf, nil +} + +// Unmarshal parses the passed byte slice and stores the result in the members. +func (t *TransportCCExtension) Unmarshal(rawData []byte) error { + if len(rawData) < transportCCExtensionSize { + return errTooSmall + } + t.TransportSequence = binary.BigEndian.Uint16(rawData[0:2]) + + return nil +} diff --git a/vendor/github.com/pion/rtp/vlaextension.go b/vendor/github.com/pion/rtp/vlaextension.go new file mode 100644 index 0000000..bf69a1a --- /dev/null +++ b/vendor/github.com/pion/rtp/vlaextension.go @@ -0,0 +1,435 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "strings" + + "github.com/pion/rtp/codecs/av1/obu" +) + +var ( + // ErrVLATooShort is returned when payload is too short. + ErrVLATooShort = errors.New("VLA payload too short") + // ErrVLAInvalidStreamCount is returned when RTP stream count is invalid. + ErrVLAInvalidStreamCount = errors.New("invalid RTP stream count in VLA") + // ErrVLAInvalidStreamID is returned when RTP stream ID is invalid. + ErrVLAInvalidStreamID = errors.New("invalid RTP stream ID in VLA") + // ErrVLAInvalidSpatialID is returned when spatial ID is invalid. + ErrVLAInvalidSpatialID = errors.New("invalid spatial ID in VLA") + // ErrVLADuplicateSpatialID is returned when spatial ID is invalid. + ErrVLADuplicateSpatialID = errors.New("duplicate spatial ID in VLA") + // ErrVLAInvalidTemporalLayer is returned when temporal layer is invalid. + ErrVLAInvalidTemporalLayer = errors.New("invalid temporal layer in VLA") +) + +// 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 + slIndices [4][4]int // index into ActiveSpatialLayer, -1 if not set + commonSLBM uint8 + requiredLen int +} + +func (v VLA) preprocessForMashaling(ctx *vlaMarshalingContext) error { //nolint:cyclop + // Initialize indices to -1 (not set) + for i := range ctx.slIndices { + for j := range ctx.slIndices[i] { + ctx.slIndices[i][j] = -1 + } + } + + for i := range v.ActiveSpatialLayer { + 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.slIndices[sl.RTPStreamID][sl.SpatialID] != -1 { + return fmt.Errorf("duplicate spatial layer: %w", ErrVLADuplicateSpatialID) + } + ctx.slIndices[sl.RTPStreamID][sl.SpatialID] = i + } + + return nil +} + +func (v VLA) calcTargetBitratesSize(ctx *vlaMarshalingContext) { + for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if idx := ctx.slIndices[rtpStreamID][spatialID]; idx >= 0 { + for _, kbps := range v.ActiveSpatialLayer[idx].TargetBitrates { + ctx.requiredLen += leb128Size(uint(kbps)) //nolint:gosec + } + } + } + } +} + +func (v VLA) analyzeVLAForMarshaling(ctx *vlaMarshalingContext) error { + // Validate RTPStreamCount + if v.RTPStreamCount <= 0 || v.RTPStreamCount > 4 { + return ErrVLAInvalidStreamCount + } + // Validate RTPStreamID + if v.RTPStreamID < 0 || v.RTPStreamID >= v.RTPStreamCount { + return ErrVLAInvalidStreamID + } + + err := v.preprocessForMashaling(ctx) + if err != nil { + return 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.calcTargetBitratesSize(ctx) + + if v.HasResolutionAndFramerate { + ctx.requiredLen += len(v.ActiveSpatialLayer) * 5 + } + + return nil +} + +// MarshalSize returns the size needed to marshal the VLA. +func (v VLA) MarshalSize() (int, error) { + var ctx vlaMarshalingContext + if err := v.analyzeVLAForMarshaling(&ctx); err != nil { + return 0, err + } + + return ctx.requiredLen, nil +} + +// MarshalTo marshals the VLA to the given buffer. +// Returns io.ErrShortBuffer if buf is too small. +func (v VLA) MarshalTo(buf []byte) (int, error) { //nolint:cyclop,gocognit + var ctx vlaMarshalingContext + if err := v.analyzeVLAForMarshaling(&ctx); err != nil { + return 0, err + } + + if len(buf) < ctx.requiredLen { + return 0, io.ErrShortBuffer + } + + offset := 0 + + // RID, NS, sl_bm fields + buf[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 { + buf[offset+streamID/2] |= ctx.slMBs[streamID] << 4 + } else { + buf[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 idx := ctx.slIndices[rtpStreamID][spatialID]; idx >= 0 { + if temporalLayerIndex >= 4 { + temporalLayerIndex = 0 + offset++ + } + buf[offset] |= byte(len(v.ActiveSpatialLayer[idx].TargetBitrates)-1) << (2 * (3 - temporalLayerIndex)) + temporalLayerIndex++ + } + } + } + + // Target bitrate fields + offset++ + for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if idx := ctx.slIndices[rtpStreamID][spatialID]; idx >= 0 { + for _, kbps := range v.ActiveSpatialLayer[idx].TargetBitrates { + offset += writeLeb128To(buf[offset:], uint(kbps)) //nolint:gosec + } + } + } + } + + // Resolution & framerate fields + if v.HasResolutionAndFramerate { + for _, sl := range v.ActiveSpatialLayer { + binary.BigEndian.PutUint16(buf[offset+0:], uint16(sl.Width-1)) //nolint:gosec + binary.BigEndian.PutUint16(buf[offset+2:], uint16(sl.Height-1)) //nolint:gosec + buf[offset+4] = byte(sl.Framerate) + offset += 5 + } + } + + return ctx.requiredLen, nil +} + +// Marshal encodes VLA into a byte slice. +func (v VLA) Marshal() ([]byte, error) { + size, err := v.MarshalSize() + if err != nil { + return nil, err + } + + buf := make([]byte, size) + _, err = v.MarshalTo(buf) + if err != nil { + return nil, err + } + + return buf, 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 { // nolint: cyclop + 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 + } + + in := int(n) // nolint: gosec + + if !ctx.checkRemainingLen(in) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + v.ActiveSpatialLayer[i].TargetBitrates[j] = int(kbps) // nolint: gosec + ctx.offset += in + } + } + + 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 +} + +// leb128Size returns the number of bytes needed to encode a value as LEB128. +func leb128Size(in uint) int { + size := 1 + for in >>= 7; in != 0; in >>= 7 { + size++ + } + + return size +} + +// writeLeb128To writes a LEB128 encoded value to buf and returns bytes written. +func writeLeb128To(buf []byte, in uint) int { + for i := range buf { + buf[i] = byte(in & 0x7f) + in >>= 7 + if in == 0 { + return i + 1 + } + buf[i] |= 0x80 + } + + return 0 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 78adea8..bb084c6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -255,6 +255,13 @@ github.com/minio/minio-go/v7/pkg/tags # github.com/mitchellh/mapstructure v1.5.0 ## explicit; go 1.14 github.com/mitchellh/mapstructure +# github.com/pion/randutil v0.1.0 +## explicit; go 1.14 +github.com/pion/randutil +# github.com/pion/rtp v1.10.1 +## explicit; go 1.21 +github.com/pion/rtp +github.com/pion/rtp/codecs/av1/obu # github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 ## explicit github.com/pmezard/go-difflib/difflib