mirror of
https://github.com/aler9/gortsplib
synced 2025-10-06 07:37:07 +08:00
add RTP/MPEG-2 audio decoder and encoder (#257)
This commit is contained in:
@@ -45,7 +45,7 @@ Features:
|
|||||||
* 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
|
* Video: H264, H265, M-JPEG, VP8, VP9
|
||||||
* Audio: G711 (PCMA, PCMU), G722, LPCM, MPEG4 Audio (AAC), Opus
|
* Audio: G711 (PCMA, PCMU), G722, LPCM, MPEG-2 Audio (MP3), MPEG-4 Audio (AAC), Opus
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
|
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.3.1
|
github.com/bluenviron/mediacommon v0.4.1
|
||||||
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.3.1 h1:C4okNqyN1Mg5CVGcGKk2tEk9Uj2hHZusHV7nqdjn1Lk=
|
github.com/bluenviron/mediacommon v0.4.1 h1:oiqvqwnZ0NbB+mCZjuyBtgY7cF88rZdhb/PTSNG9M+Q=
|
||||||
github.com/bluenviron/mediacommon v0.3.1/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8=
|
github.com/bluenviron/mediacommon v0.4.1/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,4 +1,4 @@
|
|||||||
package formats
|
package formats //nolint:dupl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package formats
|
package formats //nolint:dupl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtpmpeg2audio"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MPEG2Audio is a RTP format that uses a MPEG-1 or MPEG-2 audio codec.
|
// MPEG2Audio is a RTP format that uses a MPEG-1 or MPEG-2 audio codec.
|
||||||
@@ -44,3 +46,17 @@ func (f *MPEG2Audio) FMTP() map[string]string {
|
|||||||
func (f *MPEG2Audio) PTSEqualsDTS(*rtp.Packet) bool {
|
func (f *MPEG2Audio) PTSEqualsDTS(*rtp.Packet) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateDecoder creates a decoder able to decode the content of the format.
|
||||||
|
func (f *MPEG2Audio) CreateDecoder() *rtpmpeg2audio.Decoder {
|
||||||
|
d := &rtpmpeg2audio.Decoder{}
|
||||||
|
d.Init()
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEncoder creates an encoder able to encode the content of the format.
|
||||||
|
func (f *MPEG2Audio) CreateEncoder() *rtpmpeg2audio.Encoder {
|
||||||
|
e := &rtpmpeg2audio.Encoder{}
|
||||||
|
e.Init()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
@@ -13,3 +13,43 @@ func TestMPEG2AudioAttributes(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 TestMPEG2AudioDecEncoder(t *testing.T) {
|
||||||
|
format := &MPEG2Audio{}
|
||||||
|
|
||||||
|
enc := format.CreateEncoder()
|
||||||
|
pkts, err := enc.Encode([][]byte{{
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x00, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0x4c, 0x41, 0x4d, 0x45,
|
||||||
|
0x33, 0x2e, 0x31, 0x30, 0x30, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0xc0, 0x65, 0xf4, 0xa0, 0x31, 0x8f, 0xce,
|
||||||
|
0x8d, 0x46, 0xfc, 0x8c, 0x73, 0xb9, 0x34, 0x3e,
|
||||||
|
0xb5, 0x03, 0x39, 0xc0, 0x04, 0x01, 0x98, 0x44,
|
||||||
|
0x38, 0xe0, 0x98, 0x10, 0x9b, 0xa8, 0x0f, 0xa8,
|
||||||
|
}}, 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{{
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x00, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0x4c, 0x41, 0x4d, 0x45,
|
||||||
|
0x33, 0x2e, 0x31, 0x30, 0x30, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0xc0, 0x65, 0xf4, 0xa0, 0x31, 0x8f, 0xce,
|
||||||
|
0x8d, 0x46, 0xfc, 0x8c, 0x73, 0xb9, 0x34, 0x3e,
|
||||||
|
0xb5, 0x03, 0x39, 0xc0, 0x04, 0x01, 0x98, 0x44,
|
||||||
|
0x38, 0xe0, 0x98, 0x10, 0x9b, 0xa8, 0x0f, 0xa8,
|
||||||
|
}}, byts)
|
||||||
|
}
|
||||||
|
@@ -23,6 +23,7 @@ var ErrNonStartingPacketAndNoPrevious = errors.New(
|
|||||||
"received a non-starting fragment without any previous starting fragment")
|
"received a non-starting fragment without any previous starting fragment")
|
||||||
|
|
||||||
// Decoder is a RTP/H264 decoder.
|
// Decoder is a RTP/H264 decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc6184
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
// indicates the packetization mode.
|
// indicates the packetization mode.
|
||||||
PacketizationMode int
|
PacketizationMode int
|
||||||
|
@@ -204,7 +204,7 @@ func TestDecodeUntilMarker(t *testing.T) {
|
|||||||
require.Equal(t, [][]byte{{0x01, 0x02}, {0x01, 0x02}}, nalus)
|
require.Equal(t, [][]byte{{0x01, 0x02}, {0x01, 0x02}}, nalus)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzDecoderUnmarshal(f *testing.F) {
|
func FuzzDecoder(f *testing.F) {
|
||||||
d := &Decoder{}
|
d := &Decoder{}
|
||||||
d.Init()
|
d.Init()
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ func randUint32() uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encoder is a RTP/H264 encoder.
|
// Encoder is a RTP/H264 encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc6184
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// payload type of packets.
|
// payload type of packets.
|
||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
|
@@ -22,6 +22,7 @@ var ErrNonStartingPacketAndNoPrevious = errors.New(
|
|||||||
"received a non-starting fragment without any previous starting fragment")
|
"received a non-starting fragment without any previous starting fragment")
|
||||||
|
|
||||||
// Decoder is a RTP/H265 decoder.
|
// Decoder is a RTP/H265 decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc7798
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
// indicates that NALUs have an additional field that specifies the decoding order.
|
// indicates that NALUs have an additional field that specifies the decoding order.
|
||||||
MaxDONDiff int
|
MaxDONDiff int
|
||||||
|
@@ -56,7 +56,7 @@ func TestDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzDecoderUnmarshal(f *testing.F) {
|
func FuzzDecoder(f *testing.F) {
|
||||||
d := &Decoder{}
|
d := &Decoder{}
|
||||||
d.Init()
|
d.Init()
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@ func randUint32() uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encoder is a RTP/H265 encoder.
|
// Encoder is a RTP/H265 encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc7798
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// payload type of packets.
|
// payload type of packets.
|
||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Decoder is a RTP/LPCM decoder.
|
// Decoder is a RTP/LPCM decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc3190
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
BitDepth int
|
BitDepth int
|
||||||
SampleRate int
|
SampleRate int
|
||||||
|
@@ -54,7 +54,7 @@ func TestDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzDecoderUnmarshal(f *testing.F) {
|
func FuzzDecoder(f *testing.F) {
|
||||||
d := &Decoder{
|
d := &Decoder{
|
||||||
BitDepth: 24,
|
BitDepth: 24,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
|
@@ -21,6 +21,7 @@ func randUint32() uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encoder is a RTP/LPCM encoder.
|
// Encoder is a RTP/LPCM encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc3190
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// payload type of packets.
|
// payload type of packets.
|
||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
|
@@ -95,6 +95,7 @@ var chmAcSymbols = []byte{ //nolint:dupl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decoder is a RTP/M-JPEG decoder.
|
// Decoder is a RTP/M-JPEG decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc2435
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstPacketReceived bool
|
firstPacketReceived bool
|
||||||
|
@@ -30,7 +30,7 @@ func TestDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzDecoderUnmarshal(f *testing.F) {
|
func FuzzDecoder(f *testing.F) {
|
||||||
d := &Decoder{}
|
d := &Decoder{}
|
||||||
d.Init()
|
d.Init()
|
||||||
|
|
||||||
|
@@ -24,6 +24,7 @@ func randUint32() uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encoder is a RTP/M-JPEG encoder.
|
// Encoder is a RTP/M-JPEG encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc2435
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// SSRC of packets (optional).
|
// SSRC of packets (optional).
|
||||||
// It defaults to a random value.
|
// It defaults to a random value.
|
||||||
|
46
pkg/formats/rtpmpeg2audio/decoder.go
Normal file
46
pkg/formats/rtpmpeg2audio/decoder.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package rtpmpeg2audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg2audio"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v3/pkg/rtptime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decoder is a RTP/MPEG2-audio decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
|
||||||
|
type Decoder struct {
|
||||||
|
timeDecoder *rtptime.Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the decoder.
|
||||||
|
func (d *Decoder) Init() {
|
||||||
|
d.timeDecoder = rtptime.NewDecoder(90000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes frames from a RTP/MPEG2-audio packet.
|
||||||
|
func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
||||||
|
if len(pkt.Payload) < 5 {
|
||||||
|
return nil, 0, fmt.Errorf("payload is too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
mbz := uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1])
|
||||||
|
if mbz != 0 {
|
||||||
|
return nil, 0, fmt.Errorf("invalid MBZ: %v", mbz)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := uint16(pkt.Payload[2])<<8 | uint16(pkt.Payload[3])
|
||||||
|
if offset != 0 {
|
||||||
|
return nil, 0, fmt.Errorf("fragmented units are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
frames, err := mpeg2audio.SplitFrames(pkt.Payload[4:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames, d.timeDecoder.Decode(pkt.Timestamp), nil
|
||||||
|
}
|
39
pkg/formats/rtpmpeg2audio/decoder_test.go
Normal file
39
pkg/formats/rtpmpeg2audio/decoder_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package rtpmpeg2audio
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
frames, _, err := d.Decode(ca.pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ca.frames, frames)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzDecoder(f *testing.F) {
|
||||||
|
d := &Decoder{}
|
||||||
|
d.Init()
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {
|
||||||
|
d.Decode(&rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
PayloadType: 96,
|
||||||
|
SequenceNumber: 17645,
|
||||||
|
Timestamp: 2289527317,
|
||||||
|
SSRC: 0x9dbb7812,
|
||||||
|
},
|
||||||
|
Payload: b,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
151
pkg/formats/rtpmpeg2audio/encoder.go
Normal file
151
pkg/formats/rtpmpeg2audio/encoder.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package rtpmpeg2audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg2audio"
|
||||||
|
"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])
|
||||||
|
}
|
||||||
|
|
||||||
|
func lenAggregated(frames [][]byte, frame []byte) int {
|
||||||
|
l := 4 + len(frame)
|
||||||
|
for _, fr := range frames {
|
||||||
|
l += len(fr)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoder is a RTP/MPEG2-audio encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc2250
|
||||||
|
type Encoder struct {
|
||||||
|
// 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 frames into RTP/MPEG2-audio packets.
|
||||||
|
func (e *Encoder) Encode(frames [][]byte, pts time.Duration) ([]*rtp.Packet, error) {
|
||||||
|
var rets []*rtp.Packet
|
||||||
|
var batch [][]byte
|
||||||
|
|
||||||
|
for _, frame := range frames {
|
||||||
|
if len(frame) > e.PayloadMaxSize {
|
||||||
|
return nil, fmt.Errorf("frame is too big")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lenAggregated(batch, frame) <= e.PayloadMaxSize {
|
||||||
|
batch = append(batch, frame)
|
||||||
|
} else {
|
||||||
|
// write last batch
|
||||||
|
if batch != nil {
|
||||||
|
pkt, err := e.writeBatch(batch, pts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rets = append(rets, pkt)
|
||||||
|
|
||||||
|
for _, frame := range batch {
|
||||||
|
var h mpeg2audio.FrameHeader
|
||||||
|
err := h.Unmarshal(frame)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pts += time.Duration(h.SampleCount()) * time.Second / time.Duration(h.SampleRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize new batch
|
||||||
|
batch = [][]byte{frame}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write last batch
|
||||||
|
pkt, err := e.writeBatch(batch, pts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rets = append(rets, pkt)
|
||||||
|
|
||||||
|
return rets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) writeBatch(frames [][]byte, pts time.Duration) (*rtp.Packet, error) {
|
||||||
|
l := 4
|
||||||
|
for _, frame := range frames {
|
||||||
|
l += len(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, l)
|
||||||
|
n := 4
|
||||||
|
for _, frame := range frames {
|
||||||
|
n += copy(payload[n:], frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := &rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: rtpVersion,
|
||||||
|
PayloadType: 14,
|
||||||
|
SequenceNumber: e.sequenceNumber,
|
||||||
|
Timestamp: e.timeEncoder.Encode(pts),
|
||||||
|
SSRC: *e.SSRC,
|
||||||
|
Marker: true,
|
||||||
|
},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
e.sequenceNumber++
|
||||||
|
|
||||||
|
return pkt, nil
|
||||||
|
}
|
163
pkg/formats/rtpmpeg2audio/encoder_test.go
Normal file
163
pkg/formats/rtpmpeg2audio/encoder_test.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package rtpmpeg2audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cases = []struct {
|
||||||
|
name string
|
||||||
|
frames [][]byte
|
||||||
|
pkt *rtp.Packet
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"single",
|
||||||
|
[][]byte{
|
||||||
|
{
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x00, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0x4c, 0x41, 0x4d, 0x45,
|
||||||
|
0x33, 0x2e, 0x31, 0x30, 0x30, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0xc0, 0x65, 0xf4, 0xa0, 0x31, 0x8f, 0xce,
|
||||||
|
0x8d, 0x46, 0xfc, 0x8c, 0x73, 0xb9, 0x34, 0x3e,
|
||||||
|
0xb5, 0x03, 0x39, 0xc0, 0x04, 0x01, 0x98, 0x44,
|
||||||
|
0x38, 0xe0, 0x98, 0x10, 0x9b, 0xa8, 0x0f, 0xa8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
|
PayloadType: 14,
|
||||||
|
SequenceNumber: 17645,
|
||||||
|
Timestamp: 2289526357,
|
||||||
|
SSRC: 0x9dbb7812,
|
||||||
|
},
|
||||||
|
Payload: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x00, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0x4c, 0x41, 0x4d, 0x45,
|
||||||
|
0x33, 0x2e, 0x31, 0x30, 0x30, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0xc0, 0x65, 0xf4, 0xa0, 0x31, 0x8f, 0xce,
|
||||||
|
0x8d, 0x46, 0xfc, 0x8c, 0x73, 0xb9, 0x34, 0x3e,
|
||||||
|
0xb5, 0x03, 0x39, 0xc0, 0x04, 0x01, 0x98, 0x44,
|
||||||
|
0x38, 0xe0, 0x98, 0x10, 0x9b, 0xa8, 0x0f, 0xa8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aggregated",
|
||||||
|
[][]byte{
|
||||||
|
{
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x00, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0x4c, 0x41, 0x4d, 0x45,
|
||||||
|
0x33, 0x2e, 0x31, 0x30, 0x30, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0xc0, 0x65, 0xf4, 0xa0, 0x31, 0x8f, 0xce,
|
||||||
|
0x8d, 0x46, 0xfc, 0x8c, 0x73, 0xb9, 0x34, 0x3e,
|
||||||
|
0xb5, 0x03, 0x39, 0xc0, 0x04, 0x01, 0x98, 0x44,
|
||||||
|
0x38, 0xe0, 0x98, 0x10, 0x9b, 0xa8, 0x0f, 0xa8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x1e, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0xe6, 0x50, 0x10, 0x01,
|
||||||
|
0xca, 0x13, 0x94, 0x27, 0x4a, 0x4a, 0x64, 0xce,
|
||||||
|
0x07, 0xc2, 0x2f, 0x59, 0xc0, 0x19, 0x04, 0x05,
|
||||||
|
0xdf, 0xe7, 0xce, 0x65, 0x24, 0xed, 0xa4, 0xe3,
|
||||||
|
0xff, 0xc9, 0x00, 0x00, 0x05, 0x5f, 0x4a, 0x04,
|
||||||
|
0x0e, 0xc4, 0x24, 0xfd, 0x5e, 0x4a, 0x35, 0x72,
|
||||||
|
0x21, 0x27, 0x31, 0x08, 0x47, 0x18, 0x00, 0x06,
|
||||||
|
0xc4, 0x02, 0x72, 0x81, 0x89, 0xc3, 0xe4, 0x0a,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
|
PayloadType: 14,
|
||||||
|
SequenceNumber: 17645,
|
||||||
|
Timestamp: 2289526357,
|
||||||
|
SSRC: 0x9dbb7812,
|
||||||
|
},
|
||||||
|
Payload: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x00, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0x4c, 0x41, 0x4d, 0x45,
|
||||||
|
0x33, 0x2e, 0x31, 0x30, 0x30, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||||
|
0x55, 0xc0, 0x65, 0xf4, 0xa0, 0x31, 0x8f, 0xce,
|
||||||
|
0x8d, 0x46, 0xfc, 0x8c, 0x73, 0xb9, 0x34, 0x3e,
|
||||||
|
0xb5, 0x03, 0x39, 0xc0, 0x04, 0x01, 0x98, 0x44,
|
||||||
|
0x38, 0xe0, 0x98, 0x10, 0x9b, 0xa8, 0x0f, 0xa8,
|
||||||
|
0xff, 0xfb, 0x14, 0x64, 0x1e, 0x0f, 0xf0, 0x00,
|
||||||
|
0x00, 0x69, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x0d, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
|
||||||
|
0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34,
|
||||||
|
0x80, 0x00, 0x00, 0x04, 0xe6, 0x50, 0x10, 0x01,
|
||||||
|
0xca, 0x13, 0x94, 0x27, 0x4a, 0x4a, 0x64, 0xce,
|
||||||
|
0x07, 0xc2, 0x2f, 0x59, 0xc0, 0x19, 0x04, 0x05,
|
||||||
|
0xdf, 0xe7, 0xce, 0x65, 0x24, 0xed, 0xa4, 0xe3,
|
||||||
|
0xff, 0xc9, 0x00, 0x00, 0x05, 0x5f, 0x4a, 0x04,
|
||||||
|
0x0e, 0xc4, 0x24, 0xfd, 0x5e, 0x4a, 0x35, 0x72,
|
||||||
|
0x21, 0x27, 0x31, 0x08, 0x47, 0x18, 0x00, 0x06,
|
||||||
|
0xc4, 0x02, 0x72, 0x81, 0x89, 0xc3, 0xe4, 0x0a,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
for _, ca := range cases {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
e := &Encoder{
|
||||||
|
SSRC: func() *uint32 {
|
||||||
|
v := uint32(0x9dbb7812)
|
||||||
|
return &v
|
||||||
|
}(),
|
||||||
|
InitialSequenceNumber: func() *uint16 {
|
||||||
|
v := uint16(0x44ed)
|
||||||
|
return &v
|
||||||
|
}(),
|
||||||
|
InitialTimestamp: func() *uint32 {
|
||||||
|
v := uint32(0x88776655)
|
||||||
|
return &v
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
e.Init()
|
||||||
|
|
||||||
|
pkts, err := e.Encode(ca.frames, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ca.pkt, pkts[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeRandomInitialState(t *testing.T) {
|
||||||
|
e := &Encoder{}
|
||||||
|
e.Init()
|
||||||
|
require.NotEqual(t, nil, e.SSRC)
|
||||||
|
require.NotEqual(t, nil, e.InitialSequenceNumber)
|
||||||
|
require.NotEqual(t, nil, e.InitialTimestamp)
|
||||||
|
}
|
2
pkg/formats/rtpmpeg2audio/rtpmpeg2audio.go
Normal file
2
pkg/formats/rtpmpeg2audio/rtpmpeg2audio.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package rtpmpeg2audio contains a RTP/MPEG2-audio decoder and encoder.
|
||||||
|
package rtpmpeg2audio
|
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/582528ddfad69eb5
vendored
Normal file
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/582528ddfad69eb5
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("0")
|
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/b456f27416f443a7
vendored
Normal file
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/b456f27416f443a7
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("\x00\x00\x00\x000")
|
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/d92bed38676468bc
vendored
Normal file
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/d92bed38676468bc
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("\x00\x00000")
|
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/f34630c44c11bb13
vendored
Normal file
2
pkg/formats/rtpmpeg2audio/testdata/fuzz/FuzzDecoder/f34630c44c11bb13
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("00000")
|
@@ -16,6 +16,7 @@ import (
|
|||||||
var ErrMorePacketsNeeded = errors.New("need more packets")
|
var ErrMorePacketsNeeded = errors.New("need more packets")
|
||||||
|
|
||||||
// Decoder is a RTP/MPEG4-audio decoder.
|
// Decoder is a RTP/MPEG4-audio decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc3640
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
// sample rate of input packets.
|
// sample rate of input packets.
|
||||||
SampleRate int
|
SampleRate int
|
||||||
@@ -73,7 +74,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
var aus [][]byte
|
var aus [][]byte
|
||||||
|
|
||||||
if len(d.fragments) == 0 {
|
if len(d.fragments) == 0 {
|
||||||
if pkt.Header.Marker {
|
if pkt.Marker {
|
||||||
// AUs
|
// AUs
|
||||||
aus = make([][]byte, len(dataLens))
|
aus = make([][]byte, len(dataLens))
|
||||||
for i, dataLen := range dataLens {
|
for i, dataLen := range dataLens {
|
||||||
@@ -117,7 +118,7 @@ func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, time.Duration, error) {
|
|||||||
|
|
||||||
d.fragments = append(d.fragments, payload[:dataLens[0]])
|
d.fragments = append(d.fragments, payload[:dataLens[0]])
|
||||||
|
|
||||||
if !pkt.Header.Marker {
|
if !pkt.Marker {
|
||||||
return nil, 0, ErrMorePacketsNeeded
|
return nil, 0, ErrMorePacketsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -109,7 +109,7 @@ func TestDecodeADTS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzDecoderUnmarshal(f *testing.F) {
|
func FuzzDecoder(f *testing.F) {
|
||||||
d := &Decoder{
|
d := &Decoder{
|
||||||
SampleRate: 16000,
|
SampleRate: 16000,
|
||||||
SizeLength: 13,
|
SizeLength: 13,
|
||||||
|
@@ -22,6 +22,7 @@ func randUint32() uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encoder is a RTP/MPEG4-audio encoder.
|
// Encoder is a RTP/MPEG4-audio encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc3640
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// payload type of packets.
|
// payload type of packets.
|
||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
|
@@ -22,6 +22,7 @@ var ErrNonStartingPacketAndNoPrevious = errors.New(
|
|||||||
"received a non-starting fragment without any previous starting fragment")
|
"received a non-starting fragment without any previous starting fragment")
|
||||||
|
|
||||||
// Decoder is a RTP/VP8 decoder.
|
// Decoder is a RTP/VP8 decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc7741
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstPacketReceived bool
|
firstPacketReceived bool
|
||||||
|
@@ -52,7 +52,7 @@ func TestDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzDecoderUnmarshal(f *testing.F) {
|
func FuzzDecoder(f *testing.F) {
|
||||||
d := &Decoder{}
|
d := &Decoder{}
|
||||||
d.Init()
|
d.Init()
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ func randUint32() uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encoder is a RTP/VP8 encoder.
|
// Encoder is a RTP/VP8 encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/rfc7741
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// payload type of packets.
|
// payload type of packets.
|
||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
|
@@ -22,6 +22,7 @@ var ErrNonStartingPacketAndNoPrevious = errors.New(
|
|||||||
"received a non-starting fragment without any previous starting fragment")
|
"received a non-starting fragment without any previous starting fragment")
|
||||||
|
|
||||||
// Decoder is a RTP/VP9 decoder.
|
// Decoder is a RTP/VP9 decoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
timeDecoder *rtptime.Decoder
|
timeDecoder *rtptime.Decoder
|
||||||
firstPacketReceived bool
|
firstPacketReceived bool
|
||||||
|
@@ -52,7 +52,7 @@ func TestDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzDecoderUnmarshal(f *testing.F) {
|
func FuzzDecoder(f *testing.F) {
|
||||||
d := &Decoder{}
|
d := &Decoder{}
|
||||||
d.Init()
|
d.Init()
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ func randUint32() uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encoder is a RTP/VP9 encoder.
|
// Encoder is a RTP/VP9 encoder.
|
||||||
|
// Specification: https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// payload type of packets.
|
// payload type of packets.
|
||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
|
Reference in New Issue
Block a user