diff --git a/codecs/h264_packet.go b/codecs/h264_packet.go index 11a82fe..bc166c9 100644 --- a/codecs/h264_packet.go +++ b/codecs/h264_packet.go @@ -23,10 +23,10 @@ const ( stapaHeaderSize = 1 stapaNALULengthSize = 2 - naluTypeBitmask = 0x1F - naluRefIdcBitmask = 0x60 - fuStartBitmask = 0x80 - fuEndBitmask = 0x40 + naluTypeBitmask = byte(0x1F) + naluRefIdcBitmask = byte(0x60) + fuStartBitmask = byte(0x80) + fuEndBitmask = byte(0x40) outputStapAHeader = 0x78 ) diff --git a/codecs/h264_spreader.go b/codecs/h264_spreader.go new file mode 100644 index 0000000..805a524 --- /dev/null +++ b/codecs/h264_spreader.go @@ -0,0 +1,401 @@ +/* +H264 payload reducer. +MTU here is related to the RTP and its content (not taking acount the IP/UDP/etc layers) +Assumes that RTP packets will enter already sequenced (but may contains sequence holes ) +Assumes that if a given FU-A packet is bigger than the requested MTU, the previous related FU-A will be too big as well. +*/ + +package codecs + +import ( + "encoding/binary" + "fmt" + + "github.com/pion/rtp/v2" +) + +type H264spreader struct { + Mtu int + Spreading bool + RtpOffset uint16 + fuInProgress *fuInProgress + trailingBuf []byte +} + +type fuInProgress struct { + LastSeq uint16 + RtpHeader []byte + Trailing []byte + FuStartBytes [2]byte +} + +const ( + minRtpHeaderSize = 12 + rtpVPECsrcOffset = 0 + rtpMPtOffset = 1 + rtpSeqNumOffset = 2 + rtpSeqNumLength = 2 + + nalUnitTypeOffset = 0 + nalUnitTypeSize = 1 + fuaOverhead = 2 + fuaIndicatorOffset = 0 + fuaHeaderOffest = 1 + + stapbNALUType = 25 + mtap16NALUType = 26 + mtap24NALUType = 27 + + rtpPaddingBitMask = byte(0x20) + rtpMarkerBitMask = byte(0x80) +) + +// From rfc3550 +// =================================== +// RTP header (minimal part) +// =================================== +// +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |V=2|P|X| CC |M| PT | sequence number | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | timestamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | synchronization source (SSRC) identifier | +// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +// +// +// +// From rfc6184 +// =================================== +// Single NAL 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |F|NRI| Type | | +// +-+-+-+-+-+-+-+-+ | +// | | +// | Bytes 2..n of a single NAL unit | +// | | +// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | :...OPTIONAL RTP padding | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// =================================== +// FU-A +// =================================== +// +// RTP payload format for FU-A : +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | FU indicator | FU header | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +// | FU payload | +// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | :...OPTIONAL RTP padding | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// The FU indicator octet has the following format: +// +---------------+ +// |0|1|2|3|4|5|6|7| +// +-+-+-+-+-+-+-+-+ +// |F|NRI| Type | +// +---------------+ +// +// The FU header has the following format: +// +---------------+ +// |0|1|2|3|4|5|6|7| +// +-+-+-+-+-+-+-+-+ +// |S|E|R| Type | +// +---------------+ +// +// =================================== +// STAP-A +// =================================== +// +// An example of an RTP packet including an STAP-A containing two single-time aggregation units +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | RTP Header | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | NALU 1 Data | +// : : +// + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | NALU 2 Size | NALU 2 HDR | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | NALU 2 Data | +// : : +// | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | :...OPTIONAL RTP padding | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +func NewH264spreader(mtu uint16) H264spreader { + return H264spreader{ + Mtu: int(mtu), + Spreading: false, + RtpOffset: 0, + fuInProgress: nil, + trailingBuf: make([]byte, mtu), + } +} + +func (s *H264spreader) Process(payload []byte) (outPayloads [][]byte, err error) { + outPayloads = make([][]byte, 0, 4) + payLen := len(payload) + if payLen == 0 { + return outPayloads, nil + } else if payLen < minRtpHeaderSize { + return nil, fmt.Errorf("payload is too small: %d", payLen) + } else if !s.Spreading && (payLen <= s.Mtu) { + //best case scenario : all RTP pkts were small enough up to now, nothing to do! Pkt goes straight! + outPayloads = append(outPayloads, payload) + return outPayloads, nil + } + + s.Spreading = true + + //rtp seq offset to compensate for the previous extra pkts we inserted + seqNum := binary.BigEndian.Uint16(payload[rtpSeqNumOffset : rtpSeqNumOffset+rtpSeqNumLength]) + seqNum += s.RtpOffset + binary.BigEndian.PutUint16(payload[rtpSeqNumOffset:rtpSeqNumOffset+rtpSeqNumLength], seqNum) + + if s.fuInProgress == nil && (payLen <= s.Mtu) { + //whenever possible, forward RTP pkts without any Unmarshal() + outPayloads = append(outPayloads, payload) + return outPayloads, nil + } + + rtpPkt := &rtp.Packet{} + err = rtpPkt.Unmarshal(payload) + if err != nil { + return nil, err + } else if len(rtpPkt.Payload) < 2 { + return nil, fmt.Errorf("nal content is too small: %d", len(rtpPkt.Payload)) + } + + //avoiding repetitive RTP Marshal() by passing around the RTP header slice (as a data template) + nalData := rtpPkt.Payload + rtpHeaderSize := payLen - len(rtpPkt.Payload) - int(rtpPkt.PaddingSize) + rtpHeaderData := payload[:rtpHeaderSize] + rtpHeaderData[rtpVPECsrcOffset] &= ^rtpPaddingBitMask + + naluType := nalData[nalUnitTypeOffset] & naluTypeBitmask + if naluType != fuaNALUType && s.fuInProgress != nil { + outPayloads, seqNum = s.flushFuPending(outPayloads, seqNum) + + if payLen <= s.Mtu { + outPayloads = append(outPayloads, payload) + s.RtpOffset += uint16(len(outPayloads) - 1) + return outPayloads, nil + } + } + + outPayloads, _, err = s.handleNalTooBigOrFua(outPayloads, seqNum, naluType, rtpHeaderData, nalData) + if err != nil { + return nil, err + } + s.RtpOffset += uint16(len(outPayloads) - 1) + return outPayloads, nil +} + +func (s *H264spreader) handleNalTooBigOrFua(cumulRtp [][]byte, seqNum uint16, naluType byte, rtpHeader []byte, nalData []byte) ([][]byte, uint16, error) { + switch { + case naluType == stapaNALUType: + return s.explodeStapA(cumulRtp, seqNum, rtpHeader, nalData) + case naluType == fuaNALUType: + return s.spreadFua(cumulRtp, seqNum, rtpHeader, nalData) + case naluType == stapbNALUType || naluType == mtap16NALUType || naluType == mtap24NALUType || naluType == fubNALUType: + return nil, seqNum, fmt.Errorf("DON or MTAP are not supported") + default: + return s.spreadSingleNalToFua(cumulRtp, seqNum, rtpHeader, nalData) + } +} + +//relying on continuous seq number & start/end FU bits to sync ourselve, so not looking at RtpTimestamp +func (s *H264spreader) spreadFua(cumulRtp [][]byte, firtSeqNum uint16, rtpHeader []byte, fua []byte) ([][]byte, uint16, error) { + seqNum := firtSeqNum + if s.fuInProgress != nil { + expectedSeq := s.fuInProgress.LastSeq + 1 + if firtSeqNum != expectedSeq { + cumulRtp, seqNum = s.flushFuPending(cumulRtp, seqNum) + //restart over clean (recurse) + return s.spreadFua(cumulRtp, seqNum, rtpHeader, fua) + } + } + + entryMarker := rtpHeader[rtpMPtOffset] & rtpMarkerBitMask + rtpHeader[rtpMPtOffset] &= ^rtpMarkerBitMask + + lenRtpHeader := len(rtpHeader) + if s.fuInProgress == nil { + rtpHeaderCpy := make([]byte, lenRtpHeader) + copy(rtpHeaderCpy, rtpHeader) + s.fuInProgress = &fuInProgress{ + LastSeq: seqNum, + RtpHeader: rtpHeaderCpy, + Trailing: nil, + } + s.fuInProgress.FuStartBytes[fuaIndicatorOffset] = fua[fuaIndicatorOffset] + s.fuInProgress.FuStartBytes[fuaHeaderOffest] = fua[fuaHeaderOffest] & (^fuEndBitmask) + } + + var lastFuHeader *byte + mustFinish := (fua[fuaHeaderOffest] & fuEndBitmask) != 0 + reqSubSize := s.Mtu - lenRtpHeader - fuaOverhead + newData := fua[fuaOverhead:] + currentDataSize := len(s.fuInProgress.Trailing) + len(newData) + for currentDataSize > reqSubSize || (mustFinish && currentDataSize > 0) { + bufSize := min(s.Mtu, lenRtpHeader+fuaOverhead+currentDataSize) + rtp := make([]byte, bufSize) + binary.BigEndian.PutUint16(rtpHeader[rtpSeqNumOffset:rtpSeqNumOffset+rtpSeqNumLength], seqNum) + copy(rtp, rtpHeader) + copy(rtp[lenRtpHeader:], s.fuInProgress.FuStartBytes[:]) + lastFuHeader = &rtp[lenRtpHeader+1] + + lenTrailing := len(s.fuInProgress.Trailing) + if lenTrailing > 0 { + copy(rtp[lenRtpHeader+fuaOverhead:], s.fuInProgress.Trailing) + s.fuInProgress.Trailing = nil + } + toCopyFromNew := min(reqSubSize-lenTrailing, len(newData)) + if toCopyFromNew > 0 { + copy(rtp[lenRtpHeader+fuaOverhead+lenTrailing:], newData[:toCopyFromNew]) + newData = newData[toCopyFromNew:] + } + + cumulRtp = append(cumulRtp, rtp) + + s.fuInProgress.FuStartBytes[fuaHeaderOffest] &= ^fuStartBitmask + s.fuInProgress.LastSeq = seqNum + seqNum += 1 + currentDataSize = len(newData) + } + + if mustFinish { + *lastFuHeader |= fuEndBitmask + s.fuInProgress = nil + } else { + copy(s.trailingBuf, newData) + s.fuInProgress.Trailing = s.trailingBuf[:len(newData)] + } + + cumulRtp[len(cumulRtp)-1][rtpMPtOffset] |= entryMarker + return cumulRtp, seqNum, nil +} + +func (s *H264spreader) flushFuPending(cumulRtp [][]byte, entrySeq uint16) ([][]byte, uint16) { + seqNum := entrySeq + fuInProgress := s.fuInProgress + s.fuInProgress = nil + if fuInProgress != nil && len(fuInProgress.Trailing) > 0 { + lenPrevRtpHeader := len(fuInProgress.RtpHeader) + rtp := make([]byte, lenPrevRtpHeader+fuaOverhead+len(fuInProgress.Trailing)) + newSeq := fuInProgress.LastSeq + 1 + binary.BigEndian.PutUint16(fuInProgress.RtpHeader[rtpSeqNumOffset:rtpSeqNumOffset+rtpSeqNumLength], newSeq) + fuInProgress.FuStartBytes[fuaHeaderOffest] &= ^(fuStartBitmask | fuEndBitmask) //can't have trailing if was 'ending' before + copy(rtp, fuInProgress.RtpHeader) + copy(rtp[lenPrevRtpHeader:], fuInProgress.FuStartBytes[:]) + copy(rtp[lenPrevRtpHeader+fuaOverhead:], fuInProgress.Trailing) + + seqNum += 1 + return append(cumulRtp, rtp), seqNum + } + + return cumulRtp, seqNum +} +func (s *H264spreader) spreadSingleNalToFua(cumulRtp [][]byte, firtSeqNum uint16, rtpHeader []byte, nal []byte) ([][]byte, uint16, error) { + entryMarker := rtpHeader[rtpMPtOffset] & rtpMarkerBitMask + rtpHeader[rtpMPtOffset] &= ^rtpMarkerBitMask + naluType := nal[nalUnitTypeOffset] & naluTypeBitmask + fuHeader := naluType | fuStartBitmask + fuIndicator := (nal[nalUnitTypeOffset] ^ naluTypeBitmask) | fuaNALUType + lenRtpHeader := len(rtpHeader) + reqSubSize := s.Mtu - lenRtpHeader - fuaOverhead + + // rfc6184: + // The NAL unit type octet of the fragmented NAL unit is not included as such in the fragmentation unit payload, + // but rather the information of the NAL unit type octet of the fragmented NAL unit is conveyed in the F and NRI + // fields of the FU indicator octet of the fragmentation unit and in the type field of the FU header. + nalWithoutHeader := nal[nalUnitTypeSize:] + chunks := sliceTo(reqSubSize, nalWithoutHeader) + nbChunks := len(chunks) + buf := make([]byte, len(nalWithoutHeader)+((fuaOverhead+lenRtpHeader)*nbChunks)) + offset := 0 + seqNum := firtSeqNum + var lastFuHeader *byte + for _, chunk := range chunks { + cumulRtp = append(cumulRtp, buf[offset:offset+lenRtpHeader+fuaOverhead+len(chunk)]) + binary.BigEndian.PutUint16(rtpHeader[rtpSeqNumOffset:rtpSeqNumOffset+rtpSeqNumLength], seqNum) + copy(buf[offset:], rtpHeader) + offset += lenRtpHeader + buf[offset] = fuIndicator + offset += 1 + buf[offset] = fuHeader + lastFuHeader = &buf[offset] + offset += 1 + copy(buf[offset:], chunk) + offset += len(chunk) + + seqNum += 1 + fuHeader &= ^fuStartBitmask + } + *lastFuHeader |= fuEndBitmask + cumulRtp[len(cumulRtp)-1][rtpMPtOffset] |= entryMarker + return cumulRtp, seqNum, nil +} + +func (s *H264spreader) explodeStapA(cumulRtp [][]byte, firtSeqNum uint16, rtpHeader []byte, stapa []byte) ([][]byte, uint16, error) { + entryMarker := rtpHeader[rtpMPtOffset] & rtpMarkerBitMask + rtpHeader[rtpMPtOffset] &= ^rtpMarkerBitMask + lenRtpHeader := len(rtpHeader) + maxSize := s.Mtu - lenRtpHeader + currOffset := int(stapaHeaderSize) + lenStapA := len(stapa) + seqNum := firtSeqNum + var err error + for currOffset < lenStapA { + naluSize := int(binary.BigEndian.Uint16(stapa[currOffset:])) + currOffset += stapaNALULengthSize + + if lenStapA < currOffset+naluSize { + return nil, seqNum, fmt.Errorf("STAP-A declared size(%d) is larger than buffer(%d)", naluSize, lenStapA-currOffset) + } + + subNal := stapa[currOffset : currOffset+naluSize] + currOffset += naluSize + if naluSize <= maxSize { + rtp := make([]byte, lenRtpHeader+naluSize) + binary.BigEndian.PutUint16(rtpHeader[rtpSeqNumOffset:rtpSeqNumOffset+rtpSeqNumLength], seqNum) + copy(rtp, rtpHeader) + copy(rtp[lenRtpHeader:], subNal) + cumulRtp = append(cumulRtp, rtp) + seqNum += 1 + } else { + cumulRtp, seqNum, err = s.spreadSingleNalToFua(cumulRtp, seqNum, rtpHeader, subNal) + if err != nil { + return nil, seqNum, err + } + } + } + + cumulRtp[len(cumulRtp)-1][rtpMPtOffset] |= entryMarker + return cumulRtp, seqNum, nil +} + +func sliceTo(reqSize int, data []byte) [][]byte { + chunkNb := (len(data) + reqSize - 1) / reqSize + chunks := make([][]byte, chunkNb) + for i := 0; i < (chunkNb - 1); i++ { + rangeStart := i * reqSize + chunks[i] = data[rangeStart : rangeStart+reqSize] + } + chunks[chunkNb-1] = data[(chunkNb-1)*reqSize:] + return chunks +} diff --git a/codecs/h264_spreader_test.go b/codecs/h264_spreader_test.go new file mode 100644 index 0000000..e330d3b --- /dev/null +++ b/codecs/h264_spreader_test.go @@ -0,0 +1,312 @@ +package codecs + +import ( + "encoding/binary" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPassthrough(t *testing.T) { + assert := assert.New(t) + + //arrange + spreader := NewH264spreader(1200) + rtpPkt := append(getRtpHeader(false), getH264StapADelimiterAndSpsAndPps()...) + + //act + out, err := spreader.Process(rtpPkt) + + //assert + if assert.NoError(err, "failed to process pkt") { + assert.Equal(false, spreader.Spreading, "should not be spreading") + assert.Equal(uint16(0), spreader.RtpOffset, "Bad offset") + + assert.Equal(1, len(out), "bad pkt out count") + assert.Equal(rtpPkt, out[0], "bad payload") + } +} + +func TestSmallPktsAreOffsetedOnceSpreadingStart(t *testing.T) { + assert := assert.New(t) + + //arrange + spreader := NewH264spreader(1200) + spreader.Spreading = true + spreader.RtpOffset = 25 + rtpPkt := append(getRtpHeader(false), getH264StapADelimiterAndSpsAndPps()...) + + //act + out, err := spreader.Process(rtpPkt) + + //assert + if assert.NoError(err, "failed to process pkt") { + assert.Equal(true, spreader.Spreading, "should be spreading") + assert.Equal(uint16(25), spreader.RtpOffset, "Bad offset") + + assert.Equal(1, len(out), "bad pkt out count") + assert.Equal(append(getRtpHeaderOffSet(uint16(25), false), getH264StapADelimiterAndSpsAndPps()...), out[0], "bad payload") + } +} + +func TestExplodingStap(t *testing.T) { + assert := assert.New(t) + + //arrange + spreader := NewH264spreader(26) + rtpPkt := append(getRtpHeader(false), getH264StapADelimiterAndSpsAndPps()...) + + //act + out, err := spreader.Process(rtpPkt) + + //assert + if assert.NoError(err, "failed to process pkt") { + assert.Equal(true, spreader.Spreading, "should be spreading") + assert.Equal(uint16(3), spreader.RtpOffset, "Bad offset") + + SpsFuaStart := []byte{0x7C, 0x87, 0x4D, 0x40, 0x2A, 0xEC, 0xA0, 0x3C, 0x01, 0x13, 0xF2, 0xC2, 0x00, 0x00} + SpsFuaEnd := []byte{0x7C, 0x47, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0xF1, 0x1E, 0x30, 0x63, 0x2C} + + assert.Equal(4, len(out), "bad pkt out count: one delimiter, SPS split in two fua, one PPS") + assert.Equal(append(getRtpHeaderOffSet(0, false), getH264DelimiterNal()...), out[0], "bad payload delimiter") + assert.Equal(append(getRtpHeaderOffSet(1, false), SpsFuaStart...), out[1], "bad payload SPS start FUA") + assert.Equal(append(getRtpHeaderOffSet(2, false), SpsFuaEnd...), out[2], "bad payload SPS end FUA") + assert.Equal(append(getRtpHeaderOffSet(3, false), getH264PpsNal()...), out[3], "bad payload") + } +} + +func TestExplodingStapWithMarker(t *testing.T) { + assert := assert.New(t) + + //arrange + spreader := NewH264spreader(39) + rtpPkt := append(getRtpHeader(true), getH264StapADelimiterAndSpsAndPps()...) + + //act + out, err := spreader.Process(rtpPkt) + + //assert + if assert.NoError(err, "failed to process pkt") { + assert.Equal(true, spreader.Spreading, "should be spreading") + assert.Equal(uint16(2), spreader.RtpOffset, "Bad offset") + + assert.Equal(3, len(out), "bad pkt out count: one delimiter, SPS split in two fua, one PPS") + assert.Equal(append(getRtpHeaderOffSet(0, false), getH264DelimiterNal()...), out[0], "bad payload delimiter") + assert.Equal(append(getRtpHeaderOffSet(1, false), getH264SpsNal()...), out[1], "bad payload SPS start FUA") + assert.Equal(append(getRtpHeaderOffSet(2, true), getH264PpsNal()...), out[2], "bad payload") + } +} + +func TestSplitSingleNalInFua(t *testing.T) { + assert := assert.New(t) + + //arrange + spreader := NewH264spreader(20) + rtpPkt := append(getRtpHeader(true), getH264SpsNal()...) + + //act + out, err := spreader.Process(rtpPkt) + + //assert + if assert.NoError(err, "failed to process pkt") { + assert.Equal(true, spreader.Spreading, "should be spreading") + assert.Equal(uint16(3), spreader.RtpOffset, "Bad offset") + + SpsFuaStart := []byte{0x7C, 0x87, 0x4D, 0x40, 0x2A, 0xEC, 0xA0, 0x3C} + SpsFuaMiddle1 := []byte{0x7C, 0x07, 0x01, 0x13, 0xF2, 0xC2, 0x00, 0x00} + SpsFuaMiddle2 := []byte{0x7C, 0x07, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03} + SpsFuaEnd := []byte{0x7C, 0x47, 0x00, 0xF1, 0x1E, 0x30, 0x63, 0x2C} + + assert.Equal(4, len(out), "bad pkt out count: one delimiter, SPS split in two fua, one PPS") + assert.Equal(append(getRtpHeaderOffSet(0, false), SpsFuaStart...), out[0], "bad payload SPS start FUA") + assert.Equal(append(getRtpHeaderOffSet(1, false), SpsFuaMiddle1...), out[1], "bad payload SPS middle1 FUA") + assert.Equal(append(getRtpHeaderOffSet(2, false), SpsFuaMiddle2...), out[2], "bad payload SPS middle2 FUA") + assert.Equal(append(getRtpHeaderOffSet(3, true), SpsFuaEnd...), out[3], "bad payload SPS end FUA") + } +} + +func TestFuaAreSpreadMaxingMtuAcrossPkts(t *testing.T) { + assert := assert.New(t) + + //arrange + EntrySpsFuaStart := []byte{0x7C, 0x87, 0x4D, 0x40, 0x2A, 0xEC, 0xA0, 0x3C, 0x01, 0x13, 0xF2, 0xC2, 0x00, 0x00} + EntrySpsFuaEnd := []byte{0x7C, 0x47, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0xF1, 0x1E, 0x30, 0x63, 0x2C} + + spreader := NewH264spreader(24) + fua1 := append(getRtpHeaderOffSet(0, false), EntrySpsFuaStart...) + fua2 := append(getRtpHeaderOffSet(1, false), EntrySpsFuaEnd...) + + //act + out1, err1 := spreader.Process(fua1) + out2, err2 := spreader.Process(fua2) + out := append(out1, out2...) + + //assert + assert.NoError(err1, "failed to process pkt") + assert.NoError(err2, "failed to process pkt") + SpsFuaStart := []byte{0x7C, 0x87, 0x4D, 0x40, 0x2A, 0xEC, 0xA0, 0x3C, 0x01, 0x13, 0xF2, 0xC2} + SpsFuaMiddle := []byte{0x7C, 0x07, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0xF1} + SpsFuaEnd := []byte{0x7C, 0x47, 0x1E, 0x30, 0x63, 0x2C} + + assert.Equal(true, spreader.Spreading, "should be spreading") + assert.Equal(uint16(1), spreader.RtpOffset, "Bad offset") + + assert.Equal(3, len(out), "bad pkt out count: one delimiter, SPS split in two fua, one PPS") + assert.Equal(append(getRtpHeaderOffSet(0, false), SpsFuaStart...), out[0], "bad payload SPS start FUA") + assert.Equal(append(getRtpHeaderOffSet(1, false), SpsFuaMiddle...), out[1], "bad payload SPS middle FUA") + assert.Equal(append(getRtpHeaderOffSet(2, false), SpsFuaEnd...), out[2], "bad payload SPS end FUA") +} + +func TestFuaRealFromSameSrcBuffer(t *testing.T) { + assert := assert.New(t) + + //arrange + spreader := NewH264spreader(1200) + refRtpFuas := getRtpsFuaNonIdr() + + var refFuaConcat []byte + for _, slice := range refRtpFuas { + refFuaConcat = append(refFuaConcat, slice[14:]...) + } + + //act + rtpFuasToFeed := getRtpsFuaNonIdr() + var rtpOutsConcat []byte + alwaysSameBuf := make([]byte, 3000) + for _, rtp := range rtpFuasToFeed { + copy(alwaysSameBuf, rtp) + out, err := spreader.Process(alwaysSameBuf[:len(rtp)]) + assert.NoError(err, "failed to process pkt") + for _, singleRtp := range out { + rtpOutsConcat = append(rtpOutsConcat, singleRtp[14:]...) + } + } + + //assert + assert.Equal(len(refFuaConcat), len(rtpOutsConcat), "bad reassambly length") + assert.Equal(refFuaConcat, rtpOutsConcat, "bad reassambly") +} + +func TestChunkSlicing(t *testing.T) { + assert := assert.New(t) + + twelve := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + + slice12to20 := sliceTo(20, twelve) + assert.Equal(1, len(slice12to20), "Bad Slicing") + assert.Equal(slice12to20[0], []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, "Bad Slicing") + + slice12to12 := sliceTo(12, twelve) + assert.Equal(1, len(slice12to12), "Bad Slicing") + assert.Equal(slice12to12[0], []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, "Bad Slicing") + + slice12to4 := sliceTo(4, twelve) + assert.Equal(3, len(slice12to4), "Bad Slicing") + assert.Equal(slice12to4[0], []byte{1, 2, 3, 4}, "Bad Slicing") + assert.Equal(slice12to4[1], []byte{5, 6, 7, 8}, "Bad Slicing") + assert.Equal(slice12to4[2], []byte{9, 10, 11, 12}, "Bad Slicing") + + thirteen := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} + slice13to4 := sliceTo(4, thirteen) + assert.Equal(4, len(slice13to4), "Bad Slicing") + assert.Equal(slice13to4[0], []byte{1, 2, 3, 4}, "Bad Slicing") + assert.Equal(slice13to4[1], []byte{5, 6, 7, 8}, "Bad Slicing") + assert.Equal(slice13to4[2], []byte{9, 10, 11, 12}, "Bad Slicing") + assert.Equal(slice13to4[3], []byte{13}, "Bad Slicing") + + fourteen := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} + slice14to4 := sliceTo(4, fourteen) + assert.Equal(4, len(slice14to4), "Bad Slicing") + assert.Equal(slice14to4[0], []byte{1, 2, 3, 4}, "Bad Slicing") + assert.Equal(slice14to4[1], []byte{5, 6, 7, 8}, "Bad Slicing") + assert.Equal(slice14to4[2], []byte{9, 10, 11, 12}, "Bad Slicing") + assert.Equal(slice14to4[3], []byte{13, 14}, "Bad Slicing") + + fithteen := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + slice15to4 := sliceTo(4, fithteen) + assert.Equal(4, len(slice15to4), "Bad Slicing") + assert.Equal(slice15to4[0], []byte{1, 2, 3, 4}, "Bad Slicing") + assert.Equal(slice15to4[1], []byte{5, 6, 7, 8}, "Bad Slicing") + assert.Equal(slice15to4[2], []byte{9, 10, 11, 12}, "Bad Slicing") + assert.Equal(slice15to4[3], []byte{13, 14, 15}, "Bad Slicing") + + sixteen := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + slice16to4 := sliceTo(4, sixteen) + assert.Equal(4, len(slice16to4), "Bad Slicing") + assert.Equal(slice16to4[0], []byte{1, 2, 3, 4}, "Bad Slicing") + assert.Equal(slice16to4[1], []byte{5, 6, 7, 8}, "Bad Slicing") + assert.Equal(slice16to4[2], []byte{9, 10, 11, 12}, "Bad Slicing") + assert.Equal(slice16to4[3], []byte{13, 14, 15, 16}, "Bad Slicing") + + seventeen := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17} + slice17to4 := sliceTo(4, seventeen) + assert.Equal(5, len(slice17to4), "Bad Slicing") + assert.Equal(slice17to4[0], []byte{1, 2, 3, 4}, "Bad Slicing") + assert.Equal(slice17to4[1], []byte{5, 6, 7, 8}, "Bad Slicing") + assert.Equal(slice17to4[2], []byte{9, 10, 11, 12}, "Bad Slicing") + assert.Equal(slice17to4[3], []byte{13, 14, 15, 16}, "Bad Slicing") + assert.Equal(slice17to4[4], []byte{17}, "Bad Slicing") +} + +func getRtpHeader(mark bool) []byte { + + rtpMarkerBitMask := byte(0x80) + rtpHeader := []byte{0x80, 0x62, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x70, 0x1c, 0x77, 0x48} + if mark { + rtpHeader[1] |= rtpMarkerBitMask + } + return rtpHeader +} + +func getRtpHeaderOffSet(offset uint16, mark bool) []byte { + rtpHeader := getRtpHeader(mark) + seqNum := binary.BigEndian.Uint16(rtpHeader[2:4]) + binary.BigEndian.PutUint16(rtpHeader[2:4], seqNum+offset) + return rtpHeader +} + +func getH264StapADelimiterAndSpsAndPps() []byte { + return []byte{ + 0x18, //stap-a + 0x00, 0x02, 0x09, 0xF0, //delimiter + 0x00, 0x19, 0x67, 0x4D, 0x40, 0x2A, 0xEC, 0xA0, 0x3C, 0x01, 0x13, 0xF2, 0xC2, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0xF1, 0x1E, 0x30, 0x63, 0x2C, //sps + 0x00, 0x04, 0x68, 0xEF, 0xBC, 0x80, //pps + } +} + +func getH264DelimiterNal() []byte { + return []byte{0x09, 0xF0} +} + +func getH264SpsNal() []byte { + return []byte{ + 0x67, 0x4D, 0x40, 0x2A, 0xEC, 0xA0, 0x3C, 0x01, 0x13, 0xF2, 0xC2, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0xF1, 0x1E, 0x30, 0x63, 0x2C, + } +} + +func getH264PpsNal() []byte { + return []byte{0x68, 0xEF, 0xBC, 0x80} +} + +func getRtpsFuaNonIdr() [][]byte { + return [][]byte{ + hexit("806075251c45b0ac4b396e7a7c819af86eb935b1be6d9fe4f53f26c797bb0682651778432fe2b25f5ebc311358670511397cb09fe0bfe5f2c95972789970c784609b0610864970cd7ac906b5972d7045d1b0715f7d05f13d3a7306be2fa17be584fbbbe979a9656573502fe23b5c7cb87f15cc73d813e1ae5255dbff85bce4687aa7fdff2797cb3648ac9e5965c6e59a8b503062f2cb9055bfd7902deb284680b5dfeb182684bec2f1a27a173a5fba01a3622107c7e8f67fa01eca0b32237d7f286a0f9650d6817c4ca18f3a8fb88d02d83602f57a5be11ec5d81f65e36f2ffc90438763b246e1d11b16ff70f74523f679493012ca6e97ea18e604094c7a6b417fe1bdd9d7b0fdf08f1a09fe55e9e7e334ba5dec6c7fcdb01ca29e12f20f20ffc6d8365c7430ff2146026b46ac1fff0eeceb8c069f40ebdffcbd8be10e87de60bca9fcd4f9b2f9619bc3d72175930c853d88fffa03afdf0eec5eec3d04955eff5c15d8132bb1580c707025bcbc827982fd8be2bcc102fd3f1bbdae076c2cf0ec0f230748efd3bfbb7fdc4e30bfdfca2b1c13ef9c10f9e3d5222c8f8df7a683ff477cc1956acffbfc13597eae7c29d0d041f956717be133061c6df1bb02b54738fed57ca211e1d2ff877a06c9876b740c03aafa30efff0bf2a42127d0243d6c2ffc4f4e1945d0dd5bcabde49b244e1c88cb3737b13ac98d86384d7286fa0b942da158a5b09980cdbc8972a1ff19e519e65fd9741e5403a123c4c6c536d11a167e1f903f629d6987d02d826974ffe1fe81487dff40d0096dffe6e1d9ea7cfc4f62b153c5f1fc6ca18d016c6d61e8241e28e16a917ff5affe083a05f65c851503ffe2ef8067228fdf1bc02d81d8cc8bba01fadfff1521afd814d966eaf09e48ccb3659324b842ef596a237c33b5ad3fcb1fa9e4c831c9c9a07ac9596232de5ac9119223ae513d379eb1596abe42faf2af4465ac91b96b0ccbc95eefe492f15589c4c55057bf94cd2fc97927c4797c9135fad792b9b605abc4e15f2c25926c9582926bf2783daeca8f939bb7f2fa6aff5ea13f825f27870d7e4cab2bc149bdf8207efca6f7c237a1eb92ffd5f8bcfd62fab5725ff0637d5e4c1057049dbf7cdec66c1097b2f9b4be25f5e060fc95fcbf3d5bffe5f77cda3df277be08bd5fe4dbf64f2baff08d3fdf403bc14f828a84b5812559f583e6b7fd5fefa7e225c21e26b254692b073e0a3efbf9039eeed3ff93975cbd03aeade23c225e87e2390337c9d7857d7c29f26feff5954824dd7c235cf5f4f925c9d6b926f63cbac1cf93c823d05a5e11f1a6ebf375aca6b1f264ae6d17c33272efac21e11c2b8e1b3fff63ac85f4b5fbd75045aabef7d5e797f2d2fc101aff8117c4d513c1e55fc129bc3c8361ca10be6d77965c9e11ac417d0d75cab0817ad615931afd1f217dd4815e100a024edf29327621755cdae6a93a19e1efd3f7a2f8664c6f8cf0e792b823d78be08b40fef24b7572afc0c2093c4797c6f9011ebc52e457f217d2ff2fcf5e9fbc9e5f0cd775eb27c176ba2f8498d04fae87e0f185d7589f049f577df381fbd649729bd5cb376a5c9e27c9e201274bdf3687e8b265f23dbd58638ed72d7ac6f8df2790afa06a4f727a5e087deaf089b47e49eadffe34dd3eab2f1a5f7e43d7a2f7c10efc1e509f67afe4f32feb2eaaf5945fb6e3a0004031ec3d889e8b1b8decc6dcb341e5055ba6429fe22fe9d5bfe8fd4d24da144ca9986aafd46f561022920d12e7356bc88692b2c8adc53c4b4a3aa0ffc3152077a45da536f75d2ff042aff5d7c6fa54661034143c872eac48b88c36b1b0effb20de5c290728c32368ddef0154bee5334fff0075df500fac7e70cd79beafa2020f7e28ea245b3bedffca5f6f51be3ccb48cc84c86050696020042afe660e01d8c998ef5b8a2ca6bff1bd490451a6bab25bd47506bf41d5f993333605bed4a513ba32907f9423bc808c01e09b8d889a54590f1b4022127a2c350c1a99c69f08974c0abb5c0a1e136c4563a05107511c35a37aeebe271f"), + hexit("806075261c45b0ac4b396e7a7c01a2bedf11ac22b1730103c082fb5e22bbf558d2e11303fdbf089ba2eca09fb4604d2a98be6bf5ddfeefaf61dac3a6f5c90cedebfaf0954b77f18bdd820f2d61936bf0a97a5e4583e12a01762fe6bf58d7ed725f08faec7ed2e5f55cd7eb0817ca4b8932e61fc0f19a071df37a6b1be0513d68bfe22f93d0df57f9b7fc2f1f068f99979491759e7dfd43b0de3ba49e7e07fbed41a383beb4d05fee378aa21c84ec67f9c34b4542da3a6572d64eb374d42ff20de01ebbbf8cf8d63bfb25d4b40940d24a89ab031de10070c32babebbf5e40535706ff15984d087c442c1c2e3943ba8fa1b93ca349467aa172a9ae3d32e3603b4211388872e25332b3fe16ac3ddc261066cb686ab7869dd97fc2fe75a0ec9a1b82d16b47fe374388d1c3d4fc24e3367edc7c43ec6559a4c1ca28ddffc3036f592b47c135809d91a37421664df0ec933654adedc9ff123f47f026c3c837520fd1fc1f8dbf2faa7baac8f9eda4344542f103468911277963fa8625c60dc4161a2d0659255d67497b3fdc3148b50a5f183683386a1cdc7dd3f67e093d09df1728a1d2426034b97961a11fc14672746d487a06d0dc16631031f08763b2ec6a1987ad48303eb831047da180e2f8bf0a25d4648f408665e18bc1011296ab0c2ff0b9f8d8edcb4daff84bdd1d79395831e1119d11f67f3668eb0cf77d2eb5cbdea59aff830f1be11f041e4fabc9cddbbb0a797c2353eb2af7d6befb5e0a7c14f93c9c9e4cd924e88d796f249cde531f1bd0276365a298336e67b70e2496bafff0b71a21b1aca8b804c6adff79414f4a88cdeb6c7a1501cc12ef8cfb01f0fef9cc12a7cc8283f0a4bde9c7040f7437b1b05250d72f8cd80742507e80deab999405c2f8cf4199f40ba1a7a470c1e80b8c19e9ec560344e8b43b0b907f62d00fb7f8da1fd07edf8765dcadc68bfa8ed75361f4fbf9a33e4a89838200ffc17e1824f80c338657f7b609bd056b31bff1be3e79e80b2b8437fbaecffe3685d151a1c13b1587eab4e9ff8dbcde8d0b128f9882c0c83a40807d3d6bff8de3c1f3c3da180b454e0b4eb4d3bd9fc21488b93a186b3d02612242d8fe14d35d4969fcca5073b3a0ec098f8539c9f8c6fe7c28f696ff13e13728d7e087c6c92fb0ef7b93c9f19597cb1dd6ab11e0a2bf962e20be36f935d64f2dd4de3498f1bb1cc0c402ad8186a204029a2e49ae0f80ffb0d639d7d3fc77d0f00986c51a5386a0fcbec9e0903fec3a0ec6d7547ff942382168795604058a3c0f7d8bf1bf4d8130f61cdf6f8adaffa8dc727167b2d0ee95c655cefff192060600c92e4351e12d817fb03e190be1b779f43840e1ee2bfaf9056c56b9035c602cdc1cd8ff2ae4513ab82ad098efce93b9e41d6545b1ca3c26064c811f8c0ed9e0f3060d08a66557a655ffde8bf287b43b0b4542fbbfaf944ec2b1c6e6345e0aec076043417af21d42d0cfa0abb8d9e7ec7a03bcc0ba7aa0e4dfeca27851216e42c5328e10252e5a0e7556ac978d04de3625680f7f1b5824f0cd7045ec6eacb592b071e5f2d5066ab7829f022f93c081c9e2b5cd302faac993c911c9e81ae11e00980f21e94355a7fc3ddae78982701b1f567ffb8dcdce081eccd38afbd7497fc29d8496494841788a54a1d2f5e41387d270b42fd8078d13a0523e3e2618caa0780fc81fa0db8ea15a18c744e6457c0dc99c1a7ff820d44f5eb6b3866bd17f20ade50e58e807c48cb1da25d15812ad3ed30d18c2638915e45e80bf0bec0fee4b61ffe37d00b940f6787d081febecff830ec5e855b357ff09d3842fbc742b0f891b62b163263ec754ea54d3ff906df2987c82dce081d11ab9897f7b5fc80c31904a1d26f38033215edff87728440ed09bfa013035ebfd7c15f28c1b69202b4361d7ebe0b22443018d8203c4fd5e2ea1dcceb0f4056d01141ab06f547d8ff1b588ef58479429251389d633c6cf6148998310865ac926492e431f3eaec260fc0a31146d68feaecd77c4681f297d72680982fde805ac8225187948"), + hexit("806075271c45b0ac4b396e7a7c01ff9342fc45808c3dd03fc448307f40bdd8360be23c83adfa17df511d83f9c93b889d0b03f92f27bc7e18f084f853c29e5f2cf829f053e086a408f0864c9584487e98ccb5d62bc1cdc0839b2f872f8da02de608c6410d25ec03eadfa2fc10f81da4bc472e97bf92b11263495fcb3757a9be4ac93e0a047bf758de5195926c478d972f89ec9e10f2f93c446e10f0893602d60b7c23265eb175fad4981665eb57853c29e087cb3e0a7c1496fdc837932649f0ef88f08796ebdfeb5272fa932cb7f087843c2126082afd8dbb2f92b2446087c1045deebe493059f5a8cbdc697cbe5f2792fab5604035ae4c936435017e5bc23e11aca4f4bc1157b5512b11d7ac997ad41e6e061f0367c10f82192069f02f6b041d043a2f01105bbe271d5f2da7bf93a7e0a7c0823bdf7defa2549e4f9222f597c9138c89c69bdf89257f25610f084b89efdcd5ef2de3313aac92614e070f82096071f01199607cc9c977fc33777aaff78281fbc578adeff8c74be4ba7cd0f3c27d03e13f972a2d303e7813a23a0681e8d03c1cdfb0161f88e7bf212dc12f67e97703844740ecf60f3447a07a5c105ed4b5c04242fe520d75edfa817a4ec17022ddff88e81947f65c08f0afa078c32d6c2dbf8cf0f5f7e07fbf7c1c5f60be4f416065bf67876238eb2f40f8089821f40db84e6dbfc471d2e1f7e0ca19f40d52ffa88efdef88e623efbbe83f8ff2c37731bfe209d82e04588e61ef60f03e44f39bdfc97d9fcd67fc9d235e021627d2bf87e0abd96c2b5f5c2d35afe4f478c88e925973a8cf63dbd7eb818223b03382077e1fbec0fe4f292e411c6994079082522a0e3f2ec3f03dcf5d3ff843ca499fa05fdfb07e2f40f40fe188ba1f67f0ccd7ea0699398970efb9741782484fd9b0bf843b5760f67fcd6bf84fdecfc0d10499815b9f36c1fe277c835f50c58263e8b5dafff09d65c44d9c9fb8bd82cf44686de3a2e81746520f818e2ec130b417d4393033ae82ff9681674f8104f5dffc14f8cf85173aa80361dc7030a0d124ce4a8bba07fae053899856d0e20ce9d3955e19f0f6a079f3f9e5f4cbd5fe5f4017922b2fdec039b9bb0c87b2c6740260f3800361e87229a664fc31c3c881e9a18693fc46603b03d69ffe10f2a07db83039dec168bca2f0768ce182364749208232ccc3d19ca0802c565c242d0881c65063493e33a0fa04705b1fd1954d05e11e123bf69e4a40c0943172fe3078c178fec0edf746720b439c23d13964f7e309ee4a2c4c38083ade4b8183c3e427b00a966dfd4b160c68aa4178df246f9845de98714c8e06e302592d8cdd6dffb0a12c7f89f847a79021a0bb05e45ef28bf62e612d028f0515c350d27f8bcdf5fb7eee518fe0469ba0b87e5e81f34bd03e1a9b9010370bcdecf03f7e5e8fe5ee8d47cddaa84bc352fbe226e829604bdc21ea5e97cde524b20bed7a07c68be5c1df6a9aa2659f2685ee22c26b0f387af88b48ff1b20ff05344cc2fb06d75e2f8fc64a7f4103fa0be3e85f07ec2f8e98a529c15ef4be3ec50f387cd1c4d3f67f1181f2924f02ba5aa0722fa5d2e4297949713efc3e52f40fa0a97405f27cbaeb1be5bca6e95642f5efdf9b592f11d6af57aa93b04cf34da3fdf9057511b74ec532fb0a7829f8bd0fb7f8bf1f0d673fa0cc5bc5fb10d8e5ff84f67f0d337e1a216952217d7b85bb0c78377b6bb7fd44d85f3e70214965f77b340f921df40f4171b4a5e5fe56bfee3fa07a2a079c21f8bf6b30df011911e71ed2f82edab01745af9b8cd5405a8ea0a7c0ee9bafe55f0af9e4f40f715d340b227f7e805e13f292d01fc479a1d81f8de3417b951a6f182f07fa0a1138750176bfb8de3a16603cc312974b8f4c9bbf30286e93dc3d656ba07ff1bcc0816e8d1e1c74b2d8690eb491de0169ae8dbfd46ed580b7412024f80794c9bfdd2417ee329ce41282d1de623ef58eb947a75c1aa00b51bd5858573f3863034f4340c23c33ea8e107bb0f102bbfa7e37c0bba4dd5d14f0e64b40a0c3b5d7"), + hexit("806075281c45b0ac4b396e7a7c01fe2ad7e54fc1276686df35292fc81ce6461cb22f319fd411fdfab29bcb0f77eae13df3cd238c573eb509dfbfd945fbe057bba3793b270d7e5ebd42565fbf2023e8360f2ee10ec0ba01fb2c92fb581c65e39fc9c5f98be61e398f9bd82f2583eb9ea82ffa84a81fd2e1c889087d7cff84f674ba5821ba39097c10d01a0b739223602d02fbbb06ff8be1d5f1bcd5849fc4f40f8de7f842c1fcec103852090bd35d0f9a2e1ad4fc01690a260503fe4cc31f71940bf34ad1c406d82a17b85347d85ec3e8e46e582bf41308c3b28fb34a83d570534b63e65d97dbaaebad70f70540286a1ae197d6e96fe0daffc7f188c1a24b81666e4b1823d0344c5e80bdc3b8692e83ed48e984993b54de2dc5aa697fe19ea3b790d52ffe0b23a5eb54a24354bd93178a260d82d771ba04383ecb40b3276a97860c07e81c70dce20c54d697fe33b0d89b3a6c268425c0bd9d294978c83e219b61fb582c781ebdf802b0be2304e0c8844a808a0540180ca083e37a48322563d81b2f498692e9ffc6da0630e76eb4229408a1bde3ed34175d7fe3e1dbb93e2cd106874411f0edd8b7057ec4b60fc4d032b5a1bba13b07d49d8bdcbb02f0f785bf15abca1a9c2015d47675556a52681eff0f503b07a05b3ef6ffee086c760acea3693cf0b4304d668c1ebce100ca9f29bc08d85253f58e63fc6f1b1ef43807ed9574925954105cd1b240cf2b840ffaf4240bfc6d800273184e46e2fc03a230ab35306f65c82568c8be3d3a12c5ed7f8d903e34d8b0533c31d9474140d1c76e2c6de233d2359c41f772f5d87e8bc6c26719e1d9de8281ca1a2a5608b935f6122e914663dfe36c09586d5faa26be95e2753449dec3fa356fa067dd761ff946e86fe92fa63c0816e002f8d6db34380560379e2a7a7dbf368afe2b626130edfe3280d31d24ed0f9c2887e3f74a43bea63f40fc5587083a8885429b80ff0f43c95ed7850ea9f4158881a386da3b4953ffb8c96d03cfb1e7826e0804e00c05d813cc7f0a70da577786728fa15950f8e814180bb9f199c1655f60e74473af06107fa17ee1084ad74e968cb06251ca0b43fa8ff541ec3ec7f17ec1e5067c66362eed080f008a1039d7a0f40e8bf190dcbbd492609601eedb475aa0a1058c6a36ac7c7fc651bd3cc4909810178c236055f547dc64a0ba3618e8a1f53970b7af35fa8ca06c1ab12e93ec56535b00fdc666408d7420e5c541d01a15582f740bd950bf8ed82604c3caaa744bc30129dfc35a1c8817bfee269d9f958f86a380afebd85fb8ca0651e302fc21e4b4416e6460408a83e1e513fea32d590f80483c742fc0256786023d69262fe33cfa3816a8281b093807611c105e31fe3ac120738fe3c0bd04405405f85f183c55a2def9ce2d97fe11d09500f383180b608833a8dd81b1cb0d07d8ac10d973d5b4fff19d3384338cd8070eaee7c110317fc6e4171d079686b29d2c9ea280f5930ae3f80d7a3fd46d83d1b051c1d7e3edf5a7fee338f89edfdb386186f6bee3753fc671b8040f508f3d85006655f40bf19deb101bce8f0f405ebc1b4ab75f8ce3ceb7de70c8d9f5542e2ec2e413d68d8d5897d5dcdca5fe15dfbf967357fe53f0fa7fc6dff41bd3d476551e2ce4cdb4645374fa7e51bd01675cea22a2b01998646b306d7d7f8d8fc2313d5c6c2f8153ab9cba3824a89761a7fe3704f4aed5f286b40b6340d07e2fbfdc29e8331cec1a7d07b014ba820e0ee49ca3ec1543b1aa0ba2b82d406e654a43b7fc8149853c85bf207a80a3e243cbb2077d307e1ac5eeb5ffc2de80a766eafffc5ebf725459b3a96598101e2fc12fe5a59a18c8bf65d1e041ac5c14c5f8e0d5d5cb17dae8fe6eed606684afe97e18eba3d74ff9a12bfecf72d83b5c0c7f8bec24f7e198ba05d83f815618e3059ce9306bdbfcde1e8befdfe6f3c3cbd135b8bef4beeb5c6c98a1f9a2b00c68bbeffc6683a0f60bf7ed60439740ff1366c1fa0be5f607847f3760f81b26ec17c5f60fd160fe6b37fcbdf827f0314b"), + hexit("806075291c45b0ac4b396e7a7c01d1f0ccbd03ee6e97c5f40bd83c042cdd1f517ec16c198bf97b0b81ffc044fe6ec123f37298f1337a07a97d03f3760ef9bb347a8be61ef20cae6e81b3cb3740be5ec1d44945f2e1731964dcc212f40eb9bb572cdc77dae5f67a28be32c9f4c77d7e23a0b817c6fafc551d0740f40ba2f3447b071a18fee6de525cbd7ad71b9d8a097857637ce194e8451c28c57c41ae03cabd7f1441b41390b2e1d68c6807ce8fc35c0856c70b58718da783dd9f50c7ebf852dcb5e7cc0dd9f955b10c976950de96063f80699d3e36de18cb50ad31e26cc183f6d75ff8cbe345ca3d0118b25d923706483b478288de3814e853ea355da341207810d345afedf246d2d4821fbc75c8cdc3e2360729b16862088ad3ff51b6f411c30295511606f21804b98f76031d0195fcc93b8de8199d4680c6229431a6f3d87a2d0740a80d825fd97c6fa2218ed595119232ac90940dd45db5fee14b6e062b0b83c71f5d1d922bb0111973e10dc649d005b01a0c749dd0fb853b0220c56b049f9caf86f3060fba853ca65aaf0d07403b4360382845ded16c7519ec2ecd80827cff914d6e6f222f71ba9440f98cf8cba4abecc41e7e707fae05a85ed35bf5d34ff0775f7c25d3a5a5b29bd2c0c9275e3e4ebc1ac35d6bfdf1d27beeebf0314ddf8392828d7daf7094f5e9ff08066bf5a7fea1ce9d6bfee09fd7be7e881cf6db7ff65531702a10137ad1f5f04945e5c3645283862f9f3a27028439b5aedff93e5d3f04358387bc04e575f3f536fff0496fcba9ebb2fa8167f3fb4ff50f572ee7aed32f247d4b77eaefd774bfba7dc0812697818e4b09750fefae5dde425ee4d2f75bd5456425bfdfb277ae22c478f1fe020a23b3f1e37a0702dc461fad03f8ff602318ae30f20bc63ca21dc47aedfa8e9f3f5d9fcd0454b933bb88af2d3fe418e07187ac99f4aedd02d7d3fa857a3abdba9b04adffd4450a6bab50f329ed80bc4c7d40e8ba9e99ef6beaf915d2dc7d9b540a00ac120902bf03fc2b218aef7c34173bf4fc443b6840d08d4f60da4175f5f82386ab1b68fadffc10447b07d83c13782582fdfd03ad2ff704b2b1648e6851771d276b9623ca46bf89a35d83f81ca1adcb7b14bfe029e1aef5aff838847a7a07909750b4753a2ed503fb823cc31ce9507f7ccdeaf4bc083e05cfcf55ffe26bc83ff7274b81d62f6b40fe1e8baaf60f5517a7d03fc5ec2d03f808d8bd0f94d78268bb037ecfdcb67f0204b60fe0f21cb05d6bff9b60bc18c126cfef9a436c03f86fa554ffe08fcd36e81f034f8e9b60f71f35839e9c184257e81305e3bf0de96bd3ff2d1ff36bf96cd1f83f9b40fc3d36d6a0c7c0afa922f67ecfe6d03f24b60be6af72905c77cfa50b2e3dd517afc359d26bdffc3ddacf749e21c3341fa78e63b7811e37bd29fab9637dda5a70eff8164381581988652bde2b2891425ee23a5b7cbd3f0730fdf260cb3514d692510e03cf241de6a49d34e7fc3904bed35e2e169281fc5427a3f4baf1be02064dede0b21adf54fff88f6d03402fc9dae06e87a62fd03217fbadaff3c4dfb1fc040c17cd24b7eb6bfc351140f7a06fc10c5eff6b0fc552ebf067760d7c3d0a6cb7edfa7c5c0937ecf0a447bb3f86e22cfb06c1df111fb017da7b5c0f904f40fd2f5c0e126c1f82f92d6b87e12b361355f888f989720cfd78660b338c1dd7e9eb866e81d2e1c9365e07988d83fb582884745efdafbb07f7e4bd02f02b42b49fa2d7b7f87ee81ff7bfee605fc5de81f81764b07f0e4752bd2fbf0cf8126bdc1f781f3c2de021627ad7e08a19f46bd3fd7bfdf40b8ff0cf8e821f40e2a810bc0a7fbf40f043f857d82e86bd3f5c4760fa4aa0519b7f02878dfc476d82e96fc449ec1e2613e71ff43f5cb97272faf0421ebf2e37c570d2a49ca819b8d9e5530d23d3c701003bb12383792f36dfde03a3d751133a128fe3bf0741dee08b0b82d9c02718483a1ff27751d5cbe17cbca24fb7f83286fad75d1f8b18fdf7e327afedf38c2fb3419d83fdd7fdb0"), + hexit("8060752a1c45b0ac4b396e7a7c01ceffb3ffb3821f0f6aa5f4b04015e08a6f5cf159b34fc4e7fb0e06bdd7dbfe2f7ed7852087adddf8176092fb1fce19843d1d7ef84627a5af83482deddfbf13252a7c09d27ae5286b7dcf7fc352f5e1785f74bd35a7fe15f45041e957ab6b2bfff049eb07287031a4278ea33b334ab6fa7c361005113a57a051eb87a18f6017b57367f5c3d0defabffe19e2aeff122057bbfc042f81925f7c11810033df157fe0fe7abff58c11ec03bd411d65c10fa187e5dbfeaa3b71c0b0f56ffa8ff1e10e3a7ad3ff11f1b5d7bc235d5febd702f789d51039ec6bf5f09050116d6de50573883e645b3a167f209d0fb16abe302fa171108030bd00077891fd7381803bd5315fca9788f1a9037dbe74df8d07bc08be89c1d812446befe09c0c20baddaa57953821ab703e7a976fe0f3e12eebbf88049dbbfc39e92fefd4bedea093dd8e4abb9fc10841d7e1b8aa7fbc3b9c40fb2fa7f67f4f983a13ebdbe1df2c9eb84fc5893d7dbf81b2b5c0e93fbd3fc17c96fe0924b4be1d0c04b47dbfc2ddfdd69ff81b24f7c134f5eff8582215b5fa25bff8c068afdf8721af4d7a3fc38191b5fafc61b395a0334f6dba78392820921bea6c73e777dba7c3f5337c97ee225bf5053e37c08904fdfd96b83ead7cf5c9ffebae088bdcf3a7bfb8f9eba5ffe3b50806785c649c11f7e5d7c1078717a4880873178b1208bd892f909a5e8a2b4497994f04a19f3065eb5fbc685ee7fc4ff3250295e28ecebe0900a818bdbcff8be987f5e281e17af100f3892f0c81902fedbc280a92ab129bd3adbd01143ba9fb32f9fe7bedf377f826af7135d95c127bb3c49ebeffcf5feda847c7c39eaffb7ea09bd6bf7525bfaf0f427afdacbf822ecee71774be07badb0ce83fffab7ae10e377cb8f823bf7e090a2b5f5f1808ed77e1c0a827dbf7d7150e696b46aff82df0f957b9081ce8abbff045e0be7adffc37045ed33c3a1a949778a9f6d69ec3222ab8dc82aa6bc3504d5efcc9c33aa27390be9ec10824efd7069e88bdc08253e2e97f8c047bf2fad5410f821ad627e831c74f635ffd62e15f0ef8e0ccd839f0e97a7f5aaeb5f5ee249c1d060dd78a0280777df89e28a62e2322ab7d3f302057e521b7f96f1809ec0bd3f2e7287a996ed05553ff1ec76538c25a5e2fe04604a1ff423d919c1e029fc5741f1ea0e962363bb6dc4689e130e978e2f84880930a2bbee0d236a20e073c85e643e7833da0ffa037c356300309ed34d2b6ed707c0805f4719fcd782982eba5df83881af5f1034f5fb7ec40736b5fd7b8bedae727cd08fb5833175e32187f051d03e958e3808a093a77e04c8fedf7d2f133d7fb7983bc33e1b18086dd57b920c3aebafdbf08c31d7cd95fe9e2a0af4fc799d4946c97cfc3840ef3676f87134beedff02255f84409219f57fefc0e0347d17eedfc0b419357e094817af5f07bfe2618d277e6c5fede1d82cb5f6894d5aa558261279ca9dbfe0b0808fd24ab08c396baebfca0b5fb7878101f068bfc1294f56bfe1a2134fe1813df7f9039ed65dbfe0da0935b46fc794f7f4ff8c76bf107f53ff98a22dfbf821befc096142efe304f07c4e0c3f27a5c127041f1be4043afde5e0c3c147850be2381267adaff1f3d775f24fac16fd6afae5c417812a375ee449e21cd6cca9d3f6f02403d0df079e068658873857ff10ef1640499a9d9c84043a9a1dfcb786439b7cb45ff0e9ec66ffe61a1e9f2a7aec272ad9ff6cd234e5e482d63235be98bf82204227dcb5330d775ff0cc146ce71fd527e1728bee8167187a825ecf0bafaab5bf72cc22fe2461ecfd7e556390a14f5b24c78340ca243973693f61b84e52ffd848fc5f34c7489e539e12b06ce3e3b740b8f08826f366716d842bbfbfffc0d5045d2b75e180209b7f05f3d7d3f8408083cb3b73f2ff6bb4ffcc40558cf87f4d82161bfa363a284717a55d00978c09474782a4bfe03c65be9e158fdd8345b532b76f840670388c3df6e8ff3c3ddada93117e5fe9e68775b748819d05d6d73ff"), + hexit("8060752b1c45b0ac4b396e7a7c011e40571c2eeff47b07327344ecfe80f05240557d058fcdf9b2adc4942d4ba3ad7a7f858a7ad2a7f82e10fa5c0a37d2e0901e06ba3adbff893d74ffc40ddfec16e6033a06bc716a9fdbe628dd58325b0fa3bfd0373f6fce40fd82faa059737fdbfc1554390cebf30b0f0cfab63c220afc0e41ec820ed661c23c1490f5d7f8229bc74e3a2829bfd9a0740e876e15285253805daa7fb08234d525117b3e1d9f519e3ea447782f8cc06372bde6bec8149697709f40e81613307bf70435838219b7fc115ff7cf71bff053374be14f4090340ea422c1ffa7dc5c76c1e61a40f7b5c5103d98d486760748ced8fa7f1d0dc3ed377cb6bf597af451be686a9105763df5fede04685232d1545350d4ed25e81b0f1b5402a436becf6a9f5f4ff8628de964bafb7f89fac15c21bf66c98c841d7c4c14daf6135dbdf81161dd07f6f76abf9339480c3d0741611317783674ffc4080df7ba28a4dff84cbf13e66670cd89e1415ae50c0ff3d597fc484017f427d8e26c31faaf4515e3221c3c92febfe086c502ece160c022d83a953b205a6041c1e340b3a4e3f4fe1ed0fd2918b158dffc45f77cc346e976d5b7bc78d7fadffca2018607ec0e75d7fbf86f6edafedf8416bb282b9e16c7e049fe395ece8817983014e58f412fd3fcf8bd3fc64da5e34692bf1e3013d7eb7f0780c3cbfa097fa785c1605a5813abc2770ff0fbafa7f71de4ebae8510ed7ee09aa9f7ab9dc13c80001989dbcc288b4e426fe41810e973763ef08e80846afff1c190b6efb02738bd9dfb2ec4027ea38641fb8ddf3df96198e35fc5a7fe528eb59fd1cbfae4209e943aa678fce6260af3aabd8b2e694b85498775ffdffdbff8b125f7c12880d7b6b7ff44086972fe84e75189204f4045ff45ca14da8c1661e4347f4e87ddaf970cc3dafaf5d7d3fca0aa1c5aaa0b8f42e4138750621efc7450b730403dee1aba2712c62fe0f5febc85e1b6d9f956fe405f47f26997a3c95dbff6419efb31a064e831cefe08c100561b9f165fdf1666264745e14cbed8977d1012f49387b2a9722fb84bdf4e66ec48258c65e70c3bdf08ee551d7f820bbf5d7bca1cf41ab2ffb20fba2a4d7e555e0a79816736426f27dafb0d1011498c1f17943dbefca100a1ebf3e7ca3729057607218ebe22cdd7d7ec87f7aff27650971d0b9d58fb87695fa3cc1144722057d3d79094886bd942ba110de605fefd7f05576bd03b55e4fd0813c7133d0aedf2fc9e743e3743fa31adff329afd3f315ec2f1304b40d82e81fee2614e80f8ba0b54edb8e2415e7bfd8eee89e19bc613b02f0248775faf7ccf7fd142d4ff3046acbff89f1abbdbf92d7e085f5f8fed7405b1d8f44045ac3f1665f89286293efdfebf28407676238243d8dff20908d016de50dd80e2fca5def8f216edfa2827e3e3bfca91df9aeca5a787d080b5ec0fb2ef6fff2824f1e2438c510146812868d82a6189bcc978e088669f5dbb2a5c2c0c4164d1d8131f4d8742d7304413ed73860ce85fe19bba7aad6ff1033c3abe3ca279040da62a165120ab439194f49a33f320fde20f57ffe27903321efa120a76419527409fcb6ddf08e610edd3b7f20d049a5f7385013c9e3687d0f18a523c60380557efe1f6dfa348f140b3ca17bb7ec18f42eb4ffc643fbea9e424d5e634abf4fb08022f49fa121a942fa02ab7ebe8a092c3a1d86843f4fcf5b6bfc484447be0fe342dd83849f6fc01d77ebd8d0575428f7a9798e4819de44f121cde96ffebc8e382f3a5d091d8f8023d7f7f07a7f8dfff35fe34321ec3facf072886d734becba2c82016697bd0f2ea70e0f7821af704237c87ad3ff6404f1a3fcb72b16fb3ca11959ac741cba02774d27ae8faae18ddc5c3a86b7d7ecbf0b528d0c5d7417a69ffb6fb12109251a1dfd05fbf21b407f204f7e60dbd2cc50431c315c2d71c4f277096ff62e41ba03f7ef298c3d6d763fd3e087c816d8bb5571f7f9014f957605efefdfb9ea9ffe4ab2f8c13e80ac3de15353f942832df7b99ba"), + hexit("8060752c1c45b0ac4b396e7a7c015b1fc9e504d204034524ed72ae087b57ee1ee969be80a60c055bfd7c99837e8a08e75fce730351371a247605f641b8f0917e3f615b6baff5d820e81c550c131fa705403fb285a9b0d0446680a75d7dfe880821a44c8f6067459a38742ea416bfb7c8515b8df970c20e8814917a6d35d8dff5c7ddfbe8485e8b60540540afebf97d89ee5d7e20661f77f83ec7944774683ec1087b43d26b38a784c10164bf4fe347f69e04d9c3fcf1e0a314fe6cabce19e207f4fb4fcbb8448ed2f840a19ef6dffc684413f3305e1d93cdd84b8d05bf137b4e9d3f20eca9c64405e93effca2a818965309ec0996a11daf7dfd8d134d8a77464dbf6244f8d839d045d0a917985a1cdfcb74ff829823d27f619ce67f42ffd7c4d44abd814819983fc203b40731db6f982d32fc4873c8ccbfd78c15e4841ac37de17c6080cc7cefe0453aff122033afab7ff1126fe860ad18ea623c38a1a594a11c781096060f7ce9d8b3178200c2c1f5d794f5edfe8a3b29de0f8a8bec1205b4050ec591ab0f46fc57ffb052bd5d6bc4042c56274b6bbec68cc7e981f4b1c12ac0037df90116c3b1b745041721d8e1c5535b706ad68b9f7fce4ac23d9413f7f43ae8200b3d80fa7b15013f97e2685b7df6244d15a86a48fbeb89fb1bd902dc573a8a724ffdfa2179abe57ec4f21bb1f202d05f8e266e45b3af7fe415e83ebb07808efcf986712e7fff6d9e0e4bafc2a17d3b71b045c025fd7c84bfd8280fcc663e67b98fafbfb5d0d13ce1abc819f1a0afceac909d0fc5d025ec9d8d051d75f5e244c650fa0519179a5885fc18826d80ec7286a8371b202ecdfea7172906c31080b4103d99aaf6df59cfb5f812039dea8dbff8d2f3761ec4024b1f2ee8285d9f83d11e0f016d3e9773c3216adf5aae8bfc0b06a0e3089f1811efdb4d8efe515b028ca4da17b01f121aef5f5fc30edfe08016e9fda2a7892680f87714fffd170d4287c3effd8240a4746c6bc727bf2e32f77f27942be5c3d8753e8fef47fc81db4986e79dd86e4b0cb0eb6ffcc093c409f41dec5c200a6dc147c6cbfe39821770d8213d7a7f862086fecf29eadfff04de3a0a1a29c200f7821041d762a7ad7d7f0a85f386319134651ecfd151793304304ad0fef3c4ac1f26fe24384d7c904fe708036056fbc200836b997eb627f7fd0d0df6075f65f04224f5a7fe1f2fcbd7b0809e9cab93fa0889a2edcc11e0a49f7d170228d3f5d1ff433c689d871c07038076f905762e5ce81685e854051d137b441b1f6fe62f37c8ac763057623046bf06817cfec2dabbfd3e5379588770f040e0b2faec1fc0c028100e4c0c48c2246111845fc1085a3c0a1bc20341caaabfb2f9eabfd513b13e21d0f6fcbe40d3e6b0ec5d8d1f76b7d8a50c063a98e04136c5e81486fb01fbdff828f0a1ebebf609013ec0b9c200cac33c109b285be2410f6bf7620bccbf408010cdfebbf41057ea17f4faad7bfc1209f5e87d021139c2dc6532c0bd01049dfa213d782d0dfa4c3bff877183c5c09dae60c0eff4e8bc2a3f5f61f5f19e051043edb782df18273f6aa958997e0491560613fd8a1c937fcbe508fb01f7910489e8107857c4830dfd9d53ffe2a91023b9d0500f65f080666d982367e9fe5dd3f823bf8bc1e057a5b5ab7ff82d0b7a0f5afebe343f9037c392420ed4b2fd7dfe839c370e50fc5bff8134819dd2b0effc689c7531a738603237840232a70c53dedf07f870124f9605d78915e320f1f0e489c149eba7fe0c3c3bd83136c7ec220aafe70c3f1a4eae28816f5b7f55aff0f083d8ded7f13e097b07227a51a4c73c80804e316304a19a715e71c2030f5b7ffa981c511f5f972080dae1413a56290c7e515d8cec8e9aef0c7e14d78e455bed0e952c7e8800e727433c227aeffe1515e9587ee37daa5ef41251fb8ff5fa05187708a5323ebb1a23c3ed818050206581c48c2171b98bc5dbe0c44e0fe735f3d6bfaaf850defc826c071e04f9af27e3fc6821f0cbb9ebc41ac073043d0501574ba2b134d"), + hexit("8060752d1c45b0ac4b396e7a7c01e9878280cf63afefd9415d8a3ebd52ab2ee8a7de0a412ddfbe9d72fc21bfbd01caa3ca2b32f1b10d633f1011eb4d7afc22278716a3dd8d8f94b9a3f020824f732f87c4bebd8808dff3f6c8fcd3e4e504816bc7531b514c10175f7af650cf7ab7ff94778f01dbfe0fe1104fbf6b8bc3810bf98417d0fec2a2ac9c3094e6ae05d6fc61ebbffc126d6de050276bc6026d166cd09fa0e82cf6f28401fd347f90576ed9c306dec86f32fca0befd9fafebe50b53fdc969ffc1078c055af2040ca179d99d773a1a17f0f6d9d28f02aaff5f0cd0fd7d7f828e977d71d3d5bffa0422fdeefd8705f9843a5d904ef5163e349ba0e92ff0e01102dbf7e5cf4ff2866b5e54ffec609d8ba0342f28ec681e6acefef5c73a03e41542d931201c827c485fe360ab356259686d7e8be34117a3aac38093d2d74140bec076f62fbaff62057d850e26ed9f455c387caa54e1f020740a0b6ff2821a7b1f7282d0ffcecc741cac5afdbbff6113d5bfea8afb5e34bc8180f890b5c6c4ad02bce1b5f6ff1017e80b366bfdf50bcc1bb1e5522626bd9fd3e42ad727c29e3013ebe9a7de24105016867ebc33176b47174bd7f65e5f87a543eb7ab2767fdf60f42d82852203a4936ed8f2fef5d7ba287361f876ffc220a3bfa09fc602e90301b1dbeef034821d0febc685fa1edebd8bf60d4333fe5b5ffc4f33163a23aad105e655ec5e413ef6ff0d7775fd3f877b960dfab4356263eff9010563500f984067ba50affa1208bd7bb2843db6b41edf4302f656adedce2bf4f880b7ad73829ffd074f5dfa7f1bb5d81b4c05dad80e7df7fe2bd0b4232fe08ba7ee812822bdefd0200c77f74a1fb7e11f183f7f1919f83992203f156bca1fa16c402db2fa0ee303a0125fa587008a0c3df50cfe97ef7e1c13b0f337f1009421ec5a1dbfaf28904dd8a82b1ddd0c0eca1818f097d1d03f3c0a9effcbe0a4175ed7a6e7c67bd818f08fd8a60d266fdff15b243482074bec67cdbfb02489f76fe6048083d2dfbddf7fa1033403dfecb957e881edbf21974074feb7fbec484f2e6d51f0e86570e3c39d144ed995e9c91e8489e6ed8f40f7043e2017e300f1c01bf557ffcb206e60bf1a1d04761c7d173b78200d76eafff87497fb120b38f0e9fb70561dfe2bb52a09c37e1d049efdc3c43d5bffe1bf4fa9edfec101eabff38290c5c7848ed995e98fbaff41c1fa52a9c7213b1046863f4103d7b5eb21ebdff8402d4256074fafe89771340559332060fe2413fb1c6c4277ec6820c4a1a4f9efd30fd9f9e1b98406c0abaff104f0c741104f67cb919a6cec80be811663a1650c6308e5affc091e40fedd9d9f97ebf67e6287e2e60bfaa1eca3e094dd514d4bc84d8bdc1078f095af6741afbfe5f0a06f5d77fbc22afd071f7eca3f3880d8f190f01de972de110892c4838725b0ee10f15b283340d83fc13db4c825259fbf5370d43704b5ee19d93275f7fc60735d697fb28ab0f27861175fca505b894afc6066dfb82bbc7840bcc20ebccb1b82eef1acbf63a1227618e0efa1697e1df1d1c0608cfeb97afc338aedaffaf993e083d02bbd8e5080356fff87b6903e4041c7113afb7f41804d7d7a1fbc8159a1f9bdcfd7e513d0e83c18e23e8833982cfb790a691a3c84b0e81006ebacbbe3c44f3925fd3e7103a976de111d2c38d44af22407f8ace100cc18efc8111f6880fe8baf647eb963665c81c90301d8b1f127f8e46d70c68bfa121ef2b38d536633d7efe587fda3fdb4b29260f2f6ff450415f98ba96451264b5fe9fbd12218a68a1dc6c5cde1f898edc74fb526dd7bff204b6f74bd4166d52a58e82321c7ef67eee0aef192fdb4b1a9fd2d872e0940827d69ffe329f6b616fa01d7892c6c2566ae132f4203faa56994c359491afbfdbc210c795dc80ceff6f42417e552c1f1a8860b63fa7ca2016655547c6d0f802b0c3840cfb2823c921ff82559787015cd1ca203c753038ba7425bf7907dac8752d55e3dd9503fc8083bf0fa9c53d9fafff1c341079373"), + hexit("8060752e1c45b0ac4b396e7a7c0161b28241924bdbe5fc8f60470c1f51db29010061794302be9f873960af7fb4f2c7dd103fcb907c411b6bf907e3bbb7d3e8a7f6ffee1cf09794b2bf45f180bf9d243c95dec0e731fd3e110a87fafa686f1f908387597fb6ffd57bb202dd0fa375f730446efc0eac6bb431e80270b5f5fa3f8343c8d17ffc9e404b77efd169ca13928da4188b304f8c5ecc5049d6ef2073b0adebf906843db5f5a07b28269630074ca100fe6f2f9439efdfeba101eef7a121c22582b2ebf9e0be5f499f9bd9e60887fd96c356fd7fa7b284b3fe9fc39e11eb5880d596dadbff8917dfa5d10395ebfd3cd1f4b609e52d9eff0c534b60ca4baff6f08c235a06f01de8d4f9cc7f0ef30c258b4835d5dbd69ff08e207e9926bff93b1f40400c57ef5fef908097981d778a83b74405119227d0340a4041671441738fe043f30c1b07f705ba06805403f979035a59275a76bf0603f7dff6f641be98c83cf05ed246cd797feca11f60652518081c4c1137c48c0ac601ca3bbb7eacfa7f955cf822a7760f0f8cdf4bd82292b2ecf24be0be3eb8828bca4bdbec80a70f5c69126bf3f7ef7ec167404c0cda4f54ffe08010029d9718dc3fb7bfc4fcc4636084fb8638f059f9492edffc3f617e712c98410156ffe720ef40d03a4d8d058ff8c5ae1128c973efdfb5e40fec71a130ca8b225a8531945ab4ffb2047b1b3f38208f03c1feafad514155e18081bb253a2e18bff6edf9762423afba0f20c234d708906c61ccd331f076e3ffff54c91a759ad222b0842fa7f0c840107b1b091c8635082f392adbe9be61a0873170f98f1bb84bdeff087440d647c1c0716a9bffca0afc00c8ddf7af77f8650edefc74749b7e58cb31833876dc142fa04605efc7902152d63fd1ca487192686e5a829de948686854d909501727ec098791f18efff0ca94b94a10d2ec0bc681c2d25a820c82a3a6e606e98c3d22bad66bf7bec9cfe20a1eca92c5a06db8f060b701aebfcc51b5bf0f535dd72e61865b56d1c4ce9ebf2465dba5bea891e89bce50fdfd04303c3cac5a888cbf6fee34122026827d90f5affca7aaffe4154743615fd10107b2a335fdedfd3f853455a049744525bbb1e508697be52090df8c121deec8089bd79eff8afcab7d9a74ff040a4bc093e4045b17ddc3d60bed356b4ebebe784347e189edc5cc735194b60d036b55f949a07944077dbaa125b74ff45f427cbf0953f407930e2b0d863b11d411ed79fe7abfff0cf6ebfa7c147e2b7fbf9f1dfbf81fca23493afc082084f335b157d3f8100f25febc4823e95dc797cab5cd0e6b6e55affd040f217f4fc688ed767e47a2fcdbf8721dd6ca96dfa96a7ffe2e19e27912dffc085047b5d8e2e4e93e40524a5f38257dac2b94200ffd7b56f214f5b7fe1b2f0553d8d1ffc484affb7c20093debc86bfe50edefefd2aa17fc283415d85ae7fdfd77045b7c5c9ec21e5257ae1e867c3cc0bffc45d276d3ce0483f0e8ffb0c022aff3c221cbf5e9ff2f62435a794b6ffc840af6b5c8bfdbf942d25d7df974ffc1872031f024f901174b3df116d61ebd63789105f6f1e4045d3c89c2b0cd821c2e7b16f6ff27c39e2575e4f1bc2bec81aaf8ba7d3e088879129ffc6cf5649ffcbe153d5bffc4745f92bf86a19a6cfadffd17af8417bc0a8fd07b96fd7258ac5e197d78e1208f7d8ab824ecfddc126bb0f0957eb5941152ee78190dd781009e30115add871a7afbfc6457ad2f93c149ebf4fdc56bb7f1c52d53f82d11eba78771a1f0bd9b56747b24ff3bf4fc104f5fef8760bfb2b2ebfb5c54127a5970d437beb47fee0afd3ef5fb8b2bdaf3057881a093d29728909d3fd3c3c203bd97a6d75e9fe09615d2e97aa7ff3cbbfc49eddbff181ff7e5ed7ad2faf0e823bf8df7e97821ef83c1c043a5a5bfc1c0449ef8f821a1fcb87461ea97fc78d3d57fe604a6f7c82096bf1be14043df1794f59abfac43eefc4d60c3c10f60d57bb0e707f5d7100c388127abbffb22d701033ea97fe041ac6f8cebf049ef977e"), + hexit("8060752f1c45b0ac4b396e7a7c015127af5fe20f5d97fd4c93618f0c9ebbfc99013f405ab1fb8a0cbafc78202f6d707637af905ebf7e1115ebb558134116bb1e50b7a26ba57e9fc12944f5ebc08255f9c0ca0400e7b6bfb7e04015ddf5e0692bb5fcba7eaad8470eb18bf67f0581bd7ed7a7ea5b2fb0e9b4bc14defc33e8ffb7fb39418ab71843f5b7ff206bdd6ffe7185daf8114777f7afa208f7a0bd1411d1fee09208bb58b878141bd7945f5f6f0d0617bc9e0b033e9ad2ff02a025055d1f3e6d7170dc27b765af0f8415fc8086fe2c3bb5ffdbf227daf8625f4b16091fb78440b22fddff04019a2eb977fc3a08ec92fbc21d97ca7aa7ffc12e60817daf1573d6bfb81c423c080317b891a7ae9ff3820e20a4f75854fef4ff843cb56155ef9ea9ffd78167e27c4ad797eaff582a8bd095d78303d5bffc306b17793ca4f5c77828ab77eff573811219af5fdbf2fcda75c120d0c52fdcc9fe9e04006a1ce5457fa697c08208bd71fdfaec86e4cf27c317f76ab5f6eb1a0a2bd3f17880dfa6bedfc17825e02a212afbf8a2134bddf998c3e0c0f5effca7b1fe9e61a4f6b15e528cebd74b4fe512adc35276f825120c37bf76bffe409fb4d7e0b7ca7af5f867405bfd3b7fcb87c3019f760ffb7c3008fd2838241a16f6d175d3fe1e288bbeff05230fadaff17e0a069b7f17f08d6556ac8baf04ab95e1535ff0af08840f54ffe6f1033e087de0e249c49160f29eacbff9ebd97f270314feae9fea7cbd3fc2c55d7645cb83e0207883d53ffc87ad3fd6195ef29ea97fd10f54fff21ebbff759577e809e110b7bdeebdff83d0d5f75f5f8ef0ec9dfa9ebebf0f787e5eaf826ad703f081bdbddd297fa3d7edf808a10083d38c78c0be04425aff4f02404014f5614574361af7e0698fefb2a480fe8a374b7b0635e0f6a5f4a2d3f617b7a820edd8241606e8f97d7fa7ca2e8120165aa05e08a0af4b46c2e2c16ff08f7c8080fb07e05b9a9207f2063211e4041d53ff8a101fed50c2de3e25c65a203e5576efa78982bef192ed80be5040129072a7061041d174766baa7ff05336c1f811618a7472e30beb7ff519ebbcc0c59612e168f0a10237e624c1f2def820080bbe35ad00abe02746028ec2a4dfdc0fa305e81e8fe0e209251ee9560b218b06625765d82ffc74107961d9d1a075fd3e09e367cfefd009059412a8ffe0c04020befbf60d13efb3f87e18e90f8fed3a7abea2d85d3c0a7051ce2023e45f28c9d381243a18da3105d4e341465edfe0622863a86ff212adaff04a50e74092ab5ff1d0df0f278ad04abfef810068be8b9868f5c1fc5f3223d34be10efe3842da3ec1e065824daa5dc08610051cc70a3b9c61417702e439e8175fff847df40260f4983c1a4bca31f9bd00b04b17e419d8367e18f61740dba3fe0f21ce869adbff8505f66731d81b05856bdc1814dca30f089414760fb531703c04431e435a07ea69dbfcf0495a06c17706917d0340f60dbd7c82fa0472fa0132c1e45fa3ed60fe2fa05d036783c8bbe96970cc5f29c211cc2140fcdd9f45375323c040c39cdea9ffc0499431cd467bf0f5fe2437d6bb7fe217bc86f6dd157ba29e29bdbfca18f2af5610f08043d8bb16c6c163a1ab57ad2dbf877c4c13fb0b46965c1745f5e81f0430435ec301210c5e03b74b0b34b6972ccebf6fc0970df2d96e2f41697650a6869269c76ca9189a35ea11edb8b9213f8650cff67c08f1bc79e94f31c298eecf5ecf7a5ff075041e80d2f2fd5bffb8ce5176bb03eb1fd7f820843b9fdf9a9e08e13ed2341d78258ce38586ae827e5edb0bc12423c373fdb89236bbfc1e429dfb05b0798890d3ec715174b9f4d5f171747dfe3a2f60ba5e2218a4c2c84820e0ff6ebfb7c144145636fc1f107bf0bc5c25c2fa78ff60be129ac18717a1f81122ed1011b18fb087e1b86e6162127d7f4f80868b98188b1c2fd437fe362f945b6098fc1cc5c747cbc155ec1b5e02b26d1f86669c97c545c83c3cfbe6fa611417c790311ef2f7dfc9883e63dadbe09c8188"), + hexit("80e075301c45b0ac4b396e7a7c41f8615c180fbb15e956d7f07d17b0aa1bc9fc0891731767a04345cfc089174686e0b4637a5f05317b5b06c248d2c23051760fb5d9c1845c27c33bff2907c16c5d1989f1a0dec0ef0141177122887a030ea7a3d7888bc761475c67e0ea5907ed73901077863342d6beabc3ea7c957f4b1640c41b9a0af6be3fb1af652afd2e0688bd045a30e1941bc7e37c2347d03650f57e0f25d2848d75704d0fe41fd85220a0610bcf5c6bd2fc104de3fae1a8bd27cfd01f07b34be78f1850ff5731c692c77cb4614571cff6f1a50fd1b792afb6926cf66cc2071bcdf4888cba0bf0254105f5b3246c8307c83ce7e7fe0e617a2e69f5f7f8b820b7c841197dfca4414c7fbd7fb5812e18e8ad4f75f4e6ce78cdbba1dddc75b6fb8bf084317a086235bf5fa7e0688634120a32d9016ebf6fc3f0575d4399460f7d8f25d0643698dafec302941057a38b3e7ca2d7fd3c164145f907ef639636c695f3f9334a8240cf7a3df5f8c843de81fab2c6901356b607db808b29e7bfaf03813ca4e9fd7bcbd853c23581064cbc35e1ff59207bf7e046f07b7760fc6dec1f8424d03f045e041888e970c07dfe26496881f83988a04c1e425f0ac46c1f4bc212503fe499840fc7c4505d9fc0813d682ff051e049ba4525e0a7c175e62560f0e4551a2fa59219f60d6c2ff05b17d24b492c312fb3c778b84bdfbe1e288efef834f0433d7dbf8083f0b5f7e05088ec174f8af0c78126fd03c47837867d835a7fe051f037941179817dc1adf40d03f11e81f60b83e821e8fdc1ff82bf039df212e08a237b07941026b51a42760f87a23d03f60f021f9a4ec120793c087e19f08c35c583adaff57e80584a4e81f05f27947f3c33eaac2ff83c88f40f40b8090867d99134ffc0c1e055be81f3c4747d1a0787bf2720b70732760dbc1e49d1f511d02f4097250f963757efc1a7832f0111e0d3c08febdeafcde232476a0975079e6f047f272fc9c33f2715a857c3fe062f378117c1ef822f051e27c1a7823f039788f1bb81abc0b9e05ff0994966523e0273c15786bc25117ed7cbe16f5e05af0aea0bbfe18f043e061f1be6dc0c5e0f713437ac903df9fc08c1de046f5ebd6a27c488ecb51fa824d41ef821f045e0c3c4f8cf0c78ef17ff0a6a063f04a4e044f041e0c7511e23c0a3e051f26bad793817724093e097c2ba93d797c14f853c6f82ff05fc4eb836f0306a7f3ee0f3c1e678df1befdf834f065e06cf069e047d5eaffe23596a3b504ba83cf8ce08f52ea17f7e2b50aea062d408befc187828f13a814352f97702e78173c11f84fc091e0fbc29e12f2ea4d44ea10f029782fd40c3e061f36697114436097e07fb2ee0d4bc0dba811bc08debd7ad44f89dc7ea093507be087c117822f2793c2de10f17ff0a787bc0c7e097c089e12f063a88f11e051f026f93c0c79e049f0ff857526a5f2f88f11e08f505de0bbc0c1a9fcff2d41e783ccb"), + } +} + +func hexit(s string) []byte { + b, _ := hex.DecodeString(s) + return b +} diff --git a/go.mod b/go.mod index 19a0cc0..917a787 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/pion/rtp/v2 go 1.13 -require github.com/pion/randutil v0.1.0 +require ( + github.com/pion/randutil v0.1.0 + github.com/stretchr/testify v1.8.0 // indirect +) diff --git a/go.sum b/go.sum index 401b903..6c31089 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=