Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add H265 payloader and depacketizer #165

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"encoding/binary"
"errors"
"fmt"
"math"
"sort"
)

//
Expand Down Expand Up @@ -733,22 +735,60 @@
// 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 @@

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 @@

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
Loading