Repository: pion/rtp
Pull request #134 (2021-06-25 08:57:48)

Implement a H265/HEVC packet decoder
~~This is still very much a work-in-progress.~~

The `DONL` field is conditionally present depending on information provided in the SDP / RTP stream (see `sprop-max-don-diff` [here](https://www.iana.org/assignments/media-types/video/H265))– which I don't have access to if I replicate the way other codecs are implemented.
What would be the best way to handle this?

Also, I have no prior experience with H265 or video codecs in general, so please feel free to report any mistake / mis-implementation / ..etc

Relates to #87


codecov[bot]: # [Codecov](https://codecov.io/gh/pion/rtp/pull/134?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
> Merging [#134](https://codecov.io/gh/pion/rtp/pull/134?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (5dfadb3) into [master](https://codecov.io/gh/pion/rtp/commit/863e8351e01da61813308c8689c2aea684402450?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (863e835) will **decrease** coverage by `1.84%`.
> The diff coverage is `58.13%`.

[![Impacted file tree graph](https://codecov.io/gh/pion/rtp/pull/134/graphs/tree.svg?width=650&height=150&src=pr&token=4KrbiBsspj&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)](https://codecov.io/gh/pion/rtp/pull/134?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)

```diff
@@            Coverage Diff             @@
##           master     #134      +/-   ##
==========================================
- Coverage   88.80%   86.95%   -1.85%     
==========================================
  Files          13       14       +1     
  Lines         670      713      +43     
==========================================
+ Hits          595      620      +25     
- Misses         57       71      +14     
- Partials       18       22       +4     
```

| Flag | Coverage Δ | |
|---|---|---|
| go | `86.95% <58.13%> (-1.85%)` | :arrow_down: |
| wasm | `86.39% <58.13%> (-1.82%)` | :arrow_down: |

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.

| [Impacted Files](https://codecov.io/gh/pion/rtp/pull/134?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [codecs/h265\_packet.go](https://codecov.io/gh/pion/rtp/pull/134/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-Y29kZWNzL2gyNjVfcGFja2V0Lmdv) | `58.13% <58.13%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/rtp/pull/134?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pion/rtp/pull/134?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Last update [863e835...5dfadb3](https://codecov.io/gh/pion/rtp/pull/134?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).


Patch:
diff --git a/.github/assert-contributors.sh b/.github/generate-authors.sh
similarity index 58%
rename from .github/assert-contributors.sh
rename to .github/generate-authors.sh
index 12e6afe..182e4f5 100755
--- a/.github/assert-contributors.sh
+++ b/.github/generate-authors.sh
@@ -12,6 +12,7 @@
 set -e
 
 SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
+AUTHORS_PATH="$GITHUB_WORKSPACE/AUTHORS.txt"
 
 if [ -f ${SCRIPT_PATH}/.ci.conf ]
 then
@@ -21,18 +22,18 @@ fi
 #
 # DO NOT EDIT THIS
 #
-EXCLUDED_CONTRIBUTORS+=('John R. Bradley' 'renovate[bot]' 'Renovate Bot' 'Pion Bot')
+EXCLUDED_CONTRIBUTORS+=('John R. Bradley' 'renovate[bot]' 'Renovate Bot' 'Pion Bot' 'pionbot')
 # If you want to exclude a name from all repositories, send a PR to
 # https://github.com/pion/.goassets instead of this repository.
 # If you want to exclude a name only from this repository,
 # add EXCLUDED_CONTRIBUTORS=('name') to .github/.ci.conf
 
-MISSING_CONTRIBUTORS=()
+CONTRIBUTORS=()
 
 shouldBeIncluded () {
 	for i in "${EXCLUDED_CONTRIBUTORS[@]}"
 	do
-		if [ "$i" == "$1" ] ; then
+		if [[ $1 =~ "$i" ]]; then
 			return 1
 		fi
 	done
@@ -41,21 +42,25 @@ shouldBeIncluded () {
 
 
 IFS=$'\n' #Only split on newline
-for contributor in $(git log --format='%aN' | sort -u)
+for contributor in $(git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf)
 do
 	if shouldBeIncluded $contributor; then
-		if ! grep -q "$contributor" "$SCRIPT_PATH/../README.md"; then
-			MISSING_CONTRIBUTORS+=("$contributor")
-		fi
+		CONTRIBUTORS+=("$contributor")
 	fi
 done
 unset IFS
 
-if [ ${#MISSING_CONTRIBUTORS[@]} -ne 0 ]; then
-    echo "Please add the following contributors to the README"
-    for i in "${MISSING_CONTRIBUTORS[@]}"
+if [ ${#CONTRIBUTORS[@]} -ne 0 ]; then
+	cat >$AUTHORS_PATH <<-'EOH'
+# Thank you to everyone that made Pion possible. If you are interested in contributing
+# we would love to have you https://github.com/pion/webrtc/wiki/Contributing
+#
+# This file is auto generated, using git to list all individuals contributors.
+# see `.github/generate-authors.sh` for the scripting
+EOH
+    for i in "${CONTRIBUTORS[@]}"
     do
-	    echo "$i"
+	    echo "$i" >> $AUTHORS_PATH
     done
-    exit 1
+    exit 0
 fi
diff --git a/.github/hooks/pre-push.sh b/.github/hooks/pre-push.sh
index 7cb2365..bfe65bc 100755
--- a/.github/hooks/pre-push.sh
+++ b/.github/hooks/pre-push.sh
@@ -8,6 +8,6 @@
 
 set -e
 
-.github/assert-contributors.sh
+.github/generate-authors.sh
 
 exit 0
diff --git a/.github/workflows/generate-authors.yml b/.github/workflows/generate-authors.yml
new file mode 100644
index 0000000..83e7065
--- /dev/null
+++ b/.github/workflows/generate-authors.yml
@@ -0,0 +1,73 @@
+#
+# DO NOT EDIT THIS FILE
+#
+# It is automatically copied from https://github.com/pion/.goassets repository.
+# If this repository should have package specific CI config,
+# remove the repository name from .goassets/.github/workflows/assets-sync.yml.
+#
+# If you want to update the shared CI config, send a PR to
+# https://github.com/pion/.goassets instead of this repository.
+#
+
+name: generate-authors
+
+on:
+  pull_request:
+
+jobs:
+  checksecret:
+    runs-on: ubuntu-latest
+    outputs:
+      is_PIONBOT_PRIVATE_KEY_set: ${{ steps.checksecret_job.outputs.is_PIONBOT_PRIVATE_KEY_set }}
+    steps:
+      - id: checksecret_job
+        env:
+          PIONBOT_PRIVATE_KEY: ${{ secrets.PIONBOT_PRIVATE_KEY }}
+        run: |
+          echo "is_PIONBOT_PRIVATE_KEY_set: ${{ env.PIONBOT_PRIVATE_KEY != '' }}"
+          echo "::set-output name=is_PIONBOT_PRIVATE_KEY_set::${{ env.PIONBOT_PRIVATE_KEY != '' }}"
+
+  generate-authors:
+    needs: [checksecret]
+    if: needs.checksecret.outputs.is_PIONBOT_PRIVATE_KEY_set == 'true'
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        ref: ${{ github.head_ref }}
+        fetch-depth: 0
+        token: ${{ secrets.PIONBOT_PRIVATE_KEY }}
+
+    - name: Generate the authors file
+      run: .github/generate-authors.sh
+
+    - name: Add the authors file to git
+      run: git add AUTHORS.txt
+
+    - name: Get last commit message
+      id: last-commit-message
+      run: |
+        COMMIT_MSG=$(git log -1 --pretty=%B)
+        COMMIT_MSG="${COMMIT_MSG//'%'/'%25'}"
+        COMMIT_MSG="${COMMIT_MSG//$'\n'/'%0A'}"
+        COMMIT_MSG="${COMMIT_MSG//$'\r'/'%0D'}"
+        echo "::set-output name=msg::$COMMIT_MSG"
+
+    - name: Get last commit author
+      id: last-commit-author
+      run: |
+        echo "::set-output name=msg::$(git log -1 --pretty='%aN <%ae>')"
+
+    - name: Check if AUTHORS.txt file has changed
+      id: git-status-output
+      run: |
+        echo "::set-output name=msg::$(git status -s | wc -l)"
+
+    - name: Commit and push
+      if: ${{ steps.git-status-output.outputs.msg != '0' }}
+      run: |
+        git config user.email $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\2/')
+        git config user.name $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\1/')
+        git add AUTHORS.txt
+        git commit --amend --no-edit
+        git push --force https://github.com/${GITHUB_REPOSITORY} $(git symbolic-ref -q --short HEAD)
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 8824c34..f096078 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -1,3 +1,14 @@
+#
+# DO NOT EDIT THIS FILE
+#
+# It is automatically copied from https://github.com/pion/.goassets repository.
+# If this repository should have package specific CI config,
+# remove the repository name from .goassets/.github/workflows/assets-sync.yml.
+#
+# If you want to update the shared CI config, send a PR to
+# https://github.com/pion/.goassets instead of this repository.
+#
+
 name: Lint
 on:
   pull_request:
@@ -22,9 +33,6 @@ jobs:
         - name: File names
           run: .github/lint-filename.sh
 
-        - name: Contributors
-          run: .github/assert-contributors.sh
-
         - name: Functions
           run: .github/lint-disallowed-functions-in-library.sh
 
diff --git a/AUTHORS.txt b/AUTHORS.txt
new file mode 100644
index 0000000..5e5c680
--- /dev/null
+++ b/AUTHORS.txt
@@ -0,0 +1,30 @@
+# Thank you to everyone that made Pion possible. If you are interested in contributing
+# we would love to have you https://github.com/pion/webrtc/wiki/Contributing
+#
+# This file is auto generated, using git to list all individuals contributors.
+# see `.github/generate-authors.sh` for the scripting
+adwpc <adwpc@hotmail.com>
+aler9 <46489434+aler9@users.noreply.github.com>
+Antoine Baché <antoine.bache@epitech.eu>
+Atsushi Watanabe <atsushi.w@ieee.org>
+Bao Nguyen <bao@n4n.dev>
+debiandebiandebian <debiandebiandebiandebian@gmail.com>
+ffmiyo <leffmiyo@gmail.com>
+Guilherme <gqgs@protonmail.com>
+Haiyang Wang <ocean2811@outlook.com>
+Hugo Arregui <hugo@decentraland.org>
+John Bradley <jrb@turrettech.com>
+Juliusz Chroboczek <jch@irif.fr>
+Kazuyuki Honda <hakobera@gmail.com>
+Luke Curley <kixelated@gmail.com>
+lxb <xiangbing.li@uama.com.cn>
+Michael MacDonald <github@macdonald.cx>
+Michael MacDonald <mike.macdonald@savantsystems.com>
+Raphael Derosso Pereira <raphaelpereira@gmail.com>
+Rob Lofthouse <ri.lofthouse@gmail.com>
+Robin Raymond <robin@goheadroom.com>
+Sean DuBois <seaduboi@amazon.com>
+Sean DuBois <sean@siobud.com>
+Simone Gotti <simone.gotti@gmail.com>
+Tarrence van As <tarrence13@gmail.com>
+Woodrow Douglass <wdouglass@carnegierobotics.com>
diff --git a/README.md b/README.md
index 789e9ac..ce04599 100644
--- a/README.md
+++ b/README.md
@@ -30,29 +30,5 @@ If you need commercial support or don't want to use public methods you can conta
 ### Contributing
 Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible:
 
-* [John Bradley](https://github.com/kc5nra) - *Original Author*
-* [Sean DuBois](https://github.com/Sean-Der) - *Original Author*
-* [Woodrow Douglass](https://github.com/wdouglass) *RTCP, RTP improvements, G.722 support, Bugfixes*
-* [Michael MacDonald](https://github.com/mjmac)
-* [Luke Curley](https://github.com/kixelated) *Performance*
-* [Antoine Baché](https://github.com/Antonito) *Fixed crashes*
-* [Hugo Arregui](https://github.com/hugoArregui)
-* [Raphael Derosso Pereira](https://github.com/raphaelpereira)
-* [Atsushi Watanabe](https://github.com/at-wat)
-* [adwpc](https://github.com/adwpc) *add transport-cc extension*
-* [Bao Nguyen](https://github.com/sysbot) *add VP9 noop, bug fixes.
-* [Tarrence van As](https://github.com/tarrencev) *add audio level extension*
-* [Simone Gotti](https://github.com/sgotti)
-* [Guilherme Souza](https://github.com/gqgs)
-* [Rob Lofthouse](https://github.com/roblofthouse)
-* [Kazuyuki Honda](https://github.com/hakobera)
-* [Haiyang Wang](https://github.com/ocean2811)
-* [lxb](https://github.com/lxb531)
-* [Robin Raymond](https://github.com/robin-raymond)
-* [debiandebiandebian](https://github.com/debiandebiandebian)
-* [Juliusz Chroboczek](https://github.com/jech)
-* [ffmiyo](https://github.com/ffmiyo)
-* [Alessandro Ros](https://github.com/aler9)
-
 ### License
 MIT License - see [LICENSE](LICENSE) for full text
diff --git a/codecs/h265_packet.go b/codecs/h265_packet.go
new file mode 100644
index 0000000..a4ae843
--- /dev/null
+++ b/codecs/h265_packet.go
@@ -0,0 +1,803 @@
+package codecs
+
+import (
+	"errors"
+	"fmt"
+)
+
+//
+// Errors
+//
+
+var (
+	errH265CorruptedPacket   = errors.New("corrupted h265 packet")
+	errInvalidH265PacketType = errors.New("invalid h265 packet type")
+)
+
+//
+// Network Abstraction Unit Header implementation
+//
+
+const (
+	// sizeof(uint16)
+	h265NaluHeaderSize = 2
+	// https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2
+	h265NaluAggregationPacketType = 48
+	// https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.3
+	h265NaluFragmentationUnitType = 49
+	// https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.4
+	h265NaluPACIPacketType = 50
+)
+
+// H265NALUHeader is a H265 NAL Unit Header
+// https://datatracker.ietf.org/doc/html/rfc7798#section-1.1.4
+// +---------------+---------------+
+//  |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7|
+//  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//  |F|   Type    |  LayerID  | TID |
+//  +-------------+-----------------+
+type H265NALUHeader uint16
+
+func newH265NALUHeader(highByte, lowByte uint8) H265NALUHeader {
+	return H265NALUHeader((uint16(highByte) << 8) | uint16(lowByte))
+}
+
+// F is the forbidden bit, should always be 0.
+func (h H265NALUHeader) F() bool {
+	return (uint16(h) >> 15) != 0
+}
+
+// Type of NAL Unit.
+func (h H265NALUHeader) Type() uint8 {
+	// 01111110 00000000
+	const mask = 0b01111110 << 8
+	return uint8((uint16(h) & mask) >> (8 + 1))
+}
+
+// IsTypeVCLUnit returns whether or not the NAL Unit type is a VCL NAL unit.
+func (h H265NALUHeader) IsTypeVCLUnit() bool {
+	// Type is coded on 6 bits
+	const msbMask = 0b00100000
+	return (h.Type() & msbMask) == 0
+}
+
+// LayerID should always be 0 in non-3D HEVC context.
+func (h H265NALUHeader) LayerID() uint8 {
+	// 00000001 11111000
+	const mask = (0b00000001 << 8) | 0b11111000
+	return uint8((uint16(h) & mask) >> 3)
+}
+
+// TID is the temporal identifier of the NAL unit +1.
+func (h H265NALUHeader) TID() uint8 {
+	const mask = 0b00000111
+	return uint8(uint16(h) & mask)
+}
+
+// IsAggregationPacket returns whether or not the packet is an Aggregation packet.
+func (h H265NALUHeader) IsAggregationPacket() bool {
+	return h.Type() == h265NaluAggregationPacketType
+}
+
+// IsFragmentationUnit returns whether or not the packet is a Fragmentation Unit packet.
+func (h H265NALUHeader) IsFragmentationUnit() bool {
+	return h.Type() == h265NaluFragmentationUnitType
+}
+
+// IsPACIPacket returns whether or not the packet is a PACI packet.
+func (h H265NALUHeader) IsPACIPacket() bool {
+	return h.Type() == h265NaluPACIPacketType
+}
+
+//
+// Single NAL Unit Packet implementation
+//
+
+// H265SingleNALUnitPacket represents a NALU packet, containing exactly one NAL unit.
+//     0                   1                   2                   3
+//    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |           PayloadHdr          |      DONL (conditional)       |
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |                                                               |
+//   |                  NAL unit payload data                        |
+//   |                                                               |
+//   |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |                               :...OPTIONAL RTP padding        |
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.1
+type H265SingleNALUnitPacket struct {
+	// payloadHeader is the header of the H265 packet.
+	payloadHeader H265NALUHeader
+	// donl is a 16-bit field, that may or may not be present.
+	donl *uint16
+	// payload of the fragmentation unit.
+	payload []byte
+
+	mightNeedDONL bool
+}
+
+// WithDONL can be called to specify whether or not DONL might be parsed.
+// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream.
+func (p *H265SingleNALUnitPacket) WithDONL(value bool) {
+	p.mightNeedDONL = value
+}
+
+// Unmarshal parses the passed byte slice and stores the result in the H265SingleNALUnitPacket this method is called upon.
+func (p *H265SingleNALUnitPacket) Unmarshal(payload []byte) ([]byte, error) {
+	// sizeof(headers)
+	const totalHeaderSize = h265NaluHeaderSize
+	if payload == nil {
+		return nil, errNilPacket
+	} else if len(payload) <= totalHeaderSize {
+		return nil, fmt.Errorf("%w: %d <= %v", errShortPacket, len(payload), totalHeaderSize)
+	}
+
+	payloadHeader := newH265NALUHeader(payload[0], payload[1])
+	if payloadHeader.F() {
+		return nil, errH265CorruptedPacket
+	}
+	if payloadHeader.IsFragmentationUnit() || payloadHeader.IsPACIPacket() || payloadHeader.IsAggregationPacket() {
+		return nil, errInvalidH265PacketType
+	}
+
+	payload = payload[2:]
+
+	if p.mightNeedDONL {
+		// sizeof(uint16)
+		if len(payload) <= 2 {
+			return nil, errShortPacket
+		}
+
+		donl := (uint16(payload[0]) << 8) | uint16(payload[1])
+		p.donl = &donl
+		payload = payload[2:]
+	}
+
+	p.payloadHeader = payloadHeader
+	p.payload = payload
+
+	return nil, nil
+}
+
+// PayloadHeader returns the NALU header of the packet.
+func (p *H265SingleNALUnitPacket) PayloadHeader() H265NALUHeader {
+	return p.payloadHeader
+}
+
+// DONL returns the DONL of the packet.
+func (p *H265SingleNALUnitPacket) DONL() *uint16 {
+	return p.donl
+}
+
+// Payload returns the Fragmentation Unit packet payload.
+func (p *H265SingleNALUnitPacket) Payload() []byte {
+	return p.payload
+}
+
+func (p *H265SingleNALUnitPacket) isH265Packet() {}
+
+//
+// Aggregation Packets implementation
+//
+
+// H265AggregationUnitFirst represent the First Aggregation Unit in an AP.
+//
+//    0                   1                   2                   3
+//    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//                   :       DONL (conditional)      |   NALU size   |
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |   NALU size   |                                               |
+//   +-+-+-+-+-+-+-+-+         NAL unit                              |
+//   |                                                               |
+//   |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |                               :
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2
+type H265AggregationUnitFirst struct {
+	donl        *uint16
+	nalUnitSize uint16
+	nalUnit     []byte
+}
+
+// DONL field, when present, specifies the value of the 16 least
+// significant bits of the decoding order number of the aggregated NAL
+// unit.
+func (u H265AggregationUnitFirst) DONL() *uint16 {
+	return u.donl
+}
+
+// NALUSize represents the size, in bytes, of the NalUnit.
+func (u H265AggregationUnitFirst) NALUSize() uint16 {
+	return u.nalUnitSize
+}
+
+// NalUnit payload.
+func (u H265AggregationUnitFirst) NalUnit() []byte {
+	return u.nalUnit
+}
+
+// H265AggregationUnit represent the an Aggregation Unit in an AP, which is not the first one.
+//
+//    0                   1                   2                   3
+//    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//                   : DOND (cond)   |          NALU size            |
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |                                                               |
+//   |                       NAL unit                                |
+//   |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |                               :
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2
+type H265AggregationUnit struct {
+	dond        *uint8
+	nalUnitSize uint16
+	nalUnit     []byte
+}
+
+// DOND field plus 1 specifies the difference between
+// the decoding order number values of the current aggregated NAL unit
+// and the preceding aggregated NAL unit in the same AP.
+func (u H265AggregationUnit) DOND() *uint8 {
+	return u.dond
+}
+
+// NALUSize represents the size, in bytes, of the NalUnit.
+func (u H265AggregationUnit) NALUSize() uint16 {
+	return u.nalUnitSize
+}
+
+// NalUnit payload.
+func (u H265AggregationUnit) NalUnit() []byte {
+	return u.nalUnit
+}
+
+// H265AggregationPacket represents an Aggregation packet.
+//   0                   1                   2                   3
+//    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |    PayloadHdr (Type=48)       |                               |
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
+//   |                                                               |
+//   |             two or more aggregation units                     |
+//   |                                                               |
+//   |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//   |                               :...OPTIONAL RTP padding        |
+//   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2
+type H265AggregationPacket struct {
+	firstUnit  *H265AggregationUnitFirst
+	otherUnits []H265AggregationUnit
+
+	mightNeedDONL bool
+}
+
+// WithDONL can be called to specify whether or not DONL might be parsed.
+// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream.
+func (p *H265AggregationPacket) WithDONL(value bool) {
+	p.mightNeedDONL = value
+}
+
+// Unmarshal parses the passed byte slice and stores the result in the H265AggregationPacket this method is called upon.
+func (p *H265AggregationPacket) Unmarshal(payload []byte) ([]byte, error) {
+	// sizeof(headers)
+	const totalHeaderSize = h265NaluHeaderSize
+	if payload == nil {
+		return nil, errNilPacket
+	} else if len(payload) <= totalHeaderSize {
+		return nil, fmt.Errorf("%w: %d <= %v", errShortPacket, len(payload), totalHeaderSize)
+	}
+
+	payloadHeader := newH265NALUHeader(payload[0], payload[1])
+	if payloadHeader.F() {
+		return nil, errH265CorruptedPacket
+	}
+	if !payloadHeader.IsAggregationPacket() {
+		return nil, errInvalidH265PacketType
+	}
+
+	// First parse the first aggregation unit
+	payload = payload[2:]
+	firstUnit := &H265AggregationUnitFirst{}
+
+	if p.mightNeedDONL {
+		if len(payload) < 2 {
+			return nil, errShortPacket
+		}
+
+		donl := (uint16(payload[0]) << 8) | uint16(payload[1])
+		firstUnit.donl = &donl
+
+		payload = payload[2:]
+	}
+	if len(payload) < 2 {
+		return nil, errShortPacket
+	}
+	firstUnit.nalUnitSize = (uint16(payload[0]) << 8) | uint16(payload[1])
+	payload = payload[2:]
+
+	if len(payload) < int(firstUnit.nalUnitSize) {
+		return nil, errShortPacket
+	}
+
+	firstUnit.nalUnit = payload[:firstUnit.nalUnitSize]
+	payload = payload[firstUnit.nalUnitSize:]
+
+	// Parse remaining Aggregation Units
+	var units []H265AggregationUnit
+	for {
+		unit := H265AggregationUnit{}
+
+		if p.mightNeedDONL {
+			if len(payload) < 1 {
+				break
+			}
+
+			dond := payload[0]
+			unit.dond = &dond
+
+			payload = payload[1:]
+		}
+
+		if len(payload) < 2 {
+			break
+		}
+		unit.nalUnitSize = (uint16(payload[0]) << 8) | uint16(payload[1])
+		payload = payload[2:]
+
+		if len(payload) < int(unit.nalUnitSize) {
+			break
+		}
+
+		unit.nalUnit = payload[:unit.nalUnitSize]
+		payload = payload[unit.nalUnitSize:]
+
+		units = append(units, unit)
+	}
+
+	// There need to be **at least** two Aggregation Units (first + another one)
+	if len(units) == 0 {
+		return nil, errShortPacket
+	}
+
+	p.firstUnit = firstUnit
+	p.otherUnits = units
+
+	return nil, nil
+}
+
+// FirstUnit returns the first Aggregated Unit of the packet.
+func (p *H265AggregationPacket) FirstUnit() *H265AggregationUnitFirst {
+	return p.firstUnit
+}
+
+// OtherUnits returns the all the other Aggregated Unit of the packet (excluding the first one).
+func (p *H265AggregationPacket) OtherUnits() []H265AggregationUnit {
+	return p.otherUnits
+}
+
+func (p *H265AggregationPacket) isH265Packet() {}
+
+//
+// Fragmentation Unit implementation
+//
+
+const (
+	// sizeof(uint8)
+	h265FragmentationUnitHeaderSize = 1
+)
+
+// H265FragmentationUnitHeader is a H265 FU Header
+// +---------------+
+// |0|1|2|3|4|5|6|7|
+// +-+-+-+-+-+-+-+-+
+// |S|E|  FuType   |
+// +---------------+
+type H265FragmentationUnitHeader uint8
+
+// S represents the start of a fragmented NAL unit.
+func (h H265FragmentationUnitHeader) S() bool {
+	const mask = 0b10000000
+	return ((h & mask) >> 7) != 0
+}
+
+// E represents the end of a fragmented NAL unit.
+func (h H265FragmentationUnitHeader) E() bool {
+	const mask = 0b01000000
+	return ((h & mask) >> 6) != 0
+}
+
+// FuType MUST be equal to the field Type of the fragmented NAL unit.
+func (h H265FragmentationUnitHeader) FuType() uint8 {
+	const mask = 0b00111111
+	return uint8(h) & mask
+}
+
+// H265FragmentationUnitPacket represents a single Fragmentation Unit packet.
+//
+//  0                   1                   2                   3
+// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |    PayloadHdr (Type=49)       |   FU header   | DONL (cond)   |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
+// | DONL (cond)   |                                               |
+// |-+-+-+-+-+-+-+-+                                               |
+// |                         FU payload                            |
+// |                                                               |
+// |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |                               :...OPTIONAL RTP padding        |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.3
+type H265FragmentationUnitPacket struct {
+	// payloadHeader is the header of the H265 packet.
+	payloadHeader H265NALUHeader
+	// fuHeader is the header of the fragmentation unit
+	fuHeader H265FragmentationUnitHeader
+	// donl is a 16-bit field, that may or may not be present.
+	donl *uint16
+	// payload of the fragmentation unit.
+	payload []byte
+
+	mightNeedDONL bool
+}
+
+// WithDONL can be called to specify whether or not DONL might be parsed.
+// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream.
+func (p *H265FragmentationUnitPacket) WithDONL(value bool) {
+	p.mightNeedDONL = value
+}
+
+// Unmarshal parses the passed byte slice and stores the result in the H265FragmentationUnitPacket this method is called upon.
+func (p *H265FragmentationUnitPacket) Unmarshal(payload []byte) ([]byte, error) {
+	// sizeof(headers)
+	const totalHeaderSize = h265NaluHeaderSize + h265FragmentationUnitHeaderSize
+	if payload == nil {
+		return nil, errNilPacket
+	} else if len(payload) <= totalHeaderSize {
+		return nil, fmt.Errorf("%w: %d <= %v", errShortPacket, len(payload), totalHeaderSize)
+	}
+
+	payloadHeader := newH265NALUHeader(payload[0], payload[1])
+	if payloadHeader.F() {
+		return nil, errH265CorruptedPacket
+	}
+	if !payloadHeader.IsFragmentationUnit() {
+		return nil, errInvalidH265PacketType
+	}
+
+	fuHeader := H265FragmentationUnitHeader(payload[2])
+	payload = payload[3:]
+
+	if fuHeader.S() && p.mightNeedDONL {
+		// sizeof(uint16)
+		if len(payload) <= 2 {
+			return nil, errShortPacket
+		}
+
+		donl := (uint16(payload[0]) << 8) | uint16(payload[1])
+		p.donl = &donl
+		payload = payload[2:]
+	}
+
+	p.payloadHeader = payloadHeader
+	p.fuHeader = fuHeader
+	p.payload = payload
+
+	return nil, nil
+}
+
+// PayloadHeader returns the NALU header of the packet.
+func (p *H265FragmentationUnitPacket) PayloadHeader() H265NALUHeader {
+	return p.payloadHeader
+}
+
+// FuHeader returns the Fragmentation Unit Header of the packet.
+func (p *H265FragmentationUnitPacket) FuHeader() H265FragmentationUnitHeader {
+	return p.fuHeader
+}
+
+// DONL returns the DONL of the packet.
+func (p *H265FragmentationUnitPacket) DONL() *uint16 {
+	return p.donl
+}
+
+// Payload returns the Fragmentation Unit packet payload.
+func (p *H265FragmentationUnitPacket) Payload() []byte {
+	return p.payload
+}
+
+func (p *H265FragmentationUnitPacket) isH265Packet() {}
+
+//
+// PACI implementation
+//
+
+// H265PACIPacket represents a single H265 PACI packet.
+//
+//  0                   1                   2                   3
+// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |    PayloadHdr (Type=50)       |A|   cType   | PHSsize |F0..2|Y|
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |        Payload Header Extension Structure (PHES)              |
+// |=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=|
+// |                                                               |
+// |                  PACI payload: NAL unit                       |
+// |                   . . .                                       |
+// |                                                               |
+// |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |                               :...OPTIONAL RTP padding        |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.4
+type H265PACIPacket struct {
+	// payloadHeader is the header of the H265 packet.
+	payloadHeader H265NALUHeader
+
+	// Field which holds value for `A`, `cType`, `PHSsize`, `F0`, `F1`, `F2` and `Y` fields.
+	paciHeaderFields uint16
+
+	// phes is a header extension, of byte length `PHSsize`
+	phes []byte
+
+	// Payload contains NAL units & optional padding
+	payload []byte
+}
+
+// PayloadHeader returns the NAL Unit Header.
+func (p *H265PACIPacket) PayloadHeader() H265NALUHeader {
+	return p.payloadHeader
+}
+
+// A copies the F bit of the PACI payload NALU.
+func (p *H265PACIPacket) A() bool {
+	const mask = 0b10000000 << 8
+	return (p.paciHeaderFields & mask) != 0
+}
+
+// CType copies the Type field of the PACI payload NALU.
+func (p *H265PACIPacket) CType() uint8 {
+	const mask = 0b01111110 << 8
+	return uint8((p.paciHeaderFields & mask) >> (8 + 1))
+}
+
+// PHSsize indicates the size of the PHES field.
+func (p *H265PACIPacket) PHSsize() uint8 {
+	const mask = (0b00000001 << 8) | 0b11110000
+	return uint8((p.paciHeaderFields & mask) >> 4)
+}
+
+// F0 indicates the presence of a Temporal Scalability support extension in the PHES.
+func (p *H265PACIPacket) F0() bool {
+	const mask = 0b00001000
+	return (p.paciHeaderFields & mask) != 0
+}
+
+// F1 must be zero, reserved for future extensions.
+func (p *H265PACIPacket) F1() bool {
+	const mask = 0b00000100
+	return (p.paciHeaderFields & mask) != 0
+}
+
+// F2 must be zero, reserved for future extensions.
+func (p *H265PACIPacket) F2() bool {
+	const mask = 0b00000010
+	return (p.paciHeaderFields & mask) != 0
+}
+
+// Y must be zero, reserved for future extensions.
+func (p *H265PACIPacket) Y() bool {
+	const mask = 0b00000001
+	return (p.paciHeaderFields & mask) != 0
+}
+
+// PHES contains header extensions. Its size is indicated by PHSsize.
+func (p *H265PACIPacket) PHES() []byte {
+	return p.phes
+}
+
+// Payload is a single NALU or NALU-like struct, not including the first two octets (header).
+func (p *H265PACIPacket) Payload() []byte {
+	return p.payload
+}
+
+// TSCI returns the Temporal Scalability Control Information extension, if present.
+func (p *H265PACIPacket) TSCI() *H265TSCI {
+	if !p.F0() || p.PHSsize() < 3 {
+		return nil
+	}
+
+	tsci := H265TSCI((uint32(p.phes[0]) << 16) | (uint32(p.phes[1]) << 8) | uint32(p.phes[0]))
+	return &tsci
+}
+
+// Unmarshal parses the passed byte slice and stores the result in the H265PACIPacket this method is called upon.
+func (p *H265PACIPacket) Unmarshal(payload []byte) ([]byte, error) {
+	// sizeof(headers)
+	const totalHeaderSize = h265NaluHeaderSize + 2
+	if payload == nil {
+		return nil, errNilPacket
+	} else if len(payload) <= totalHeaderSize {
+		return nil, fmt.Errorf("%w: %d <= %v", errShortPacket, len(payload), totalHeaderSize)
+	}
+
+	payloadHeader := newH265NALUHeader(payload[0], payload[1])
+	if payloadHeader.F() {
+		return nil, errH265CorruptedPacket
+	}
+	if !payloadHeader.IsPACIPacket() {
+		return nil, errInvalidH265PacketType
+	}
+
+	paciHeaderFields := (uint16(payload[2]) << 8) | uint16(payload[3])
+	payload = payload[4:]
+
+	p.paciHeaderFields = paciHeaderFields
+	headerExtensionSize := p.PHSsize()
+
+	if len(payload) < int(headerExtensionSize)+1 {
+		p.paciHeaderFields = 0
+		return nil, errShortPacket
+	}
+
+	p.payloadHeader = payloadHeader
+
+	if headerExtensionSize > 0 {
+		p.phes = payload[:headerExtensionSize]
+	}
+
+	payload = payload[headerExtensionSize:]
+	p.payload = payload
+
+	return nil, nil
+}
+
+func (p *H265PACIPacket) isH265Packet() {}
+
+//
+// Temporal Scalability Control Information
+//
+
+// H265TSCI is a Temporal Scalability Control Information header extension.
+// Reference: https://datatracker.ietf.org/doc/html/rfc7798#section-4.5
+type H265TSCI uint32
+
+// TL0PICIDX see RFC7798 for more details.
+func (h H265TSCI) TL0PICIDX() uint8 {
+	const m1 = 0xFFFF0000
+	const m2 = 0xFF00
+	return uint8((((h & m1) >> 16) & m2) >> 8)
+}
+
+// IrapPicID see RFC7798 for more details.
+func (h H265TSCI) IrapPicID() uint8 {
+	const m1 = 0xFFFF0000
+	const m2 = 0x00FF
+	return uint8(((h & m1) >> 16) & m2)
+}
+
+// S see RFC7798 for more details.
+func (h H265TSCI) S() bool {
+	const m1 = 0xFF00
+	const m2 = 0b10000000
+	return (uint8((h&m1)>>8) & m2) != 0
+}
+
+// E see RFC7798 for more details.
+func (h H265TSCI) E() bool {
+	const m1 = 0xFF00
+	const m2 = 0b01000000
+	return (uint8((h&m1)>>8) & m2) != 0
+}
+
+// RES see RFC7798 for more details.
+func (h H265TSCI) RES() uint8 {
+	const m1 = 0xFF00
+	const m2 = 0b00111111
+	return uint8((h&m1)>>8) & m2
+}
+
+//
+// H265 Packet interface
+//
+
+type isH265Packet interface {
+	isH265Packet()
+}
+
+var (
+	_ isH265Packet = (*H265FragmentationUnitPacket)(nil)
+	_ isH265Packet = (*H265PACIPacket)(nil)
+	_ isH265Packet = (*H265SingleNALUnitPacket)(nil)
+	_ isH265Packet = (*H265AggregationPacket)(nil)
+)
+
+//
+// Packet implementation
+//
+
+// H265Packet represents a H265 packet, stored in the payload of an RTP packet.
+type H265Packet struct {
+	packet        isH265Packet
+	mightNeedDONL bool
+}
+
+// WithDONL can be called to specify whether or not DONL might be parsed.
+// DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream.
+func (p *H265Packet) WithDONL(value bool) {
+	p.mightNeedDONL = value
+}
+
+// Unmarshal parses the passed byte slice and stores the result in the H265Packet this method is called upon
+func (p *H265Packet) Unmarshal(payload []byte) ([]byte, error) {
+	if payload == nil {
+		return nil, errNilPacket
+	} else if len(payload) <= h265NaluHeaderSize {
+		return nil, fmt.Errorf("%w: %d <= %v", errShortPacket, len(payload), h265NaluHeaderSize)
+	}
+
+	payloadHeader := newH265NALUHeader(payload[0], payload[1])
+	if payloadHeader.F() {
+		return nil, errH265CorruptedPacket
+	}
+
+	switch {
+	case payloadHeader.IsPACIPacket():
+		decoded := &H265PACIPacket{}
+		if _, err := decoded.Unmarshal(payload); err != nil {
+			return nil, err
+		}
+
+		p.packet = decoded
+
+	case payloadHeader.IsFragmentationUnit():
+		decoded := &H265FragmentationUnitPacket{}
+		decoded.WithDONL(p.mightNeedDONL)
+
+		if _, err := decoded.Unmarshal(payload); err != nil {
+			return nil, err
+		}
+
+		p.packet = decoded
+
+	case payloadHeader.IsAggregationPacket():
+		decoded := &H265AggregationPacket{}
+		decoded.WithDONL(p.mightNeedDONL)
+
+		if _, err := decoded.Unmarshal(payload); err != nil {
+			return nil, err
+		}
+
+		p.packet = decoded
+
+	default:
+		decoded := &H265SingleNALUnitPacket{}
+		decoded.WithDONL(p.mightNeedDONL)
+
+		if _, err := decoded.Unmarshal(payload); err != nil {
+			return nil, err
+		}
+
+		p.packet = decoded
+	}
+
+	return nil, nil
+}
+
+// Packet returns the populated packet.
+// Must be casted to one of:
+// - *H265SingleNALUnitPacket
+// - *H265FragmentationUnitPacket
+// - *H265AggregationPacket
+// - *H265PACIPacket
+// nolint:golint
+func (p *H265Packet) Packet() isH265Packet {
+	return p.packet
+}
diff --git a/codecs/vp8_packet.go b/codecs/vp8_packet.go
index b602ef5..24e7200 100644
--- a/codecs/vp8_packet.go
+++ b/codecs/vp8_packet.go
@@ -2,7 +2,8 @@ package codecs
 
 // VP8Payloader payloads VP8 packets
 type VP8Payloader struct {
-	pictureID uint16
+	EnablePictureID bool
+	pictureID       uint16
 }
 
 const (
@@ -33,12 +34,14 @@ func (p *VP8Payloader) Payload(mtu int, payload []byte) [][]byte {
 	 */
 
 	usingHeaderSize := vp8HeaderSize
-	switch {
-	case p.pictureID == 0:
-	case p.pictureID < 128:
-		usingHeaderSize = vp8HeaderSize + 2
-	default:
-		usingHeaderSize = vp8HeaderSize + 3
+	if p.EnablePictureID {
+		switch {
+		case p.pictureID == 0:
+		case p.pictureID < 128:
+			usingHeaderSize = vp8HeaderSize + 2
+		default:
+			usingHeaderSize = vp8HeaderSize + 3
+		}
 	}
 
 	maxFragmentSize := mtu - usingHeaderSize
@@ -62,17 +65,19 @@ func (p *VP8Payloader) Payload(mtu int, payload []byte) [][]byte {
 			out[0] = 0x10
 			first = false
 		}
-		switch usingHeaderSize {
-		case vp8HeaderSize:
-		case vp8HeaderSize + 2:
-			out[0] |= 0x80
-			out[1] |= 0x80
-			out[2] |= uint8(p.pictureID & 0x7F)
-		case vp8HeaderSize + 3:
-			out[0] |= 0x80
-			out[1] |= 0x80
-			out[2] |= 0x80 | uint8((p.pictureID>>8)&0x7F)
-			out[3] |= uint8(p.pictureID & 0xFF)
+		if p.EnablePictureID {
+			switch usingHeaderSize {
+			case vp8HeaderSize:
+			case vp8HeaderSize + 2:
+				out[0] |= 0x80
+				out[1] |= 0x80
+				out[2] |= uint8(p.pictureID & 0x7F)
+			case vp8HeaderSize + 3:
+				out[0] |= 0x80
+				out[1] |= 0x80
+				out[2] |= 0x80 | uint8((p.pictureID>>8)&0x7F)
+				out[3] |= uint8(p.pictureID & 0xFF)
+			}
 		}
 
 		copy(out[usingHeaderSize:], payloadData[payloadDataIndex:payloadDataIndex+currentFragmentSize])
diff --git a/codecs/vp9_packet.go b/codecs/vp9_packet.go
index 0396554..9d05340 100644
--- a/codecs/vp9_packet.go
+++ b/codecs/vp9_packet.go
@@ -25,12 +25,12 @@ const (
 // Payload fragments an VP9 packet across one or more byte arrays
 func (p *VP9Payloader) Payload(mtu int, payload []byte) [][]byte {
 	/*
-	 * https://www.ietf.org/id/draft-ietf-payload-vp9-10.txt
+	 * https://www.ietf.org/id/draft-ietf-payload-vp9-13.txt
 	 *
 	 * Flexible mode (F=1)
 	 *        0 1 2 3 4 5 6 7
 	 *       +-+-+-+-+-+-+-+-+
-	 *       |I|P|L|F|B|E|V|-| (REQUIRED)
+	 *       |I|P|L|F|B|E|V|Z| (REQUIRED)
 	 *       +-+-+-+-+-+-+-+-+
 	 *  I:   |M| PICTURE ID  | (REQUIRED)
 	 *       +-+-+-+-+-+-+-+-+
@@ -47,7 +47,7 @@ func (p *VP9Payloader) Payload(mtu int, payload []byte) [][]byte {
 	 * Non-flexible mode (F=0)
 	 *        0 1 2 3 4 5 6 7
 	 *       +-+-+-+-+-+-+-+-+
-	 *       |I|P|L|F|B|E|V|-| (REQUIRED)
+	 *       |I|P|L|F|B|E|V|Z| (REQUIRED)
 	 *       +-+-+-+-+-+-+-+-+
 	 *  I:   |M| PICTURE ID  | (RECOMMENDED)
 	 *       +-+-+-+-+-+-+-+-+
@@ -121,6 +121,7 @@ type VP9Packet struct {
 	B bool // Start of a frame
 	E bool // End of a frame
 	V bool // Scalability structure (SS) data present
+	Z bool // Not a reference frame for upper spatial layers
 
 	// Recommended headers
 	PictureID uint16 // 7 or 16 bits, picture ID
@@ -171,6 +172,7 @@ func (p *VP9Packet) Unmarshal(packet []byte) ([]byte, error) {
 	p.B = packet[0]&0x08 != 0
 	p.E = packet[0]&0x04 != 0
 	p.V = packet[0]&0x02 != 0
+	p.Z = packet[0]&0x01 != 0
 
 	pos := 1
 	var err error
diff --git a/renovate.json b/renovate.json
index f84608c..08c1e39 100644
--- a/renovate.json
+++ b/renovate.json
@@ -15,5 +15,12 @@
       "packagePatterns": ["^golang.org/x/"],
       "schedule": ["on the first day of the month"]
     }
+  ],
+  "ignorePaths": [
+    ".github/workflows/generate-authors.yml",
+    ".github/workflows/lint.yaml",
+    ".github/workflows/renovate-go-mod-fix.yaml",
+    ".github/workflows/test.yaml",
+    ".github/workflows/tidy-check.yaml"
   ]
 }


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 5b7a43b..7a72c2c 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,3 +1,14 @@
+#
+# DO NOT EDIT THIS FILE
+#
+# It is automatically copied from https://github.com/pion/.goassets repository.
+# If this repository should have package specific CI config,
+# remove the repository name from .goassets/.github/workflows/assets-sync.yml.
+#
+# If you want to update the shared CI config, send a PR to
+# https://github.com/pion/.goassets instead of this repository.
+#
+
 name: Test
 on:
   push:
@@ -39,9 +50,17 @@ jobs:
 
       - name: Run test
         run: |
+          TEST_BENCH_OPTION="-bench=."
+          if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi
+
           go-acc -o cover.out ./... -- \
-          -bench=. \
-          -v -race
+            ${TEST_BENCH_OPTION} \
+            -v -race
+
+      - name: Run TEST_HOOK
+        run: |
+          if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi
+          if [ -n "${TEST_HOOK}" ]; then ${TEST_HOOK}; fi
 
       - uses: codecov/codecov-action@v1
         with:
@@ -73,17 +92,17 @@ jobs:
         run: |
           mkdir -p $HOME/go/pkg/mod $HOME/.cache
           docker run \
-          -u $(id -u):$(id -g) \
-          -e "GO111MODULE=on" \
-          -e "CGO_ENABLED=0" \
-          -v $GITHUB_WORKSPACE:/go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
-          -v $HOME/go/pkg/mod:/go/pkg/mod \
-          -v $HOME/.cache:/.cache \
-          -w /go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
-          i386/golang:${{matrix.go}}-alpine \
-          /usr/local/go/bin/go test \
-            ${TEST_EXTRA_ARGS:-} \
-            -v ./...
+            -u $(id -u):$(id -g) \
+            -e "GO111MODULE=on" \
+            -e "CGO_ENABLED=0" \
+            -v $GITHUB_WORKSPACE:/go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
+            -v $HOME/go/pkg/mod:/go/pkg/mod \
+            -v $HOME/.cache:/.cache \
+            -w /go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
+            i386/golang:${{matrix.go}}-alpine \
+            /usr/local/go/bin/go test \
+              ${TEST_EXTRA_ARGS:-} \
+              -v ./...
 
   test-wasm:
     runs-on: ubuntu-latest
@@ -126,10 +145,11 @@ jobs:
 
       - name: Run Tests
         run: |
+          if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi
           GOOS=js GOARCH=wasm $GOPATH/bin/go test \
-          -coverprofile=cover.out -covermode=atomic \
-          -exec="${GO_JS_WASM_EXEC}" \
-          -v ./...
+            -coverprofile=cover.out -covermode=atomic \
+            -exec="${GO_JS_WASM_EXEC}" \
+            -v ./...
 
       - uses: codecov/codecov-action@v1
         with:
diff --git a/codecs/h265_packet_test.go b/codecs/h265_packet_test.go
new file mode 100644
index 0000000..7c25a9c
--- /dev/null
+++ b/codecs/h265_packet_test.go
@@ -0,0 +1,844 @@
+package codecs
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestH265_NALU_Header(t *testing.T) {
+	tt := [...]struct {
+		RawHeader []byte
+
+		FBit    bool
+		Type    uint8
+		LayerID uint8
+		TID     uint8
+
+		IsAP   bool
+		IsFU   bool
+		IsPACI bool
+	}{
+		// FBit
+		{
+			RawHeader: []byte{0x80, 0x00},
+			Type:      0,
+			LayerID:   0,
+			TID:       0,
+			FBit:      true,
+		},
+		// VPS_NUT
+		{
+			RawHeader: []byte{0x40, 0x01},
+			Type:      32,
+			LayerID:   0,
+			TID:       1,
+		},
+		// SPS_NUT
+		{
+			RawHeader: []byte{0x42, 0x01},
+			Type:      33,
+			LayerID:   0,
+			TID:       1,
+		},
+		// PPS_NUT
+		{
+			RawHeader: []byte{0x44, 0x01},
+			Type:      34,
+			LayerID:   0,
+			TID:       1,
+		},
+		// PREFIX_SEI_NUT
+		{
+			RawHeader: []byte{0x4e, 0x01},
+			Type:      39,
+			LayerID:   0,
+			TID:       1,
+		},
+		// Fragmentation Unit
+		{
+			RawHeader: []byte{0x62, 0x01},
+			Type:      h265NaluFragmentationUnitType,
+			LayerID:   0,
+			TID:       1,
+			IsFU:      true,
+		},
+	}
+
+	for _, cur := range tt {
+		header := newH265NALUHeader(cur.RawHeader[0], cur.RawHeader[1])
+
+		if header.F() != cur.FBit {
+			t.Fatal("invalid F bit")
+		}
+
+		if header.Type() != cur.Type {
+			t.Fatal("invalid Type")
+		}
+
+		// For any type < 32, NAL is a VLC NAL unit.
+		if header.IsTypeVCLUnit() != (header.Type() < 32) {
+			t.Fatal("invalid IsTypeVCLUnit")
+		}
+
+		if header.IsAggregationPacket() != cur.IsAP {
+			t.Fatal("invalid Type (aggregation packet)")
+		}
+
+		if header.IsFragmentationUnit() != cur.IsFU {
+			t.Fatal("invalid Type (fragmentation unit)")
+		}
+
+		if header.IsPACIPacket() != cur.IsPACI {
+			t.Fatal("invalid Type (PACI)")
+		}
+
+		if header.LayerID() != cur.LayerID {
+			t.Fatal("invalid LayerID")
+		}
+
+		if header.TID() != cur.TID {
+			t.Fatal("invalid TID")
+		}
+	}
+}
+
+func TestH265_FU_Header(t *testing.T) {
+	tt := [...]struct {
+		header H265FragmentationUnitHeader
+
+		S    bool
+		E    bool
+		Type uint8
+	}{
+		// Start | IDR_W_RADL
+		{
+			header: H265FragmentationUnitHeader(0x93),
+			S:      true,
+			E:      false,
+			Type:   19,
+		},
+		// Continuation | IDR_W_RADL
+		{
+			header: H265FragmentationUnitHeader(0x13),
+			S:      false,
+			E:      false,
+			Type:   19,
+		},
+		// End | IDR_W_RADL
+		{
+			header: H265FragmentationUnitHeader(0x53),
+			S:      false,
+			E:      true,
+			Type:   19,
+		},
+		// Start | TRAIL_R
+		{
+			header: H265FragmentationUnitHeader(0x81),
+			S:      true,
+			E:      false,
+			Type:   1,
+		},
+		// Continuation | TRAIL_R
+		{
+			header: H265FragmentationUnitHeader(0x01),
+			S:      false,
+			E:      false,
+			Type:   1,
+		},
+		// End | TRAIL_R
+		{
+			header: H265FragmentationUnitHeader(0x41),
+			S:      false,
+			E:      true,
+			Type:   1,
+		},
+	}
+
+	for _, cur := range tt {
+		if cur.header.S() != cur.S {
+			t.Fatal("invalid S field")
+		}
+
+		if cur.header.E() != cur.E {
+			t.Fatal("invalid E field")
+		}
+
+		if cur.header.FuType() != cur.Type {
+			t.Fatal("invalid FuType field")
+		}
+	}
+}
+
+func TestH265_SingleNALUnitPacket(t *testing.T) {
+	tt := [...]struct {
+		Raw            []byte
+		WithDONL       bool
+		ExpectedPacket *H265SingleNALUnitPacket
+		ExpectedErr    error
+	}{
+		{
+			Raw:         nil,
+			ExpectedErr: errNilPacket,
+		},
+		{
+			Raw:         []byte{},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62, 0x01, 0x93},
+			ExpectedErr: errShortPacket,
+		},
+		// FBit enabled in H265NALUHeader
+		{
+			Raw:         []byte{0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errH265CorruptedPacket,
+		},
+		// Type '49' in H265NALUHeader
+		{
+			Raw:         []byte{0x62, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errInvalidH265PacketType,
+		},
+		// Type '50' in H265NALUHeader
+		{
+			Raw:         []byte{0x64, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errInvalidH265PacketType,
+		},
+		{
+			Raw: []byte{0x01, 0x01, 0xab, 0xcd, 0xef},
+			ExpectedPacket: &H265SingleNALUnitPacket{
+				payloadHeader: newH265NALUHeader(0x01, 0x01),
+				payload:       []byte{0xab, 0xcd, 0xef},
+			},
+		},
+		// DONL, payload too small
+		{
+			Raw:         []byte{0x01, 0x01, 0x93, 0xaf},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		{
+			Raw: []byte{0x01, 0x01, 0xaa, 0xbb, 0xcc},
+			ExpectedPacket: &H265SingleNALUnitPacket{
+				payloadHeader: newH265NALUHeader(0x01, 0x01),
+				donl:          uint16ptr((uint16(0xaa) << 8) | uint16(0xbb)),
+				payload:       []byte{0xcc},
+			},
+			WithDONL: true,
+		},
+	}
+
+	for _, cur := range tt {
+		parsed := &H265SingleNALUnitPacket{}
+		if cur.WithDONL {
+			parsed.WithDONL(cur.WithDONL)
+		}
+
+		// Just for code coverage sake
+		parsed.isH265Packet()
+
+		_, err := parsed.Unmarshal(cur.Raw)
+
+		if cur.ExpectedErr != nil && err == nil {
+			t.Fatal("should error")
+		} else if cur.ExpectedErr == nil && err != nil {
+			t.Fatal("should not error")
+		}
+
+		if cur.ExpectedPacket == nil {
+			continue
+		}
+
+		if cur.ExpectedPacket.PayloadHeader() != parsed.PayloadHeader() {
+			t.Fatal("invalid payload header")
+		}
+
+		if cur.ExpectedPacket.DONL() != nil && (*parsed.DONL() != *cur.ExpectedPacket.DONL()) {
+			t.Fatal("invalid DONL")
+		}
+
+		if !reflect.DeepEqual(cur.ExpectedPacket.Payload(), parsed.Payload()) {
+			t.Fatal("invalid payload")
+		}
+	}
+}
+
+func TestH265_AggregationPacket(t *testing.T) {
+	tt := [...]struct {
+		Raw            []byte
+		WithDONL       bool
+		ExpectedPacket *H265AggregationPacket
+		ExpectedErr    error
+	}{
+		{
+			Raw:         nil,
+			ExpectedErr: errNilPacket,
+		},
+		{
+			Raw:         []byte{},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62, 0x01, 0x93},
+			ExpectedErr: errShortPacket,
+		},
+		// FBit enabled in H265NALUHeader
+		{
+			Raw:         []byte{0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errH265CorruptedPacket,
+		},
+		// Type '48' in H265NALUHeader
+		{
+			Raw:         []byte{0xE0, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errInvalidH265PacketType,
+		},
+		// Small payload
+		{
+			Raw:         []byte{0x60, 0x01, 0x00, 0x1},
+			ExpectedErr: errShortPacket,
+		},
+		// Small payload
+		{
+			Raw:         []byte{0x60, 0x01, 0x00},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		// Small payload
+		{
+			Raw:         []byte{0x60, 0x01, 0x00, 0x1},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		// Small payload
+		{
+			Raw:         []byte{0x60, 0x01, 0x00, 0x01, 0x02},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		// Single Aggregation Unit
+		{
+			Raw:         []byte{0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		// Incomplete second Aggregation Unit
+		{
+			Raw: []byte{
+				0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00,
+				// DONL
+				0x00,
+			},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		// Incomplete second Aggregation Unit
+		{
+			Raw: []byte{
+				0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00,
+				// DONL, NAL Unit size (2 bytes)
+				0x00, 0x55, 0x55,
+			},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		// Valid Second Aggregation Unit
+		{
+			Raw: []byte{
+				0x60, 0x01, 0xcc, 0xdd, 0x00, 0x02, 0xff, 0xee,
+				// DONL, NAL Unit size (2 bytes), Payload
+				0x77, 0x00, 0x01, 0xaa,
+			},
+			WithDONL: true,
+			ExpectedPacket: &H265AggregationPacket{
+				firstUnit: &H265AggregationUnitFirst{
+					donl:        uint16ptr(0xccdd),
+					nalUnitSize: 2,
+					nalUnit:     []byte{0xff, 0xee},
+				},
+				otherUnits: []H265AggregationUnit{
+					{
+						dond:        uint8ptr(0x77),
+						nalUnitSize: 1,
+						nalUnit:     []byte{0xaa},
+					},
+				},
+			},
+		},
+	}
+
+	for _, cur := range tt {
+		parsed := &H265AggregationPacket{}
+		if cur.WithDONL {
+			parsed.WithDONL(cur.WithDONL)
+		}
+
+		// Just for code coverage sake
+		parsed.isH265Packet()
+
+		_, err := parsed.Unmarshal(cur.Raw)
+
+		if cur.ExpectedErr != nil && err == nil {
+			t.Fatal("should error")
+		} else if cur.ExpectedErr == nil && err != nil {
+			t.Fatal("should not error")
+		}
+
+		if cur.ExpectedPacket == nil {
+			continue
+		}
+
+		if cur.ExpectedPacket.FirstUnit() != nil {
+			if parsed.FirstUnit().NALUSize() != cur.ExpectedPacket.FirstUnit().NALUSize() {
+				t.Fatal("invalid first unit NALUSize")
+			}
+
+			if cur.ExpectedPacket.FirstUnit().DONL() != nil && *cur.ExpectedPacket.FirstUnit().DONL() != *parsed.FirstUnit().DONL() {
+				t.Fatal("invalid first unit DONL")
+			}
+
+			if !reflect.DeepEqual(cur.ExpectedPacket.FirstUnit().NalUnit(), parsed.FirstUnit().NalUnit()) {
+				t.Fatal("invalid first unit NalUnit")
+			}
+		}
+
+		if len(cur.ExpectedPacket.OtherUnits()) != len(parsed.OtherUnits()) {
+			t.Fatal("number of other units mismatch")
+		}
+
+		for ndx, unit := range cur.ExpectedPacket.OtherUnits() {
+			if parsed.OtherUnits()[ndx].NALUSize() != unit.NALUSize() {
+				t.Fatal("invalid unit NALUSize")
+			}
+
+			if unit.DOND() != nil && *unit.DOND() != *parsed.OtherUnits()[ndx].DOND() {
+				t.Fatal("invalid unit DOND")
+			}
+
+			if !reflect.DeepEqual(unit.NalUnit(), parsed.OtherUnits()[ndx].NalUnit()) {
+				t.Fatal("invalid first unit NalUnit")
+			}
+		}
+
+		if !reflect.DeepEqual(cur.ExpectedPacket.OtherUnits(), parsed.OtherUnits()) {
+			t.Fatal("invalid payload")
+		}
+	}
+}
+
+func TestH265_FragmentationUnitPacket(t *testing.T) {
+	tt := [...]struct {
+		Raw         []byte
+		WithDONL    bool
+		ExpectedFU  *H265FragmentationUnitPacket
+		ExpectedErr error
+	}{
+		{
+			Raw:         nil,
+			ExpectedErr: errNilPacket,
+		},
+		{
+			Raw:         []byte{},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62, 0x01},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62, 0x01, 0x93},
+			ExpectedErr: errShortPacket,
+		},
+		// FBit enabled in H265NALUHeader
+		{
+			Raw:         []byte{0x80, 0x01, 0x93, 0xaf},
+			ExpectedErr: errH265CorruptedPacket,
+		},
+		// Type not '49' in H265NALUHeader
+		{
+			Raw:         []byte{0x40, 0x01, 0x93, 0xaf},
+			ExpectedErr: errInvalidH265PacketType,
+		},
+		{
+			Raw: []byte{0x62, 0x01, 0x93, 0xaf},
+			ExpectedFU: &H265FragmentationUnitPacket{
+				payloadHeader: newH265NALUHeader(0x62, 0x01),
+				fuHeader:      H265FragmentationUnitHeader(0x93),
+				donl:          nil,
+				payload:       []byte{0xaf},
+			},
+		},
+		{
+			Raw:         []byte{0x62, 0x01, 0x93, 0xcc},
+			WithDONL:    true,
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:      []byte{0x62, 0x01, 0x93, 0xcc, 0xdd, 0xaf, 0x0d, 0x5a},
+			WithDONL: true,
+			ExpectedFU: &H265FragmentationUnitPacket{
+				payloadHeader: newH265NALUHeader(0x62, 0x01),
+				fuHeader:      H265FragmentationUnitHeader(0x93),
+				donl:          uint16ptr((uint16(0xcc) << 8) | uint16(0xdd)),
+				payload:       []byte{0xaf, 0x0d, 0x5a},
+			},
+		},
+	}
+
+	for _, cur := range tt {
+		parsed := &H265FragmentationUnitPacket{}
+		if cur.WithDONL {
+			parsed.WithDONL(cur.WithDONL)
+		}
+
+		// Just for code coverage sake
+		parsed.isH265Packet()
+
+		_, err := parsed.Unmarshal(cur.Raw)
+
+		if cur.ExpectedErr != nil && err == nil {
+			t.Fatal("should error")
+		} else if cur.ExpectedErr == nil && err != nil {
+			t.Fatal("should not error")
+		}
+
+		if cur.ExpectedFU == nil {
+			continue
+		}
+
+		if parsed.PayloadHeader() != cur.ExpectedFU.PayloadHeader() {
+			t.Fatal("invalid payload header")
+		}
+
+		if parsed.FuHeader() != cur.ExpectedFU.FuHeader() {
+			t.Fatal("invalid FU header")
+		}
+
+		if cur.ExpectedFU.DONL() != nil && (*parsed.DONL() != *cur.ExpectedFU.DONL()) {
+			t.Fatal("invalid DONL")
+		}
+
+		if !reflect.DeepEqual(parsed.Payload(), cur.ExpectedFU.Payload()) {
+			t.Fatal("invalid Payload")
+		}
+	}
+}
+
+func TestH265_TemporalScalabilityControlInformation(t *testing.T) {
+	tt := [...]struct {
+		Value             H265TSCI
+		ExpectedTL0PICIDX uint8
+		ExpectedIrapPicID uint8
+		ExpectedS         bool
+		ExpectedE         bool
+		ExpectedRES       uint8
+	}{
+		{},
+		{
+			Value:             H265TSCI((uint32(0xCA) << 24) | (uint32(0xFE) << 16)),
+			ExpectedTL0PICIDX: 0xCA,
+			ExpectedIrapPicID: 0xFE,
+		},
+		{
+			Value:     H265TSCI(uint32(1) << 15),
+			ExpectedS: true,
+		},
+		{
+			Value:     H265TSCI(uint32(1) << 14),
+			ExpectedE: true,
+		},
+		{
+			Value:       H265TSCI(uint32(0x0A) << 8),
+			ExpectedRES: 0x0A,
+		},
+		// Sets RES, and force sets S and E to 0.
+		{
+			Value:       H265TSCI((uint32(0xAA) << 8) & ^(uint32(1) << 15) & ^(uint32(1) << 14)),
+			ExpectedRES: 0xAA & 0b00111111,
+		},
+	}
+
+	for _, cur := range tt {
+		if cur.Value.TL0PICIDX() != cur.ExpectedTL0PICIDX {
+			t.Fatal("invalid TL0PICIDX")
+		}
+
+		if cur.Value.IrapPicID() != cur.ExpectedIrapPicID {
+			t.Fatal("invalid IrapPicID")
+		}
+
+		if cur.Value.S() != cur.ExpectedS {
+			t.Fatal("invalid S")
+		}
+
+		if cur.Value.E() != cur.ExpectedE {
+			t.Fatal("invalid E")
+		}
+
+		if cur.Value.RES() != cur.ExpectedRES {
+			t.Fatal("invalid RES")
+		}
+	}
+}
+
+func TestH265_PACI_Packet(t *testing.T) {
+	tt := [...]struct {
+		Raw         []byte
+		ExpectedFU  *H265PACIPacket
+		ExpectedErr error
+	}{
+		{
+			Raw:         nil,
+			ExpectedErr: errNilPacket,
+		},
+		{
+			Raw:         []byte{},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62, 0x01, 0x93},
+			ExpectedErr: errShortPacket,
+		},
+		// FBit enabled in H265NALUHeader
+		{
+			Raw:         []byte{0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errH265CorruptedPacket,
+		},
+		// Type not '50' in H265NALUHeader
+		{
+			Raw:         []byte{0x40, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errInvalidH265PacketType,
+		},
+		// Invalid header extension size
+		{
+			Raw:         []byte{0x64, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errInvalidH265PacketType,
+		},
+		// No Header Extension
+		{
+			Raw: []byte{0x64, 0x01, 0x64, 0x00, 0xab, 0xcd, 0xef},
+			ExpectedFU: &H265PACIPacket{
+				payloadHeader:    newH265NALUHeader(0x64, 0x01),
+				paciHeaderFields: (uint16(0x64) << 8) | uint16(0x00),
+				phes:             nil,
+				payload:          []byte{0xab, 0xcd, 0xef},
+			},
+		},
+		// Header Extension 1 byte
+		{
+			Raw: []byte{0x64, 0x01, 0x64, 0x10, 0xff, 0xab, 0xcd, 0xef},
+			ExpectedFU: &H265PACIPacket{
+				payloadHeader:    newH265NALUHeader(0x64, 0x01),
+				paciHeaderFields: (uint16(0x64) << 8) | uint16(0x10),
+				phes:             []byte{0xff},
+				payload:          []byte{0xab, 0xcd, 0xef},
+			},
+		},
+		// Header Extension TSCI
+		{
+			Raw: []byte{0x64, 0x01, 0x64, 0b00111000, 0xaa, 0xbb, 0x80, 0xab, 0xcd, 0xef},
+			ExpectedFU: &H265PACIPacket{
+				payloadHeader:    newH265NALUHeader(0x64, 0x01),
+				paciHeaderFields: (uint16(0x64) << 8) | uint16(0b00111000),
+				phes:             []byte{0xaa, 0xbb, 0x80},
+				payload:          []byte{0xab, 0xcd, 0xef},
+			},
+		},
+	}
+
+	for _, cur := range tt {
+		parsed := &H265PACIPacket{}
+		_, err := parsed.Unmarshal(cur.Raw)
+
+		// Just for code coverage sake
+		parsed.isH265Packet()
+
+		if cur.ExpectedErr != nil && err == nil {
+			t.Fatal("should error")
+		} else if cur.ExpectedErr == nil && err != nil {
+			t.Fatal("should not error")
+		}
+
+		if cur.ExpectedFU == nil {
+			continue
+		}
+
+		if cur.ExpectedFU.PayloadHeader() != parsed.PayloadHeader() {
+			t.Fatal("invalid PayloadHeader")
+		}
+
+		if cur.ExpectedFU.A() != parsed.A() {
+			t.Fatal("invalid A")
+		}
+
+		if cur.ExpectedFU.CType() != parsed.CType() {
+			t.Fatal("invalid CType")
+		}
+
+		if cur.ExpectedFU.PHSsize() != parsed.PHSsize() {
+			t.Fatal("invalid PHSsize")
+		}
+
+		if cur.ExpectedFU.F0() != parsed.F0() {
+			t.Fatal("invalid F0")
+		}
+
+		if cur.ExpectedFU.F1() != parsed.F1() {
+			t.Fatal("invalid F1")
+		}
+
+		if cur.ExpectedFU.F2() != parsed.F2() {
+			t.Fatal("invalid F2")
+		}
+
+		if cur.ExpectedFU.Y() != parsed.Y() {
+			t.Fatal("invalid Y")
+		}
+
+		if !reflect.DeepEqual(cur.ExpectedFU.PHES(), parsed.PHES()) {
+			t.Fatal("invalid PHES")
+		}
+
+		if !reflect.DeepEqual(cur.ExpectedFU.Payload(), parsed.Payload()) {
+			t.Fatal("invalid Payload")
+		}
+
+		if cur.ExpectedFU.TSCI() != nil && (*cur.ExpectedFU.TSCI() != *parsed.TSCI()) {
+			t.Fatal("invalid TSCI")
+		}
+	}
+}
+
+func TestH265_Packet(t *testing.T) {
+	tt := [...]struct {
+		Raw                []byte
+		WithDONL           bool
+		ExpectedPacketType reflect.Type
+		ExpectedErr        error
+	}{
+		{
+			Raw:         nil,
+			ExpectedErr: errNilPacket,
+		},
+		{
+			Raw:         []byte{},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x62, 0x01, 0x93},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x64, 0x01, 0x93, 0xaf},
+			ExpectedErr: errShortPacket,
+		},
+		{
+			Raw:         []byte{0x01, 0x01},
+			WithDONL:    true,
+			ExpectedErr: errShortPacket,
+		},
+		// FBit enabled in H265NALUHeader
+		{
+			Raw:         []byte{0x80, 0x01, 0x93, 0xaf, 0xaf, 0xaf, 0xaf},
+			ExpectedErr: errH265CorruptedPacket,
+		},
+		// Valid H265SingleNALUnitPacket
+		{
+			Raw:                []byte{0x01, 0x01, 0xab, 0xcd, 0xef},
+			ExpectedPacketType: reflect.TypeOf((*H265SingleNALUnitPacket)(nil)),
+		},
+		// Invalid H265SingleNALUnitPacket
+		{
+			Raw:         []byte{0x01, 0x01, 0x93, 0xaf},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+		// Valid H265PACIPacket
+		{
+			Raw:                []byte{0x64, 0x01, 0x64, 0b00111000, 0xaa, 0xbb, 0x80, 0xab, 0xcd, 0xef},
+			ExpectedPacketType: reflect.TypeOf((*H265PACIPacket)(nil)),
+		},
+		// Valid H265FragmentationUnitPacket
+		{
+			Raw:                []byte{0x62, 0x01, 0x93, 0xcc, 0xdd, 0xaf, 0x0d, 0x5a},
+			ExpectedPacketType: reflect.TypeOf((*H265FragmentationUnitPacket)(nil)),
+			WithDONL:           true,
+		},
+		// Valid H265AggregationPacket
+		{
+			Raw:                []byte{0x60, 0x01, 0xcc, 0xdd, 0x00, 0x02, 0xff, 0xee, 0x77, 0x00, 0x01, 0xaa},
+			ExpectedPacketType: reflect.TypeOf((*H265AggregationPacket)(nil)),
+			WithDONL:           true,
+		},
+		// Invalid H265AggregationPacket
+		{
+			Raw:         []byte{0x60, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00},
+			ExpectedErr: errShortPacket,
+			WithDONL:    true,
+		},
+	}
+
+	for _, cur := range tt {
+		pck := &H265Packet{}
+		if cur.WithDONL {
+			pck.WithDONL(true)
+		}
+
+		_, err := pck.Unmarshal(cur.Raw)
+
+		if cur.ExpectedErr != nil && err == nil {
+			t.Fatal("should error")
+		} else if cur.ExpectedErr == nil && err != nil {
+			t.Fatal("should not error")
+		}
+
+		if cur.ExpectedErr != nil {
+			continue
+		}
+
+		if reflect.TypeOf(pck.Packet()) != cur.ExpectedPacketType {
+			t.Fatal("invalid packet type")
+		}
+	}
+}
+
+func TestH265_Packet_Real(t *testing.T) {
+	// Tests decoding of real H265 payloads extracted from a Wireshark dump.
+
+	tt := [...]string{
+		"\x40\x01\x0c\x01\xff\xff\x01\x60\x00\x00\x03\x00\xb0\x00\x00\x03\x00\x00\x03\x00\x7b\xac\x09",
+		"\x42\x01\x01\x01\x60\x00\x00\x03\x00\xb0\x00\x00\x03\x00\x00\x03\x00\x7b\xa0\x03\xc0\x80\x10\xe5\x8d\xae\x49\x32\xf4\xdc\x04\x04\x04\x02",
+		"\x44\x01\xc0\xf2\xf0\x3c\x90",
+		"\x4e\x01\xe5\x04\x61\x0c\x00\x00\x80",
+		"\x62\x01\x93\xaf\x0d\x5a\xfe\x67\x77\x29\xc0\x74\xf3\x57\x4c\x16\x94\xaa\x7c\x2a\x64\x5f\xe9\xa5\xb7\x2a\xa3\x95\x9d\x94\xa7\xb4\xd3\xc4\x4a\xb1\xb7\x69\xca\xbe\x75\xc5\x64\xa8\x97\x4b\x8a\xbf\x7e\xf0\x0f\xc3\x22\x60\x67\xab\xae\x96\xd6\x99\xca\x7a\x8d\x35\x93\x1a\x67\x60\xe7\xbe\x7e\x13\x95\x3c\xe0\x11\xc1\xc1\xa7\x48\xef\xf7\x7b\xb0\xeb\x35\x49\x81\x4e\x4e\x54\xf7\x31\x6a\x38\xa1\xa7\x0c\xd6\xbe\x3b\x25\xba\x08\x19\x0b\x49\xfd\x90\xbb\x73\x7a\x45\x8c\xb9\x73\x43\x04\xc5\x5f\xda\x0f\xd5\x70\x4c\x11\xee\x72\xb8\x6a\xb4\x95\x62\x64\xb6\x23\x14\x7e\xdb\x0e\xa5\x0f\x86\x31\xe4\xd1\x64\x56\x43\xf6\xb7\xe7\x1b\x93\x4a\xeb\xd0\xa6\xe3\x1f\xce\xda\x15\x67\x05\xb6\x77\x36\x8b\x27\x5b\xc6\xf2\x95\xb8\x2b\xcc\x9b\x0a\x03\x05\xbe\xc3\xd3\x85\xf5\x69\xb6\x19\x1f\x63\x2d\x8b\x65\x9e\xc3\x9d\xd2\x44\xb3\x7c\x86\x3b\xea\xa8\x5d\x02\xe5\x40\x03\x20\x76\x48\xff\xf6\x2b\x0d\x18\xd6\x4d\x49\x70\x1a\x5e\xb2\x89\xca\xec\x71\x41\x79\x4e\x94\x17\x0c\x57\x51\x55\x14\x61\x40\x46\x4b\x3e\x17\xb2\xc8\xbd\x1c\x06\x13\x91\x72\xf8\xc8\xfc\x6f\xb0\x30\x9a\xec\x3b\xa6\xc9\x33\x0b\xa5\xe5\xf4\x65\x7a\x29\x8b\x76\x62\x81\x12\xaf\x20\x4c\xd9\x21\x23\x9e\xeb\xc9\x0e\x5b\x29\x35\x7f\x41\xcd\xce\xa1\xc4\xbe\x01\x30\xb9\x11\xc3\xb1\xe4\xce\x45\xd2\x5c\xb3\x1e\x69\x78\xba\xb1\x72\xe4\x88\x54\xd8\x5d\xd0\xa8\x3a\x74\xad\xe5\xc7\xc1\x59\x7c\x78\x15\x26\x37\x3d\x50\xae\xb3\xa4\x5b\x6c\x7d\x65\x66\x85\x4d\x16\x9a\x67\x74\xad\x55\x32\x3a\x84\x85\x0b\x6a\xeb\x24\x97\xb4\x20\x4d\xca\x41\x61\x7a\xd1\x7b\x60\xdb\x7f\xd5\x61\x22\xcf\xd1\x7e\x4c\xf3\x85\xfd\x13\x63\xe4\x9d\xed\xac\x13\x0a\xa0\x92\xb7\x34\xde\x65\x0f\xd9\x0f\x9b\xac\xe2\x47\xe8\x5c\xb3\x11\x8e\xc6\x08\x19\xd0\xb0\x85\x52\xc8\x5c\x1b\x08\x0a\xce\xc9\x6b\xa7\xef\x95\x2f\xd0\xb8\x63\xe5\x4c\xd4\xed\x6e\x87\xe9\xd4\x0a\xe6\x11\x44\x63\x00\x94\x18\xe9\x28\xba\xcf\x92\x43\x06\x59\xdd\x37\x4f\xd3\xef\x9d\x31\x5e\x9b\x48\xf9\x1f\x3e\x7b\x95\x3a\xbd\x1f\x71\x55\x0c\x06\xf9\x86\xf8\x3d\x39\x16\x50\xb3\x21\x11\x19\x6f\x70\xa9\x48\xe8\xbb\x0a\x11\x23\xf8\xab\xfe\x44\xe0\xbb\xe8\x64\xfa\x85\xe4\x02\x55\x88\x41\xc6\x30\x7f\x10\xad\x75\x02\x4b\xef\xe1\x0b\x06\x3c\x10\x49\x83\xf9\xd1\x3e\x3e\x67\x86\x4c\xf8\x9d\xde\x5a\xc4\xc8\xcf\xb6\xf4\xb0\xd3\x34\x58\xd4\x7b\x4d\xd3\x37\x63\xb2\x48\x8a\x7e\x20\x00\xde\xb4\x42\x8f\xda\xe9\x43\x9e\x0c\x16\xce\x79\xac\x2c\x70\xc1\x89\x05\x36\x62\x6e\xd9\xbc\xfb\x63\xc6\x79\x89\x3c\x90\x89\x2b\xd1\x8c\xe0\xc2\x54\xc7\xd6\xb4\xe8\x9e\x96\x55\x6e\x7b\xd5\x7f\xac\xd4\xa7\x1c\xa0\xdf\x01\x30\xad\xc0\x9f\x69\x06\x10\x43\x7f\xf4\x5d\x62\xa3\xea\x73\xf2\x14\x79\x19\x13\xea\x59\x14\x79\xa8\xe7\xce\xce\x44\x25\x13\x41\x18\x57\xdd\xce\xe4\xbe\xcc\x20\x80\x29\x71\x73\xa7\x7c\x86\x39\x76\xf4\xa7\x1c\x63\x24\x21\x93\x1e\xb5\x9a\x5c\x8a\x9e\xda\x8b\x9d\x88\x97\xfc\x98\x7d\x26\x74\x04\x1f\xa8\x10\x4f\x45\xcd\x46\xe8\x28\xe4\x8e\x59\x67\x63\x4a\xcf\x1e\xed\xdd\xbb\x79\x2f\x8d\x94\xab\xfc\xdb\xc5\x79\x1a\x4d\xcd\x53\x41\xdf\xd1\x7a\x8f\x46\x3e\x1f\x79\x88\xe3\xee\x9f\xc4\xc1\xe6\x2e\x89\x4d\x28\xc9\xca\x28\xc2\x0a\xc5\xc7\xf1\x22\xcd\xb3\x36\xfa\xe3\x7e\xa6\xcd\x95\x55\x5e\x0e\x1a\x75\x7f\x65\x27\xd3\x37\x4f\x23\xc5\xab\x49\x68\x4e\x02\xb5\xbf\xd7\x95\xc0\x78\x67\xbc\x1a\xe9\xae\x6f\x44\x58\x8a\xc2\xce\x42\x98\x4e\x77\xc7\x2a\xa0\xa7\x7d\xe4\x3b\xd1\x20\x82\x1a\xd3\xe2\xc7\x76\x5d\x06\x46\xb5\x24\xd7\xfb\x57\x63\x2b\x19\x51\x48\x65\x6d\xfb\xe0\x98\xd1\x14\x0e\x17\x64\x29\x34\x6f\x6e\x66\x9e\x8d\xc9\x89\x49\x69\xee\x74\xf3\x35\xe6\x8b\x67\x56\x95\x7f\x1b\xe9\xed\x8c\x0f\xe2\x19\x59\xbf\x03\x35\x55\x3c\x04\xbc\x40\x52\x90\x10\x08\xad\xa7\x65\xe0\x31\xcb\xcf\x3d\xd4\x62\x68\x01\x0d\xed\xf5\x28\x64\x2d\xaa\x7c\x99\x15\x8d\x70\x32\x53\xb8\x9d\x0a\x3c\xbf\x91\x02\x04\xd0\xee\x87\xce\x04\xcc\x3e\xa8\x20\xfd\x97\xdf\xbf\x4a\xbc\xfc\xc9\x7c\x77\x21\xcc\x23\x6f\x59\x38\xd8\xd9\xa0\x0e\xb1\x23\x4e\x04\x3f\x14\x9e\xcc\x05\x54\xab\x20\x69\xed\xa4\xd5\x1d\xb4\x1b\x52\xed\x6a\xea\xeb\x7f\xd1\xbc\xfd\x75\x20\xa0\x1c\x59\x8c\x5a\xa1\x2a\x70\x64\x11\xb1\x7b\xc1\x24\x80\x28\x51\x4c\x94\xa1\x95\x64\x72\xe8\x90\x67\x38\x74\x2b\xab\x38\x46\x12\x71\xce\x19\x98\x98\xf7\x89\xd4\xfe\x2f\x2a\xc5\x61\x20\xd0\xa4\x1a\x51\x3c\x82\xc8\x18\x31\x7a\x10\xe8\x1c\xc6\x95\x5a\xa0\x82\x88\xce\x8f\x4b\x47\x85\x7e\x89\x95\x95\x52\x1e\xac\xce\x45\x57\x61\x38\x97\x2b\x62\xa5\x14\x6f\xc3\xaa\x6c\x35\x83\xc9\xa3\x1e\x30\x89\xf4\xb1\xea\x4f\x39\xde\xde\xc7\x46\x5c\x0e\x85\x41\xec\x6a\xa4\xcb\xee\x70\x9c\x57\xd9\xf4\xa1\xc3\x9c\x2a\x0a\xf0\x5d\x58\xb0\xae\xd4\xdc\xc5\x6a\xa8\x34\xfa\x23\xef\xef\x08\x39\xc3\x3d\xea\x11\x6e\x6a\xe0\x1e\xd0\x52\xa8\xc3\x6e\xc9\x1c\xfc\xd0\x0c\x4c\xea\x0d\x82\xcb\xdd\x29\x1a\xc4\x4f\x6e\xa3\x4d\xcb\x7a\x38\x77\xe5\x15\x6e\xad\xfa\x9d\x2f\x02\xb6\x39\x84\x3a\x60\x8f\x71\x9f\x92\xe5\x24\x4f\xbd\x18\x49\xd5\xef\xbf\x70\xfb\xd1\x4c\x2e\xfc\x2f\x36\xf3\x00\x31\x2e\x90\x18\xcc\xf4\x71\xb9\xe4\xf9\xbe\xcb\x5e\xff\xf3\xe7\xf8\xca\x03\x60\x66\xb3\xc9\x5a\xf9\x74\x09\x02\x57\xb6\x90\x94\xfc\x41\x35\xdc\x35\x3f\x32\x7a\xa6\xa5\xcd\x8a\x8f\xc8\x3d\xc8\x81\xc3\xec\x37\x74\x86\x61\x41\x0d\xc5\xe2\xc8\x0c\x84\x2b\x3b\x71\x58\xde\x1b\xe3\x20\x65\x2e\x76\xf4\x98\xd8\xaa\x78\xe6\xeb\xb8\x85\x0d\xa0\xd0\xf5\x57\x64\x01\x58\x55\x82\xd5\x0f\x2d\x9c\x3e\x2a\xa0\x7e\xaf\x42\xf3\x37\xd1\xb3\xaf\xda\x5b\xa9\xda\xe3\x89\x5d\xf1\xca\xa5\x12\x3d\xe7\x91\x95\x53\x21\x72\xca\x7f\xf6\x79\x59\x21\xcf\x30\x18\xfb\x78\x55\x40\x59\xc3\xf9\xf1\xdd\x58\x44\x5e\x83\x11\x5c\x2d\x1d\x91\xf6\x01\x3d\x3f\xd4\x33\x81\x66\x6c\x40\x7a\x9d\x70\x10\x58\xe6\x53\xad\x85\x11\x99\x3e\x4b\xbc\x31\xc6\x78\x9d\x79\xc5\xde\x9f\x2e\x43\xfa\x76\x84\x2f\xfd\x28\x75\x12\x48\x25\xfd\x15\x8c\x29\x6a\x91\xa4\x63\xc0\xa2\x8c\x41\x3c\xf1\xb0\xf8\xdf\x66\xeb\xbd\x14\x88\xa9\x81\xa7\x35\xc4\x41\x40\x6c\x10\x3f\x09\xbd\xb5\xd3\x7a\xee\x4b\xd5\x86\xff\x36\x03\x6b\x78\xde",
+		"\x62\x01\x53\x8a\xe9\x25\xe1\x06\x09\x8e\xba\x12\x74\x87\x09\x9a\x95\xe4\x86\x62\x2b\x4b\xf9\xa6\x2e\x7b\x35\x43\xf7\x39\x99\x0f\x3b\x6f\xfd\x1a\x6e\x23\x54\x70\xb5\x1d\x10\x1c\x63\x40\x96\x99\x41\xb6\x96\x0b\x70\x98\xec\x17\xb0\xaa\xdc\x4a\xab\xe8\x3b\xb7\x6b\x00\x1c\x5b\xc3\xe0\xa2\x8b\x7c\x17\xc8\x92\xc9\xb0\x92\xb6\x70\x84\x95\x30",
+		"\x4e\x01\xe5\x04\x35\xac\x00\x00\x80",
+		"\x62\x01\x41\xb0\x75\x5c\x27\x46\xef\x8a\xe7\x1d\x50\x38\xb2\x13\x33\xe0\x79\x35\x1b\xc2\xb5\x79\x73\xe7\xc2\x6f\xb9\x1a\x8c\x21\x0e\xa9\x54\x17\x6c\x41\xab\xc8\x16\x57\xec\x5e\xeb\x89\x3b\xa9\x90\x8c\xff\x4d\x46\x8b\xf0\xd9\xc0\xd0\x51\xcf\x8b\x88\xf1\x5f\x1e\x9e\xc1\xb9\x1f\xe3\x06\x45\x35\x8a\x47\xe8\x9a\xf2\x4f\x19\x4c\xf8\xce\x68\x1b\x63\x34\x11\x75\xea\xe5\xb1\x0f\x38\xcc\x05\x09\x8b\x3e\x2b\x88\x84\x9d\xc5\x03\xc3\xc0\x90\x32\xe2\x45\x69\xb1\xe5\xf7\x68\x6b\x16\x90\xa0\x40\xe6\x18\x74\xd8\x68\xf3\x34\x38\x99\xf2\x6c\xb7\x1a\x35\x21\xca\x52\x56\x4c\x7f\xb2\xa3\xd5\xb8\x40\x50\x48\x3e\xdc\xdf\x0b\xf5\x54\x5a\x15\x1a\xe2\xc3\xb4\x94\xda\x3f\xb5\x34\xa2\xca\xbc\x2f\xe0\xa4\xe5\x69\xf4\xbf\x62\x4d\x15\x21\x1b\x11\xfc\x39\xaa\x86\x74\x96\x63\xfd\x07\x53\x26\xf6\x34\x72\xeb\x14\x37\x98\x0d\xf4\x68\x91\x2c\x6b\x46\x83\x88\x82\x04\x8b\x9f\xb8\x32\x73\x75\x8b\xf9\xac\x71\x42\xd1\x2d\xb4\x28\x28\xf5\x78\xe0\x32\xf3\xe1\xfc\x43\x6b\xf9\x92\xf7\x48\xfe\x7f\xc0\x17\xbd\xfd\xba\x2f\x58\x6f\xee\x84\x03\x18\xce\xb0\x9d\x8d\xeb\x22\xf1\xfc\xb1\xcf\xff\x2f\xb2\x9f\x6c\xe5\xb4\x69\xdc\xdd\x20\x93\x00\x30\xad\x56\x04\x66\x7e\xa3\x3c\x18\x4b\x43\x66\x00\x27\x1e\x1c\x09\x11\xd8\xf4\x8a\x9e\xc5\x6a\x94\xe5\xae\x0b\x8a\xbe\x84\xda\xe5\x44\x7f\x38\x1c\xe7\xbb\x03\x19\x66\xe1\x5d\x1d\xc1\xbd\x3d\xc6\xb7\xe3\xff\x7f\x8e\xff\x1e\xf6\x9e\x6f\x58\x27\x74\x65\xef\x02\x5d\xa4\xde\x27\x7f\x51\xe3\x4b\x9e\x3f\x79\x83\xbd\x1b\x8f\x0d\x77\xfb\xbc\xc5\x9f\x15\xa7\x4e\x05\x8a\x24\x97\x66\xb2\x7c\xf6\xe1\x84\x54\xdb\x39\x5e\xf6\x1b\x8f\x05\x73\x1d\xb6\x8e\xd7\x09\x9a\xc5\x92\x80",
+	}
+
+	for _, cur := range tt {
+		pck := &H265Packet{}
+		_, err := pck.Unmarshal([]byte(cur))
+		if err != nil {
+			t.Fatal("invalid packet type")
+		}
+	}
+}
+
+func uint8ptr(v uint8) *uint8 {
+	return &v
+}
+
+func uint16ptr(v uint16) *uint16 {
+	return &v
+}
diff --git a/codecs/vp8_packet_test.go b/codecs/vp8_packet_test.go
index 1c062c3..37d160d 100644
--- a/codecs/vp8_packet_test.go
+++ b/codecs/vp8_packet_test.go
@@ -2,6 +2,7 @@ package codecs
 
 import (
 	"errors"
+	"reflect"
 	"testing"
 )
 
@@ -109,33 +110,102 @@ func TestVP8Packet_Unmarshal(t *testing.T) {
 }
 
 func TestVP8Payloader_Payload(t *testing.T) {
-	pck := VP8Payloader{}
-	payload := []byte{0x90, 0x90, 0x90}
-
-	// Positive MTU, nil payload
-	res := pck.Payload(1, nil)
-	if len(res) != 0 {
-		t.Fatal("Generated payload should be empty")
-	}
-
-	// Positive MTU, small payload
-	// MTU of 1 results in fragment size of 0
-	res = pck.Payload(1, payload)
-	if len(res) != 0 {
-		t.Fatal("Generated payload should be empty")
-	}
+	testCases := map[string]struct {
+		payloader VP8Payloader
+		mtu       int
+		payload   [][]byte
+		expected  [][][]byte
+	}{
+		"WithoutPictureID": {
+			payloader: VP8Payloader{},
+			mtu:       2,
+			payload: [][]byte{
+				{0x90, 0x90, 0x90},
+				{0x91, 0x91},
+			},
+			expected: [][][]byte{
+				{{0x10, 0x90}, {0x00, 0x90}, {0x00, 0x90}},
+				{{0x10, 0x91}, {0x00, 0x91}},
+			},
+		},
+		"WithPictureID_1byte": {
+			payloader: VP8Payloader{
+				EnablePictureID: true,
+				pictureID:       0x20,
+			},
+			mtu: 5,
+			payload: [][]byte{
+				{0x90, 0x90, 0x90},
+				{0x91, 0x91},
+			},
+			expected: [][][]byte{
+				{
+					{0x90, 0x80, 0x20, 0x90, 0x90},
+					{0x80, 0x80, 0x20, 0x90},
+				},
+				{
+					{0x90, 0x80, 0x21, 0x91, 0x91},
+				},
+			},
+		},
+		"WithPictureID_2bytes": {
+			payloader: VP8Payloader{
+				EnablePictureID: true,
+				pictureID:       0x120,
+			},
+			mtu: 6,
+			payload: [][]byte{
+				{0x90, 0x90, 0x90},
+				{0x91, 0x91},
+			},
+			expected: [][][]byte{
+				{
+					{0x90, 0x80, 0x81, 0x20, 0x90, 0x90},
+					{0x80, 0x80, 0x81, 0x20, 0x90},
+				},
+				{
+					{0x90, 0x80, 0x81, 0x21, 0x91, 0x91},
+				},
+			},
+		},
+	}
+	for name, testCase := range testCases {
+		testCase := testCase
+		t.Run(name, func(t *testing.T) {
+			pck := testCase.payloader
+
+			for i := range testCase.payload {
+				res := pck.Payload(testCase.mtu, testCase.payload[i])
+				if !reflect.DeepEqual(testCase.expected[i], res) {
+					t.Fatalf("Generated packet[%d] differs, expected: %v, got: %v", i, testCase.expected[i], res)
+				}
+			}
+		})
+	}
+
+	t.Run("Error", func(t *testing.T) {
+		pck := VP8Payloader{}
+		payload := []byte{0x90, 0x90, 0x90}
+
+		// Positive MTU, nil payload
+		res := pck.Payload(1, nil)
+		if len(res) != 0 {
+			t.Fatal("Generated payload should be empty")
+		}
 
-	// Negative MTU, small payload
-	res = pck.Payload(-1, payload)
-	if len(res) != 0 {
-		t.Fatal("Generated payload should be empty")
-	}
+		// Positive MTU, small payload
+		// MTU of 1 results in fragment size of 0
+		res = pck.Payload(1, payload)
+		if len(res) != 0 {
+			t.Fatal("Generated payload should be empty")
+		}
 
-	// Positive MTU, small payload
-	res = pck.Payload(2, payload)
-	if len(res) != len(payload) {
-		t.Fatal("Generated payload should be the same size as original payload size")
-	}
+		// Negative MTU, small payload
+		res = pck.Payload(-1, payload)
+		if len(res) != 0 {
+			t.Fatal("Generated payload should be empty")
+		}
+	})
 }
 
 func TestVP8PartitionHeadChecker_IsPartitionHead(t *testing.T) {