Skip to content

Commit

Permalink
Add H265 payloader and depacketizer
Browse files Browse the repository at this point in the history
This change completes the H265 implementation.
  • Loading branch information
kevmo314 authored and Sean-Der committed Sep 10, 2024
1 parent a21194e commit dd05d22
Show file tree
Hide file tree
Showing 2 changed files with 319 additions and 12 deletions.
329 changes: 318 additions & 11 deletions codecs/h265_packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/binary"
"errors"
"fmt"
"math"
"sort"
)

//
Expand Down Expand Up @@ -733,22 +735,60 @@ var (
// Packet implementation
//

type donKeyedNALU struct {
DON int
NALU []byte
}

// H265Packet represents a H265 packet, stored in the payload of an RTP packet.
type H265Packet struct {
packet isH265Packet
mightNeedDONL bool
packet isH265Packet
maxDONDiff uint16
depackBufNALUs uint16

prevDON *uint16
prevAbsDON *int

naluBuffer []donKeyedNALU
fuBuffer []byte

videoDepacketizer
}

// 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
func toAbsDON(don uint16, prevDON *uint16, prevAbsDON *int) int {
if prevDON == nil || prevAbsDON == nil {
return int(don)
}
if don == *prevDON {
return *prevAbsDON

Check warning on line 763 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L763

Added line #L763 was not covered by tests
}
if don > *prevDON && don-*prevDON < 32768 {
return *prevAbsDON + int(don-*prevDON)
}
if don < *prevDON && *prevDON-don >= 32768 {
return *prevAbsDON + 65536 + int(*prevDON-don)

Check warning on line 769 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L768-L769

Added lines #L768 - L769 were not covered by tests
}
if don > *prevDON && don-*prevDON >= 32768 {
return *prevAbsDON - (int(*prevDON) + 65536 - int(don))

Check warning on line 772 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L771-L772

Added lines #L771 - L772 were not covered by tests
}
if don < *prevDON && *prevDON-don < 32768 {
return *prevAbsDON - int(*prevDON-don)

Check warning on line 775 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L774-L775

Added lines #L774 - L775 were not covered by tests
}
return 0

Check warning on line 777 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L777

Added line #L777 was not covered by tests
}

// WithMaxDONDiff sets the maximum difference between DON values before being emitted.
func (p *H265Packet) WithMaxDONDiff(value uint16) {
p.maxDONDiff = value
}

// WithDepackBufNALUs sets the maximum number of NALUs to be buffered.
func (p *H265Packet) WithDepackBufNALUs(value uint16) {
p.depackBufNALUs = value

Check warning on line 787 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L786-L787

Added lines #L786 - L787 were not covered by tests
}

// 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) {
func (p *H265Packet) Unmarshal(payload []byte) ([]byte, error) { //nolint: gocognit
if payload == nil {
return nil, errNilPacket
} else if len(payload) <= h265NaluHeaderSize {
Expand All @@ -771,36 +811,121 @@ func (p *H265Packet) Unmarshal(payload []byte) ([]byte, error) {

case payloadHeader.IsFragmentationUnit():
decoded := &H265FragmentationUnitPacket{}
decoded.WithDONL(p.mightNeedDONL)
decoded.WithDONL(p.maxDONDiff > 0)

if _, err := decoded.Unmarshal(payload); err != nil {
return nil, err
}

p.packet = decoded

if decoded.FuHeader().S() {
// push the nalu header
header := decoded.PayloadHeader()
p.fuBuffer = []byte{
(uint8(header>>8) & 0b10000001) | (decoded.FuHeader().FuType() << 1),
uint8(header),
}
}
p.fuBuffer = append(p.fuBuffer, decoded.Payload()...)
if decoded.FuHeader().E() {
var absDON int
if p.maxDONDiff > 0 {
absDON = toAbsDON(*decoded.DONL(), p.prevDON, p.prevAbsDON)
p.prevDON = decoded.DONL()
p.prevAbsDON = &absDON

Check warning on line 836 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L834-L836

Added lines #L834 - L836 were not covered by tests
}
p.naluBuffer = append(p.naluBuffer, donKeyedNALU{
DON: absDON,
NALU: p.fuBuffer,
})
p.fuBuffer = nil
}

case payloadHeader.IsAggregationPacket():
decoded := &H265AggregationPacket{}
decoded.WithDONL(p.mightNeedDONL)
decoded.WithDONL(p.maxDONDiff > 0)

if _, err := decoded.Unmarshal(payload); err != nil {
return nil, err
}

p.packet = decoded

var absDON int
if p.maxDONDiff > 0 {
absDON = toAbsDON(*decoded.FirstUnit().DONL(), p.prevDON, p.prevAbsDON)
p.prevDON = decoded.FirstUnit().DONL()
p.prevAbsDON = &absDON
}
p.naluBuffer = append(p.naluBuffer, donKeyedNALU{DON: absDON, NALU: decoded.FirstUnit().NalUnit()})
for _, unit := range decoded.OtherUnits() {
if p.maxDONDiff > 0 {
donl := uint16(*unit.DOND()) + 1 + *decoded.FirstUnit().DONL()
absDON = toAbsDON(donl, p.prevDON, p.prevAbsDON)
p.prevDON = &donl
p.prevAbsDON = &absDON
}
p.naluBuffer = append(p.naluBuffer, donKeyedNALU{DON: absDON, NALU: unit.NalUnit()})
}

default:
decoded := &H265SingleNALUnitPacket{}
decoded.WithDONL(p.mightNeedDONL)
decoded.WithDONL(p.maxDONDiff > 0)

if _, err := decoded.Unmarshal(payload); err != nil {
return nil, err
}

p.packet = decoded

buf := make([]byte, 2+len(decoded.payload))
binary.BigEndian.PutUint16(buf[0:2], uint16(decoded.payloadHeader))
copy(buf[2:], decoded.payload)

var absDON int
if p.maxDONDiff > 0 {
absDON = toAbsDON(*decoded.DONL(), p.prevDON, p.prevAbsDON)
p.prevDON = decoded.DONL()
p.prevAbsDON = &absDON

Check warning on line 890 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L888-L890

Added lines #L888 - L890 were not covered by tests
}
p.naluBuffer = append(p.naluBuffer, donKeyedNALU{DON: absDON, NALU: buf})
}

return nil, nil
buf := []byte{}
if p.maxDONDiff > 0 {
// https://datatracker.ietf.org/doc/html/rfc7798#section-6
// sort by AbsDON
sort.Slice(p.naluBuffer, func(i, j int) bool {
return p.naluBuffer[i].DON < p.naluBuffer[j].DON
})
// find the max DONL value
var maxDONL int
for _, nalu := range p.naluBuffer {
if nalu.DON > maxDONL {
maxDONL = nalu.DON
}
}
minDONL := maxDONL - int(p.maxDONDiff)
// merge all NALUs while condition A or condition B are true
for len(p.naluBuffer) > 0 && (p.naluBuffer[0].DON < minDONL || len(p.naluBuffer) > int(p.depackBufNALUs)) {
// nolint
// TODO: this is not actually correct following B.2.2, not all NALUs have a 4-byte start code.
buf = append(buf, annexbNALUStartCode...)
buf = append(buf, p.naluBuffer[0].NALU...)
p.naluBuffer = p.naluBuffer[1:]
}
} else {
// return the nalu buffer joined together
for _, val := range p.naluBuffer {
// nolint
// TODO: this is not actually correct following B.2.2, not all NALUs have a 4-byte start code.
buf = append(buf, annexbNALUStartCode...)
buf = append(buf, val.NALU...)
}
p.naluBuffer = nil
}
return buf, nil
}

// Packet returns the populated packet.
Expand All @@ -826,3 +951,185 @@ func (*H265Packet) IsPartitionHead(payload []byte) bool {

return true
}

// H265Payloader payloads H265 packets
type H265Payloader struct {
AddDONL bool
SkipAggregation bool
donl uint16
}

// Payload fragments a H265 packet across one or more byte arrays
func (p *H265Payloader) Payload(mtu uint16, payload []byte) [][]byte { //nolint: gocognit
var payloads [][]byte
if len(payload) == 0 {
return payloads

Check warning on line 966 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L963-L966

Added lines #L963 - L966 were not covered by tests
}

bufferedNALUs := make([][]byte, 0)
aggregationBufferSize := 0

Check warning on line 970 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L969-L970

Added lines #L969 - L970 were not covered by tests

flushBufferedNals := func() {
if len(bufferedNALUs) == 0 {
return

Check warning on line 974 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L972-L974

Added lines #L972 - L974 were not covered by tests
}
if len(bufferedNALUs) == 1 {

Check warning on line 976 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L976

Added line #L976 was not covered by tests
// emit this as a single NALU packet
nalu := bufferedNALUs[0]

Check warning on line 978 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L978

Added line #L978 was not covered by tests

if p.AddDONL {
buf := make([]byte, len(nalu)+2)

Check warning on line 981 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L980-L981

Added lines #L980 - L981 were not covered by tests

// copy the NALU header to the payload header
copy(buf[0:h265NaluHeaderSize], nalu[0:h265NaluHeaderSize])

Check warning on line 984 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L984

Added line #L984 was not covered by tests

// copy the DONL into the header
binary.BigEndian.PutUint16(buf[h265NaluHeaderSize:h265NaluHeaderSize+2], p.donl)

Check warning on line 987 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L987

Added line #L987 was not covered by tests

// write the payload
copy(buf[h265NaluHeaderSize+2:], nalu[h265NaluHeaderSize:])

Check warning on line 990 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L990

Added line #L990 was not covered by tests

p.donl++

Check warning on line 992 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L992

Added line #L992 was not covered by tests

payloads = append(payloads, buf)
} else {

Check warning on line 995 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L994-L995

Added lines #L994 - L995 were not covered by tests
// write the nalu directly to the payload
payloads = append(payloads, nalu)

Check warning on line 997 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L997

Added line #L997 was not covered by tests
}
} else {

Check warning on line 999 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L999

Added line #L999 was not covered by tests
// construct an aggregation packet
aggregationPacketSize := aggregationBufferSize + 2
buf := make([]byte, aggregationPacketSize)

Check warning on line 1002 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1001-L1002

Added lines #L1001 - L1002 were not covered by tests

layerID := uint8(math.MaxUint8)
tid := uint8(math.MaxUint8)
for _, nalu := range bufferedNALUs {
header := newH265NALUHeader(nalu[0], nalu[1])
headerLayerID := header.LayerID()
headerTID := header.TID()
if headerLayerID < layerID {
layerID = headerLayerID

Check warning on line 1011 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1004-L1011

Added lines #L1004 - L1011 were not covered by tests
}
if headerTID < tid {
tid = headerTID

Check warning on line 1014 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1013-L1014

Added lines #L1013 - L1014 were not covered by tests
}
}

binary.BigEndian.PutUint16(buf[0:2], (uint16(h265NaluAggregationPacketType)<<9)|(uint16(layerID)<<3)|uint16(tid))

Check warning on line 1018 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1018

Added line #L1018 was not covered by tests

index := 2
for i, nalu := range bufferedNALUs {
if p.AddDONL {
if i == 0 {
binary.BigEndian.PutUint16(buf[index:index+2], p.donl)
index += 2
} else {
buf[index] = byte(i - 1)
index++

Check warning on line 1028 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1020-L1028

Added lines #L1020 - L1028 were not covered by tests
}
}
binary.BigEndian.PutUint16(buf[index:index+2], uint16(len(nalu)))
index += 2
index += copy(buf[index:], nalu)

Check warning on line 1033 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1031-L1033

Added lines #L1031 - L1033 were not covered by tests
}
payloads = append(payloads, buf)

Check warning on line 1035 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1035

Added line #L1035 was not covered by tests
}
// clear the buffered NALUs
bufferedNALUs = make([][]byte, 0)
aggregationBufferSize = 0

Check warning on line 1039 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1038-L1039

Added lines #L1038 - L1039 were not covered by tests
}

emitNalus(payload, func(nalu []byte) {
if len(nalu) == 0 {
return

Check warning on line 1044 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1042-L1044

Added lines #L1042 - L1044 were not covered by tests
}

if len(nalu) <= int(mtu) {

Check warning on line 1047 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1047

Added line #L1047 was not covered by tests
// this nalu fits into a single packet, either it can be emitted as
// a single nalu or appended to the previous aggregation packet

marginalAggregationSize := len(nalu) + 2
if p.AddDONL {
marginalAggregationSize++

Check warning on line 1053 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1051-L1053

Added lines #L1051 - L1053 were not covered by tests
}

if aggregationBufferSize+marginalAggregationSize > int(mtu) {
flushBufferedNals()

Check warning on line 1057 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1056-L1057

Added lines #L1056 - L1057 were not covered by tests
}
bufferedNALUs = append(bufferedNALUs, nalu)
aggregationBufferSize += marginalAggregationSize
if p.SkipAggregation {

Check warning on line 1061 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1059-L1061

Added lines #L1059 - L1061 were not covered by tests
// emit this immediately.
flushBufferedNals()

Check warning on line 1063 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1063

Added line #L1063 was not covered by tests
}
} else {

Check warning on line 1065 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1065

Added line #L1065 was not covered by tests
// if this nalu doesn't fit in the current mtu, it needs to be fragmented
fuPacketHeaderSize := h265FragmentationUnitHeaderSize + 2 /* payload header size */
if p.AddDONL {
fuPacketHeaderSize += 2

Check warning on line 1069 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1067-L1069

Added lines #L1067 - L1069 were not covered by tests
}

// then, fragment the nalu
maxFUPayloadSize := int(mtu) - fuPacketHeaderSize

Check warning on line 1073 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1073

Added line #L1073 was not covered by tests

naluHeader := newH265NALUHeader(nalu[0], nalu[1])

Check warning on line 1075 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1075

Added line #L1075 was not covered by tests

// the nalu header is omitted from the fragmentation packet payload
nalu = nalu[h265NaluHeaderSize:]

Check warning on line 1078 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1078

Added line #L1078 was not covered by tests

if maxFUPayloadSize == 0 || len(nalu) == 0 {
return

Check warning on line 1081 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1080-L1081

Added lines #L1080 - L1081 were not covered by tests
}

// flush any buffered aggregation packets.
flushBufferedNals()

Check warning on line 1085 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1085

Added line #L1085 was not covered by tests

fullNALUSize := len(nalu)
for len(nalu) > 0 {
curentFUPayloadSize := len(nalu)
if curentFUPayloadSize > maxFUPayloadSize {
curentFUPayloadSize = maxFUPayloadSize

Check warning on line 1091 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1087-L1091

Added lines #L1087 - L1091 were not covered by tests
}

out := make([]byte, fuPacketHeaderSize+curentFUPayloadSize)

Check warning on line 1094 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1094

Added line #L1094 was not covered by tests

// write the payload header
binary.BigEndian.PutUint16(out[0:2], uint16(naluHeader))
out[0] = (out[0] & 0b10000001) | h265NaluFragmentationUnitType<<1

Check warning on line 1098 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1097-L1098

Added lines #L1097 - L1098 were not covered by tests

// write the fragment header
out[2] = byte(H265FragmentationUnitHeader(naluHeader.Type()))
if len(nalu) == fullNALUSize {

Check warning on line 1102 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1101-L1102

Added lines #L1101 - L1102 were not covered by tests
// Set start bit
out[2] |= 1 << 7
} else if len(nalu)-curentFUPayloadSize == 0 {

Check warning on line 1105 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1104-L1105

Added lines #L1104 - L1105 were not covered by tests
// Set end bit
out[2] |= 1 << 6

Check warning on line 1107 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1107

Added line #L1107 was not covered by tests
}

if p.AddDONL {

Check warning on line 1110 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1110

Added line #L1110 was not covered by tests
// write the DONL header
binary.BigEndian.PutUint16(out[3:5], p.donl)

Check warning on line 1112 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1112

Added line #L1112 was not covered by tests

p.donl++

Check warning on line 1114 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1114

Added line #L1114 was not covered by tests

// copy the fragment payload
copy(out[5:], nalu[0:curentFUPayloadSize])
} else {

Check warning on line 1118 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1117-L1118

Added lines #L1117 - L1118 were not covered by tests
// copy the fragment payload
copy(out[3:], nalu[0:curentFUPayloadSize])

Check warning on line 1120 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1120

Added line #L1120 was not covered by tests
}

// append the fragment to the payload
payloads = append(payloads, out)

Check warning on line 1124 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1124

Added line #L1124 was not covered by tests

// advance the nalu data pointer
nalu = nalu[curentFUPayloadSize:]

Check warning on line 1127 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1127

Added line #L1127 was not covered by tests
}
}
})

flushBufferedNals()

Check warning on line 1132 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1132

Added line #L1132 was not covered by tests

return payloads

Check warning on line 1134 in codecs/h265_packet.go

View check run for this annotation

Codecov / codecov/patch

codecs/h265_packet.go#L1134

Added line #L1134 was not covered by tests
}
2 changes: 1 addition & 1 deletion codecs/h265_packet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ func TestH265_Packet(t *testing.T) {
for _, cur := range tt {
pck := &H265Packet{}
if cur.WithDONL {
pck.WithDONL(true)
pck.WithMaxDONDiff(1)
}

_, err := pck.Unmarshal(cur.Raw)
Expand Down

0 comments on commit dd05d22

Please sign in to comment.