From 930bd859f2cae6196e1def460c6474089f5b738b Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Wed, 4 Oct 2023 12:18:12 -0400 Subject: [PATCH 01/14] Remove redundant state in H264 Packetization No code changes. Just removes an additional variable --- codecs/h264_packet.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/codecs/h264_packet.go b/codecs/h264_packet.go index e53cea0..583e189 100644 --- a/codecs/h264_packet.go +++ b/codecs/h264_packet.go @@ -133,18 +133,17 @@ func (p *H264Payloader) Payload(mtu uint16, payload []byte) [][]byte { // the FU header. An FU payload MAY have any number of octets and MAY // be empty. - naluData := nalu // According to the RFC, the first octet is skipped due to redundant information - naluDataIndex := 1 - naluDataLength := len(nalu) - naluDataIndex - naluDataRemaining := naluDataLength + naluIndex := 1 + naluLength := len(nalu) - naluIndex + naluRemaining := naluLength - if min(maxFragmentSize, naluDataRemaining) <= 0 { + if min(maxFragmentSize, naluRemaining) <= 0 { return } - for naluDataRemaining > 0 { - currentFragmentSize := min(maxFragmentSize, naluDataRemaining) + for naluRemaining > 0 { + currentFragmentSize := min(maxFragmentSize, naluRemaining) out := make([]byte, fuaHeaderSize+currentFragmentSize) // +---------------+ @@ -162,19 +161,19 @@ func (p *H264Payloader) Payload(mtu uint16, payload []byte) [][]byte { // +---------------+ out[1] = naluType - if naluDataRemaining == naluDataLength { + if naluRemaining == naluLength { // Set start bit out[1] |= 1 << 7 - } else if naluDataRemaining-currentFragmentSize == 0 { + } else if naluRemaining-currentFragmentSize == 0 { // Set end bit out[1] |= 1 << 6 } - copy(out[fuaHeaderSize:], naluData[naluDataIndex:naluDataIndex+currentFragmentSize]) + copy(out[fuaHeaderSize:], nalu[naluIndex:naluIndex+currentFragmentSize]) payloads = append(payloads, out) - naluDataRemaining -= currentFragmentSize - naluDataIndex += currentFragmentSize + naluRemaining -= currentFragmentSize + naluIndex += currentFragmentSize } }) From 7dc2af56736b663e76f1400ba403532ba590bceb Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Thu, 9 Nov 2023 10:53:12 -0500 Subject: [PATCH 02/14] Combine OneByte and TwoByte extension parsing Reduces duplicated logic and increase coverage. Missing tests for bounds check on Padding and Payload Sizes Co-Authored-By: Juho Nurminen --- AUTHORS.txt | 3 +++ packet.go | 69 ++++++++++++++++++++++---------------------------- packet_test.go | 28 ++++++++++++++++++++ 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 727aaaa..f8dbb32 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -18,6 +18,7 @@ Guilherme Haiyang Wang Hugo Arregui John Bradley +Juho Nurminen Juliusz Chroboczek kawaway Kazuyuki Honda @@ -31,6 +32,7 @@ Raphael Derosso Pereira Rob Lofthouse Robin Raymond Sean +Sean Sean DuBois Sean DuBois Sean DuBois @@ -40,6 +42,7 @@ Steffen Vogel Tarrence van As wangzixiang Woodrow Douglass +訾明华 <565209960@qq.com> # List of contributors not appearing in Git history diff --git a/packet.go b/packet.go index 7aebb14..af88af3 100644 --- a/packet.go +++ b/packet.go @@ -149,64 +149,55 @@ func (h *Header) Unmarshal(buf []byte) (n int, err error) { //nolint:gocognit n += 2 extensionLength := int(binary.BigEndian.Uint16(buf[n:])) * 4 n += 2 + extensionEnd := n + extensionLength - if expected := n + extensionLength; len(buf) < expected { - return n, fmt.Errorf("size %d < %d: %w", - len(buf), expected, - errHeaderSizeInsufficientForExtension, - ) + if len(buf) < extensionEnd { + return n, fmt.Errorf("size %d < %d: %w", len(buf), extensionEnd, errHeaderSizeInsufficientForExtension) } - switch h.ExtensionProfile { - // RFC 8285 RTP One Byte Header Extension - case extensionProfileOneByte: - end := n + extensionLength - for n < end { + if h.ExtensionProfile == extensionProfileOneByte || h.ExtensionProfile == extensionProfileTwoByte { + var ( + extid uint8 + payloadLen int + ) + + for n < extensionEnd { if buf[n] == 0x00 { // padding n++ continue } - extid := buf[n] >> 4 - payloadLen := int(buf[n]&^0xF0 + 1) - n++ + if h.ExtensionProfile == extensionProfileOneByte { + extid = buf[n] >> 4 + payloadLen = int(buf[n]&^0xF0 + 1) + n++ - if extid == extensionIDReserved { - break - } + if extid == extensionIDReserved { + break + } + } else { + extid = buf[n] + n++ - extension := Extension{id: extid, payload: buf[n : n+payloadLen]} - h.Extensions = append(h.Extensions, extension) - n += payloadLen - } + if len(buf) <= n { + return n, fmt.Errorf("size %d < %d: %w", len(buf), n, errHeaderSizeInsufficientForExtension) + } - // RFC 8285 RTP Two Byte Header Extension - case extensionProfileTwoByte: - end := n + extensionLength - for n < end { - if buf[n] == 0x00 { // padding + payloadLen = int(buf[n]) n++ - continue } - extid := buf[n] - n++ - - payloadLen := int(buf[n]) - n++ + if extensionPayloadEnd := n + payloadLen; len(buf) <= extensionPayloadEnd { + return n, fmt.Errorf("size %d < %d: %w", len(buf), extensionPayloadEnd, errHeaderSizeInsufficientForExtension) + } extension := Extension{id: extid, payload: buf[n : n+payloadLen]} h.Extensions = append(h.Extensions, extension) n += payloadLen } - - default: // RFC3550 Extension - if len(buf) < n+extensionLength { - return n, fmt.Errorf("%w: %d < %d", - errHeaderSizeInsufficientForExtension, len(buf), n+extensionLength) - } - - extension := Extension{id: 0, payload: buf[n : n+extensionLength]} + } else { + // RFC3550 Extension + extension := Extension{id: 0, payload: buf[n:extensionEnd]} h.Extensions = append(h.Extensions, extension) n += len(h.Extensions[0].payload) } diff --git a/packet_test.go b/packet_test.go index 3044c0d..98dfb40 100644 --- a/packet_test.go +++ b/packet_test.go @@ -1192,6 +1192,34 @@ func TestRFC8285TwoByteSetExtensionShouldErrorWhenPayloadTooLarge(t *testing.T) } } +func TestRFC8285Padding(t *testing.T) { + header := &Header{} + + for _, payload := range [][]byte{ + { + 0b00010000, // header.Extension = true + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // SequenceNumber, Timestamp, SSRC + 0xBE, 0xDE, // header.ExtensionProfile = extensionProfileOneByte + 0, 1, // extensionLength + 0, 0, 0, // padding + 1, // extid + }, + { + 0b00010000, // header.Extension = true + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // SequenceNumber, Timestamp, SSRC + 0x10, 0x00, // header.ExtensionProfile = extensionProfileOneByte + 0, 1, // extensionLength + 0, 0, 0, // padding + 1, // extid + }, + } { + _, err := header.Unmarshal(payload) + if !errors.Is(err, errHeaderSizeInsufficientForExtension) { + t.Fatal("Expected errHeaderSizeInsufficientForExtension") + } + } +} + func TestRFC3550SetExtensionShouldErrorWhenNonZero(t *testing.T) { payload := []byte{ // Payload From 5fb93805d93bb5e3589469b2780eaaf38c5ed53d Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Tue, 2 Jan 2024 11:43:10 -0500 Subject: [PATCH 03/14] Remove 'Generate Authors' workflow pion/.goassets#185 --- .github/workflows/generate-authors.yml | 23 ------------ AUTHORS.txt | 48 -------------------------- 2 files changed, 71 deletions(-) delete mode 100644 .github/workflows/generate-authors.yml delete mode 100644 AUTHORS.txt diff --git a/.github/workflows/generate-authors.yml b/.github/workflows/generate-authors.yml deleted file mode 100644 index ec7446c..0000000 --- a/.github/workflows/generate-authors.yml +++ /dev/null @@ -1,23 +0,0 @@ -# -# DO NOT EDIT THIS FILE -# -# It is automatically copied from https://github.com/pion/.goassets repository. -# If this repository should have package specific CI config, -# remove the repository name from .goassets/.github/workflows/assets-sync.yml. -# -# If you want to update the shared CI config, send a PR to -# https://github.com/pion/.goassets instead of this repository. -# -# SPDX-FileCopyrightText: 2023 The Pion community -# SPDX-License-Identifier: MIT - -name: Generate Authors - -on: - pull_request: - -jobs: - generate: - uses: pion/.goassets/.github/workflows/generate-authors.reusable.yml@master - secrets: - token: ${{ secrets.PIONBOT_PRIVATE_KEY }} diff --git a/AUTHORS.txt b/AUTHORS.txt deleted file mode 100644 index f8dbb32..0000000 --- a/AUTHORS.txt +++ /dev/null @@ -1,48 +0,0 @@ -# Thank you to everyone that made Pion possible. If you are interested in contributing -# we would love to have you https://github.com/pion/webrtc/wiki/Contributing -# -# This file is auto generated, using git to list all individuals contributors. -# see https://github.com/pion/.goassets/blob/master/scripts/generate-authors.sh for the scripting -Aaron Boushley -adwpc -aler9 <46489434+aler9@users.noreply.github.com> -Antoine Baché -Antoine Baché -Atsushi Watanabe -baiyufei -Bao Nguyen -boks1971 -debiandebiandebian -ffmiyo -Guilherme -Haiyang Wang -Hugo Arregui -John Bradley -Juho Nurminen -Juliusz Chroboczek -kawaway -Kazuyuki Honda -Kevin Wang -Luke Curley -lxb -Michael MacDonald -Michael MacDonald -Michael Uti -Raphael Derosso Pereira -Rob Lofthouse -Robin Raymond -Sean -Sean -Sean DuBois -Sean DuBois -Sean DuBois -Sean DuBois -Simone Gotti -Steffen Vogel -Tarrence van As -wangzixiang -Woodrow Douglass -訾明华 <565209960@qq.com> - -# List of contributors not appearing in Git history - From 83ef1446c4934e78074b6d0248baa4751f46d07f Mon Sep 17 00:00:00 2001 From: Pion <59523206+pionbot@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:16:03 +0000 Subject: [PATCH 04/14] Update CI configs to v0.11.0 Update lint scripts and CI configs. --- .golangci.yml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4e3eddf..6dd80c8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,7 +29,6 @@ linters: - bodyclose # checks whether HTTP response body is closed successfully - contextcheck # check the function whether use a non-inherited context - decorder # check declaration order and count of types, constants, variables and functions - - depguard # Go linter that checks if package imports are in a list of acceptable packages - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dupl # Tool for code clone detection - durationcheck # check for two durations multiplied together @@ -63,7 +62,6 @@ linters: - importas # Enforces consistent import aliases - ineffassign # Detects when assignments to existing variables are not used - misspell # Finds commonly misspelled English words in comments - - nakedret # Finds naked returns in functions greater than a specified function length - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. - noctx # noctx finds sending http request without context.Context @@ -81,6 +79,7 @@ linters: - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: + - depguard # Go linter that checks if package imports are in a list of acceptable packages - containedctx # containedctx is a linter that detects struct contained context.Context field - cyclop # checks function and package cyclomatic complexity - exhaustivestruct # Checks if all struct's fields are initialized @@ -94,6 +93,7 @@ linters: - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - maligned # Tool to detect Go structs that would take less memory if their fields were sorted + - nakedret # Finds naked returns in functions greater than a specified function length - nestif # Reports deeply nested if statements - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - nolintlint # Reports ill-formed or insufficient nolint directives @@ -111,22 +111,11 @@ linters: issues: exclude-use-default: false exclude-rules: - # Allow complex tests, better to be self contained - - path: _test\.go + # Allow complex tests and examples, better to be self contained + - path: (examples|main\.go|_test\.go) linters: - - gocognit - forbidigo - - # Allow complex main function in examples - - path: examples - text: "of func `main` is high" - linters: - gocognit - - # Allow forbidden identifiers in examples - - path: examples - linters: - - forbidigo # Allow forbidden identifiers in CLI commands - path: cmd From 314bd8ed85d7c7611dc67c5279f2b0d522ce3e88 Mon Sep 17 00:00:00 2001 From: Pion <59523206+pionbot@users.noreply.github.com> Date: Fri, 5 Jan 2024 00:04:59 +0000 Subject: [PATCH 05/14] Update CI configs to v0.11.3 Update lint scripts and CI configs. --- .github/workflows/test.yaml | 4 ++-- .github/workflows/tidy-check.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 31aada4..c8294ef 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,7 @@ jobs: uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: - go: ['1.20', '1.19'] # auto-update/supported-go-version-list + go: ['1.21', '1.20'] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} @@ -32,7 +32,7 @@ jobs: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: - go: ['1.20', '1.19'] # auto-update/supported-go-version-list + go: ['1.21', '1.20'] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} diff --git a/.github/workflows/tidy-check.yaml b/.github/workflows/tidy-check.yaml index 4d346d4..33d6b50 100644 --- a/.github/workflows/tidy-check.yaml +++ b/.github/workflows/tidy-check.yaml @@ -22,4 +22,4 @@ jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master with: - go-version: '1.20' # auto-update/latest-go-version + go-version: '1.21' # auto-update/latest-go-version From 78da5a2169f8f40819ba7847d62c3e71abb75549 Mon Sep 17 00:00:00 2001 From: Yutaka Takeda Date: Tue, 26 Dec 2023 14:12:32 -0800 Subject: [PATCH 06/14] Add VLA extention header parser Added VLA parser, builder and unit tests. --- codecs/av1/obu/leb128.go | 16 + codecs/av1/obu/leb128_test.go | 33 +++ vlaextension.go | 360 +++++++++++++++++++++++ vlaextension_test.go | 532 ++++++++++++++++++++++++++++++++++ 4 files changed, 941 insertions(+) create mode 100644 vlaextension.go create mode 100644 vlaextension_test.go diff --git a/codecs/av1/obu/leb128.go b/codecs/av1/obu/leb128.go index 38ce090..f5fcbf6 100644 --- a/codecs/av1/obu/leb128.go +++ b/codecs/av1/obu/leb128.go @@ -67,3 +67,19 @@ func ReadLeb128(in []byte) (uint, uint, error) { 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/codecs/av1/obu/leb128_test.go b/codecs/av1/obu/leb128_test.go index f92fff4..2b2336a 100644 --- a/codecs/av1/obu/leb128_test.go +++ b/codecs/av1/obu/leb128_test.go @@ -4,7 +4,10 @@ package obu import ( + "encoding/hex" "errors" + "fmt" + "math" "testing" ) @@ -40,3 +43,33 @@ func TestReadLeb128(t *testing.T) { t.Fatal("ReadLeb128 on a buffer with all MSB set should fail") } } + +func TestWriteToLeb128(t *testing.T) { + type testVector struct { + value uint + leb128 string + } + testVectors := []testVector{ + {150, "9601"}, + {240, "f001"}, + {400, "9003"}, + {720, "d005"}, + {1200, "b009"}, + {999999, "bf843d"}, + {0, "00"}, + {math.MaxUint32, "ffffffff0f"}, + } + + runTest := func(t *testing.T, v testVector) { + b := WriteToLeb128(v.value) + if v.leb128 != hex.EncodeToString(b) { + t.Errorf("Expected %s, got %s", v.leb128, hex.EncodeToString(b)) + } + } + + for _, v := range testVectors { + t.Run(fmt.Sprintf("encode %d", v.value), func(t *testing.T) { + runTest(t, v) + }) + } +} diff --git a/vlaextension.go b/vlaextension.go new file mode 100644 index 0000000..e10820a --- /dev/null +++ b/vlaextension.go @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "encoding/binary" + "errors" + "fmt" + "strings" + + "github.com/pion/rtp/codecs/av1/obu" +) + +var ( + ErrVLATooShort = errors.New("VLA payload too short") // ErrVLATooShort is returned when payload is too short + ErrVLAInvalidStreamCount = errors.New("invalid RTP stream count in VLA") // ErrVLAInvalidStreamCount is returned when RTP stream count is invalid + ErrVLAInvalidStreamID = errors.New("invalid RTP stream ID in VLA") // ErrVLAInvalidStreamID is returned when RTP stream ID is invalid + ErrVLAInvalidSpatialID = errors.New("invalid spatial ID in VLA") // ErrVLAInvalidSpatialID is returned when spatial ID is invalid + ErrVLADuplicateSpatialID = errors.New("duplicate spatial ID in VLA") // ErrVLADuplicateSpatialID is returned when spatial ID is invalid + ErrVLAInvalidTemporalLayer = errors.New("invalid temporal layer in VLA") // ErrVLAInvalidTemporalLayer is returned when temporal layer is invalid +) + +// SpatialLayer is a spatial layer in VLA. +type SpatialLayer struct { + RTPStreamID int + SpatialID int + TargetBitrates []int // target bitrates per temporal layer + + // Following members are valid only when HasResolutionAndFramerate is true + Width int + Height int + Framerate int +} + +// VLA is a Video Layer Allocation (VLA) extension. +// See https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/video-layers-allocation00 +type VLA struct { + RTPStreamID int // 0-origin RTP stream ID (RID) this allocation is sent on (0..3) + RTPStreamCount int // Number of RTP streams (1..4) + ActiveSpatialLayer []SpatialLayer + HasResolutionAndFramerate bool +} + +type vlaMarshalingContext struct { + slMBs [4]uint8 + sls [4][4]*SpatialLayer + commonSLBM uint8 + encodedTargetBitrates [][]byte + requiredLen int +} + +func (v VLA) preprocessForMashaling(ctx *vlaMarshalingContext) error { + for i := 0; i < len(v.ActiveSpatialLayer); i++ { + sl := v.ActiveSpatialLayer[i] + if sl.RTPStreamID < 0 || sl.RTPStreamID >= v.RTPStreamCount { + return fmt.Errorf("invalid RTP streamID %d:%w", sl.RTPStreamID, ErrVLAInvalidStreamID) + } + if sl.SpatialID < 0 || sl.SpatialID >= 4 { + return fmt.Errorf("invalid spatial ID %d: %w", sl.SpatialID, ErrVLAInvalidSpatialID) + } + if len(sl.TargetBitrates) == 0 || len(sl.TargetBitrates) > 4 { + return fmt.Errorf("invalid temporal layer count %d: %w", len(sl.TargetBitrates), ErrVLAInvalidTemporalLayer) + } + ctx.slMBs[sl.RTPStreamID] |= 1 << sl.SpatialID + if ctx.sls[sl.RTPStreamID][sl.SpatialID] != nil { + return fmt.Errorf("duplicate spatial layer: %w", ErrVLADuplicateSpatialID) + } + ctx.sls[sl.RTPStreamID][sl.SpatialID] = &sl + } + return nil +} + +func (v VLA) encodeTargetBitrates(ctx *vlaMarshalingContext) { + for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if sl := ctx.sls[rtpStreamID][spatialID]; sl != nil { + for _, kbps := range sl.TargetBitrates { + leb128 := obu.WriteToLeb128(uint(kbps)) + ctx.encodedTargetBitrates = append(ctx.encodedTargetBitrates, leb128) + ctx.requiredLen += len(leb128) + } + } + } + } +} + +func (v VLA) analyzeVLAForMarshaling() (*vlaMarshalingContext, error) { + // Validate RTPStreamCount + if v.RTPStreamCount <= 0 || v.RTPStreamCount > 4 { + return nil, ErrVLAInvalidStreamCount + } + // Validate RTPStreamID + if v.RTPStreamID < 0 || v.RTPStreamID >= v.RTPStreamCount { + return nil, ErrVLAInvalidStreamID + } + + ctx := &vlaMarshalingContext{} + err := v.preprocessForMashaling(ctx) + if err != nil { + return nil, err + } + + ctx.commonSLBM = commonSLBMValues(ctx.slMBs[:]) + + // RID, NS, sl_bm fields + if ctx.commonSLBM != 0 { + ctx.requiredLen = 1 + } else { + ctx.requiredLen = 3 + } + + // #tl fields + ctx.requiredLen += (len(v.ActiveSpatialLayer)-1)/4 + 1 + + v.encodeTargetBitrates(ctx) + + if v.HasResolutionAndFramerate { + ctx.requiredLen += len(v.ActiveSpatialLayer) * 5 + } + + return ctx, nil +} + +// Marshal encodes VLA into a byte slice. +func (v VLA) Marshal() ([]byte, error) { + ctx, err := v.analyzeVLAForMarshaling() + if err != nil { + return nil, err + } + + payload := make([]byte, ctx.requiredLen) + offset := 0 + + // RID, NS, sl_bm fields + payload[offset] = byte(v.RTPStreamID<<6) | byte(v.RTPStreamCount-1)<<4 | ctx.commonSLBM + + if ctx.commonSLBM == 0 { + offset++ + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + if streamID%2 == 0 { + payload[offset+streamID/2] |= ctx.slMBs[streamID] << 4 + } else { + payload[offset+streamID/2] |= ctx.slMBs[streamID] + } + } + offset += (v.RTPStreamCount - 1) / 2 + } + + // #tl fields + offset++ + var temporalLayerIndex int + for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if sl := ctx.sls[rtpStreamID][spatialID]; sl != nil { + if temporalLayerIndex >= 4 { + temporalLayerIndex = 0 + offset++ + } + payload[offset] |= byte(len(sl.TargetBitrates)-1) << (2 * (3 - temporalLayerIndex)) + temporalLayerIndex++ + } + } + } + + // Target bitrate fields + offset++ + for _, encodedKbps := range ctx.encodedTargetBitrates { + encodedSize := len(encodedKbps) + copy(payload[offset:], encodedKbps) + offset += encodedSize + } + + // Resolution & framerate fields + if v.HasResolutionAndFramerate { + for _, sl := range v.ActiveSpatialLayer { + binary.BigEndian.PutUint16(payload[offset+0:], uint16(sl.Width-1)) + binary.BigEndian.PutUint16(payload[offset+2:], uint16(sl.Height-1)) + payload[offset+4] = byte(sl.Framerate) + offset += 5 + } + } + + return payload, nil +} + +func commonSLBMValues(slMBs []uint8) uint8 { + var common uint8 + for i := 0; i < len(slMBs); i++ { + if slMBs[i] == 0 { + continue + } + if common == 0 { + common = slMBs[i] + continue + } + if slMBs[i] != common { + return 0 + } + } + return common +} + +type vlaUnmarshalingContext struct { + payload []byte + offset int + slBMField uint8 + slBMs [4]uint8 +} + +func (ctx *vlaUnmarshalingContext) checkRemainingLen(requiredLen int) bool { + return len(ctx.payload)-ctx.offset >= requiredLen +} + +func (v *VLA) unmarshalSpatialLayers(ctx *vlaUnmarshalingContext) error { + if !ctx.checkRemainingLen(1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + v.RTPStreamID = int(ctx.payload[ctx.offset] >> 6 & 0b11) + v.RTPStreamCount = int(ctx.payload[ctx.offset]>>4&0b11) + 1 + + // sl_bm fields + ctx.slBMField = ctx.payload[ctx.offset] & 0b1111 + ctx.offset++ + + if ctx.slBMField != 0 { + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + ctx.slBMs[streamID] = ctx.slBMField + } + } else { + if !ctx.checkRemainingLen((v.RTPStreamCount-1)/2 + 1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + // slX_bm fields + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + var bm uint8 + if streamID%2 == 0 { + bm = ctx.payload[ctx.offset+streamID/2] >> 4 & 0b1111 + } else { + bm = ctx.payload[ctx.offset+streamID/2] & 0b1111 + } + ctx.slBMs[streamID] = bm + } + ctx.offset += 1 + (v.RTPStreamCount-1)/2 + } + + return nil +} + +func (v *VLA) unmarshalTemporalLayers(ctx *vlaUnmarshalingContext) error { + if !ctx.checkRemainingLen(1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + + var temporalLayerIndex int + for streamID := 0; streamID < v.RTPStreamCount; streamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + if ctx.slBMs[streamID]&(1<= 4 { + temporalLayerIndex = 0 + ctx.offset++ + if !ctx.checkRemainingLen(1) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + } + tlCount := int(ctx.payload[ctx.offset]>>(2*(3-temporalLayerIndex))&0b11) + 1 + temporalLayerIndex++ + sl := SpatialLayer{ + RTPStreamID: streamID, + SpatialID: spatialID, + TargetBitrates: make([]int, tlCount), + } + v.ActiveSpatialLayer = append(v.ActiveSpatialLayer, sl) + } + } + ctx.offset++ + + // target bitrates + for i, sl := range v.ActiveSpatialLayer { + for j := range sl.TargetBitrates { + kbps, n, err := obu.ReadLeb128(ctx.payload[ctx.offset:]) + if err != nil { + return err + } + if !ctx.checkRemainingLen(int(n)) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + v.ActiveSpatialLayer[i].TargetBitrates[j] = int(kbps) + ctx.offset += int(n) + } + } + + return nil +} + +func (v *VLA) unmarshalResolutionAndFramerate(ctx *vlaUnmarshalingContext) error { + if !ctx.checkRemainingLen(len(v.ActiveSpatialLayer) * 5) { + return fmt.Errorf("failed to unmarshal VLA (offset=%d): %w", ctx.offset, ErrVLATooShort) + } + + v.HasResolutionAndFramerate = true + + for i := range v.ActiveSpatialLayer { + v.ActiveSpatialLayer[i].Width = int(binary.BigEndian.Uint16(ctx.payload[ctx.offset+0:])) + 1 + v.ActiveSpatialLayer[i].Height = int(binary.BigEndian.Uint16(ctx.payload[ctx.offset+2:])) + 1 + v.ActiveSpatialLayer[i].Framerate = int(ctx.payload[ctx.offset+4]) + ctx.offset += 5 + } + + return nil +} + +// Unmarshal decodes VLA from a byte slice. +func (v *VLA) Unmarshal(payload []byte) (int, error) { + ctx := &vlaUnmarshalingContext{ + payload: payload, + } + + err := v.unmarshalSpatialLayers(ctx) + if err != nil { + return ctx.offset, err + } + + // #tl fields (build the list ActiveSpatialLayer at the same time) + err = v.unmarshalTemporalLayers(ctx) + if err != nil { + return ctx.offset, err + } + + if len(ctx.payload) == ctx.offset { + return ctx.offset, nil + } + + // resolution & framerate (optional) + err = v.unmarshalResolutionAndFramerate(ctx) + if err != nil { + return ctx.offset, err + } + + return ctx.offset, nil +} + +// String makes VLA printable. +func (v VLA) String() string { + out := fmt.Sprintf("RID:%d,RTPStreamCount:%d", v.RTPStreamID, v.RTPStreamCount) + var slOut []string + for _, sl := range v.ActiveSpatialLayer { + out2 := fmt.Sprintf("RTPStreamID:%d", sl.RTPStreamID) + out2 += fmt.Sprintf(",TargetBitrates:%v", sl.TargetBitrates) + if v.HasResolutionAndFramerate { + out2 += fmt.Sprintf(",Resolution:(%d,%d)", sl.Width, sl.Height) + out2 += fmt.Sprintf(",Framerate:%d", sl.Framerate) + } + slOut = append(slOut, out2) + } + out += fmt.Sprintf(",ActiveSpatialLayers:{%s}", strings.Join(slOut, ",")) + return out +} diff --git a/vlaextension_test.go b/vlaextension_test.go new file mode 100644 index 0000000..b9b8066 --- /dev/null +++ b/vlaextension_test.go @@ -0,0 +1,532 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +import ( + "bytes" + "encoding/hex" + "errors" + "reflect" + "testing" +) + +func TestVLAMarshal(t *testing.T) { + requireNoError := func(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } + } + + t.Run("3 streams no resolution and framerate", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 3, + ActiveSpatialLayer: []SpatialLayer{ + { + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{150}, + }, + { + RTPStreamID: 1, + SpatialID: 0, + TargetBitrates: []int{240, 400}, + }, + { + RTPStreamID: 2, + SpatialID: 0, + TargetBitrates: []int{720, 1200}, + }, + }, + } + + bytesActual, err := vla.Marshal() + requireNoError(t, err) + bytesExpected, err := hex.DecodeString("21149601f0019003d005b009") + requireNoError(t, err) + if !bytes.Equal(bytesExpected, bytesActual) { + t.Fatalf("expected %s, actual %s", hex.EncodeToString(bytesExpected), hex.EncodeToString(bytesActual)) + } + }) + + t.Run("3 streams with resolution and framerate", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 2, + RTPStreamCount: 3, + ActiveSpatialLayer: []SpatialLayer{ + { + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{150}, + Width: 320, + Height: 180, + Framerate: 30, + }, + { + RTPStreamID: 1, + SpatialID: 0, + TargetBitrates: []int{240, 400}, + Width: 640, + Height: 360, + Framerate: 30, + }, + { + RTPStreamID: 2, + SpatialID: 0, + TargetBitrates: []int{720, 1200}, + Width: 1280, + Height: 720, + Framerate: 30, + }, + }, + HasResolutionAndFramerate: true, + } + + bytesActual, err := vla.Marshal() + requireNoError(t, err) + bytesExpected, err := hex.DecodeString("a1149601f0019003d005b009013f00b31e027f01671e04ff02cf1e") + requireNoError(t, err) + if !bytes.Equal(bytesExpected, bytesActual) { + t.Fatalf("expected %s, actual %s", hex.EncodeToString(bytesExpected), hex.EncodeToString(bytesActual)) + } + }) + + t.Run("Negative RTPStreamCount", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: -1, + ActiveSpatialLayer: []SpatialLayer{}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamCount) { + t.Fatal("expected ErrVLAInvalidRTPStreamCount") + } + }) + + t.Run("RTPStreamCount too large", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 5, + ActiveSpatialLayer: []SpatialLayer{{}, {}, {}, {}, {}}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamCount) { + t.Fatal("expected ErrVLAInvalidRTPStreamCount") + } + }) + + t.Run("Negative RTPStreamID", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: -1, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{}}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidRTPStreamID, actual %v", err) + } + }) + + t.Run("RTPStreamID to large", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 1, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{}}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidRTPStreamID: %v", err) + } + }) + + t.Run("Invalid stream ID in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: -1, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidStreamID: %v", err) + } + vla = &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 1, + }}, + } + _, err = vla.Marshal() + if !errors.Is(err, ErrVLAInvalidStreamID) { + t.Fatalf("expected ErrVLAInvalidStreamID: %v", err) + } + }) + + t.Run("Invalid spatial ID in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: -1, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidSpatialID) { + t.Fatalf("expected ErrVLAInvalidSpatialID: %v", err) + } + vla = &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 5, + }}, + } + _, err = vla.Marshal() + if !errors.Is(err, ErrVLAInvalidSpatialID) { + t.Fatalf("expected ErrVLAInvalidSpatialID: %v", err) + } + }) + + t.Run("Invalid temporal layer in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{}, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLAInvalidTemporalLayer) { + t.Fatalf("expected ErrVLAInvalidTemporalLayer: %v", err) + } + vla = &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{100, 200, 300, 400, 500}, + }}, + } + _, err = vla.Marshal() + if !errors.Is(err, ErrVLAInvalidTemporalLayer) { + t.Fatalf("expected ErrVLAInvalidTemporalLayer: %v", err) + } + }) + + t.Run("Duplicate spatial ID in the spatial layer", func(t *testing.T) { + vla := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 1, + ActiveSpatialLayer: []SpatialLayer{{ + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{100}, + }, { + RTPStreamID: 0, + SpatialID: 0, + TargetBitrates: []int{200}, + }}, + } + _, err := vla.Marshal() + if !errors.Is(err, ErrVLADuplicateSpatialID) { + t.Fatalf("expected ErrVLADuplicateSpatialID: %v", err) + } + }) +} + +func TestVLAUnmarshal(t *testing.T) { + requireEqualInt := func(t *testing.T, expected, actual int) { + if expected != actual { + t.Fatalf("expected %d, actual %d", expected, actual) + } + } + requireNoError := func(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } + } + requireTrue := func(t *testing.T, val bool) { + if !val { + t.Fatal("expected true") + } + } + requireFalse := func(t *testing.T, val bool) { + if val { + t.Fatal("expected false") + } + } + + t.Run("3 streams no resolution and framerate", func(t *testing.T) { + // two layer ("low", "high") + b, err := hex.DecodeString("21149601f0019003d005b009") + requireNoError(t, err) + if err != nil { + t.Fatal("failed to decode input data") + } + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 0, vla.RTPStreamID) + requireEqualInt(t, 3, vla.RTPStreamCount) + requireEqualInt(t, 3, len(vla.ActiveSpatialLayer)) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 150, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 1, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 240, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 400, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireFalse(t, vla.HasResolutionAndFramerate) + + requireEqualInt(t, 2, vla.ActiveSpatialLayer[2].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[2].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[2].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[2].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[2].TargetBitrates[1]) + }) + + t.Run("3 streams with resolution and framerate", func(t *testing.T) { + b, err := hex.DecodeString("a1149601f0019003d005b009013f00b31e027f01671e04ff02cf1e") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 2, vla.RTPStreamID) + requireEqualInt(t, 3, vla.RTPStreamCount) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 150, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 1, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 240, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 400, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireEqualInt(t, 2, vla.ActiveSpatialLayer[2].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[2].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[2].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[2].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[2].TargetBitrates[1]) + + requireTrue(t, vla.HasResolutionAndFramerate) + + requireEqualInt(t, 320, vla.ActiveSpatialLayer[0].Width) + requireEqualInt(t, 180, vla.ActiveSpatialLayer[0].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[0].Framerate) + requireEqualInt(t, 640, vla.ActiveSpatialLayer[1].Width) + requireEqualInt(t, 360, vla.ActiveSpatialLayer[1].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[1].Framerate) + requireEqualInt(t, 1280, vla.ActiveSpatialLayer[2].Width) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[2].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[2].Framerate) + }) + + t.Run("2 streams", func(t *testing.T) { + // two layer ("low", "high") + b, err := hex.DecodeString("1110c801d005b009") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 0, vla.RTPStreamID) + requireEqualInt(t, 2, vla.RTPStreamCount) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer)) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 200, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 1, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireFalse(t, vla.HasResolutionAndFramerate) + }) + + t.Run("3 streams mid paused with resolution and framerate", func(t *testing.T) { + b, err := hex.DecodeString("601010109601d005b009013f00b31e04ff02cf1e") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + requireEqualInt(t, 1, vla.RTPStreamID) + requireEqualInt(t, 3, vla.RTPStreamCount) + + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[0].SpatialID) + requireEqualInt(t, 1, len(vla.ActiveSpatialLayer[0].TargetBitrates)) + requireEqualInt(t, 150, vla.ActiveSpatialLayer[0].TargetBitrates[0]) + + requireEqualInt(t, 2, vla.ActiveSpatialLayer[1].RTPStreamID) + requireEqualInt(t, 0, vla.ActiveSpatialLayer[1].SpatialID) + requireEqualInt(t, 2, len(vla.ActiveSpatialLayer[1].TargetBitrates)) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[1].TargetBitrates[0]) + requireEqualInt(t, 1200, vla.ActiveSpatialLayer[1].TargetBitrates[1]) + + requireTrue(t, vla.HasResolutionAndFramerate) + + requireEqualInt(t, 320, vla.ActiveSpatialLayer[0].Width) + requireEqualInt(t, 180, vla.ActiveSpatialLayer[0].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[0].Framerate) + requireEqualInt(t, 1280, vla.ActiveSpatialLayer[1].Width) + requireEqualInt(t, 720, vla.ActiveSpatialLayer[1].Height) + requireEqualInt(t, 30, vla.ActiveSpatialLayer[1].Framerate) + }) + + t.Run("extra 1", func(t *testing.T) { + b, err := hex.DecodeString("a0001040ac02f403") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + }) + + t.Run("extra 2", func(t *testing.T) { + b, err := hex.DecodeString("a00010409405cc08") + requireNoError(t, err) + + vla := &VLA{} + n, err := vla.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + }) +} + +func TestVLAMarshalThenUnmarshal(t *testing.T) { + requireEqualInt := func(t *testing.T, expected, actual int) { + if expected != actual { + t.Fatalf("expected %d, actual %d", expected, actual) + } + } + requireNoError := func(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } + } + + t.Run("multiple spatial layers", func(t *testing.T) { + var spatialLayers []SpatialLayer + for streamID := 0; streamID < 3; streamID++ { + for spatialID := 0; spatialID < 4; spatialID++ { + spatialLayers = append(spatialLayers, SpatialLayer{ + RTPStreamID: streamID, + SpatialID: spatialID, + TargetBitrates: []int{150, 200}, + Width: 320, + Height: 180, + Framerate: 30, + }) + } + } + + vla0 := &VLA{ + RTPStreamID: 2, + RTPStreamCount: 3, + ActiveSpatialLayer: spatialLayers, + HasResolutionAndFramerate: true, + } + + b, err := vla0.Marshal() + requireNoError(t, err) + + vla1 := &VLA{} + n, err := vla1.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + if !reflect.DeepEqual(vla0, vla1) { + t.Fatalf("expected %v, actual %v", vla0, vla1) + } + }) + + t.Run("different spatial layer bitmasks", func(t *testing.T) { + var spatialLayers []SpatialLayer + for streamID := 0; streamID < 4; streamID++ { + for spatialID := 0; spatialID < streamID+1; spatialID++ { + spatialLayers = append(spatialLayers, SpatialLayer{ + RTPStreamID: streamID, + SpatialID: spatialID, + TargetBitrates: []int{150, 200}, + Width: 320, + Height: 180, + Framerate: 30, + }) + } + } + + vla0 := &VLA{ + RTPStreamID: 0, + RTPStreamCount: 4, + ActiveSpatialLayer: spatialLayers, + HasResolutionAndFramerate: true, + } + + b, err := vla0.Marshal() + requireNoError(t, err) + if b[0]&0x0f != 0 { + t.Error("expects sl_bm to be 0") + } + if b[1] != 0x13 { + t.Error("expects sl0_bm,sl1_bm to be b0001,b0011") + } + if b[2] != 0x7f { + t.Error("expects sl1_bm,sl2_bm to be b0111,b1111") + } + t.Logf("b: %s", hex.EncodeToString(b)) + + vla1 := &VLA{} + n, err := vla1.Unmarshal(b) + requireNoError(t, err) + requireEqualInt(t, len(b), n) + + if !reflect.DeepEqual(vla0, vla1) { + t.Fatalf("expected %v, actual %v", vla0, vla1) + } + }) +} + +func FuzzVLAUnmarshal(f *testing.F) { + f.Add([]byte{0}) + f.Add([]byte("70")) + + f.Fuzz(func(t *testing.T, data []byte) { + vla := &VLA{} + _, err := vla.Unmarshal(data) + if err != nil { + t.Skip() // If the function returns an error, we skip the test case + } + }) +} From 410c5824dbb85774f20bc8f74f3fa2da41e51ff4 Mon Sep 17 00:00:00 2001 From: Yutaka Takeda Date: Sat, 9 Mar 2024 14:56:46 -0800 Subject: [PATCH 07/14] Fix a bug in AbsCpatureTimeExtension --- abscapturetimeextension.go | 16 +++++ abscapturetimeextension_test.go | 106 ++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 33 deletions(-) diff --git a/abscapturetimeextension.go b/abscapturetimeextension.go index 56b783d..5e96cff 100644 --- a/abscapturetimeextension.go +++ b/abscapturetimeextension.go @@ -66,7 +66,15 @@ func (t AbsCaptureTimeExtension) EstimatedCaptureClockOffsetDuration() *time.Dur 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 } @@ -80,9 +88,17 @@ func NewAbsCaptureTimeExtension(captureTime time.Time) *AbsCaptureTimeExtension // NewAbsCaptureTimeExtensionWithCaptureClockOffset makes new AbsCaptureTimeExtension from time.Time and a clock offset. func NewAbsCaptureTimeExtensionWithCaptureClockOffset(captureTime time.Time, captureClockOffset time.Duration) *AbsCaptureTimeExtension { ns := captureClockOffset.Nanoseconds() + negative := false + if ns < 0 { + ns = -ns + negative = true + } lsb := (ns / 1e9) & 0xFFFFFFFF msb := (((ns % 1e9) * (1 << 32)) / 1e9) & 0xFFFFFFFF offset := (lsb << 32) | msb + if negative { + offset = -offset + } return &AbsCaptureTimeExtension{ Timestamp: toNtpTime(captureTime), EstimatedCaptureClockOffset: &offset, diff --git a/abscapturetimeextension_test.go b/abscapturetimeextension_test.go index 15c434a..038e677 100644 --- a/abscapturetimeextension_test.go +++ b/abscapturetimeextension_test.go @@ -9,38 +9,78 @@ import ( ) func TestAbsCaptureTimeExtension_Roundtrip(t *testing.T) { - t0 := time.Now() - e1 := NewAbsCaptureTimeExtension(t0) - b1, err1 := e1.Marshal() - if err1 != nil { - t.Fatal(err1) - } - var o1 AbsCaptureTimeExtension - if err := o1.Unmarshal(b1); err != nil { - t.Fatal(err) - } - dt1 := o1.CaptureTime().Sub(t0).Seconds() - if dt1 < -0.001 || dt1 > 0.001 { - t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o1.CaptureTime(), dt1) - } - if o1.EstimatedCaptureClockOffsetDuration() != nil { - t.Fatalf("duration differs, want nil got %d", o1.EstimatedCaptureClockOffsetDuration()) - } + t.Run("positive captureClockOffset", func(t *testing.T) { + t0 := time.Now() + e1 := NewAbsCaptureTimeExtension(t0) + b1, err1 := e1.Marshal() + if err1 != nil { + t.Fatal(err1) + } + var o1 AbsCaptureTimeExtension + if err := o1.Unmarshal(b1); err != nil { + t.Fatal(err) + } + dt1 := o1.CaptureTime().Sub(t0).Seconds() + if dt1 < -0.001 || dt1 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o1.CaptureTime(), dt1) + } + if o1.EstimatedCaptureClockOffsetDuration() != nil { + t.Fatalf("duration differs, want nil got %d", o1.EstimatedCaptureClockOffsetDuration()) + } - e2 := NewAbsCaptureTimeExtensionWithCaptureClockOffset(t0, 1250*time.Millisecond) - b2, err2 := e2.Marshal() - if err2 != nil { - t.Fatal(err2) - } - var o2 AbsCaptureTimeExtension - if err := o2.Unmarshal(b2); err != nil { - t.Fatal(err) - } - dt2 := o1.CaptureTime().Sub(t0).Seconds() - if dt2 < -0.001 || dt2 > 0.001 { - t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o2.CaptureTime(), dt2) - } - if *o2.EstimatedCaptureClockOffsetDuration() != 1250*time.Millisecond { - t.Fatalf("duration differs, want 250ms got %d", *o2.EstimatedCaptureClockOffsetDuration()) - } + e2 := NewAbsCaptureTimeExtensionWithCaptureClockOffset(t0, 1250*time.Millisecond) + b2, err2 := e2.Marshal() + if err2 != nil { + t.Fatal(err2) + } + var o2 AbsCaptureTimeExtension + if err := o2.Unmarshal(b2); err != nil { + t.Fatal(err) + } + dt2 := o1.CaptureTime().Sub(t0).Seconds() + if dt2 < -0.001 || dt2 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o2.CaptureTime(), dt2) + } + if *o2.EstimatedCaptureClockOffsetDuration() != 1250*time.Millisecond { + t.Fatalf("duration differs, want 250ms got %d", *o2.EstimatedCaptureClockOffsetDuration()) + } + }) + + // This test can verify the for for the issue 247 + t.Run("negative captureClockOffset", func(t *testing.T) { + t0 := time.Now() + e1 := NewAbsCaptureTimeExtension(t0) + b1, err1 := e1.Marshal() + if err1 != nil { + t.Fatal(err1) + } + var o1 AbsCaptureTimeExtension + if err := o1.Unmarshal(b1); err != nil { + t.Fatal(err) + } + dt1 := o1.CaptureTime().Sub(t0).Seconds() + if dt1 < -0.001 || dt1 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o1.CaptureTime(), dt1) + } + if o1.EstimatedCaptureClockOffsetDuration() != nil { + t.Fatalf("duration differs, want nil got %d", o1.EstimatedCaptureClockOffsetDuration()) + } + + e2 := NewAbsCaptureTimeExtensionWithCaptureClockOffset(t0, -250*time.Millisecond) + b2, err2 := e2.Marshal() + if err2 != nil { + t.Fatal(err2) + } + var o2 AbsCaptureTimeExtension + if err := o2.Unmarshal(b2); err != nil { + t.Fatal(err) + } + dt2 := o1.CaptureTime().Sub(t0).Seconds() + if dt2 < -0.001 || dt2 > 0.001 { + t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o2.CaptureTime(), dt2) + } + if *o2.EstimatedCaptureClockOffsetDuration() != -250*time.Millisecond { + t.Fatalf("duration differs, want -250ms got %v", *o2.EstimatedCaptureClockOffsetDuration()) + } + }) } From c52c1e79bffe943d48ff777b54f29243e9f78b30 Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Sat, 16 Mar 2024 20:05:04 -0400 Subject: [PATCH 08/14] Fix broken link in README.md AUTHORS.txt has been deleted --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 37b7cc0..c84621c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ We are always looking to support **your projects**. Please reach out if you have 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: [AUTHORS.txt](./AUTHORS.txt) +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 \ No newline at end of file +MIT License - see [LICENSE](LICENSE) for full text From 057eda35a6580f672dc827cfafae06adc5e151ac Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Thu, 14 Mar 2024 21:33:23 +0200 Subject: [PATCH 09/14] Add static RTP PayloadTypes as a constant Defined in IANA [0] [0] https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml --- payload_types.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 payload_types.go diff --git a/payload_types.go b/payload_types.go new file mode 100644 index 0000000..a9bf227 --- /dev/null +++ b/payload_types.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2024 The Pion community +// SPDX-License-Identifier: MIT + +package rtp + +// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +// https://en.wikipedia.org/wiki/RTP_payload_formats + +// Audio Payload Types as defined in https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +const ( + // PayloadTypePCMU is a payload type for ITU-T G.711 PCM μ-Law audio 64 kbit/s (RFC 3551). + PayloadTypePCMU = 0 + // PayloadTypeGSM is a payload type for European GSM Full Rate audio 13 kbit/s (GSM 06.10). + PayloadTypeGSM = 3 + // PayloadTypeG723 is a payload type for ITU-T G.723.1 audio (RFC 3551). + PayloadTypeG723 = 4 + // PayloadTypeDVI4_8000 is a payload type for IMA ADPCM audio 32 kbit/s (RFC 3551). + PayloadTypeDVI4_8000 = 5 + // PayloadTypeDVI4_16000 is a payload type for IMA ADPCM audio 64 kbit/s (RFC 3551). + PayloadTypeDVI4_16000 = 6 + // PayloadTypeLPC is a payload type for Experimental Linear Predictive Coding audio 5.6 kbit/s (RFC 3551). + PayloadTypeLPC = 7 + // PayloadTypePCMA is a payload type for ITU-T G.711 PCM A-Law audio 64 kbit/s (RFC 3551). + PayloadTypePCMA = 8 + // PayloadTypeG722 is a payload type for ITU-T G.722 audio 64 kbit/s (RFC 3551). + PayloadTypeG722 = 9 + // PayloadTypeL16Stereo is a payload type for Linear PCM 16-bit Stereo audio 1411.2 kbit/s, uncompressed (RFC 3551). + PayloadTypeL16Stereo = 10 + // PayloadTypeL16Mono is a payload type for Linear PCM 16-bit audio 705.6 kbit/s, uncompressed (RFC 3551). + PayloadTypeL16Mono = 11 + // PayloadTypeQCELP is a payload type for Qualcomm Code Excited Linear Prediction (RFC 2658, RFC 3551). + PayloadTypeQCELP = 12 + // PayloadTypeCN is a payload type for Comfort noise (RFC 3389). + PayloadTypeCN = 13 + // PayloadTypeMPA is a payload type for MPEG-1 or MPEG-2 audio only (RFC 3551, RFC 2250). + PayloadTypeMPA = 14 + // PayloadTypeG728 is a payload type for ITU-T G.728 audio 16 kbit/s (RFC 3551). + PayloadTypeG728 = 15 + // PayloadTypeDVI4_11025 is a payload type for IMA ADPCM audio 44.1 kbit/s (RFC 3551). + PayloadTypeDVI4_11025 = 16 + // PayloadTypeDVI4_22050 is a payload type for IMA ADPCM audio 88.2 kbit/s (RFC 3551). + PayloadTypeDVI4_22050 = 17 + // PayloadTypeG729 is a payload type for ITU-T G.729 and G.729a audio 8 kbit/s (RFC 3551, RFC 3555). + PayloadTypeG729 = 18 +) + +// Video Payload Types as defined in https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +const ( + // PayloadTypeCELLB is a payload type for Sun CellB video (RFC 2029). + PayloadTypeCELLB = 25 + // PayloadTypeJPEG is a payload type for JPEG video (RFC 2435). + PayloadTypeJPEG = 26 + // PayloadTypeNV is a payload type for Xerox PARC's Network Video (nv, RFC 3551). + PayloadTypeNV = 28 + // PayloadTypeH261 is a payload type for ITU-T H.261 video (RFC 4587). + PayloadTypeH261 = 31 + // PayloadTypeMPV is a payload type for MPEG-1 and MPEG-2 video (RFC 2250). + PayloadTypeMPV = 32 + // PayloadTypeMP2T is a payload type for MPEG-2 transport stream (RFC 2250). + PayloadTypeMP2T = 33 + // PayloadTypeH263 is a payload type for H.263 video, first version (1996, RFC 3551, RFC 2190). + PayloadTypeH263 = 34 +) + +const ( + // PayloadTypeFirstDynamic is a first non-static payload type. + PayloadTypeFirstDynamic = 35 +) From 39052f8c2cb3824d479703048fe4419e1a9b0678 Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Thu, 15 Feb 2024 16:24:52 +0600 Subject: [PATCH 10/14] Add padding support to Packetizer To add padding-only samples call GeneratePadding --- packetizer.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packetizer.go b/packetizer.go index 7ecebde..7a6a46d 100644 --- a/packetizer.go +++ b/packetizer.go @@ -15,6 +15,7 @@ type Payloader interface { // Packetizer packetizes a payload type Packetizer interface { Packetize(payload []byte, samples uint32) []*Packet + GeneratePadding(samples uint32) []*Packet EnableAbsSendTime(value int) SkipSamples(skippedSamples uint32) } @@ -98,6 +99,38 @@ func (p *packetizer) Packetize(payload []byte, samples uint32) []*Packet { return packets } +// GeneratePadding returns required padding-only packages +func (p *packetizer) GeneratePadding(samples uint32) []*Packet { + // Guard against an empty payload + if samples == 0 { + return nil + } + + packets := make([]*Packet, samples) + + for i := 0; i < int(samples); i++ { + pp := make([]byte, 255) + pp[254] = 255 + + packets[i] = &Packet{ + Header: Header{ + Version: 2, + Padding: true, + Extension: false, + Marker: false, + PayloadType: p.PayloadType, + SequenceNumber: p.Sequencer.NextSequenceNumber(), + Timestamp: p.Timestamp, // Use latest timestamp + SSRC: p.SSRC, + CSRC: []uint32{}, + }, + Payload: pp, + } + } + + return packets +} + // SkipSamples causes a gap in sample count between Packetize requests so the // RTP payloads produced have a gap in timestamps func (p *packetizer) SkipSamples(skippedSamples uint32) { From a18e24dc84b5797056cb45c7cab0a6aff0118f32 Mon Sep 17 00:00:00 2001 From: Pion <59523206+pionbot@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:59:37 +0000 Subject: [PATCH 11/14] Update CI configs to v0.11.4 Update lint scripts and CI configs. --- .github/workflows/api.yaml | 20 ++++++++++++++++++++ .github/workflows/release.yml | 2 +- .github/workflows/test.yaml | 6 +++--- .github/workflows/tidy-check.yaml | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/api.yaml diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml new file mode 100644 index 0000000..1032179 --- /dev/null +++ b/.github/workflows/api.yaml @@ -0,0 +1,20 @@ +# +# DO NOT EDIT THIS FILE +# +# It is automatically copied from https://github.com/pion/.goassets repository. +# If this repository should have package specific CI config, +# remove the repository name from .goassets/.github/workflows/assets-sync.yml. +# +# If you want to update the shared CI config, send a PR to +# https://github.com/pion/.goassets instead of this repository. +# +# SPDX-FileCopyrightText: 2023 The Pion community +# SPDX-License-Identifier: MIT + +name: API +on: + pull_request: + +jobs: + check: + uses: pion/.goassets/.github/workflows/api.reusable.yml@master diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01227e2..0e72ea4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,4 +21,4 @@ jobs: release: uses: pion/.goassets/.github/workflows/release.reusable.yml@master with: - go-version: '1.20' # auto-update/latest-go-version + go-version: "1.22" # auto-update/latest-go-version diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c8294ef..ad6eb90 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,7 @@ jobs: uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: - go: ['1.21', '1.20'] # auto-update/supported-go-version-list + go: ["1.22", "1.21"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} @@ -32,7 +32,7 @@ jobs: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: - go: ['1.21', '1.20'] # auto-update/supported-go-version-list + go: ["1.22", "1.21"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} @@ -40,4 +40,4 @@ jobs: test-wasm: uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master with: - go-version: '1.20' # auto-update/latest-go-version + go-version: "1.22" # auto-update/latest-go-version diff --git a/.github/workflows/tidy-check.yaml b/.github/workflows/tidy-check.yaml index 33d6b50..417e730 100644 --- a/.github/workflows/tidy-check.yaml +++ b/.github/workflows/tidy-check.yaml @@ -22,4 +22,4 @@ jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master with: - go-version: '1.21' # auto-update/latest-go-version + go-version: "1.22" # auto-update/latest-go-version From 14c61dc0359c379f52978826c66bfcd9d90f7ff7 Mon Sep 17 00:00:00 2001 From: Uladzimir Filipchenkau Date: Fri, 29 Mar 2024 16:08:30 +0300 Subject: [PATCH 12/14] Fix out of range access in VP8 Unmarshal When unmarshaling 0x81, 0x81, 0x94 panic: runtime error: index out of range [3] with length 3 --- codecs/vp8_packet.go | 5 ++++- codecs/vp8_packet_test.go | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/codecs/vp8_packet.go b/codecs/vp8_packet.go index 5d0a654..4ddd15b 100644 --- a/codecs/vp8_packet.go +++ b/codecs/vp8_packet.go @@ -123,7 +123,7 @@ type VP8Packet struct { } // Unmarshal parses the passed byte slice and stores the result in the VP8Packet this method is called upon -func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) { +func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) { //nolint: gocognit if payload == nil { return nil, errNilPacket } @@ -163,6 +163,9 @@ func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) { return nil, errShortPacket } if payload[payloadIndex]&0x80 > 0 { // M == 1, PID is 16bit + if payloadIndex+1 >= payloadLen { + return nil, errShortPacket + } p.PictureID = (uint16(payload[payloadIndex]&0x7F) << 8) | uint16(payload[payloadIndex+1]) payloadIndex += 2 } else { diff --git a/codecs/vp8_packet_test.go b/codecs/vp8_packet_test.go index bb09ef1..3abb520 100644 --- a/codecs/vp8_packet_test.go +++ b/codecs/vp8_packet_test.go @@ -106,7 +106,7 @@ func TestVP8Packet_Unmarshal(t *testing.T) { // attention to partition boundaries. In that case, it may // produce packets with minimal headers. - // The next two have been witnessed in nature. + // The next three have been witnessed in nature. _, err = pck.Unmarshal([]byte{0x00}) if err != nil { t.Errorf("Empty packet with trivial header: %v", err) @@ -115,6 +115,13 @@ func TestVP8Packet_Unmarshal(t *testing.T) { if err != nil { t.Errorf("Non-empty packet with trivial header: %v", err) } + raw, err = pck.Unmarshal([]byte{0x81, 0x81, 0x94}) + if raw != nil { + t.Fatal("Result should be nil in case of error") + } + if !errors.Is(err, errShortPacket) { + t.Fatal("Error should be:", errShortPacket) + } // The following two were invented. _, err = pck.Unmarshal([]byte{0x80, 0x00}) From 0a5cc325a1e78c11760212ae70a96692d69a3cfd Mon Sep 17 00:00:00 2001 From: Pion <59523206+pionbot@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:43:05 +0000 Subject: [PATCH 13/14] Update CI configs to v0.11.7 Update lint scripts and CI configs. --- .golangci.yml | 7 +++---- .reuse/dep5 | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6dd80c8..e06de4d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,7 +3,8 @@ linters-settings: govet: - check-shadowing: true + enable: + - shadow misspell: locale: US exhaustive: @@ -110,6 +111,7 @@ linters: issues: exclude-use-default: false + exclude-dirs-use-default: false exclude-rules: # Allow complex tests and examples, better to be self contained - path: (examples|main\.go|_test\.go) @@ -121,6 +123,3 @@ issues: - path: cmd linters: - forbidigo - -run: - skip-dirs-use-default: false diff --git a/.reuse/dep5 b/.reuse/dep5 index 717f0c1..eb7fac2 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -2,7 +2,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Pion Source: https://github.com/pion/ -Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples/examples.json +Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json Copyright: 2023 The Pion community License: MIT From 74a9dc74432a406dcfdb70db77ce7fb5a2a2f496 Mon Sep 17 00:00:00 2001 From: Pion <59523206+pionbot@users.noreply.github.com> Date: Tue, 9 Apr 2024 03:11:05 +0000 Subject: [PATCH 14/14] Update CI configs to v0.11.12 Update lint scripts and CI configs. --- .github/workflows/test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ad6eb90..08e4272 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,6 +27,7 @@ jobs: fail-fast: false with: go-version: ${{ matrix.go }} + secrets: inherit test-i386: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master @@ -41,3 +42,4 @@ jobs: uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master with: go-version: "1.22" # auto-update/latest-go-version + secrets: inherit