mirror of
https://github.com/aler9/gortsplib
synced 2025-10-06 15:46:51 +08:00
add RTP/AV1 decoder and encoder (#270)
This commit is contained in:
@@ -44,8 +44,8 @@ Features:
|
|||||||
* Utilities
|
* Utilities
|
||||||
* Parse RTSP elements
|
* Parse RTSP elements
|
||||||
* Encode/decode format-specific frames into/from RTP packets. The following formats are supported:
|
* Encode/decode format-specific frames into/from RTP packets. The following formats are supported:
|
||||||
* Video: H264, H265, M-JPEG, VP8, VP9, MPEG-4 Video (H263, Xvid)
|
* Video: AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), M-JPEG
|
||||||
* Audio: G711 (PCMA, PCMU), G722, LPCM, MPEG-2 Audio (MP3), MPEG-4 Audio (AAC), Opus
|
* Audio: Opus, MPEG-4 Audio (AAC), MPEG-2 Audio (MP3), G722, G711 (PCMA, PCMU), LPCM
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ https://pkg.go.dev/github.com/bluenviron/gortsplib/v3#pkg-index
|
|||||||
|
|
||||||
## Standards
|
## Standards
|
||||||
|
|
||||||
|
* [Codec standards](https://github.com/bluenviron/mediacommon#standards)
|
||||||
* [RFC2326, RTSP 1.0](https://datatracker.ietf.org/doc/html/rfc2326)
|
* [RFC2326, RTSP 1.0](https://datatracker.ietf.org/doc/html/rfc2326)
|
||||||
* [RFC7826, RTSP 2.0](https://datatracker.ietf.org/doc/html/rfc7826)
|
* [RFC7826, RTSP 2.0](https://datatracker.ietf.org/doc/html/rfc7826)
|
||||||
* [RFC8866, SDP: Session Description Protocol](https://datatracker.ietf.org/doc/html/rfc8866)
|
* [RFC8866, SDP: Session Description Protocol](https://datatracker.ietf.org/doc/html/rfc8866)
|
||||||
@@ -113,9 +114,6 @@ https://pkg.go.dev/github.com/bluenviron/gortsplib/v3#pkg-index
|
|||||||
* [RFC7587, RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)
|
* [RFC7587, RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)
|
||||||
* [RFC3640, RTP Payload Format for Transport of MPEG-4 Elementary Streams](https://datatracker.ietf.org/doc/html/rfc3640)
|
* [RFC3640, RTP Payload Format for Transport of MPEG-4 Elementary Streams](https://datatracker.ietf.org/doc/html/rfc3640)
|
||||||
* [RTP Payload Format For AV1 (v1.0)](https://aomediacodec.github.io/av1-rtp-spec/)
|
* [RTP Payload Format For AV1 (v1.0)](https://aomediacodec.github.io/av1-rtp-spec/)
|
||||||
* [ITU-T Rec. H.264 (08/2021)](https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-202108-I!!PDF-E&type=items)
|
|
||||||
* [ITU-T Rec. H.265 (08/2021)](https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.265-202108-I!!PDF-E&type=items)
|
|
||||||
* ISO 14496-3, Coding of audio-visual objects, part 3, Audio
|
|
||||||
* [Golang project layout](https://github.com/golang-standards/project-layout)
|
* [Golang project layout](https://github.com/golang-standards/project-layout)
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.18
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asticode/go-astits v1.11.0
|
github.com/asticode/go-astits v1.11.0
|
||||||
github.com/bluenviron/mediacommon v0.4.2
|
github.com/bluenviron/mediacommon v0.5.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/pion/rtcp v1.2.10
|
github.com/pion/rtcp v1.2.10
|
||||||
github.com/pion/rtp v0.0.0-20230107162714-c3ea6851e25b
|
github.com/pion/rtp v0.0.0-20230107162714-c3ea6851e25b
|
||||||
|
4
go.sum
4
go.sum
@@ -2,8 +2,8 @@ github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflx
|
|||||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
|
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
|
||||||
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
github.com/bluenviron/mediacommon v0.4.2 h1:rdghY3g70+fdviapO2hL6CHpOGeTd7KbH1aEZnMwh88=
|
github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY=
|
||||||
github.com/bluenviron/mediacommon v0.4.2/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8=
|
github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
package formats
|
package formats //nolint:dupl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtpav1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AV1 is a RTP format that uses the AV1 codec.
|
// AV1 is a RTP format that uses the AV1 codec.
|
||||||
@@ -94,3 +96,19 @@ func (f *AV1) FMTP() map[string]string {
|
|||||||
func (f *AV1) PTSEqualsDTS(*rtp.Packet) bool {
|
func (f *AV1) PTSEqualsDTS(*rtp.Packet) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateDecoder creates a decoder able to decode the content of the format.
|
||||||
|
func (f *AV1) CreateDecoder() *rtpav1.Decoder {
|
||||||
|
d := &rtpav1.Decoder{}
|
||||||
|
d.Init()
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEncoder creates an encoder able to encode the content of the format.
|
||||||
|
func (f *AV1) CreateEncoder() *rtpav1.Encoder {
|
||||||
|
e := &rtpav1.Encoder{
|
||||||
|
PayloadType: f.PayloadTyp,
|
||||||
|
}
|
||||||
|
e.Init()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
@@ -15,3 +15,17 @@ func TestAV1Attributes(t *testing.T) {
|
|||||||
require.Equal(t, 90000, format.ClockRate())
|
require.Equal(t, 90000, format.ClockRate())
|
||||||
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
|
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAV1DecEncoder(t *testing.T) {
|
||||||
|
format := &AV1{}
|
||||||
|
|
||||||
|
enc := format.CreateEncoder()
|
||||||
|
pkts, err := enc.Encode([][]byte{{0x01, 0x02, 0x03, 0x04}}, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, format.PayloadType(), pkts[0].PayloadType)
|
||||||
|
|
||||||
|
dec := format.CreateDecoder()
|
||||||
|
byts, _, err := dec.Decode(pkts[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, [][]byte{{0x01, 0x02, 0x03, 0x04}}, byts)
|
||||||
|
}
|
||||||
|
150
pkg/formats/rtpav1/decoder.go
Normal file
150
pkg/formats/rtpav1/decoder.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package rtpav1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/codecs/av1"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"github.com/pion/rtp/codecs"
|
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v3/pkg/rtptime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrMorePacketsNeeded is returned when more packets are needed.
|
||||||
|
var ErrMorePacketsNeeded = errors.New("need more packets")
|
||||||
|
|
||||||
|
// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting
|
||||||
|
// packet of a fragmented NALU and we didn't received anything before.
|
||||||
|
// It's normal to receive this when decoding a stream that has been already
|
||||||
|
// running for some time.
|
||||||
|
var ErrNonStartingPacketAndNoPrevious = errors.New(
|
||||||
|
"received a non-starting fragment without any previous starting fragment")
|
||||||
|
|
||||||
|
func joinFragments(fragments [][]byte, size int) []byte {
|
||||||
|
ret := make([]byte, size)
|
||||||
|
n := 0
|
||||||
|
for _, p := range fragments {
|
||||||
|
n += copy(ret[n:], p)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoder is a RTP/AV1 decoder.
|
||||||
|
// Specification: https://aomediacodec.github.io/av1-rtp-spec/
|
||||||
|
type Decoder struct {
|
||||||
|
timeDecoder *rtptime.Decoder
|
||||||
|
firstPacketReceived bool
|
||||||
|
fragmentsSize int
|
||||||
|
fragments [][]byte
|
||||||
|
|
||||||
|
// for DecodeUntilMarker()
|
||||||
|
obuBuffer [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the decoder.
|
||||||
|
func (d *Decoder) Init() {
|
||||||
|
d.timeDecoder = rtptime.NewDecoder(90000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes OBUs from a RTP packet.
|
||||||
|
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
||||||
|
var av1header codecs.AV1Packet
|
||||||
|
_, err := av1header.Unmarshal(pkt.Payload)
|
||||||
|
if err != nil {
|
||||||
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
|
d.fragmentsSize = 0
|
||||||
|
return nil, 0, fmt.Errorf("invalid header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, el := range av1header.OBUElements {
|
||||||
|
if len(el) == 0 {
|
||||||
|
return nil, 0, fmt.Errorf("invalid OBU fragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if av1header.Z {
|
||||||
|
if len(d.fragments) == 0 {
|
||||||
|
if !d.firstPacketReceived {
|
||||||
|
return nil, 0, ErrNonStartingPacketAndNoPrevious
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, 0, fmt.Errorf("received a subsequent fragment without previous fragments")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.fragmentsSize += len(av1header.OBUElements[0])
|
||||||
|
if d.fragmentsSize > av1.MaxOBUSize {
|
||||||
|
d.fragments = d.fragments[:0]
|
||||||
|
d.fragmentsSize = 0
|
||||||
|
return nil, 0, fmt.Errorf("OBU size (%d) is too big, maximum is %d", d.fragmentsSize, av1.MaxOBUSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.fragments = append(d.fragments, av1header.OBUElements[0])
|
||||||
|
av1header.OBUElements = av1header.OBUElements[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
d.firstPacketReceived = true
|
||||||
|
|
||||||
|
var obus [][]byte
|
||||||
|
|
||||||
|
if len(av1header.OBUElements) > 0 {
|
||||||
|
if d.fragmentsSize != 0 {
|
||||||
|
obus = append(obus, joinFragments(d.fragments, d.fragmentsSize))
|
||||||
|
d.fragments = d.fragments[:0]
|
||||||
|
d.fragmentsSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if av1header.Y {
|
||||||
|
elementCount := len(av1header.OBUElements)
|
||||||
|
|
||||||
|
d.fragmentsSize += len(av1header.OBUElements[elementCount-1])
|
||||||
|
if d.fragmentsSize > av1.MaxOBUSize {
|
||||||
|
d.fragments = d.fragments[:0]
|
||||||
|
d.fragmentsSize = 0
|
||||||
|
return nil, 0, fmt.Errorf("OBU size (%d) is too big, maximum is %d", d.fragmentsSize, av1.MaxOBUSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.fragments = append(d.fragments, av1header.OBUElements[elementCount-1])
|
||||||
|
av1header.OBUElements = av1header.OBUElements[:elementCount-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
obus = append(obus, av1header.OBUElements...)
|
||||||
|
} else if !av1header.Y {
|
||||||
|
obus = append(obus, joinFragments(d.fragments, d.fragmentsSize))
|
||||||
|
d.fragments = d.fragments[:0]
|
||||||
|
d.fragmentsSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(obus) == 0 {
|
||||||
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
return obus, d.timeDecoder.Decode(pkt.Timestamp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeUntilMarker decodes OBUs from a RTP packet and puts them in a buffer.
|
||||||
|
// When a packet has the marker flag (meaning that all OBUs with the same PTS have
|
||||||
|
// been received), the buffer is returned.
|
||||||
|
func (d *Decoder) DecodeUntilMarker(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
||||||
|
obus, pts, err := d.Decode(pkt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(d.obuBuffer) + len(obus)) > av1.MaxOBUsPerTemporalUnit {
|
||||||
|
return nil, 0, fmt.Errorf("OBU count (%d) exceeds maximum allowed (%d)",
|
||||||
|
len(d.obuBuffer)+len(obus), av1.MaxOBUsPerTemporalUnit)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.obuBuffer = append(d.obuBuffer, obus...)
|
||||||
|
|
||||||
|
if !pkt.Marker {
|
||||||
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := d.obuBuffer
|
||||||
|
d.obuBuffer = d.obuBuffer[:0]
|
||||||
|
|
||||||
|
return ret, pts, nil
|
||||||
|
}
|
62
pkg/formats/rtpav1/decoder_test.go
Normal file
62
pkg/formats/rtpav1/decoder_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package rtpav1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
for _, ca := range cases {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
d := &Decoder{}
|
||||||
|
d.Init()
|
||||||
|
|
||||||
|
var obus [][]byte
|
||||||
|
|
||||||
|
for _, pkt := range ca.pkts {
|
||||||
|
addOBUs, _, err := d.Decode(pkt)
|
||||||
|
if err == ErrMorePacketsNeeded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
obus = append(obus, addOBUs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, ca.obus, obus)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzDecoder(f *testing.F) {
|
||||||
|
f.Fuzz(func(t *testing.T, a []byte, am bool, b []byte, bm bool) {
|
||||||
|
d := &Decoder{}
|
||||||
|
d.Init()
|
||||||
|
|
||||||
|
d.Decode(&rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: am,
|
||||||
|
PayloadType: 96,
|
||||||
|
SequenceNumber: 17645,
|
||||||
|
Timestamp: 2289527317,
|
||||||
|
SSRC: 0x9dbb7812,
|
||||||
|
},
|
||||||
|
Payload: a,
|
||||||
|
})
|
||||||
|
|
||||||
|
d.Decode(&rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: bm,
|
||||||
|
PayloadType: 96,
|
||||||
|
SequenceNumber: 17646,
|
||||||
|
Timestamp: 2289527317,
|
||||||
|
SSRC: 0x9dbb7812,
|
||||||
|
},
|
||||||
|
Payload: b,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
147
pkg/formats/rtpav1/encoder.go
Normal file
147
pkg/formats/rtpav1/encoder.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package rtpav1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/codecs/av1"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v3/pkg/rtptime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rtpVersion = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func randUint32() uint32 {
|
||||||
|
var b [4]byte
|
||||||
|
rand.Read(b[:])
|
||||||
|
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoder is a RTP/AV1 encoder.
|
||||||
|
// Specification: https://aomediacodec.github.io/av1-rtp-spec/
|
||||||
|
type Encoder struct {
|
||||||
|
// payload type of packets.
|
||||||
|
PayloadType uint8
|
||||||
|
|
||||||
|
// SSRC of packets (optional).
|
||||||
|
// It defaults to a random value.
|
||||||
|
SSRC *uint32
|
||||||
|
|
||||||
|
// initial sequence number of packets (optional).
|
||||||
|
// It defaults to a random value.
|
||||||
|
InitialSequenceNumber *uint16
|
||||||
|
|
||||||
|
// initial timestamp of packets (optional).
|
||||||
|
// It defaults to a random value.
|
||||||
|
InitialTimestamp *uint32
|
||||||
|
|
||||||
|
// maximum size of packet payloads (optional).
|
||||||
|
// It defaults to 1460.
|
||||||
|
PayloadMaxSize int
|
||||||
|
|
||||||
|
sequenceNumber uint16
|
||||||
|
timeEncoder *rtptime.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the encoder.
|
||||||
|
func (e *Encoder) Init() {
|
||||||
|
if e.SSRC == nil {
|
||||||
|
v := randUint32()
|
||||||
|
e.SSRC = &v
|
||||||
|
}
|
||||||
|
if e.InitialSequenceNumber == nil {
|
||||||
|
v := uint16(randUint32())
|
||||||
|
e.InitialSequenceNumber = &v
|
||||||
|
}
|
||||||
|
if e.InitialTimestamp == nil {
|
||||||
|
v := randUint32()
|
||||||
|
e.InitialTimestamp = &v
|
||||||
|
}
|
||||||
|
if e.PayloadMaxSize == 0 {
|
||||||
|
e.PayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.sequenceNumber = *e.InitialSequenceNumber
|
||||||
|
e.timeEncoder = rtptime.NewEncoder(90000, *e.InitialTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes OBUs into RTP packets.
|
||||||
|
func (e *Encoder) Encode(obus [][]byte, pts time.Duration) ([]*rtp.Packet, error) {
|
||||||
|
isKeyFrame, err := av1.ContainsKeyFrame(obus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := e.timeEncoder.Encode(pts)
|
||||||
|
var curPacket *rtp.Packet
|
||||||
|
var packets []*rtp.Packet
|
||||||
|
curPayloadLen := 0
|
||||||
|
|
||||||
|
createNewPacket := func(z bool) {
|
||||||
|
curPacket = &rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: rtpVersion,
|
||||||
|
PayloadType: e.PayloadType,
|
||||||
|
SequenceNumber: e.sequenceNumber,
|
||||||
|
Timestamp: ts,
|
||||||
|
SSRC: *e.SSRC,
|
||||||
|
},
|
||||||
|
Payload: []byte{0},
|
||||||
|
}
|
||||||
|
e.sequenceNumber++
|
||||||
|
packets = append(packets, curPacket)
|
||||||
|
curPayloadLen = 1
|
||||||
|
|
||||||
|
if z {
|
||||||
|
curPacket.Payload[0] |= 1 << 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizeCurPacket := func(y bool) {
|
||||||
|
if y {
|
||||||
|
curPacket.Payload[0] |= 1 << 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewPacket(false)
|
||||||
|
|
||||||
|
for _, obu := range obus {
|
||||||
|
for {
|
||||||
|
avail := e.PayloadMaxSize - curPayloadLen
|
||||||
|
obuLen := len(obu)
|
||||||
|
needed := obuLen + 2
|
||||||
|
|
||||||
|
if needed <= avail {
|
||||||
|
le := av1.LEB128Marshal(uint(obuLen))
|
||||||
|
curPacket.Payload = append(curPacket.Payload, le...)
|
||||||
|
curPacket.Payload = append(curPacket.Payload, obu...)
|
||||||
|
curPayloadLen += len(le) + obuLen
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if avail > 2 {
|
||||||
|
fragmentLen := avail - 2
|
||||||
|
le := av1.LEB128Marshal(uint(fragmentLen))
|
||||||
|
curPacket.Payload = append(curPacket.Payload, le...)
|
||||||
|
curPacket.Payload = append(curPacket.Payload, obu[:fragmentLen]...)
|
||||||
|
obu = obu[fragmentLen:]
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizeCurPacket(true)
|
||||||
|
createNewPacket(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizeCurPacket(false)
|
||||||
|
|
||||||
|
if isKeyFrame {
|
||||||
|
packets[0].Payload[0] |= 1 << 3
|
||||||
|
}
|
||||||
|
|
||||||
|
packets[len(packets)-1].Marker = true
|
||||||
|
|
||||||
|
return packets, nil
|
||||||
|
}
|
1011
pkg/formats/rtpav1/encoder_test.go
Normal file
1011
pkg/formats/rtpav1/encoder_test.go
Normal file
File diff suppressed because it is too large
Load Diff
2
pkg/formats/rtpav1/rtpav1.go
Normal file
2
pkg/formats/rtpav1/rtpav1.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package rtpav1 contains a RTP/AV1 decoder and encoder.
|
||||||
|
package rtpav1
|
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/079a435e5445c35b
vendored
Normal file
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/079a435e5445c35b
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("")
|
||||||
|
bool(false)
|
||||||
|
[]byte("")
|
||||||
|
bool(false)
|
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/1bcf1d62b055a123
vendored
Normal file
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/1bcf1d62b055a123
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("\x190")
|
||||||
|
bool(false)
|
||||||
|
[]byte("\xd00")
|
||||||
|
bool(false)
|
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/894f7b51b885dbb1
vendored
Normal file
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/894f7b51b885dbb1
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("0\x00")
|
||||||
|
bool(true)
|
||||||
|
[]byte("0")
|
||||||
|
bool(false)
|
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/bf1e5b208e57f701
vendored
Normal file
5
pkg/formats/rtpav1/testdata/fuzz/FuzzDecoder/bf1e5b208e57f701
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("\xd00")
|
||||||
|
bool(false)
|
||||||
|
[]byte("")
|
||||||
|
bool(false)
|
@@ -39,7 +39,7 @@ type Decoder struct {
|
|||||||
|
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstPacketReceived bool
|
firstPacketReceived bool
|
||||||
fragmentedSize int
|
fragmentsSize int
|
||||||
fragments [][]byte
|
fragments [][]byte
|
||||||
annexBMode bool
|
annexBMode bool
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(pkt.Payload) < 1 {
|
if len(pkt.Payload) < 1 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("payload is too short")
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
end := (pkt.Payload[1] >> 6) & 0x01
|
end := (pkt.Payload[1] >> 6) & 0x01
|
||||||
|
|
||||||
if start == 1 {
|
if start == 1 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
|
|
||||||
if end != 0 {
|
if end != 0 {
|
||||||
return nil, 0, fmt.Errorf("invalid FU-A packet (can't contain both a start and end bit)")
|
return nil, 0, fmt.Errorf("invalid FU-A packet (can't contain both a start and end bit)")
|
||||||
@@ -84,7 +84,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
|
|
||||||
nri := (pkt.Payload[0] >> 5) & 0x03
|
nri := (pkt.Payload[0] >> 5) & 0x03
|
||||||
typ := pkt.Payload[1] & 0x1F
|
typ := pkt.Payload[1] & 0x1F
|
||||||
d.fragmentedSize = len(pkt.Payload[1:])
|
d.fragmentsSize = len(pkt.Payload[1:])
|
||||||
d.fragments = append(d.fragments, []byte{(nri << 5) | typ}, pkt.Payload[2:])
|
d.fragments = append(d.fragments, []byte{(nri << 5) | typ}, pkt.Payload[2:])
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
|
|
||||||
@@ -99,10 +99,10 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
return nil, 0, fmt.Errorf("invalid FU-A packet (non-starting)")
|
return nil, 0, fmt.Errorf("invalid FU-A packet (non-starting)")
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragmentedSize += len(pkt.Payload[2:])
|
d.fragmentsSize += len(pkt.Payload[2:])
|
||||||
if d.fragmentedSize > h264.MaxNALUSize {
|
if d.fragmentsSize > h264.MaxNALUSize {
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
return nil, 0, fmt.Errorf("NALU size (%d) is too big (maximum is %d)", d.fragmentedSize, h264.MaxNALUSize)
|
return nil, 0, fmt.Errorf("NALU size (%d) is too big, maximum is %d", d.fragmentsSize, h264.MaxNALUSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = append(d.fragments, pkt.Payload[2:])
|
d.fragments = append(d.fragments, pkt.Payload[2:])
|
||||||
@@ -111,12 +111,12 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
nalus = [][]byte{joinFragments(d.fragments, d.fragmentedSize)}
|
nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
|
||||||
|
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
|
|
||||||
case h264.NALUTypeSTAPA:
|
case h264.NALUTypeSTAPA:
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
|
|
||||||
payload := pkt.Payload[1:]
|
payload := pkt.Payload[1:]
|
||||||
|
|
||||||
@@ -149,12 +149,12 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
|
|
||||||
case h264.NALUTypeSTAPB, h264.NALUTypeMTAP16,
|
case h264.NALUTypeSTAPB, h264.NALUTypeMTAP16,
|
||||||
h264.NALUTypeMTAP24, h264.NALUTypeFUB:
|
h264.NALUTypeMTAP24, h264.NALUTypeFUB:
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
return nil, 0, fmt.Errorf("packet type not supported (%v)", typ)
|
return nil, 0, fmt.Errorf("packet type not supported (%v)", typ)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
nalus = [][]byte{pkt.Payload}
|
nalus = [][]byte{pkt.Payload}
|
||||||
}
|
}
|
||||||
|
@@ -146,8 +146,10 @@ func (e *Encoder) writeSingle(nalu []byte, pts time.Duration, marker bool) ([]*r
|
|||||||
func (e *Encoder) writeFragmented(nalu []byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) {
|
func (e *Encoder) writeFragmented(nalu []byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) {
|
||||||
// use only FU-A, not FU-B, since we always use non-interleaved mode
|
// use only FU-A, not FU-B, since we always use non-interleaved mode
|
||||||
// (packetization-mode=1)
|
// (packetization-mode=1)
|
||||||
packetCount := (len(nalu) - 1) / (e.PayloadMaxSize - 2)
|
avail := e.PayloadMaxSize - 2
|
||||||
lastPacketSize := (len(nalu) - 1) % (e.PayloadMaxSize - 2)
|
le := len(nalu) - 1
|
||||||
|
packetCount := le / avail
|
||||||
|
lastPacketSize := le % avail
|
||||||
if lastPacketSize > 0 {
|
if lastPacketSize > 0 {
|
||||||
packetCount++
|
packetCount++
|
||||||
}
|
}
|
||||||
@@ -167,7 +169,7 @@ func (e *Encoder) writeFragmented(nalu []byte, pts time.Duration, marker bool) (
|
|||||||
start = 1
|
start = 1
|
||||||
}
|
}
|
||||||
end := uint8(0)
|
end := uint8(0)
|
||||||
le := e.PayloadMaxSize - 2
|
le := avail
|
||||||
if i == (packetCount - 1) {
|
if i == (packetCount - 1) {
|
||||||
end = 1
|
end = 1
|
||||||
le = lastPacketSize
|
le = lastPacketSize
|
||||||
|
@@ -38,7 +38,7 @@ type Decoder struct {
|
|||||||
|
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstPacketReceived bool
|
firstPacketReceived bool
|
||||||
fragmentedSize int
|
fragmentsSize int
|
||||||
fragments [][]byte
|
fragments [][]byte
|
||||||
|
|
||||||
// for DecodeUntilMarker()
|
// for DecodeUntilMarker()
|
||||||
@@ -57,7 +57,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(pkt.Payload) < 2 {
|
if len(pkt.Payload) < 2 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("payload is too short")
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
|
|
||||||
switch typ {
|
switch typ {
|
||||||
case h265.NALUType_AggregationUnit:
|
case h265.NALUType_AggregationUnit:
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
|
|
||||||
payload := pkt.Payload[2:]
|
payload := pkt.Payload[2:]
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
|
|
||||||
case h265.NALUType_FragmentationUnit:
|
case h265.NALUType_FragmentationUnit:
|
||||||
if len(pkt.Payload) < 3 {
|
if len(pkt.Payload) < 3 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("payload is too short")
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
end := (pkt.Payload[2] >> 6) & 0x01
|
end := (pkt.Payload[2] >> 6) & 0x01
|
||||||
|
|
||||||
if start == 1 {
|
if start == 1 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
|
|
||||||
if end != 0 {
|
if end != 0 {
|
||||||
return nil, 0, fmt.Errorf("invalid fragmentation unit (can't contain both a start and end bit)")
|
return nil, 0, fmt.Errorf("invalid fragmentation unit (can't contain both a start and end bit)")
|
||||||
@@ -114,7 +114,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
|
|
||||||
typ := pkt.Payload[2] & 0b111111
|
typ := pkt.Payload[2] & 0b111111
|
||||||
head := uint16(pkt.Payload[0]&0b10000001)<<8 | uint16(typ)<<9 | uint16(pkt.Payload[1])
|
head := uint16(pkt.Payload[0]&0b10000001)<<8 | uint16(typ)<<9 | uint16(pkt.Payload[1])
|
||||||
d.fragmentedSize = len(pkt.Payload[1:])
|
d.fragmentsSize = len(pkt.Payload[1:])
|
||||||
d.fragments = append(d.fragments, []byte{byte(head >> 8), byte(head)}, pkt.Payload[3:])
|
d.fragments = append(d.fragments, []byte{byte(head >> 8), byte(head)}, pkt.Payload[3:])
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
|
|
||||||
@@ -129,10 +129,10 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
return nil, 0, fmt.Errorf("invalid fragmentation unit (non-starting)")
|
return nil, 0, fmt.Errorf("invalid fragmentation unit (non-starting)")
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragmentedSize += len(pkt.Payload[3:])
|
d.fragmentsSize += len(pkt.Payload[3:])
|
||||||
if d.fragmentedSize > h265.MaxNALUSize {
|
if d.fragmentsSize > h265.MaxNALUSize {
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
return nil, 0, fmt.Errorf("NALU size (%d) is too big (maximum is %d)", d.fragmentedSize, h265.MaxNALUSize)
|
return nil, 0, fmt.Errorf("NALU size (%d) is too big, maximum is %d", d.fragmentsSize, h265.MaxNALUSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = append(d.fragments, pkt.Payload[3:])
|
d.fragments = append(d.fragments, pkt.Payload[3:])
|
||||||
@@ -141,17 +141,17 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
nalus = [][]byte{joinFragments(d.fragments, d.fragmentedSize)}
|
nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
|
||||||
|
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
|
|
||||||
case h265.NALUType_PACI:
|
case h265.NALUType_PACI:
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
return nil, 0, fmt.Errorf("PACI packets are not supported (yet)")
|
return nil, 0, fmt.Errorf("PACI packets are not supported (yet)")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
nalus = [][]byte{pkt.Payload}
|
nalus = [][]byte{pkt.Payload}
|
||||||
}
|
}
|
||||||
|
@@ -144,8 +144,10 @@ func (e *Encoder) writeSingle(nalu []byte, pts time.Duration, marker bool) ([]*r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) writeFragmentationUnits(nalu []byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) {
|
func (e *Encoder) writeFragmentationUnits(nalu []byte, pts time.Duration, marker bool) ([]*rtp.Packet, error) {
|
||||||
n := (len(nalu) - 2) / (e.PayloadMaxSize - 3)
|
avail := e.PayloadMaxSize - 3
|
||||||
lastPacketSize := (len(nalu) - 2) % (e.PayloadMaxSize - 3)
|
le := len(nalu) - 2
|
||||||
|
n := le / avail
|
||||||
|
lastPacketSize := le % avail
|
||||||
if lastPacketSize > 0 {
|
if lastPacketSize > 0 {
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
@@ -162,7 +164,7 @@ func (e *Encoder) writeFragmentationUnits(nalu []byte, pts time.Duration, marker
|
|||||||
start = 1
|
start = 1
|
||||||
}
|
}
|
||||||
end := uint8(0)
|
end := uint8(0)
|
||||||
le := e.PayloadMaxSize - 3
|
le := avail
|
||||||
if i == (n - 1) {
|
if i == (n - 1) {
|
||||||
end = 1
|
end = 1
|
||||||
le = lastPacketSize
|
le = lastPacketSize
|
||||||
|
@@ -108,7 +108,7 @@ func joinFragments(fragments [][]byte, size int) []byte {
|
|||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstPacketReceived bool
|
firstPacketReceived bool
|
||||||
fragmentedSize int
|
fragmentsSize int
|
||||||
fragments [][]byte
|
fragments [][]byte
|
||||||
firstJpegHeader *headers.JPEG
|
firstJpegHeader *headers.JPEG
|
||||||
firstQTHeader *headers.QuantizationTable
|
firstQTHeader *headers.QuantizationTable
|
||||||
@@ -139,8 +139,8 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, time.Duration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if jh.FragmentOffset == 0 {
|
if jh.FragmentOffset == 0 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
|
|
||||||
if jh.Quantization >= 128 {
|
if jh.Quantization >= 128 {
|
||||||
@@ -155,20 +155,20 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, time.Duration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = append(d.fragments, byts)
|
d.fragments = append(d.fragments, byts)
|
||||||
d.fragmentedSize = len(byts)
|
d.fragmentsSize = len(byts)
|
||||||
d.firstJpegHeader = &jh
|
d.firstJpegHeader = &jh
|
||||||
} else {
|
} else {
|
||||||
if int(jh.FragmentOffset) != d.fragmentedSize {
|
if int(jh.FragmentOffset) != d.fragmentsSize {
|
||||||
if !d.firstPacketReceived {
|
if !d.firstPacketReceived {
|
||||||
return nil, 0, ErrNonStartingPacketAndNoPrevious
|
return nil, 0, ErrNonStartingPacketAndNoPrevious
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
return nil, 0, fmt.Errorf("received wrong fragment")
|
return nil, 0, fmt.Errorf("received wrong fragment")
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragmentedSize += len(byts)
|
d.fragmentsSize += len(byts)
|
||||||
d.fragments = append(d.fragments, byts)
|
d.fragments = append(d.fragments, byts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,14 +176,14 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, time.Duration, error) {
|
|||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.fragmentedSize < 2 {
|
if d.fragmentsSize < 2 {
|
||||||
return nil, 0, fmt.Errorf("invalid data")
|
return nil, 0, fmt.Errorf("invalid data")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := joinFragments(d.fragments, d.fragmentedSize)
|
data := joinFragments(d.fragments, d.fragmentsSize)
|
||||||
|
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
|
|
||||||
var buf []byte
|
var buf []byte
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@ type Decoder struct {
|
|||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstPacketReceived bool
|
firstPacketReceived bool
|
||||||
fragments [][]byte
|
fragments [][]byte
|
||||||
fragmentedSize int
|
fragmentsSize int
|
||||||
fragmentsExpected int
|
fragmentsExpected int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,15 +48,15 @@ func (d *Decoder) Init() {
|
|||||||
// Decode decodes frames from a RTP packet.
|
// Decode decodes frames from a RTP packet.
|
||||||
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
||||||
if len(pkt.Payload) < 5 {
|
if len(pkt.Payload) < 5 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
return nil, 0, fmt.Errorf("payload is too short")
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
mbz := uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1])
|
mbz := uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1])
|
||||||
if mbz != 0 {
|
if mbz != 0 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
return nil, 0, fmt.Errorf("invalid MBZ: %v", mbz)
|
return nil, 0, fmt.Errorf("invalid MBZ: %v", mbz)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +65,8 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
var frames [][]byte
|
var frames [][]byte
|
||||||
|
|
||||||
if offset == 0 {
|
if offset == 0 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
|
|
||||||
buf := pkt.Payload[4:]
|
buf := pkt.Payload[4:]
|
||||||
@@ -91,29 +91,29 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = append(d.fragments, buf)
|
d.fragments = append(d.fragments, buf)
|
||||||
d.fragmentedSize = bl
|
d.fragmentsSize = bl
|
||||||
d.fragmentsExpected = fl - bl
|
d.fragmentsExpected = fl - bl
|
||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if int(offset) != d.fragmentedSize {
|
if int(offset) != d.fragmentsSize {
|
||||||
if !d.firstPacketReceived {
|
if !d.firstPacketReceived {
|
||||||
return nil, 0, ErrNonStartingPacketAndNoPrevious
|
return nil, 0, ErrNonStartingPacketAndNoPrevious
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
return nil, 0, fmt.Errorf("unexpected offset %v, expected %v", offset, d.fragmentedSize)
|
return nil, 0, fmt.Errorf("unexpected offset %v, expected %v", offset, d.fragmentsSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
bl := len(pkt.Payload[4:])
|
bl := len(pkt.Payload[4:])
|
||||||
d.fragmentedSize += bl
|
d.fragmentsSize += bl
|
||||||
d.fragmentsExpected -= bl
|
d.fragmentsExpected -= bl
|
||||||
|
|
||||||
if d.fragmentsExpected < 0 {
|
if d.fragmentsExpected < 0 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
return nil, 0, fmt.Errorf("fragment is too big")
|
return nil, 0, fmt.Errorf("fragment is too big")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +123,10 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
frames = [][]byte{joinFragments(d.fragments, d.fragmentedSize)}
|
frames = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
|
||||||
|
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
d.fragmentedSize = 0
|
d.fragmentsSize = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return frames, d.timeDecoder.Decode(pkt.Timestamp), nil
|
return frames, d.timeDecoder.Decode(pkt.Timestamp), nil
|
||||||
|
@@ -125,10 +125,10 @@ func (e *Encoder) writeBatch(frames [][]byte, pts time.Duration) ([]*rtp.Packet,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) writeFragmented(frame []byte, pts time.Duration) ([]*rtp.Packet, error) {
|
func (e *Encoder) writeFragmented(frame []byte, pts time.Duration) ([]*rtp.Packet, error) {
|
||||||
availPerPacket := e.PayloadMaxSize - 4
|
avail := e.PayloadMaxSize - 4
|
||||||
le := len(frame)
|
le := len(frame)
|
||||||
packetCount := le / availPerPacket
|
packetCount := le / avail
|
||||||
lastPacketSize := le % availPerPacket
|
lastPacketSize := le % avail
|
||||||
if lastPacketSize > 0 {
|
if lastPacketSize > 0 {
|
||||||
packetCount++
|
packetCount++
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ func (e *Encoder) writeFragmented(frame []byte, pts time.Duration) ([]*rtp.Packe
|
|||||||
for i := range ret {
|
for i := range ret {
|
||||||
var le int
|
var le int
|
||||||
if i != (packetCount - 1) {
|
if i != (packetCount - 1) {
|
||||||
le = availPerPacket
|
le = avail
|
||||||
} else {
|
} else {
|
||||||
le = lastPacketSize
|
le = lastPacketSize
|
||||||
}
|
}
|
||||||
|
@@ -39,11 +39,11 @@ type Decoder struct {
|
|||||||
// The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header.
|
// The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header.
|
||||||
IndexDeltaLength int
|
IndexDeltaLength int
|
||||||
|
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstAUParsed bool
|
firstAUParsed bool
|
||||||
adtsMode bool
|
adtsMode bool
|
||||||
fragments [][]byte
|
fragments [][]byte
|
||||||
fragmentedSize int
|
fragmentsSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the decoder.
|
// Init initializes the decoder.
|
||||||
@@ -56,14 +56,14 @@ func (d *Decoder) Init() {
|
|||||||
// The PTS of subsequent AUs can be calculated by adding time.Second*mpeg4audio.SamplesPerAccessUnit/clockRate.
|
// The PTS of subsequent AUs can be calculated by adding time.Second*mpeg4audio.SamplesPerAccessUnit/clockRate.
|
||||||
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
||||||
if len(pkt.Payload) < 2 {
|
if len(pkt.Payload) < 2 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("payload is too short")
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AU-headers-length (16 bits)
|
// AU-headers-length (16 bits)
|
||||||
headersLen := int(uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1]))
|
headersLen := int(uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1]))
|
||||||
if headersLen == 0 {
|
if headersLen == 0 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("invalid AU-headers-length")
|
return nil, 0, fmt.Errorf("invalid AU-headers-length")
|
||||||
}
|
}
|
||||||
payload := pkt.Payload[2:]
|
payload := pkt.Payload[2:]
|
||||||
@@ -71,7 +71,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
// AU-headers
|
// AU-headers
|
||||||
dataLens, err := d.readAUHeaders(payload, headersLen)
|
dataLens, err := d.readAUHeaders(payload, headersLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,26 +104,26 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
return nil, 0, fmt.Errorf("payload is too short")
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragmentedSize = int(dataLens[0])
|
d.fragmentsSize = int(dataLens[0])
|
||||||
d.fragments = append(d.fragments, payload[:dataLens[0]])
|
d.fragments = append(d.fragments, payload[:dataLens[0]])
|
||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// we are decoding a fragmented AU
|
// we are decoding a fragmented AU
|
||||||
if len(dataLens) != 1 {
|
if len(dataLens) != 1 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("a fragmented packet can only contain one AU")
|
return nil, 0, fmt.Errorf("a fragmented packet can only contain one AU")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(payload) < int(dataLens[0]) {
|
if len(payload) < int(dataLens[0]) {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("payload is too short")
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragmentedSize += int(dataLens[0])
|
d.fragmentsSize += int(dataLens[0])
|
||||||
if d.fragmentedSize > mpeg4audio.MaxAccessUnitSize {
|
if d.fragmentsSize > mpeg4audio.MaxAccessUnitSize {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("AU size (%d) is too big (maximum is %d)", d.fragmentedSize, mpeg4audio.MaxAccessUnitSize)
|
return nil, 0, fmt.Errorf("AU size (%d) is too big, maximum is %d", d.fragmentsSize, mpeg4audio.MaxAccessUnitSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = append(d.fragments, payload[:dataLens[0]])
|
d.fragments = append(d.fragments, payload[:dataLens[0]])
|
||||||
@@ -132,7 +132,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
aus = [][]byte{joinFragments(d.fragments, d.fragmentedSize)}
|
aus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)}
|
||||||
|
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
}
|
}
|
||||||
|
@@ -132,10 +132,10 @@ func (e *Encoder) writeFragmented(au []byte, pts time.Duration) ([]*rtp.Packet,
|
|||||||
auHeadersLenBytes++
|
auHeadersLenBytes++
|
||||||
}
|
}
|
||||||
|
|
||||||
auMaxSize := e.PayloadMaxSize - 2 - auHeadersLenBytes
|
avail := e.PayloadMaxSize - 2 - auHeadersLenBytes
|
||||||
le := len(au)
|
le := len(au)
|
||||||
packetCount := le / auMaxSize
|
packetCount := le / avail
|
||||||
lastPacketSize := le % auMaxSize
|
lastPacketSize := le % avail
|
||||||
if lastPacketSize > 0 {
|
if lastPacketSize > 0 {
|
||||||
packetCount++
|
packetCount++
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ func (e *Encoder) writeFragmented(au []byte, pts time.Duration) ([]*rtp.Packet,
|
|||||||
for i := range ret {
|
for i := range ret {
|
||||||
var le int
|
var le int
|
||||||
if i != (packetCount - 1) {
|
if i != (packetCount - 1) {
|
||||||
le = auMaxSize
|
le = avail
|
||||||
} else {
|
} else {
|
||||||
le = lastPacketSize
|
le = lastPacketSize
|
||||||
}
|
}
|
||||||
|
@@ -29,9 +29,9 @@ func joinFragments(fragments [][]byte, size int) []byte {
|
|||||||
// Decoder is a RTP/MPEG-4 Video decoder.
|
// Decoder is a RTP/MPEG-4 Video decoder.
|
||||||
// Specification: https://datatracker.ietf.org/doc/html/rfc6416
|
// Specification: https://datatracker.ietf.org/doc/html/rfc6416
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
fragments [][]byte
|
fragments [][]byte
|
||||||
fragmentedSize int
|
fragmentsSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the decoder.
|
// Init initializes the decoder.
|
||||||
@@ -47,15 +47,15 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, time.Duration, error) {
|
|||||||
if pkt.Marker {
|
if pkt.Marker {
|
||||||
frame = pkt.Payload
|
frame = pkt.Payload
|
||||||
} else {
|
} else {
|
||||||
d.fragmentedSize = len(pkt.Payload)
|
d.fragmentsSize = len(pkt.Payload)
|
||||||
d.fragments = append(d.fragments, pkt.Payload)
|
d.fragments = append(d.fragments, pkt.Payload)
|
||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
d.fragmentedSize += len(pkt.Payload)
|
d.fragmentsSize += len(pkt.Payload)
|
||||||
if d.fragmentedSize > maxFrameSize {
|
if d.fragmentsSize > maxFrameSize {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("frame size (%d) is too big (maximum is %d)", d.fragmentedSize, maxFrameSize)
|
return nil, 0, fmt.Errorf("frame size (%d) is too big, maximum is %d", d.fragmentsSize, maxFrameSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.fragments = append(d.fragments, pkt.Payload)
|
d.fragments = append(d.fragments, pkt.Payload)
|
||||||
@@ -64,7 +64,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, time.Duration, error) {
|
|||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
frame = joinFragments(d.fragments, d.fragmentedSize)
|
frame = joinFragments(d.fragments, d.fragmentsSize)
|
||||||
d.fragments = d.fragments[:0]
|
d.fragments = d.fragments[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -69,10 +69,10 @@ func (e *Encoder) Init() {
|
|||||||
|
|
||||||
// Encode encodes a frame into RTP packets.
|
// Encode encodes a frame into RTP packets.
|
||||||
func (e *Encoder) Encode(frame []byte, pts time.Duration) ([]*rtp.Packet, error) {
|
func (e *Encoder) Encode(frame []byte, pts time.Duration) ([]*rtp.Packet, error) {
|
||||||
availPerPacket := e.PayloadMaxSize
|
avail := e.PayloadMaxSize
|
||||||
le := len(frame)
|
le := len(frame)
|
||||||
packetCount := le / availPerPacket
|
packetCount := le / avail
|
||||||
lastPacketSize := le % availPerPacket
|
lastPacketSize := le % avail
|
||||||
if lastPacketSize > 0 {
|
if lastPacketSize > 0 {
|
||||||
packetCount++
|
packetCount++
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ func (e *Encoder) Encode(frame []byte, pts time.Duration) ([]*rtp.Packet, error)
|
|||||||
for i := range ret {
|
for i := range ret {
|
||||||
var le int
|
var le int
|
||||||
if i != (packetCount - 1) {
|
if i != (packetCount - 1) {
|
||||||
le = availPerPacket
|
le = avail
|
||||||
} else {
|
} else {
|
||||||
le = lastPacketSize
|
le = lastPacketSize
|
||||||
}
|
}
|
||||||
|
@@ -48,19 +48,19 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, time.Duration, error) {
|
|||||||
var vpkt codecs.VP8Packet
|
var vpkt codecs.VP8Packet
|
||||||
_, err := vpkt.Unmarshal(pkt.Payload)
|
_, err := vpkt.Unmarshal(pkt.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if vpkt.PID != 0 {
|
if vpkt.PID != 0 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, fmt.Errorf("packets containing single partitions are not supported")
|
return nil, 0, fmt.Errorf("packets containing single partitions are not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
var frame []byte
|
var frame []byte
|
||||||
|
|
||||||
if vpkt.S == 1 {
|
if vpkt.S == 1 {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
|
|
||||||
if !pkt.Marker {
|
if !pkt.Marker {
|
||||||
|
@@ -48,14 +48,14 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, time.Duration, error) {
|
|||||||
var vpkt codecs.VP9Packet
|
var vpkt codecs.VP9Packet
|
||||||
_, err := vpkt.Unmarshal(pkt.Payload)
|
_, err := vpkt.Unmarshal(pkt.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var frame []byte
|
var frame []byte
|
||||||
|
|
||||||
if vpkt.B {
|
if vpkt.B {
|
||||||
d.fragments = d.fragments[:0] // discard pending fragmented packets
|
d.fragments = d.fragments[:0] // discard pending fragments
|
||||||
d.firstPacketReceived = true
|
d.firstPacketReceived = true
|
||||||
|
|
||||||
if !vpkt.E {
|
if !vpkt.E {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package formats
|
package formats //nolint:dupl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
Reference in New Issue
Block a user