Repository: pion/rtp
Pull request #294 (2025-02-16 21:21:55)

Add AV1 OBU header parser and OBU types utilities
#### Description

Implement AV1 bitstream format OBU header parser and utility functions for OBU types.

#### Reference issue

https://github.com/pion/webrtc/issues/3036


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pion/rtp/pull/294?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.57%. Comparing base [(`a78a4a6`)](https://app.codecov.io/gh/pion/rtp/commit/a78a4a60645cad0737b033d4d89ffa73974570bf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) to head [(`db9d28e`)](https://app.codecov.io/gh/pion/rtp/commit/db9d28e4bd1c8b4f1e00be9dfdf0299bb49d03f9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #294      +/-   ##
==========================================
- Coverage   84.72%   84.57%   -0.15%     
==========================================
  Files          24       25       +1     
  Lines        2703     2788      +85     
==========================================
+ Hits         2290     2358      +68     
- Misses        354      366      +12     
- Partials       59       64       +5     
```

| [Flag](https://app.codecov.io/gh/pion/rtp/pull/294/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [go](https://app.codecov.io/gh/pion/rtp/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | `?` | |
| [wasm](https://app.codecov.io/gh/pion/rtp/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | `84.57% <100.00%> (+0.48%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pion/rtp/pull/294?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).

JoeTurki: @Sean-Der Hello, This is a base for the packetizer and the depacketizer. After this is merged I'll do:

1. Continue the rest of my PR https://github.com/pion/rtp/pull/291 and make it construct a correct low bitstream, and construct OBU_temporal_delimiters.
2. Fix the packetizer, omit OBU_temporal_delimiters and construct correct RTP packets, I have a local version working, But I need to make correct tests.
3. Implement annex-b writer & reader, and fix the ivf writer and reader, and make OBU parsers (Need to parse the Sequence Headers at least).
4. Make more AV1 examples, and a Gstreamer test / example.

If things went well I'll be able to do all of this by or before the next weekend.


Sean-Der: Amazing work @JoeTurki, full steam ahead :) 

The only thing I would do is add GStreamer/Chrome tests of AV1. I the effort vs value isn't worth it (IMO). I think it is ok to have a bunch of example video files and call it a day!

AV1 isn't going to change. However running GStreamer/Chrome is CI is frustrating. Especially if then a user has to run it locally to debug anything. 

Patch:
diff --git a/codecs/av1/obu/errors.go b/codecs/av1/obu/errors.go
new file mode 100644
index 0000000..eeba3a4
--- /dev/null
+++ b/codecs/av1/obu/errors.go
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// 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/codecs/av1/obu/obu.go b/codecs/av1/obu/obu.go
new file mode 100644
index 0000000..6bc91f1
--- /dev/null
+++ b/codecs/av1/obu/obu.go
@@ -0,0 +1,219 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// 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...)
+}


Test patch:
diff --git a/codecs/av1/obu/obu_test.go b/codecs/av1/obu/obu_test.go
new file mode 100644
index 0000000..a8ab1cd
--- /dev/null
+++ b/codecs/av1/obu/obu_test.go
@@ -0,0 +1,365 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package obu
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+)
+
+func TestOBUType(t *testing.T) {
+	for _, test := range []struct {
+		Type      Type
+		TypeValue uint8
+		Str       string
+	}{
+		{OBUSequenceHeader, 1, "OBU_SEQUENCE_HEADER"},
+		{OBUTemporalDelimiter, 2, "OBU_TEMPORAL_DELIMITER"},
+		{OBUFrameHeader, 3, "OBU_FRAME_HEADER"},
+		{OBUTileGroup, 4, "OBU_TILE_GROUP"},
+		{OBUMetadata, 5, "OBU_METADATA"},
+		{OBUFrame, 6, "OBU_FRAME"},
+		{OBURedundantFrameHeader, 7, "OBU_REDUNDANT_FRAME_HEADER"},
+		{OBUTileList, 8, "OBU_TILE_LIST"},
+		{OBUPadding, 15, "OBU_PADDING"},
+		{Type(0), 0, "OBU_RESERVED"},
+		{Type(9), 9, "OBU_RESERVED"},
+	} {
+		test := test
+
+		if test.Type.String() != test.Str {
+			t.Errorf("Expected %s, got %s", test.Str, test.Type.String())
+		}
+
+		if uint8(test.Type) != test.TypeValue {
+			t.Errorf("Expected %d, got %d", test.TypeValue, uint8(test.Type))
+		}
+	}
+}
+
+func TestOBUHeader_NoExtension(t *testing.T) {
+	tests := []struct {
+		Value  byte
+		Header Header
+	}{
+		{0b0_0000_0_0_0, Header{Type: Type(0), HasSizeField: false, Reserved1Bit: false}},
+		{0b0_0001_0_0_0, Header{Type: OBUSequenceHeader, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_0010_0_0_0, Header{Type: OBUTemporalDelimiter, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_0011_0_0_0, Header{Type: OBUFrameHeader, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_0100_0_0_0, Header{Type: OBUTileGroup, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_0101_0_0_0, Header{Type: OBUMetadata, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_0110_0_0_0, Header{Type: OBUFrame, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_0111_0_0_0, Header{Type: OBURedundantFrameHeader, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_1000_0_0_0, Header{Type: OBUTileList, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_1111_0_0_0, Header{Type: OBUPadding, HasSizeField: false, Reserved1Bit: false}},
+		{0b0_1001_0_0_0, Header{Type: Type(9), HasSizeField: false, Reserved1Bit: false}},
+		{0b0_1001_0_1_0, Header{Type: Type(9), HasSizeField: true, Reserved1Bit: false}},
+		{0b0_1001_0_1_1, Header{Type: Type(9), HasSizeField: true, Reserved1Bit: true}},
+		{0b0_1001_0_0_1, Header{Type: Type(9), HasSizeField: false, Reserved1Bit: true}},
+	}
+
+	for _, test := range tests {
+		test := test
+
+		buff := []byte{test.Value}
+		header, err := ParseOBUHeader(buff)
+		if err != nil {
+			t.Fatalf("Unexpected error: %v", err)
+		}
+
+		if *header != test.Header {
+			t.Errorf("Expected %v, got %v", test.Header, *header)
+		}
+
+		if test.Header.Size() != 1 {
+			t.Errorf("Expected size 1 for header without extension, got %d", test.Header.Size())
+		}
+
+		value := test.Header.Marshal()
+
+		if len(value) != 1 {
+			t.Errorf("Expected size 1 for header without extension, got %d", len(value))
+		}
+
+		if value[0] != test.Value {
+			t.Errorf("Expected %d for header value, got %d", test.Value, value[0])
+		}
+	}
+}
+
+func TestOBUHeader_Extension(t *testing.T) {
+	tests := []struct {
+		HeaderValue          byte
+		Header               Header
+		ExtensionHeaderValue byte
+		ExtensionHeader      ExtensionHeader
+	}{
+		{
+			HeaderValue:          0b0_1001_1_0_0,
+			Header:               Header{Type: Type(9), HasSizeField: false, Reserved1Bit: false},
+			ExtensionHeaderValue: 0b001_01_000,
+			ExtensionHeader:      ExtensionHeader{TemporalID: 1, SpatialID: 1},
+		},
+		{
+			HeaderValue:          0b0_1001_1_1_1,
+			Header:               Header{Type: Type(9), HasSizeField: true, Reserved1Bit: true},
+			ExtensionHeaderValue: 0b010_01_000,
+			ExtensionHeader:      ExtensionHeader{TemporalID: 2, SpatialID: 1},
+		},
+		{
+			HeaderValue:          0b0_1001_1_1_1,
+			Header:               Header{Type: Type(9), HasSizeField: true, Reserved1Bit: true},
+			ExtensionHeaderValue: 0b011_01_000,
+			ExtensionHeader:      ExtensionHeader{TemporalID: 3, SpatialID: 1},
+		},
+		{
+			HeaderValue:          0b0_1001_1_1_1,
+			Header:               Header{Type: Type(9), HasSizeField: true, Reserved1Bit: true},
+			ExtensionHeaderValue: 0b111_10_000,
+			ExtensionHeader:      ExtensionHeader{TemporalID: 7, SpatialID: 2},
+		},
+		{
+			HeaderValue:          0b0_1001_1_1_1,
+			Header:               Header{Type: Type(9), HasSizeField: true, Reserved1Bit: true},
+			ExtensionHeaderValue: 0b111_11_001,
+			ExtensionHeader:      ExtensionHeader{TemporalID: 7, SpatialID: 3, Reserved3Bits: 1},
+		},
+		{
+			HeaderValue:          0b0_1001_1_1_1,
+			Header:               Header{Type: Type(9), HasSizeField: true, Reserved1Bit: true},
+			ExtensionHeaderValue: 0b111_11_111,
+			ExtensionHeader:      ExtensionHeader{TemporalID: 7, SpatialID: 3, Reserved3Bits: 7},
+		},
+	}
+
+	for _, test := range tests {
+		test := test
+
+		buff := []byte{test.HeaderValue, test.ExtensionHeaderValue}
+		header, err := ParseOBUHeader(buff)
+		if err != nil {
+			t.Fatalf("Unexpected error: %v", err)
+		}
+
+		expected := Header{
+			Type:         test.Header.Type,
+			HasSizeField: test.Header.HasSizeField,
+			Reserved1Bit: test.Header.Reserved1Bit,
+		}
+		if expected != test.Header {
+			t.Errorf("Expected %v, got %v", test.Header, *header)
+		}
+
+		if header.Size() != 2 {
+			t.Errorf("Expected size 2 for header with extension, got %d", test.Header.Size())
+		}
+
+		extension := header.ExtensionHeader
+		if extension == nil {
+			t.Fatalf("Expected extension header to be present")
+		}
+
+		if *extension != test.ExtensionHeader {
+			t.Errorf("Expected %v, got %v", test.ExtensionHeader, *extension)
+		}
+
+		if extension.Marshal() != test.ExtensionHeaderValue {
+			t.Errorf("Expected %d for extension header value, got %d", test.ExtensionHeaderValue, extension.Marshal())
+		}
+
+		value := header.Marshal()
+		if len(value) != 2 {
+			t.Errorf("Expected size 2 for header with extension, got %d", len(value))
+		}
+
+		if !bytes.Equal(value, buff) {
+			t.Errorf("Expected %v for header value, got %v", buff, value)
+		}
+	}
+}
+
+func TestOBUHeader_Short(t *testing.T) {
+	_, err := ParseOBUHeader([]byte{})
+	if err == nil {
+		t.Fatalf("Expected error, got nil")
+	}
+	if !errors.Is(err, ErrShortHeader) {
+		t.Errorf("Expected ErrShortHeader, got %v", err)
+	}
+
+	// Missing extension header
+	_, err = ParseOBUHeader([]byte{0b0_0000_1_0_0})
+	if err == nil {
+		t.Fatalf("Expected error, got nil")
+	}
+
+	if !errors.Is(err, ErrShortHeader) {
+		t.Errorf("Expected ErrShortHeader, got %v", err)
+	}
+}
+
+func TestOBUHeader_Invalid(t *testing.T) {
+	_, err := ParseOBUHeader([]byte{0b1_0010_0_0_1})
+	if err == nil {
+		t.Fatalf("Expected error, got nil")
+	}
+	if !errors.Is(err, ErrInvalidOBUHeader) {
+		t.Errorf("Expected ErrInvalidOBUHeader, got %v", err)
+	}
+}
+
+func TestOBUHeader_MarshalOutbound(t *testing.T) {
+	// Marshal should turnicate the extension header values.
+	header := Header{Type: Type(255)}
+	if header.Marshal()[0] != 0b0_1111_000 {
+		t.Errorf("Expected 0b0_1111_000, got %b", header.Marshal()[0])
+	}
+
+	extentionHeader := ExtensionHeader{TemporalID: 255}
+
+	if extentionHeader.Marshal() != 0b111_00_000 {
+		t.Errorf("Expected 0b111_00_000, got %b", extentionHeader.Marshal())
+	}
+
+	extensionHeader := ExtensionHeader{SpatialID: 255}
+	if extensionHeader.Marshal() != 0b000_11_000 {
+		t.Errorf("Expected 0b000_11_000, got %b", extensionHeader.Marshal())
+	}
+
+	extensionHeader = ExtensionHeader{Reserved3Bits: 255}
+	if extensionHeader.Marshal() != 0b000_00_111 {
+		t.Errorf("Expected 0b000_00_111, got %b", extensionHeader.Marshal())
+	}
+}
+
+func TestOBUMarshal(t *testing.T) {
+	obu := OBU{
+		Header: Header{
+			Type:         OBUFrame,
+			HasSizeField: false,
+			Reserved1Bit: false,
+		},
+		Payload: []byte{0x01, 0x02, 0x03},
+	}
+
+	data := obu.Marshal()
+
+	if len(data) != 4 {
+		t.Fatalf("Expected 4 bytes, got %d", len(data))
+	}
+
+	if data[0] != obu.Header.Marshal()[0] {
+		t.Errorf("Expected header to be %v, got %v", obu.Header.Marshal(), data[0])
+	}
+
+	if !bytes.Equal(data[1:], obu.Payload) {
+		t.Errorf("Expected payload to be %v, got %v", obu.Payload, data[1:])
+	}
+}
+
+func TestOBUMarshal_ExtensionHeader(t *testing.T) {
+	obu := OBU{
+		Header: Header{
+			Type:         OBUFrame,
+			HasSizeField: false,
+			Reserved1Bit: false,
+			ExtensionHeader: &ExtensionHeader{
+				TemporalID: 1,
+				SpatialID:  2,
+			},
+		},
+		Payload: []byte{0x01, 0x02, 0x03},
+	}
+
+	data := obu.Marshal()
+
+	if len(data) != 5 {
+		t.Fatalf("Expected 5 bytes, got %d", len(data))
+	}
+
+	if data[0] != obu.Header.Marshal()[0] {
+		t.Errorf("Expected header to be %v, got %v", obu.Header.Marshal(), data[0])
+	}
+
+	if data[1] != obu.Header.ExtensionHeader.Marshal() {
+		t.Errorf("Expected extension header to be %v, got %v", obu.Header.ExtensionHeader.Marshal(), data[1])
+	}
+
+	if !bytes.Equal(data[2:], obu.Payload) {
+		t.Errorf("Expected payload to be %v, got %v", obu.Payload, data[1:])
+	}
+}
+
+func TestOBUMarshal_HasOBUSize(t *testing.T) {
+	const payloadSize = 128
+	payload := make([]byte, payloadSize)
+
+	for i := 0; i < payloadSize; i++ {
+		payload[i] = byte(i)
+	}
+
+	obu := OBU{
+		Header: Header{
+			Type:         OBUFrame,
+			HasSizeField: true,
+			Reserved1Bit: false,
+		},
+		Payload: payload,
+	}
+	expected := append(
+		obu.Header.Marshal(),
+		append(
+			// obu_size leb128 (128)
+			[]byte{0x80, 0x01},
+			obu.Payload...,
+		)...,
+	)
+
+	data := obu.Marshal()
+
+	if len(data) != payloadSize+3 {
+		t.Fatalf("Expected 4 bytes, got %d", len(data))
+	}
+
+	if data[0] != obu.Header.Marshal()[0] {
+		t.Errorf("Expected header to be %v, got %v", obu.Header.Marshal(), data[0])
+	}
+
+	if !bytes.Equal(data, expected) {
+		t.Errorf("Expected payload to be %v, got %v", expected, data)
+	}
+}
+
+func TestOBUMarshal_ZeroPayload(t *testing.T) {
+	obu := OBU{
+		Header: Header{
+			Type:         OBUTemporalDelimiter,
+			HasSizeField: false,
+		},
+	}
+
+	data := obu.Marshal()
+
+	if len(data) != 1 {
+		t.Fatalf("Expected 1 byte, got %d", len(data))
+	}
+
+	obu = OBU{
+		Header: Header{
+			Type:         OBUTemporalDelimiter,
+			HasSizeField: true,
+		},
+	}
+
+	data = obu.Marshal()
+
+	if len(data) != 2 {
+		t.Fatalf("Expected two bytes, got %d", len(data))
+	}
+
+	if data[1] != 0 {
+		t.Errorf("Expected 0 for size, got %d", data[1])
+	}
+}