diff --git a/README.md b/README.md index 9edcf47d..850b0bf8 100644 --- a/README.md +++ b/README.md @@ -106,11 +106,11 @@ Features: ## RTP Payload Formats -In RTSP, media streams are transmitted by using RTP packets, which are encoded in a specific, codec-dependent, format. This library supports formats for the following codecs: +In RTSP, media streams are transmitted by using RTP packets, which are encoded in a specific, codec-dependent, format. This library supports the following formats: ### Video -|codec|documentation|encoder and decoder available| +|format|documentation|encoder and decoder available| |------|-------------|-----------------------------| |AV1|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#AV1)|:heavy_check_mark:| |VP9|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#VP9)|:heavy_check_mark:| @@ -123,11 +123,12 @@ In RTSP, media streams are transmitted by using RTP packets, which are encoded i ### Audio -|codec|documentation|encoder and decoder available| +|format|documentation|encoder and decoder available| |------|-------------|-----------------------------| |Opus|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Opus)|:heavy_check_mark:| |Vorbis|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Vorbis)|| |MPEG-4 Audio (AAC)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG4Audio)|:heavy_check_mark:| +|MPEG-4 Audio LATM (AAC-LATM)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG4AudioLATM)|:heavy_check_mark:| |MPEG-1/2 Audio (MP3)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG1Audio)|:heavy_check_mark:| |AC-3|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#AC3)|:heavy_check_mark:| |Speex|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Speex)|| @@ -138,7 +139,7 @@ In RTSP, media streams are transmitted by using RTP packets, which are encoded i ### Other -|codec|documentation|encoder and decoder available| +|format|documentation|encoder and decoder available| |------|-------------|-----------------------------| |MPEG-TS|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEGTS)|| |KLV|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#KLV)|:heavy_check_mark:| @@ -165,7 +166,7 @@ In RTSP, media streams are transmitted by using RTP packets, which are encoded i |[Multiopus in libwebrtc](https://webrtc-review.googlesource.com/c/src/+/129768)|payload formats / Opus| |[RFC5215, RTP Payload Format for Vorbis Encoded Audio](https://datatracker.ietf.org/doc/html/rfc5215)|payload formats / Vorbis| |[RFC4184, RTP Payload Format for AC-3 Audio](https://datatracker.ietf.org/doc/html/rfc4184)|payload formats / AC-3| -|[RFC6416, RTP Payload Format for MPEG-4 Audio/Visual Streams](https://datatracker.ietf.org/doc/html/rfc6416)|payload formats / MPEG-4 audio| +|[RFC6416, RTP Payload Format for MPEG-4 Audio/Visual Streams](https://datatracker.ietf.org/doc/html/rfc6416)|payload formats / MPEG-4 audio LATM| |[RFC5574, RTP Payload Format for the Speex Codec](https://datatracker.ietf.org/doc/html/rfc5574)|payload formats / Speex| |[RFC3551, RTP Profile for Audio and Video Conferences with Minimal Control](https://datatracker.ietf.org/doc/html/rfc3551)|payload formats / G726, G722, G711, LPCM| |[RFC3190, RTP Payload Format for 12-bit DAT Audio and 20- and 24-bit Linear Sampled Audio](https://datatracker.ietf.org/doc/html/rfc3190)|payload formats / LPCM| diff --git a/client_play_test.go b/client_play_test.go index 03d4ff67..a997188b 100644 --- a/client_play_test.go +++ b/client_play_test.go @@ -122,7 +122,7 @@ func TestClientPlayFormats(t *testing.T) { Type: description.MediaTypeAudio, Formats: []format.Format{&format.MPEG4Audio{ PayloadTyp: 96, - Config: &mpeg4audio.Config{ + Config: &mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 44100, ChannelCount: 2, @@ -137,7 +137,7 @@ func TestClientPlayFormats(t *testing.T) { Type: description.MediaTypeAudio, Formats: []format.Format{&format.MPEG4Audio{ PayloadTyp: 96, - Config: &mpeg4audio.Config{ + Config: &mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 96000, ChannelCount: 2, diff --git a/examples/client-record-format-h264-from-disk/main.go b/examples/client-record-format-h264-from-disk/main.go index bba57afb..c2ec2b20 100644 --- a/examples/client-record-format-h264-from-disk/main.go +++ b/examples/client-record-format-h264-from-disk/main.go @@ -10,7 +10,6 @@ import ( "os" "time" - "github.com/asticode/go-astits" "github.com/bluenviron/gortsplib/v4" "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/format" @@ -149,7 +148,7 @@ func main() { err := r.Read() if err != nil { // file has ended - if errors.Is(err, astits.ErrNoMorePackets) { + if errors.Is(err, io.EOF) { log.Printf("file has ended, rewinding") // rewind to start position diff --git a/examples/server-play-format-h264-from-disk/file_streamer.go b/examples/server-play-format-h264-from-disk/file_streamer.go index b63c8e3f..6f507ad9 100644 --- a/examples/server-play-format-h264-from-disk/file_streamer.go +++ b/examples/server-play-format-h264-from-disk/file_streamer.go @@ -9,7 +9,6 @@ import ( "os" "time" - "github.com/asticode/go-astits" "github.com/bluenviron/gortsplib/v4" "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts" @@ -138,7 +137,7 @@ func (r *fileStreamer) run() { err := mr.Read() if err != nil { // file has ended - if errors.Is(err, astits.ErrNoMorePackets) { + if errors.Is(err, io.EOF) { log.Printf("file has ended, rewinding") // rewind to start position diff --git a/go.mod b/go.mod index c92541f0..3b8ecf41 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module github.com/bluenviron/gortsplib/v4 go 1.23.0 require ( - github.com/asticode/go-astits v1.13.0 - github.com/bluenviron/mediacommon/v2 v2.3.0 + github.com/bluenviron/mediacommon/v2 v2.3.1-0.20250720151422-8fb6595e0eac github.com/google/uuid v1.6.0 github.com/pion/rtcp v1.2.15 github.com/pion/rtp v1.8.21 @@ -16,6 +15,7 @@ require ( require ( github.com/asticode/go-astikit v0.30.0 // indirect + github.com/asticode/go-astits v1.13.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pion/logging v0.2.3 // indirect github.com/pion/randutil v0.1.0 // indirect diff --git a/go.sum b/go.sum index 10c8e528..e5dc5a4d 100644 --- a/go.sum +++ b/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-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= -github.com/bluenviron/mediacommon/v2 v2.3.0 h1:FigBBZi0UUFNhvvcQi1n9vd5uZKts2Uv1Ramq1VFUV8= -github.com/bluenviron/mediacommon/v2 v2.3.0/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GYLLrX2R7ZkUE+1pwV0= +github.com/bluenviron/mediacommon/v2 v2.3.1-0.20250720151422-8fb6595e0eac h1:EgZce5Y9ZV6lcvc6p9ZqywjJKkCdpx4V0h5sAgHrLGo= +github.com/bluenviron/mediacommon/v2 v2.3.1-0.20250720151422-8fb6595e0eac/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GYLLrX2R7ZkUE+1pwV0= 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= diff --git a/pkg/format/format.go b/pkg/format/format.go index 7d2f19ae..9f792cc1 100644 --- a/pkg/format/format.go +++ b/pkg/format/format.go @@ -160,9 +160,12 @@ func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error) case codec == "vorbis" && payloadType >= 96 && payloadType <= 127: return &Vorbis{} - case (codec == "mpeg4-generic" || codec == "mp4a-latm") && payloadType >= 96 && payloadType <= 127: + case codec == "mpeg4-generic" && payloadType >= 96 && payloadType <= 127: return &MPEG4Audio{} + case codec == "mp4a-latm" && payloadType >= 96 && payloadType <= 127: + return &MPEG4AudioLATM{} + case codec == "ac3" && payloadType >= 96 && payloadType <= 127: return &AC3{} diff --git a/pkg/format/format_test.go b/pkg/format/format_test.go index 17f02509..b25b1a5f 100644 --- a/pkg/format/format_test.go +++ b/pkg/format/format_test.go @@ -282,7 +282,7 @@ var casesFormat = []struct { &MPEG4Audio{ PayloadTyp: 96, ProfileLevelID: 1, - Config: &mpeg4audio.Config{ + Config: &mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 48000, ChannelCount: 2, @@ -314,7 +314,7 @@ var casesFormat = []struct { &MPEG4Audio{ PayloadTyp: 96, ProfileLevelID: 1, - Config: &mpeg4audio.Config{ + Config: &mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 48000, ChannelCount: 2, @@ -346,7 +346,7 @@ var casesFormat = []struct { &MPEG4Audio{ PayloadTyp: 96, ProfileLevelID: 14, - Config: &mpeg4audio.Config{ + Config: &mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 48000, ChannelCount: 2, @@ -374,7 +374,7 @@ var casesFormat = []struct { &MPEG4Audio{ PayloadTyp: 96, ProfileLevelID: 48, - Config: &mpeg4audio.Config{ + Config: &mpeg4audio.AudioSpecificConfig{ Type: 2, ExtensionType: 29, ExtensionSampleRate: 48000, @@ -401,8 +401,7 @@ var casesFormat = []struct { "a=rtpmap:96 MP4A-LATM/24000/2\n" + "a=fmtp:96 profile-level-id=1; " + "bitrate=64000; cpresent=0; object=2; config=400026203fc0\n", - &MPEG4Audio{ - LATM: true, + &MPEG4AudioLATM{ PayloadTyp: 96, ProfileLevelID: 1, Bitrate: intPtr(64000), @@ -410,7 +409,7 @@ var casesFormat = []struct { StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{ - AudioSpecificConfig: &mpeg4audio.Config{ + AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{ Type: 2, SampleRate: 24000, ChannelCount: 2, @@ -438,8 +437,7 @@ var casesFormat = []struct { "a=rtpmap:110 MP4A-LATM/24000/1\n" + "a=fmtp:110 profile-level-id=15; " + "cpresent=0; object=2; config=400026103fc0; sbr-enabled=1\n", - &MPEG4Audio{ - LATM: true, + &MPEG4AudioLATM{ PayloadTyp: 110, ProfileLevelID: 15, CPresent: false, @@ -447,7 +445,7 @@ var casesFormat = []struct { StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{ - AudioSpecificConfig: &mpeg4audio.Config{ + AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{ Type: 2, SampleRate: 24000, ChannelCount: 1, @@ -475,8 +473,7 @@ var casesFormat = []struct { "a=rtpmap:110 MP4A-LATM/48000/2\n" + "a=fmtp:110 profile-level-id=44; " + "bitrate=64000; cpresent=0; config=40005623101fe0; sbr-enabled=1\n", - &MPEG4Audio{ - LATM: true, + &MPEG4AudioLATM{ PayloadTyp: 110, ProfileLevelID: 44, CPresent: false, @@ -485,7 +482,7 @@ var casesFormat = []struct { StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{ - AudioSpecificConfig: &mpeg4audio.Config{ + AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{ Type: 2, ExtensionType: 5, ExtensionSampleRate: 48000, @@ -516,8 +513,7 @@ var casesFormat = []struct { "a=rtpmap:110 MP4A-LATM/48000/2\n" + "a=fmtp:110 profile-level-id=48; " + "bitrate=64000; cpresent=0; config=4001d613101fe0\n", - &MPEG4Audio{ - LATM: true, + &MPEG4AudioLATM{ PayloadTyp: 110, ProfileLevelID: 48, Bitrate: intPtr(64000), @@ -525,7 +521,7 @@ var casesFormat = []struct { StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{ - AudioSpecificConfig: &mpeg4audio.Config{ + AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{ Type: 2, ExtensionType: 29, ExtensionSampleRate: 48000, @@ -555,15 +551,14 @@ var casesFormat = []struct { "a=rtpmap:110 MP4A-LATM/48000\n" + "a=fmtp:110 profile-level-id=30; " + "cpresent=0; config=40002310\n", - &MPEG4Audio{ - LATM: true, + &MPEG4AudioLATM{ PayloadTyp: 110, ProfileLevelID: 30, CPresent: false, StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{ - AudioSpecificConfig: &mpeg4audio.Config{ + AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{ Type: 2, SampleRate: 48000, ChannelCount: 1, @@ -587,16 +582,15 @@ var casesFormat = []struct { "v=0\n" + "s=\n" + "m=audio 0 RTP/AVP 96\n" + - "a=rtpmap:96 MP4A-LATM/48000/2\n" + + "a=rtpmap:96 MP4A-LATM/90000/1\n" + "a=fmtp:96 cpresent=1\n", - &MPEG4Audio{ + &MPEG4AudioLATM{ PayloadTyp: 96, - LATM: true, ProfileLevelID: 30, CPresent: true, }, 96, - "MP4A-LATM/16000/1", + "MP4A-LATM/90000/1", map[string]string{ "cpresent": "1", "profile-level-id": "30", diff --git a/pkg/format/mpeg4_audio.go b/pkg/format/mpeg4_audio.go index aeea28dd..1532277d 100644 --- a/pkg/format/mpeg4_audio.go +++ b/pkg/format/mpeg4_audio.go @@ -14,153 +14,83 @@ import ( // MPEG4Audio is the RTP format for a MPEG-4 Audio codec. // Specification: RFC3640 -// Specification: RFC6416, section 7.3 type MPEG4Audio struct { // payload type of packets. PayloadTyp uint8 - // use LATM format (RFC6416) instead of generic format (RFC3640). - LATM bool - - // profile level ID. - ProfileLevelID int - - // generic only - Config *mpeg4audio.Config + ProfileLevelID int + Config *mpeg4audio.AudioSpecificConfig SizeLength int IndexLength int IndexDeltaLength int - - // LATM only - Bitrate *int - CPresent bool - StreamMuxConfig *mpeg4audio.StreamMuxConfig - SBREnabled *bool } func (f *MPEG4Audio) unmarshal(ctx *unmarshalContext) error { f.PayloadTyp = ctx.payloadType - f.LATM = (ctx.codec != "mpeg4-generic") - if !f.LATM { - for key, val := range ctx.fmtp { - switch key { - case "streamtype": - if val != "5" { // AudioStream in ISO 14496-1 - return fmt.Errorf("streamtype of AAC must be 5") - } - - case "mode": - if strings.ToLower(val) != "aac-hbr" && strings.ToLower(val) != "aac_hbr" { - return fmt.Errorf("unsupported AAC mode: %v", val) - } - - case "profile-level-id": - tmp, err := strconv.ParseUint(val, 10, 31) - if err != nil { - return fmt.Errorf("invalid profile-level-id: %v", val) - } - - f.ProfileLevelID = int(tmp) - - case "config": - enc, err := hex.DecodeString(val) - if err != nil { - return fmt.Errorf("invalid AAC config: %w", err) - } - - f.Config = &mpeg4audio.Config{} - err = f.Config.Unmarshal(enc) - if err != nil { - return fmt.Errorf("invalid AAC config: %w", err) - } - - case "sizelength": - n, err := strconv.ParseUint(val, 10, 31) - if err != nil || n > 100 { - return fmt.Errorf("invalid AAC SizeLength: %v", val) - } - f.SizeLength = int(n) - - case "indexlength": - n, err := strconv.ParseUint(val, 10, 31) - if err != nil || n > 100 { - return fmt.Errorf("invalid AAC IndexLength: %v", val) - } - f.IndexLength = int(n) - - case "indexdeltalength": - n, err := strconv.ParseUint(val, 10, 31) - if err != nil || n > 100 { - return fmt.Errorf("invalid AAC IndexDeltaLength: %v", val) - } - f.IndexDeltaLength = int(n) + for key, val := range ctx.fmtp { + switch key { + case "streamtype": + if val != "5" { // AudioStream in ISO 14496-1 + return fmt.Errorf("streamtype of AAC must be 5") } - } - if f.Config == nil { - return fmt.Errorf("config is missing") - } - - if f.SizeLength == 0 { - return fmt.Errorf("sizelength is missing") - } - } else { - // default value set by specification - f.ProfileLevelID = 30 - f.CPresent = true - - for key, val := range ctx.fmtp { - switch key { - case "profile-level-id": - tmp, err := strconv.ParseUint(val, 10, 31) - if err != nil { - return fmt.Errorf("invalid profile-level-id: %v", val) - } - - f.ProfileLevelID = int(tmp) - - case "bitrate": - tmp, err := strconv.ParseUint(val, 10, 31) - if err != nil { - return fmt.Errorf("invalid bitrate: %v", val) - } - - v := int(tmp) - f.Bitrate = &v - - case "cpresent": - f.CPresent = (val == "1") - - case "config": - enc, err := hex.DecodeString(val) - if err != nil { - return fmt.Errorf("invalid AAC config: %w", err) - } - - f.StreamMuxConfig = &mpeg4audio.StreamMuxConfig{} - err = f.StreamMuxConfig.Unmarshal(enc) - if err != nil { - return fmt.Errorf("invalid AAC config: %w", err) - } - - case "sbr-enabled": - v := (val == "1") - f.SBREnabled = &v + case "mode": + if strings.ToLower(val) != "aac-hbr" && strings.ToLower(val) != "aac_hbr" { + return fmt.Errorf("unsupported AAC mode: %v", val) } - } - if f.CPresent { - if f.StreamMuxConfig != nil { - return fmt.Errorf("config and cpresent can't be used at the same time") + case "profile-level-id": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid profile-level-id: %v", val) } - } else { - if f.StreamMuxConfig == nil { - return fmt.Errorf("config is missing") + + f.ProfileLevelID = int(tmp) + + case "config": + enc, err := hex.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid AAC config: %v", val) } + + f.Config = &mpeg4audio.AudioSpecificConfig{} + err = f.Config.Unmarshal(enc) + if err != nil { + return fmt.Errorf("invalid AAC config: %v", val) + } + + case "sizelength": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil || n > 100 { + return fmt.Errorf("invalid AAC SizeLength: %v", val) + } + f.SizeLength = int(n) + + case "indexlength": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil || n > 100 { + return fmt.Errorf("invalid AAC IndexLength: %v", val) + } + f.IndexLength = int(n) + + case "indexdeltalength": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil || n > 100 { + return fmt.Errorf("invalid AAC IndexDeltaLength: %v", val) + } + f.IndexDeltaLength = int(n) } } + if f.Config == nil { + return fmt.Errorf("config is missing") + } + + if f.SizeLength == 0 { + return fmt.Errorf("sizelength is missing") + } + return nil } @@ -171,13 +101,7 @@ func (f *MPEG4Audio) Codec() string { // ClockRate implements Format. func (f *MPEG4Audio) ClockRate() int { - if !f.LATM { - return f.Config.SampleRate - } - if f.CPresent { - return 16000 - } - return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.SampleRate + return f.Config.SampleRate } // PayloadType implements Format. @@ -187,107 +111,52 @@ func (f *MPEG4Audio) PayloadType() uint8 { // RTPMap implements Format. func (f *MPEG4Audio) RTPMap() string { - if !f.LATM { - sampleRate := f.Config.SampleRate - if f.Config.ExtensionSampleRate != 0 { - sampleRate = f.Config.ExtensionSampleRate - } - - channelCount := f.Config.ChannelCount - if f.Config.ExtensionType == mpeg4audio.ObjectTypePS { - channelCount = 2 - } - - return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + - "/" + strconv.FormatInt(int64(channelCount), 10) + sampleRate := f.Config.SampleRate + if f.Config.ExtensionSampleRate != 0 { + sampleRate = f.Config.ExtensionSampleRate } - if f.CPresent { - return "MP4A-LATM/16000/1" - } - - aoc := f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig - - sampleRate := aoc.SampleRate - if aoc.ExtensionSampleRate != 0 { - sampleRate = aoc.ExtensionSampleRate - } - - channelCount := aoc.ChannelCount - if aoc.ExtensionType == mpeg4audio.ObjectTypePS { + channelCount := f.Config.ChannelCount + if f.Config.ExtensionType == mpeg4audio.ObjectTypePS { channelCount = 2 } - return "MP4A-LATM/" + strconv.FormatInt(int64(sampleRate), 10) + + return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + "/" + strconv.FormatInt(int64(channelCount), 10) } // FMTP implements Format. func (f *MPEG4Audio) FMTP() map[string]string { - if !f.LATM { - enc, err := f.Config.Marshal() - if err != nil { - return nil - } + enc, err := f.Config.Marshal() + if err != nil { + return nil + } - profileLevelID := f.ProfileLevelID - if profileLevelID == 0 { // support legacy definition which didn't include profile-level-id - profileLevelID = 1 - } - - fmtp := map[string]string{ - "streamtype": "5", - "mode": "AAC-hbr", - "profile-level-id": strconv.FormatInt(int64(profileLevelID), 10), - } - - if f.SizeLength > 0 { - fmtp["sizelength"] = strconv.FormatInt(int64(f.SizeLength), 10) - } - - if f.IndexLength > 0 { - fmtp["indexlength"] = strconv.FormatInt(int64(f.IndexLength), 10) - } - - if f.IndexDeltaLength > 0 { - fmtp["indexdeltalength"] = strconv.FormatInt(int64(f.IndexDeltaLength), 10) - } - - fmtp["config"] = hex.EncodeToString(enc) - - return fmtp + profileLevelID := f.ProfileLevelID + if profileLevelID == 0 { // support legacy definition which didn't include profile-level-id + profileLevelID = 1 } fmtp := map[string]string{ - "profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10), + "streamtype": "5", + "mode": "AAC-hbr", + "profile-level-id": strconv.FormatInt(int64(profileLevelID), 10), } - if f.Bitrate != nil { - fmtp["bitrate"] = strconv.FormatInt(int64(*f.Bitrate), 10) + if f.SizeLength > 0 { + fmtp["sizelength"] = strconv.FormatInt(int64(f.SizeLength), 10) } - if f.CPresent { - fmtp["cpresent"] = "1" - } else { - fmtp["cpresent"] = "0" - - enc, err := f.StreamMuxConfig.Marshal() - if err != nil { - return nil - } - - fmtp["config"] = hex.EncodeToString(enc) - fmtp["object"] = strconv.FormatInt(int64(f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.Type), 10) + if f.IndexLength > 0 { + fmtp["indexlength"] = strconv.FormatInt(int64(f.IndexLength), 10) } - if f.SBREnabled != nil { - if *f.SBREnabled { - fmtp["SBR-enabled"] = "1" - } else { - fmtp["SBR-enabled"] = "0" - } + if f.IndexDeltaLength > 0 { + fmtp["indexdeltalength"] = strconv.FormatInt(int64(f.IndexDeltaLength), 10) } + fmtp["config"] = hex.EncodeToString(enc) + return fmtp } @@ -299,7 +168,6 @@ func (f *MPEG4Audio) PTSEqualsDTS(*rtp.Packet) bool { // CreateDecoder creates a decoder able to decode the content of the format. func (f *MPEG4Audio) CreateDecoder() (*rtpmpeg4audio.Decoder, error) { d := &rtpmpeg4audio.Decoder{ - LATM: f.LATM, SizeLength: f.SizeLength, IndexLength: f.IndexLength, IndexDeltaLength: f.IndexDeltaLength, @@ -316,7 +184,6 @@ func (f *MPEG4Audio) CreateDecoder() (*rtpmpeg4audio.Decoder, error) { // CreateEncoder creates an encoder able to encode the content of the format. func (f *MPEG4Audio) CreateEncoder() (*rtpmpeg4audio.Encoder, error) { e := &rtpmpeg4audio.Encoder{ - LATM: f.LATM, PayloadType: f.PayloadTyp, SizeLength: f.SizeLength, IndexLength: f.IndexLength, @@ -332,12 +199,8 @@ func (f *MPEG4Audio) CreateEncoder() (*rtpmpeg4audio.Encoder, error) { } // GetConfig returns the MPEG-4 Audio configuration. +// +// Deprecated: redundant. Use f.Config. func (f *MPEG4Audio) GetConfig() *mpeg4audio.Config { - if !f.LATM { - return f.Config - } - if f.CPresent { - return nil - } - return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig + return f.Config } diff --git a/pkg/format/mpeg4_audio_latm.go b/pkg/format/mpeg4_audio_latm.go new file mode 100644 index 00000000..d6a5c4cd --- /dev/null +++ b/pkg/format/mpeg4_audio_latm.go @@ -0,0 +1,223 @@ +package format + +import ( + "encoding/hex" + "fmt" + "strconv" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpfragmented" +) + +func allLayersHaveSameTypeRateChannelsExtType(c *mpeg4audio.StreamMuxConfig) bool { + typ := c.Programs[0].Layers[0].AudioSpecificConfig.Type + rate := c.Programs[0].Layers[0].AudioSpecificConfig.SampleRate + channels := c.Programs[0].Layers[0].AudioSpecificConfig.ChannelCount + extensionType := c.Programs[0].Layers[0].AudioSpecificConfig.ExtensionType + + for i, p := range c.Programs { + for j, l := range p.Layers { + if i == 0 && j == 0 { + continue + } + + if l.AudioSpecificConfig.Type != typ || + l.AudioSpecificConfig.SampleRate != rate || + l.AudioSpecificConfig.ChannelCount != channels || + l.AudioSpecificConfig.ExtensionType != extensionType { + return false + } + } + } + + return true +} + +// MPEG4AudioLATM is the RTP format for a MPEG-4 Audio codec, LATM-encoded. +// Specification: RFC6416, section 7.3 +type MPEG4AudioLATM struct { + // payload type of packets. + PayloadTyp uint8 + + ProfileLevelID int + Bitrate *int + CPresent bool + StreamMuxConfig *mpeg4audio.StreamMuxConfig + SBREnabled *bool +} + +func (f *MPEG4AudioLATM) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + // default value set by specification + f.ProfileLevelID = 30 + f.CPresent = true + + for key, val := range ctx.fmtp { + switch key { + case "profile-level-id": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid profile-level-id: %v", val) + } + + f.ProfileLevelID = int(tmp) + + case "bitrate": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid bitrate: %v", val) + } + + v := int(tmp) + f.Bitrate = &v + + case "cpresent": + f.CPresent = (val == "1") + + case "config": + enc, err := hex.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid AAC config: %v", val) + } + + f.StreamMuxConfig = &mpeg4audio.StreamMuxConfig{} + err = f.StreamMuxConfig.Unmarshal(enc) + if err != nil { + return fmt.Errorf("invalid AAC config: %w", err) + } + + case "sbr-enabled": + v := (val == "1") + f.SBREnabled = &v + } + } + + if f.CPresent { + if f.StreamMuxConfig != nil { + return fmt.Errorf("config and cpresent can't be used at the same time") + } + + if ctx.clock != "90000/1" { + return fmt.Errorf("when cpresent=1, clock rate must be 90000/1, but is %s", ctx.clock) + } + } else { + if f.StreamMuxConfig == nil { + return fmt.Errorf("config is missing") + } + + if !allLayersHaveSameTypeRateChannelsExtType(f.StreamMuxConfig) { + return fmt.Errorf("all LATM layers must have the same type, rate, channel count, extension type") + } + } + return nil +} + +// Codec implements Format. +func (f *MPEG4AudioLATM) Codec() string { + return "MPEG-4 Audio LATM" +} + +// ClockRate implements Format. +func (f *MPEG4AudioLATM) ClockRate() int { + if f.CPresent { + return 90000 + } + + return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.SampleRate +} + +// PayloadType implements Format. +func (f *MPEG4AudioLATM) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *MPEG4AudioLATM) RTPMap() string { + if f.CPresent { + return "MP4A-LATM/90000/1" + } + + aoc := f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig + + sampleRate := aoc.SampleRate + if aoc.ExtensionSampleRate != 0 { + sampleRate = aoc.ExtensionSampleRate + } + + channelCount := aoc.ChannelCount + if aoc.ExtensionType == mpeg4audio.ObjectTypePS { + channelCount = 2 + } + + return "MP4A-LATM/" + strconv.FormatInt(int64(sampleRate), 10) + + "/" + strconv.FormatInt(int64(channelCount), 10) +} + +// FMTP implements Format. +func (f *MPEG4AudioLATM) FMTP() map[string]string { + fmtp := map[string]string{ + "profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10), + } + + if f.Bitrate != nil { + fmtp["bitrate"] = strconv.FormatInt(int64(*f.Bitrate), 10) + } + + if f.CPresent { + fmtp["cpresent"] = "1" + } else { + fmtp["cpresent"] = "0" + + enc, err := f.StreamMuxConfig.Marshal() + if err != nil { + return nil + } + + fmtp["config"] = hex.EncodeToString(enc) + fmtp["object"] = strconv.FormatInt(int64(f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.Type), 10) + } + + if f.SBREnabled != nil { + if *f.SBREnabled { + fmtp["SBR-enabled"] = "1" + } else { + fmtp["SBR-enabled"] = "0" + } + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *MPEG4AudioLATM) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *MPEG4AudioLATM) CreateDecoder() (*rtpfragmented.Decoder, error) { + d := &rtpfragmented.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *MPEG4AudioLATM) CreateEncoder() (*rtpfragmented.Encoder, error) { + e := &rtpfragmented.Encoder{ + PayloadType: f.PayloadTyp, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/pkg/format/mpeg4_audio_latm_test.go b/pkg/format/mpeg4_audio_latm_test.go new file mode 100644 index 00000000..8ae51c6a --- /dev/null +++ b/pkg/format/mpeg4_audio_latm_test.go @@ -0,0 +1,64 @@ +package format + +import ( + "testing" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" + "github.com/pion/rtp" + "github.com/stretchr/testify/require" +) + +func TestMPEG4AudioLATMAttributes(t *testing.T) { + format := &MPEG4AudioLATM{ + PayloadTyp: 96, + ProfileLevelID: 1, + StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ + Programs: []*mpeg4audio.StreamMuxConfigProgram{{ + Layers: []*mpeg4audio.StreamMuxConfigLayer{{ + AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + LatmBufferFullness: 255, + }}, + }}, + }, + } + require.Equal(t, "MPEG-4 Audio LATM", format.Codec()) + require.Equal(t, 44100, format.ClockRate()) + require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{})) +} + +func TestMPEG4AudioLATMDecEncoder(t *testing.T) { + format := &MPEG4AudioLATM{ + PayloadTyp: 96, + ProfileLevelID: 1, + StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ + Programs: []*mpeg4audio.StreamMuxConfigProgram{{ + Layers: []*mpeg4audio.StreamMuxConfigLayer{{ + AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{ + Type: 2, + SampleRate: 48000, + ChannelCount: 2, + }, + LatmBufferFullness: 255, + }}, + }}, + }, + } + + enc, err := format.CreateEncoder() + require.NoError(t, err) + + pkts, err := enc.Encode([]byte{0x01, 0x02, 0x03, 0x04}) + require.NoError(t, err) + require.Equal(t, format.PayloadType(), pkts[0].PayloadType) + + dec, err := format.CreateDecoder() + require.NoError(t, err) + + byts, err := dec.Decode(pkts[0]) + require.NoError(t, err) + require.Equal(t, []byte{0x01, 0x02, 0x03, 0x04}, byts) +} diff --git a/pkg/format/mpeg4_audio_test.go b/pkg/format/mpeg4_audio_test.go index 133b4c4c..72dba4c0 100644 --- a/pkg/format/mpeg4_audio_test.go +++ b/pkg/format/mpeg4_audio_test.go @@ -10,117 +10,51 @@ import ( ) func TestMPEG4AudioAttributes(t *testing.T) { - t.Run("generic", func(t *testing.T) { - format := &MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 48000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } - require.Equal(t, "MPEG-4 Audio", format.Codec()) - require.Equal(t, 48000, format.ClockRate()) - require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{})) - require.Equal(t, &mpeg4audio.Config{ + format := &MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 48000, ChannelCount: 2, - }, format.GetConfig()) - }) - - t.Run("latm", func(t *testing.T) { - format := &MPEG4Audio{ - LATM: true, - PayloadTyp: 96, - ProfileLevelID: 1, - StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ - Programs: []*mpeg4audio.StreamMuxConfigProgram{{ - Layers: []*mpeg4audio.StreamMuxConfigLayer{{ - AudioSpecificConfig: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - LatmBufferFullness: 255, - }}, - }}, - }, - } - require.Equal(t, "MPEG-4 Audio", format.Codec()) - require.Equal(t, 44100, format.ClockRate()) - require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{})) - require.Equal(t, &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, format.GetConfig()) - }) + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + } + require.Equal(t, "MPEG-4 Audio", format.Codec()) + require.Equal(t, 48000, format.ClockRate()) + require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{})) + require.Equal(t, &mpeg4audio.AudioSpecificConfig{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 48000, + ChannelCount: 2, + }, format.GetConfig()) } func TestMPEG4AudioDecEncoder(t *testing.T) { - t.Run("generic", func(t *testing.T) { - format := &MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 48000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } + format := &MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.AudioSpecificConfig{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 48000, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + } - enc, err := format.CreateEncoder() - require.NoError(t, err) + enc, err := format.CreateEncoder() + require.NoError(t, err) - pkts, err := enc.Encode([][]byte{{0x01, 0x02, 0x03, 0x04}}) - require.NoError(t, err) - require.Equal(t, format.PayloadType(), pkts[0].PayloadType) + pkts, err := enc.Encode([][]byte{{0x01, 0x02, 0x03, 0x04}}) + require.NoError(t, err) + require.Equal(t, format.PayloadType(), pkts[0].PayloadType) - dec, err := format.CreateDecoder() - require.NoError(t, err) + dec, err := format.CreateDecoder() + require.NoError(t, err) - byts, err := dec.Decode(pkts[0]) - require.NoError(t, err) - require.Equal(t, [][]byte{{0x01, 0x02, 0x03, 0x04}}, byts) - }) - - t.Run("latm", func(t *testing.T) { - format := &MPEG4Audio{ - LATM: true, - PayloadTyp: 96, - ProfileLevelID: 1, - StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ - Programs: []*mpeg4audio.StreamMuxConfigProgram{{ - Layers: []*mpeg4audio.StreamMuxConfigLayer{{ - AudioSpecificConfig: &mpeg4audio.Config{ - Type: 2, - SampleRate: 48000, - ChannelCount: 2, - }, - LatmBufferFullness: 255, - }}, - }}, - }, - } - - enc, err := format.CreateEncoder() - require.NoError(t, err) - - pkts, err := enc.Encode([][]byte{{0x01, 0x02, 0x03, 0x04}}) - require.NoError(t, err) - require.Equal(t, format.PayloadType(), pkts[0].PayloadType) - - dec, err := format.CreateDecoder() - require.NoError(t, err) - - byts, err := dec.Decode(pkts[0]) - require.NoError(t, err) - require.Equal(t, [][]byte{{0x01, 0x02, 0x03, 0x04}}, byts) - }) + byts, err := dec.Decode(pkts[0]) + require.NoError(t, err) + require.Equal(t, [][]byte{{0x01, 0x02, 0x03, 0x04}}, byts) } diff --git a/pkg/format/mpeg4_video.go b/pkg/format/mpeg4_video.go index 54462a67..babb0162 100644 --- a/pkg/format/mpeg4_video.go +++ b/pkg/format/mpeg4_video.go @@ -10,7 +10,7 @@ import ( "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video" "github.com/pion/rtp" - "github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video" + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpfragmented" ) // MPEG4Video is the RTP format for a MPEG-4 Video codec. @@ -93,8 +93,8 @@ func (f *MPEG4Video) PTSEqualsDTS(*rtp.Packet) bool { } // CreateDecoder creates a decoder able to decode the content of the format. -func (f *MPEG4Video) CreateDecoder() (*rtpmpeg4video.Decoder, error) { - d := &rtpmpeg4video.Decoder{} +func (f *MPEG4Video) CreateDecoder() (*rtpfragmented.Decoder, error) { + d := &rtpfragmented.Decoder{} err := d.Init() if err != nil { @@ -105,8 +105,8 @@ func (f *MPEG4Video) CreateDecoder() (*rtpmpeg4video.Decoder, error) { } // CreateEncoder creates an encoder able to encode the content of the format. -func (f *MPEG4Video) CreateEncoder() (*rtpmpeg4video.Encoder, error) { - e := &rtpmpeg4video.Encoder{ +func (f *MPEG4Video) CreateEncoder() (*rtpfragmented.Encoder, error) { + e := &rtpfragmented.Encoder{ PayloadType: f.PayloadTyp, } diff --git a/pkg/format/rtpfragmented/decoder.go b/pkg/format/rtpfragmented/decoder.go new file mode 100644 index 00000000..e7f598d7 --- /dev/null +++ b/pkg/format/rtpfragmented/decoder.go @@ -0,0 +1,81 @@ +package rtpfragmented + +import ( + "errors" + "fmt" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +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 decoder for codecs with access units that can be fragmented. +type Decoder struct { + fragments [][]byte + fragmentsSize int + fragmentNextSeqNum uint16 +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes a frame from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + var frame []byte + + if d.fragmentsSize == 0 { + if pkt.Marker { + frame = pkt.Payload + } else { + d.fragmentsSize = len(pkt.Payload) + d.fragments = append(d.fragments, pkt.Payload) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + } + } else { + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += len(pkt.Payload) + + if d.fragmentsSize > mpeg4video.MaxFrameSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d", + errSize, mpeg4video.MaxFrameSize) + } + + d.fragments = append(d.fragments, pkt.Payload) + d.fragmentNextSeqNum++ + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + frame = joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + } + + return frame, nil +} diff --git a/pkg/format/rtpmpeg4audio/decoder_latm_test.go b/pkg/format/rtpfragmented/decoder_test.go similarity index 55% rename from pkg/format/rtpmpeg4audio/decoder_latm_test.go rename to pkg/format/rtpfragmented/decoder_test.go index c219f707..748efac4 100644 --- a/pkg/format/rtpmpeg4audio/decoder_latm_test.go +++ b/pkg/format/rtpfragmented/decoder_test.go @@ -1,7 +1,6 @@ -package rtpmpeg4audio +package rtpfragmented import ( - "bytes" "errors" "testing" @@ -9,25 +8,17 @@ import ( "github.com/stretchr/testify/require" ) -func TestDecodeLATM(t *testing.T) { - for _, ca := range casesLATM { +func TestDecode(t *testing.T) { + for _, ca := range cases { t.Run(ca.name, func(t *testing.T) { - d := &Decoder{ - LATM: true, - } + d := &Decoder{} err := d.Init() require.NoError(t, err) - var aus [][]byte + var frame []byte for _, pkt := range ca.pkts { - clone := pkt.Clone() - - aus, err = d.Decode(pkt) - - // test input integrity - require.Equal(t, clone, pkt) - + frame, err = d.Decode(pkt) if errors.Is(err, ErrMorePacketsNeeded) { continue } @@ -35,37 +26,13 @@ func TestDecodeLATM(t *testing.T) { require.NoError(t, err) } - require.Equal(t, ca.au, aus[0]) + require.Equal(t, ca.frame, frame) }) } } -func TestDecodeLATMOtherData(t *testing.T) { - d := &Decoder{ - LATM: true, - } - err := d.Init() - require.NoError(t, err) - - aus, err := d.Decode(&rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 2646308882, - }, - Payload: []byte{ - 0x04, 0x01, 0x02, 0x03, 0x04, 5, 6, - }, - }) - require.NoError(t, err) - - require.Equal(t, []byte{1, 2, 3, 4}, aus[0]) -} - -func TestDecodeLATMErrorMissingPacket(t *testing.T) { - d := &Decoder{LATM: true} +func TestDecodeErrorMissingPacket(t *testing.T) { + d := &Decoder{} err := d.Init() require.NoError(t, err) @@ -77,12 +44,7 @@ func TestDecodeLATMErrorMissingPacket(t *testing.T) { SequenceNumber: 17645, SSRC: 0x9dbb7812, }, - Payload: mergeBytes( - bytes.Repeat([]byte{0xff}, 16), - []byte{0x10}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 180), - []byte{0, 1, 2}, - ), + Payload: []byte{0x01, 0x02, 0x03, 0x04}, }) require.Equal(t, ErrMorePacketsNeeded, err) @@ -94,20 +56,14 @@ func TestDecodeLATMErrorMissingPacket(t *testing.T) { SequenceNumber: 17647, SSRC: 0x9dbb7812, }, - Payload: mergeBytes( - []byte{3, 4, 5, 6, 7}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 181), - []byte{0, 1, 2, 3, 4, 5, 6}, - ), + Payload: []byte{0x01, 0x02, 0x03, 0x04}, }) require.EqualError(t, err, "discarding frame since a RTP packet is missing") } -func FuzzDecoderLATM(f *testing.F) { +func FuzzDecoder(f *testing.F) { f.Fuzz(func(t *testing.T, a []byte, am bool, b []byte, bm bool) { - d := &Decoder{ - LATM: true, - } + d := &Decoder{} err := d.Init() require.NoError(t, err) diff --git a/pkg/format/rtpfragmented/encoder.go b/pkg/format/rtpfragmented/encoder.go new file mode 100644 index 00000000..54c76c96 --- /dev/null +++ b/pkg/format/rtpfragmented/encoder.go @@ -0,0 +1,107 @@ +package rtpfragmented + +import ( + "crypto/rand" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1450 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) - 10 (SRTP overhead) +) + +func randUint32() (uint32, error) { + var b [4]byte + _, err := rand.Read(b[:]) + if err != nil { + return 0, err + } + return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil +} + +func packetCount(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +// Encoder is a RTP encoder for codecs with access units that can be fragmented. +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 + + // maximum size of packet payloads (optional). + // It defaults to 1450. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes a frame into RTP packets. +func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) { + avail := e.PayloadMaxSize + le := len(frame) + packetCount := packetCount(avail, le) + + ret := make([]*rtp.Packet, packetCount) + pos := 0 + le = avail + + for i := range ret { + if i == (packetCount - 1) { + le = len(frame[pos:]) + } + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: (i == packetCount-1), + }, + Payload: frame[pos : pos+le], + } + + pos += le + e.sequenceNumber++ + } + + return ret, nil +} diff --git a/pkg/format/rtpfragmented/encoder_test.go b/pkg/format/rtpfragmented/encoder_test.go new file mode 100644 index 00000000..d425d3c4 --- /dev/null +++ b/pkg/format/rtpfragmented/encoder_test.go @@ -0,0 +1,97 @@ +package rtpfragmented + +import ( + "bytes" + "testing" + + "github.com/pion/rtp" + "github.com/stretchr/testify/require" +) + +func uint16Ptr(v uint16) *uint16 { + return &v +} + +func uint32Ptr(v uint32) *uint32 { + return &v +} + +var cases = []struct { + name string + frame []byte + pkts []*rtp.Packet +}{ + { + "single", + []byte{0x01, 0x02, 0x03, 0x04}, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x01, 0x02, 0x03, 0x04, + }, + }, + }, + }, + { + "fragmented", + bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 150/4), + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: false, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 100/4), + }, + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17646, + SSRC: 0x9dbb7812, + }, + Payload: bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 50/4), + }, + }, + }, +} + +func TestEncode(t *testing.T) { + for _, ca := range cases { + t.Run(ca.name, func(t *testing.T) { + e := &Encoder{ + PayloadType: 96, + SSRC: uint32Ptr(0x9dbb7812), + InitialSequenceNumber: uint16Ptr(0x44ed), + PayloadMaxSize: 100, + } + err := e.Init() + require.NoError(t, err) + + pkts, err := e.Encode(ca.frame) + require.NoError(t, err) + require.Equal(t, ca.pkts, pkts) + }) + } +} + +func TestEncodeRandomInitialState(t *testing.T) { + e := &Encoder{ + PayloadType: 96, + } + err := e.Init() + require.NoError(t, err) + require.NotEqual(t, nil, e.SSRC) + require.NotEqual(t, nil, e.InitialSequenceNumber) +} diff --git a/pkg/format/rtpfragmented/rtpfragmented.go b/pkg/format/rtpfragmented/rtpfragmented.go new file mode 100644 index 00000000..95b560f7 --- /dev/null +++ b/pkg/format/rtpfragmented/rtpfragmented.go @@ -0,0 +1,2 @@ +// Package rtpfragmented contains a RTP decoder and encoder for codecs with access units that can be fragmented. +package rtpfragmented diff --git a/pkg/format/rtpmpeg4audio/decoder.go b/pkg/format/rtpmpeg4audio/decoder.go index 4681fd82..976d04c8 100644 --- a/pkg/format/rtpmpeg4audio/decoder.go +++ b/pkg/format/rtpmpeg4audio/decoder.go @@ -2,7 +2,10 @@ package rtpmpeg4audio import ( "errors" + "fmt" + "github.com/bluenviron/mediacommon/v2/pkg/bits" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" "github.com/pion/rtp" ) @@ -20,16 +23,13 @@ func joinFragments(fragments [][]byte, size int) []byte { // Decoder is a RTP/MPEG-4 Audio decoder. // Specification: RFC3640 -// Specification: RFC6416, section 7.3 type Decoder struct { - // use RFC6416 (LATM) instead of RFC3640 (generic). - LATM bool - - // Generic-only // The number of bits in which the AU-size field is encoded in the AU-header. SizeLength int + // The number of bits in which the AU-Index is encoded in the first AU-header. IndexLength int + // The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header. IndexDeltaLength int @@ -37,7 +37,6 @@ type Decoder struct { adtsMode bool fragments [][]byte fragmentsSize int - fragmentsExpected int fragmentNextSeqNum uint16 } @@ -51,10 +50,195 @@ func (d *Decoder) resetFragments() { d.fragmentsSize = 0 } -// Decode decodes AUs from a RTP packet. +// Decode decodes AUs (non-LATM) or audioMuxElements (LATM) from a RTP packet. func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) { - if !d.LATM { - return d.decodeGeneric(pkt) + if len(pkt.Payload) < 2 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") } - return d.decodeLATM(pkt) + + // AU-headers-length (16 bits) + headersLen := int(uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1])) + if headersLen == 0 { + d.resetFragments() + return nil, fmt.Errorf("invalid AU-headers-length") + } + payload := pkt.Payload[2:] + + // AU-headers + dataLens, err := d.readAUHeaders(payload, headersLen) + if err != nil { + d.resetFragments() + return nil, err + } + + pos := (headersLen / 8) + if (headersLen % 8) != 0 { + pos++ + } + payload = payload[pos:] + + var aus [][]byte + + if d.fragmentsSize == 0 { + d.resetFragments() + + if pkt.Marker { + // AUs + aus = make([][]byte, len(dataLens)) + for i, dataLen := range dataLens { + if len(payload) < int(dataLen) { + return nil, fmt.Errorf("payload is too short") + } + + aus[i] = payload[:dataLen] + payload = payload[dataLen:] + } + } else { + if len(dataLens) != 1 { + return nil, fmt.Errorf("a fragmented packet can only contain one AU") + } + + if len(payload) < int(dataLens[0]) { + return nil, fmt.Errorf("payload is too short") + } + + d.fragmentsSize = int(dataLens[0]) + d.fragments = append(d.fragments, payload[:dataLens[0]]) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + } + } else { + // we are decoding a fragmented AU + if len(dataLens) != 1 { + d.resetFragments() + return nil, fmt.Errorf("a fragmented packet can only contain one AU") + } + + if len(payload) < int(dataLens[0]) { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += int(dataLens[0]) + + if d.fragmentsSize > mpeg4audio.MaxAccessUnitSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d", + errSize, mpeg4audio.MaxAccessUnitSize) + } + + d.fragments = append(d.fragments, payload[:dataLens[0]]) + d.fragmentNextSeqNum++ + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + aus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} + d.resetFragments() + } + + return d.removeADTS(aus) +} + +func (d *Decoder) readAUHeaders(buf []byte, headersLen int) ([]uint64, error) { + firstRead := false + + count := 0 + for i := 0; i < headersLen; { + if i == 0 { + i += d.SizeLength + i += d.IndexLength + } else { + i += d.SizeLength + i += d.IndexDeltaLength + } + count++ + } + + dataLens := make([]uint64, count) + + pos := 0 + i := 0 + + for headersLen > 0 { + dataLen, err := bits.ReadBits(buf, &pos, d.SizeLength) + if err != nil { + return nil, err + } + headersLen -= d.SizeLength + + if !firstRead { + firstRead = true + if d.IndexLength > 0 { + auIndex, err := bits.ReadBits(buf, &pos, d.IndexLength) + if err != nil { + return nil, err + } + headersLen -= d.IndexLength + + if auIndex != 0 { + return nil, fmt.Errorf("AU-index different than zero is not supported") + } + } + } else if d.IndexDeltaLength > 0 { + auIndexDelta, err := bits.ReadBits(buf, &pos, d.IndexDeltaLength) + if err != nil { + return nil, err + } + headersLen -= d.IndexDeltaLength + + if auIndexDelta != 0 { + return nil, fmt.Errorf("AU-index-delta different than zero is not supported") + } + } + + dataLens[i] = dataLen + i++ + } + + return dataLens, nil +} + +// some cameras wrap AUs into ADTS +func (d *Decoder) removeADTS(aus [][]byte) ([][]byte, error) { + if !d.firstAUParsed { + d.firstAUParsed = true + + if len(aus) == 1 && len(aus[0]) >= 2 { + if aus[0][0] == 0xFF && (aus[0][1]&0xF0) == 0xF0 { + var pkts mpeg4audio.ADTSPackets + err := pkts.Unmarshal(aus[0]) + if err == nil && len(pkts) == 1 { + d.adtsMode = true + aus[0] = pkts[0].AU + } + } + } + } else if d.adtsMode { + if len(aus) != 1 { + return nil, fmt.Errorf("multiple AUs in ADTS mode are not supported") + } + + var pkts mpeg4audio.ADTSPackets + err := pkts.Unmarshal(aus[0]) + if err != nil { + return nil, fmt.Errorf("unable to decode ADTS: %w", err) + } + + if len(pkts) != 1 { + return nil, fmt.Errorf("multiple ADTS packets are not supported") + } + + aus[0] = pkts[0].AU + } + + return aus, nil } diff --git a/pkg/format/rtpmpeg4audio/decoder_generic.go b/pkg/format/rtpmpeg4audio/decoder_generic.go deleted file mode 100644 index db69ae7a..00000000 --- a/pkg/format/rtpmpeg4audio/decoder_generic.go +++ /dev/null @@ -1,201 +0,0 @@ -package rtpmpeg4audio - -import ( - "fmt" - - "github.com/bluenviron/mediacommon/v2/pkg/bits" - "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" - "github.com/pion/rtp" -) - -func (d *Decoder) decodeGeneric(pkt *rtp.Packet) ([][]byte, error) { - if len(pkt.Payload) < 2 { - d.resetFragments() - return nil, fmt.Errorf("payload is too short") - } - - // AU-headers-length (16 bits) - headersLen := int(uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1])) - if headersLen == 0 { - d.resetFragments() - return nil, fmt.Errorf("invalid AU-headers-length") - } - payload := pkt.Payload[2:] - - // AU-headers - dataLens, err := d.readAUHeaders(payload, headersLen) - if err != nil { - d.resetFragments() - return nil, err - } - - pos := (headersLen / 8) - if (headersLen % 8) != 0 { - pos++ - } - payload = payload[pos:] - - var aus [][]byte - - if d.fragmentsSize == 0 { - d.resetFragments() - - if pkt.Marker { - // AUs - aus = make([][]byte, len(dataLens)) - for i, dataLen := range dataLens { - if len(payload) < int(dataLen) { - return nil, fmt.Errorf("payload is too short") - } - - aus[i] = payload[:dataLen] - payload = payload[dataLen:] - } - } else { - if len(dataLens) != 1 { - return nil, fmt.Errorf("a fragmented packet can only contain one AU") - } - - if len(payload) < int(dataLens[0]) { - return nil, fmt.Errorf("payload is too short") - } - - d.fragmentsSize = int(dataLens[0]) - d.fragments = append(d.fragments, payload[:dataLens[0]]) - d.fragmentNextSeqNum = pkt.SequenceNumber + 1 - return nil, ErrMorePacketsNeeded - } - } else { - // we are decoding a fragmented AU - if len(dataLens) != 1 { - d.resetFragments() - return nil, fmt.Errorf("a fragmented packet can only contain one AU") - } - - if len(payload) < int(dataLens[0]) { - d.resetFragments() - return nil, fmt.Errorf("payload is too short") - } - - if pkt.SequenceNumber != d.fragmentNextSeqNum { - d.resetFragments() - return nil, fmt.Errorf("discarding frame since a RTP packet is missing") - } - - d.fragmentsSize += int(dataLens[0]) - - if d.fragmentsSize > mpeg4audio.MaxAccessUnitSize { - errSize := d.fragmentsSize - d.resetFragments() - return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d", - errSize, mpeg4audio.MaxAccessUnitSize) - } - - d.fragments = append(d.fragments, payload[:dataLens[0]]) - d.fragmentNextSeqNum++ - - if !pkt.Marker { - return nil, ErrMorePacketsNeeded - } - - aus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} - d.resetFragments() - } - - return d.removeADTS(aus) -} - -func (d *Decoder) readAUHeaders(buf []byte, headersLen int) ([]uint64, error) { - firstRead := false - - count := 0 - for i := 0; i < headersLen; { - if i == 0 { - i += d.SizeLength - i += d.IndexLength - } else { - i += d.SizeLength - i += d.IndexDeltaLength - } - count++ - } - - dataLens := make([]uint64, count) - - pos := 0 - i := 0 - - for headersLen > 0 { - dataLen, err := bits.ReadBits(buf, &pos, d.SizeLength) - if err != nil { - return nil, err - } - headersLen -= d.SizeLength - - if !firstRead { - firstRead = true - if d.IndexLength > 0 { - auIndex, err := bits.ReadBits(buf, &pos, d.IndexLength) - if err != nil { - return nil, err - } - headersLen -= d.IndexLength - - if auIndex != 0 { - return nil, fmt.Errorf("AU-index different than zero is not supported") - } - } - } else if d.IndexDeltaLength > 0 { - auIndexDelta, err := bits.ReadBits(buf, &pos, d.IndexDeltaLength) - if err != nil { - return nil, err - } - headersLen -= d.IndexDeltaLength - - if auIndexDelta != 0 { - return nil, fmt.Errorf("AU-index-delta different than zero is not supported") - } - } - - dataLens[i] = dataLen - i++ - } - - return dataLens, nil -} - -// some cameras wrap AUs into ADTS -func (d *Decoder) removeADTS(aus [][]byte) ([][]byte, error) { - if !d.firstAUParsed { - d.firstAUParsed = true - - if len(aus) == 1 && len(aus[0]) >= 2 { - if aus[0][0] == 0xFF && (aus[0][1]&0xF0) == 0xF0 { - var pkts mpeg4audio.ADTSPackets - err := pkts.Unmarshal(aus[0]) - if err == nil && len(pkts) == 1 { - d.adtsMode = true - aus[0] = pkts[0].AU - } - } - } - } else if d.adtsMode { - if len(aus) != 1 { - return nil, fmt.Errorf("multiple AUs in ADTS mode are not supported") - } - - var pkts mpeg4audio.ADTSPackets - err := pkts.Unmarshal(aus[0]) - if err != nil { - return nil, fmt.Errorf("unable to decode ADTS: %w", err) - } - - if len(pkts) != 1 { - return nil, fmt.Errorf("multiple ADTS packets are not supported") - } - - aus[0] = pkts[0].AU - } - - return aus, nil -} diff --git a/pkg/format/rtpmpeg4audio/decoder_latm.go b/pkg/format/rtpmpeg4audio/decoder_latm.go deleted file mode 100644 index cdaec8ec..00000000 --- a/pkg/format/rtpmpeg4audio/decoder_latm.go +++ /dev/null @@ -1,63 +0,0 @@ -package rtpmpeg4audio - -import ( - "fmt" - - "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" - "github.com/pion/rtp" -) - -func (d *Decoder) decodeLATM(pkt *rtp.Packet) ([][]byte, error) { - var au []byte - buf := pkt.Payload - - if d.fragmentsSize == 0 { - pl, n, err := payloadLengthInfoDecode(buf) - if err != nil { - return nil, err - } - - buf = buf[n:] - bl := len(buf) - - if pl <= bl { - au = buf[:pl] - // there could be other data, due to otherDataPresent. Ignore it. - } else { - if pl > mpeg4audio.MaxAccessUnitSize { - errSize := pl - d.resetFragments() - return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d", - errSize, mpeg4audio.MaxAccessUnitSize) - } - - d.fragments = append(d.fragments, buf) - d.fragmentsSize = pl - d.fragmentsExpected = pl - bl - d.fragmentNextSeqNum = pkt.SequenceNumber + 1 - return nil, ErrMorePacketsNeeded - } - } else { - if pkt.SequenceNumber != d.fragmentNextSeqNum { - d.resetFragments() - return nil, fmt.Errorf("discarding frame since a RTP packet is missing") - } - - bl := len(buf) - - if d.fragmentsExpected > bl { - d.fragments = append(d.fragments, buf) - d.fragmentsExpected -= bl - d.fragmentNextSeqNum++ - return nil, ErrMorePacketsNeeded - } - - d.fragments = append(d.fragments, buf[:d.fragmentsExpected]) - // there could be other data, due to otherDataPresent. Ignore it. - - au = joinFragments(d.fragments, d.fragmentsSize) - d.resetFragments() - } - - return [][]byte{au}, nil -} diff --git a/pkg/format/rtpmpeg4audio/decoder_generic_test.go b/pkg/format/rtpmpeg4audio/decoder_test.go similarity index 100% rename from pkg/format/rtpmpeg4audio/decoder_generic_test.go rename to pkg/format/rtpmpeg4audio/decoder_test.go diff --git a/pkg/format/rtpmpeg4audio/encoder.go b/pkg/format/rtpmpeg4audio/encoder.go index 9259aa38..f7176c58 100644 --- a/pkg/format/rtpmpeg4audio/encoder.go +++ b/pkg/format/rtpmpeg4audio/encoder.go @@ -3,6 +3,8 @@ package rtpmpeg4audio import ( "crypto/rand" + "github.com/bluenviron/mediacommon/v2/pkg/bits" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" "github.com/pion/rtp" ) @@ -20,16 +22,20 @@ func randUint32() (uint32, error) { return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil } +func packetCountGeneric(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + // Encoder is a RTP/MPEG-4 audio encoder. // Specification: RFC3640 -// Specification: RFC6416, section 7.3 type Encoder struct { // payload type of packets. PayloadType uint8 - // use RFC6416 (LATM) instead of RFC3640 (generic). - LATM bool - // The number of bits in which the AU-size field is encoded in the AU-header. SizeLength int @@ -81,8 +87,181 @@ func (e *Encoder) Init() error { // Encode encodes AUs (non-LATM) or AudioMuxElements (LATM) into RTP packets. func (e *Encoder) Encode(aus [][]byte) ([]*rtp.Packet, error) { - if !e.LATM { - return e.encodeGeneric(aus) + var rets []*rtp.Packet + var batch [][]byte + timestamp := uint32(0) + + // split AUs into batches + for _, au := range aus { + if e.lenGenericAggregated(batch, au) <= e.PayloadMaxSize { + // add to existing batch + batch = append(batch, au) + } else { + // write current batch + if batch != nil { + pkts, err := e.writeGenericBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + timestamp += uint32(len(batch)) * mpeg4audio.SamplesPerAccessUnit + } + + // initialize new batch + batch = [][]byte{au} + } } - return e.encodeLATM(aus) + + // write last batch + pkts, err := e.writeGenericBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + return rets, nil +} + +func (e *Encoder) writeGenericBatch(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + if len(aus) != 1 || e.lenGenericAggregated(aus, nil) < e.PayloadMaxSize { + return e.writeGenericAggregated(aus, timestamp) + } + + return e.writeGenericFragmented(aus[0], timestamp) +} + +func (e *Encoder) writeGenericFragmented(au []byte, timestamp uint32) ([]*rtp.Packet, error) { + auHeadersLen := e.SizeLength + e.IndexLength + auHeadersLenBytes := auHeadersLen / 8 + if (auHeadersLen % 8) != 0 { + auHeadersLenBytes++ + } + + avail := e.PayloadMaxSize - 2 - auHeadersLenBytes + le := len(au) + packetCount := packetCountGeneric(avail, le) + + ret := make([]*rtp.Packet, packetCount) + le = avail + + for i := range ret { + if i == (packetCount - 1) { + le = len(au) + } + + payload := make([]byte, 2+auHeadersLenBytes+le) + + // AU-headers-length + payload[0] = byte(auHeadersLen >> 8) + payload[1] = byte(auHeadersLen) + + // AU-headers + pos := 0 + bits.WriteBitsUnsafe(payload[2:], &pos, uint64(le), e.SizeLength) + bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength) + + // AU + copy(payload[2+auHeadersLenBytes:], au) + au = au[le:] + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: (i == packetCount-1), + }, + Payload: payload, + } + + e.sequenceNumber++ + } + + return ret, nil +} + +func (e *Encoder) lenGenericAggregated(aus [][]byte, addAU []byte) int { + n := 2 // AU-headers-length + + // AU-headers + auHeadersLen := 0 + i := 0 + for range aus { + if i == 0 { + auHeadersLen += e.SizeLength + e.IndexLength + } else { + auHeadersLen += e.SizeLength + e.IndexDeltaLength + } + i++ + } + if addAU != nil { + if i == 0 { + auHeadersLen += e.SizeLength + e.IndexLength + } else { + auHeadersLen += e.SizeLength + e.IndexDeltaLength + } + } + n += auHeadersLen / 8 + if (auHeadersLen % 8) != 0 { + n++ + } + + // AU + for _, au := range aus { + n += len(au) + } + n += len(addAU) + + return n +} + +func (e *Encoder) writeGenericAggregated(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + payload := make([]byte, e.lenGenericAggregated(aus, nil)) + + // AU-headers + written := 0 + pos := 0 + for i, au := range aus { + bits.WriteBitsUnsafe(payload[2:], &pos, uint64(len(au)), e.SizeLength) + written += e.SizeLength + if i == 0 { + bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength) + written += e.IndexLength + } else { + bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexDeltaLength) + written += e.IndexDeltaLength + } + } + pos = 2 + (written / 8) + if (written % 8) != 0 { + pos++ + } + + // AU-headers-length + payload[0] = byte(written >> 8) + payload[1] = byte(written) + + // AUs + for _, au := range aus { + auLen := copy(payload[pos:], au) + pos += auLen + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: true, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil } diff --git a/pkg/format/rtpmpeg4audio/encoder_generic.go b/pkg/format/rtpmpeg4audio/encoder_generic.go deleted file mode 100644 index 5c56bcda..00000000 --- a/pkg/format/rtpmpeg4audio/encoder_generic.go +++ /dev/null @@ -1,196 +0,0 @@ -package rtpmpeg4audio - -import ( - "github.com/pion/rtp" - - "github.com/bluenviron/mediacommon/v2/pkg/bits" - "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" -) - -func packetCountGeneric(avail, le int) int { - n := le / avail - if (le % avail) != 0 { - n++ - } - return n -} - -func (e *Encoder) encodeGeneric(aus [][]byte) ([]*rtp.Packet, error) { - var rets []*rtp.Packet - var batch [][]byte - timestamp := uint32(0) - - // split AUs into batches - for _, au := range aus { - if e.lenGenericAggregated(batch, au) <= e.PayloadMaxSize { - // add to existing batch - batch = append(batch, au) - } else { - // write current batch - if batch != nil { - pkts, err := e.writeGenericBatch(batch, timestamp) - if err != nil { - return nil, err - } - rets = append(rets, pkts...) - timestamp += uint32(len(batch)) * mpeg4audio.SamplesPerAccessUnit - } - - // initialize new batch - batch = [][]byte{au} - } - } - - // write last batch - pkts, err := e.writeGenericBatch(batch, timestamp) - if err != nil { - return nil, err - } - rets = append(rets, pkts...) - - return rets, nil -} - -func (e *Encoder) writeGenericBatch(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) { - if len(aus) != 1 || e.lenGenericAggregated(aus, nil) < e.PayloadMaxSize { - return e.writeGenericAggregated(aus, timestamp) - } - - return e.writeGenericFragmented(aus[0], timestamp) -} - -func (e *Encoder) writeGenericFragmented(au []byte, timestamp uint32) ([]*rtp.Packet, error) { - auHeadersLen := e.SizeLength + e.IndexLength - auHeadersLenBytes := auHeadersLen / 8 - if (auHeadersLen % 8) != 0 { - auHeadersLenBytes++ - } - - avail := e.PayloadMaxSize - 2 - auHeadersLenBytes - le := len(au) - packetCount := packetCountGeneric(avail, le) - - ret := make([]*rtp.Packet, packetCount) - le = avail - - for i := range ret { - if i == (packetCount - 1) { - le = len(au) - } - - payload := make([]byte, 2+auHeadersLenBytes+le) - - // AU-headers-length - payload[0] = byte(auHeadersLen >> 8) - payload[1] = byte(auHeadersLen) - - // AU-headers - pos := 0 - bits.WriteBitsUnsafe(payload[2:], &pos, uint64(le), e.SizeLength) - bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength) - - // AU - copy(payload[2+auHeadersLenBytes:], au) - au = au[le:] - - ret[i] = &rtp.Packet{ - Header: rtp.Header{ - Version: rtpVersion, - PayloadType: e.PayloadType, - SequenceNumber: e.sequenceNumber, - Timestamp: timestamp, - SSRC: *e.SSRC, - Marker: (i == packetCount-1), - }, - Payload: payload, - } - - e.sequenceNumber++ - } - - return ret, nil -} - -func (e *Encoder) lenGenericAggregated(aus [][]byte, addAU []byte) int { - n := 2 // AU-headers-length - - // AU-headers - auHeadersLen := 0 - i := 0 - for range aus { - if i == 0 { - auHeadersLen += e.SizeLength + e.IndexLength - } else { - auHeadersLen += e.SizeLength + e.IndexDeltaLength - } - i++ - } - if addAU != nil { - if i == 0 { - auHeadersLen += e.SizeLength + e.IndexLength - } else { - auHeadersLen += e.SizeLength + e.IndexDeltaLength - } - } - n += auHeadersLen / 8 - if (auHeadersLen % 8) != 0 { - n++ - } - - // AU - for _, au := range aus { - n += len(au) - } - n += len(addAU) - - return n -} - -func (e *Encoder) writeGenericAggregated(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) { - payload := make([]byte, e.lenGenericAggregated(aus, nil)) - - // AU-headers - written := 0 - pos := 0 - for i, au := range aus { - bits.WriteBitsUnsafe(payload[2:], &pos, uint64(len(au)), e.SizeLength) - written += e.SizeLength - if i == 0 { - bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength) - written += e.IndexLength - } else { - bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexDeltaLength) - written += e.IndexDeltaLength - } - } - pos = 2 + (written / 8) - if (written % 8) != 0 { - pos++ - } - - // AU-headers-length - payload[0] = byte(written >> 8) - payload[1] = byte(written) - - // AUs - for _, au := range aus { - auLen := copy(payload[pos:], au) - pos += auLen - } - - pkt := &rtp.Packet{ - Header: rtp.Header{ - Version: rtpVersion, - PayloadType: e.PayloadType, - SequenceNumber: e.sequenceNumber, - Timestamp: timestamp, - SSRC: *e.SSRC, - Marker: true, - }, - Payload: payload, - } - - e.sequenceNumber++ - - return []*rtp.Packet{pkt}, nil -} diff --git a/pkg/format/rtpmpeg4audio/encoder_generic_test.go b/pkg/format/rtpmpeg4audio/encoder_generic_test.go deleted file mode 100644 index 0b1c35c3..00000000 --- a/pkg/format/rtpmpeg4audio/encoder_generic_test.go +++ /dev/null @@ -1,488 +0,0 @@ -package rtpmpeg4audio - -import ( - "bytes" - "testing" - - "github.com/pion/rtp" - "github.com/stretchr/testify/require" -) - -func uint16Ptr(v uint16) *uint16 { - return &v -} - -func uint32Ptr(v uint32) *uint32 { - return &v -} - -func mergeBytes(vals ...[]byte) []byte { - size := 0 - for _, v := range vals { - size += len(v) - } - res := make([]byte, size) - - pos := 0 - for _, v := range vals { - n := copy(res[pos:], v) - pos += n - } - - return res -} - -var casesGeneric = []struct { - name string - sizeLength int - indexLength int - indexDeltaLength int - aus [][]byte - pkts []*rtp.Packet -}{ - { - "single", - 13, - 3, - 3, - [][]byte{ - { - 0x21, 0x1a, 0xd4, 0xf5, 0x9e, 0x20, 0xc5, 0x42, - 0x89, 0x40, 0xa2, 0x9b, 0x3c, 0x94, 0xdd, 0x28, - 0x94, 0x48, 0xd5, 0x8b, 0xb0, 0x2, 0xdb, 0x1b, - 0xeb, 0xe0, 0xfa, 0x9f, 0xea, 0x91, 0xa7, 0x3, - 0xe8, 0x6b, 0xe5, 0x5, 0x95, 0x6, 0x62, 0x88, - 0x13, 0xa, 0x15, 0xa0, 0xeb, 0xef, 0x40, 0x82, - 0xdf, 0x49, 0xf2, 0xe0, 0x26, 0xfc, 0x52, 0x5b, - 0x6c, 0x2a, 0x2d, 0xe8, 0xa5, 0x70, 0xc5, 0xaf, - 0xfc, 0x98, 0x9a, 0x2f, 0x1f, 0xbb, 0xa2, 0xcb, - 0xb8, 0x26, 0xb6, 0x6e, 0x4c, 0x15, 0x6c, 0x21, - 0x3d, 0x35, 0xf6, 0xcf, 0xa4, 0x3b, 0x72, 0x26, - 0xe1, 0x3a, 0x3a, 0x99, 0xd8, 0x2d, 0x6a, 0x22, - 0xcd, 0x97, 0xa, 0xef, 0x52, 0x9c, 0x5f, 0xcd, - 0x5c, 0xd9, 0xd3, 0x12, 0x7e, 0x45, 0x45, 0xb3, - 0x24, 0xef, 0xd3, 0x4f, 0x2f, 0x96, 0xd9, 0x8b, - 0x9c, 0xc2, 0xcd, 0x54, 0xb, 0x6e, 0x19, 0x84, - 0x56, 0xeb, 0x85, 0x52, 0x63, 0x64, 0x28, 0xb2, - 0xf2, 0xcf, 0xb8, 0xa8, 0x71, 0x53, 0x6, 0x82, - 0x88, 0xf2, 0xc4, 0xe1, 0x7d, 0x65, 0x54, 0xe0, - 0x5e, 0xc8, 0x38, 0x75, 0x9d, 0xb0, 0x58, 0x65, - 0x41, 0xa2, 0xcd, 0xdb, 0x1b, 0x9e, 0xac, 0xd1, - 0xbe, 0xc9, 0x22, 0xf5, 0xe9, 0xc6, 0x6f, 0xaf, - 0xf8, 0xb1, 0x4c, 0xcb, 0xa2, 0x56, 0x11, 0xa4, - 0xd7, 0xfd, 0xe5, 0xef, 0x8e, 0xbf, 0xce, 0x4b, - 0xef, 0xe1, 0xd, 0xc0, 0x27, 0x18, 0xe2, 0x64, - 0x63, 0x5, 0x16, 0x6, 0xc, 0x34, 0xe, 0xf3, 0x62, - 0xc2, 0xd6, 0x42, 0x5d, 0x66, 0x81, 0x4, 0x65, - 0x76, 0xaa, 0xe7, 0x39, 0xdd, 0x8e, 0xfe, 0x48, - 0x23, 0x3a, 0x1, 0xc4, 0xd3, 0x65, 0x80, 0x28, - 0x6f, 0x9b, 0xc9, 0xb7, 0x4e, 0x44, 0x4c, 0x98, - 0x6a, 0x5f, 0x3b, 0x97, 0x81, 0x9b, 0xa9, 0xab, - 0xfd, 0xcf, 0x8e, 0x78, 0xbd, 0x4d, 0x70, 0x81, - 0x9b, 0x2d, 0x85, 0x94, 0x74, 0x2a, 0x3a, 0xb4, - 0xff, 0x4a, 0x13, 0x70, 0x76, 0x2c, 0x2f, 0x13, - 0x5b, 0x43, 0xf9, 0x17, 0xee, 0x26, 0x37, 0x1, - 0xbc, 0x9f, 0xb, 0xe, 0x68, 0xcb, 0x87, 0x65, - 0x86, 0xcc, 0x4c, 0x2f, 0x7a, 0x14, 0xd, 0xd1, - 0xb9, 0x57, 0xbd, 0x50, 0xb6, 0x95, 0x44, 0x1a, - 0xd, 0xc0, 0x15, 0xf, 0xd2, 0xc3, 0x72, 0x4d, - 0x6e, 0x4f, 0x8e, 0x6d, 0x64, 0xdc, 0x64, 0x1f, - 0x33, 0x53, 0x4e, 0xd8, 0xa4, 0x74, 0xf3, 0x33, - 0x4, 0x68, 0xd9, 0x92, 0xf3, 0x6e, 0xb7, 0x5b, - 0xe6, 0xf6, 0xc3, 0x55, 0x14, 0x54, 0x87, 0x0, - 0xaf, 0x7, - }, - }, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: []byte{ - 0x00, 0x10, 0x0a, 0xd8, - 0x21, 0x1a, 0xd4, 0xf5, 0x9e, 0x20, 0xc5, 0x42, - 0x89, 0x40, 0xa2, 0x9b, 0x3c, 0x94, 0xdd, 0x28, - 0x94, 0x48, 0xd5, 0x8b, 0xb0, 0x02, 0xdb, 0x1b, - 0xeb, 0xe0, 0xfa, 0x9f, 0xea, 0x91, 0xa7, 0x03, - 0xe8, 0x6b, 0xe5, 0x05, 0x95, 0x06, 0x62, 0x88, - 0x13, 0x0a, 0x15, 0xa0, 0xeb, 0xef, 0x40, 0x82, - 0xdf, 0x49, 0xf2, 0xe0, 0x26, 0xfc, 0x52, 0x5b, - 0x6c, 0x2a, 0x2d, 0xe8, 0xa5, 0x70, 0xc5, 0xaf, - 0xfc, 0x98, 0x9a, 0x2f, 0x1f, 0xbb, 0xa2, 0xcb, - 0xb8, 0x26, 0xb6, 0x6e, 0x4c, 0x15, 0x6c, 0x21, - 0x3d, 0x35, 0xf6, 0xcf, 0xa4, 0x3b, 0x72, 0x26, - 0xe1, 0x3a, 0x3a, 0x99, 0xd8, 0x2d, 0x6a, 0x22, - 0xcd, 0x97, 0x0a, 0xef, 0x52, 0x9c, 0x5f, 0xcd, - 0x5c, 0xd9, 0xd3, 0x12, 0x7e, 0x45, 0x45, 0xb3, - 0x24, 0xef, 0xd3, 0x4f, 0x2f, 0x96, 0xd9, 0x8b, - 0x9c, 0xc2, 0xcd, 0x54, 0x0b, 0x6e, 0x19, 0x84, - 0x56, 0xeb, 0x85, 0x52, 0x63, 0x64, 0x28, 0xb2, - 0xf2, 0xcf, 0xb8, 0xa8, 0x71, 0x53, 0x06, 0x82, - 0x88, 0xf2, 0xc4, 0xe1, 0x7d, 0x65, 0x54, 0xe0, - 0x5e, 0xc8, 0x38, 0x75, 0x9d, 0xb0, 0x58, 0x65, - 0x41, 0xa2, 0xcd, 0xdb, 0x1b, 0x9e, 0xac, 0xd1, - 0xbe, 0xc9, 0x22, 0xf5, 0xe9, 0xc6, 0x6f, 0xaf, - 0xf8, 0xb1, 0x4c, 0xcb, 0xa2, 0x56, 0x11, 0xa4, - 0xd7, 0xfd, 0xe5, 0xef, 0x8e, 0xbf, 0xce, 0x4b, - 0xef, 0xe1, 0x0d, 0xc0, 0x27, 0x18, 0xe2, 0x64, - 0x63, 0x05, 0x16, 0x06, 0x0c, 0x34, 0x0e, 0xf3, - 0x62, 0xc2, 0xd6, 0x42, 0x5d, 0x66, 0x81, 0x04, - 0x65, 0x76, 0xaa, 0xe7, 0x39, 0xdd, 0x8e, 0xfe, - 0x48, 0x23, 0x3a, 0x01, 0xc4, 0xd3, 0x65, 0x80, - 0x28, 0x6f, 0x9b, 0xc9, 0xb7, 0x4e, 0x44, 0x4c, - 0x98, 0x6a, 0x5f, 0x3b, 0x97, 0x81, 0x9b, 0xa9, - 0xab, 0xfd, 0xcf, 0x8e, 0x78, 0xbd, 0x4d, 0x70, - 0x81, 0x9b, 0x2d, 0x85, 0x94, 0x74, 0x2a, 0x3a, - 0xb4, 0xff, 0x4a, 0x13, 0x70, 0x76, 0x2c, 0x2f, - 0x13, 0x5b, 0x43, 0xf9, 0x17, 0xee, 0x26, 0x37, - 0x01, 0xbc, 0x9f, 0x0b, 0x0e, 0x68, 0xcb, 0x87, - 0x65, 0x86, 0xcc, 0x4c, 0x2f, 0x7a, 0x14, 0x0d, - 0xd1, 0xb9, 0x57, 0xbd, 0x50, 0xb6, 0x95, 0x44, - 0x1a, 0x0d, 0xc0, 0x15, 0x0f, 0xd2, 0xc3, 0x72, - 0x4d, 0x6e, 0x4f, 0x8e, 0x6d, 0x64, 0xdc, 0x64, - 0x1f, 0x33, 0x53, 0x4e, 0xd8, 0xa4, 0x74, 0xf3, - 0x33, 0x04, 0x68, 0xd9, 0x92, 0xf3, 0x6e, 0xb7, - 0x5b, 0xe6, 0xf6, 0xc3, 0x55, 0x14, 0x54, 0x87, - 0x00, 0xaf, 0x07, - }, - }, - }, - }, - { - "aggregated", - 13, - 3, - 3, - [][]byte{ - {0x00, 0x01, 0x02, 0x03}, - {0x04, 0x05, 0x06, 0x07}, - {0x08, 0x09, 0x0A, 0x0B}, - }, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: []byte{ - 0x0, 0x30, 0x0, 0x20, - 0x0, 0x20, 0x0, 0x20, 0x0, 0x1, 0x2, 0x3, - 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, - }, - }, - }, - }, - { //nolint:dupl - "fragmented", - 13, - 3, - 3, - [][]byte{ - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 187), - }, - []*rtp.Packet{ //nolint:dupl - { - Header: rtp.Header{ - Version: 2, - Marker: false, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x0, 0x10, 0x1f, 0x20}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 124), - []byte{0, 1, 2, 3}, - ), - }, - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17646, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x00, 0x10, 0x0f, 0xa0}, - []byte{4, 5, 6, 7}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 62), - ), - }, - }, - }, - { - "fragmented to the limit", - 13, - 3, - 3, - [][]byte{bytes.Repeat([]byte{1}, 1992)}, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: false, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x0, 0x10, 0x1f, 0x20}, - bytes.Repeat([]byte{1}, 996), - ), - }, - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17646, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x0, 0x10, 0x1f, 0x20}, - bytes.Repeat([]byte{1}, 996), - ), - }, - }, - }, - { - "aggregated followed by fragmented", - 13, - 3, - 3, - [][]byte{ - {0x00, 0x01, 0x02, 0x03}, - {0x04, 0x05, 0x06, 0x07}, - {0x08, 0x09, 0x0A, 0x0B}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 187), - }, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: []byte{ - 0x0, 0x30, 0x0, 0x20, - 0x0, 0x20, 0x0, 0x20, 0x0, 0x1, 0x2, 0x3, - 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, - }, - }, - { - Header: rtp.Header{ - Version: 2, - Marker: false, - PayloadType: 96, - SequenceNumber: 17646, - Timestamp: 3072, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x0, 0x10, 0x1f, 0x20}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 124), - []byte{0, 1, 2, 3}, - ), - }, - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17647, - Timestamp: 3072, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x00, 0x10, 0x0f, 0xa0}, - []byte{4, 5, 6, 7}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 62), - ), - }, - }, - }, - { - "single, custom sized", - 6, - 2, - 2, - [][]byte{ - {0x01, 0x02, 0x03, 0x04}, - }, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: []byte{ - 0x00, 0x08, 0x10, - 0x01, 0x02, 0x03, 0x04, - }, - }, - }, - }, - { - "single, custom sized, padded", - 13, - 0, - 0, - [][]byte{ - {0x01, 0x02, 0x03, 0x04}, - }, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: []byte{ - 0x00, 0x0d, 0x00, 0x20, - 0x01, 0x02, 0x03, 0x04, - }, - }, - }, - }, - { - "aggregated, custom sized, padded", - 13, - 0, - 0, - [][]byte{ - {0x01, 0x02, 0x03, 0x04}, - {0x05, 0x06, 0x07, 0x08}, - }, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: []byte{ - 0x00, 0x1a, 0x00, 0x20, 0x01, 0x00, - 0x01, 0x02, 0x03, 0x04, - 0x05, 0x06, 0x07, 0x08, - }, - }, - }, - }, - { - "fragmented, custom sized", - 21, - 3, - 3, - [][]byte{ - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 187), - }, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: false, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x0, 0x18, 0x00, 0x1f, 0x18}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 124), - []byte{0, 1, 2}, - ), - }, - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17646, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x00, 0x18, 0x00, 0x0f, 0xa8}, - []byte{3, 4, 5, 6, 7}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 62), - ), - }, - }, - }, - { //nolint:dupl - "fragmented, custom sized, padded", - 13, - 0, - 0, - [][]byte{ - bytes.Repeat([]byte{0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, 187), - }, - []*rtp.Packet{ //nolint:dupl - { - Header: rtp.Header{ - Version: 2, - Marker: false, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x0, 0x0d, 0x1f, 0x20}, - bytes.Repeat([]byte{0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, 124), - []byte{0x08, 0x09, 0x0A, 0x0B}, - ), - }, - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17646, - SSRC: 0x9dbb7812, - }, - Payload: mergeBytes( - []byte{0x0, 0x0d, 0x0f, 0xa0}, - []byte{0x0C, 0x0D, 0x0E, 0x0F}, - bytes.Repeat([]byte{0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, 62), - ), - }, - }, - }, -} - -func TestEncodeGeneric(t *testing.T) { - for _, ca := range casesGeneric { - t.Run(ca.name, func(t *testing.T) { - e := &Encoder{ - PayloadType: 96, - SSRC: uint32Ptr(0x9dbb7812), - InitialSequenceNumber: uint16Ptr(0x44ed), - SizeLength: ca.sizeLength, - IndexLength: ca.indexLength, - IndexDeltaLength: ca.indexDeltaLength, - PayloadMaxSize: 1000, - } - err := e.Init() - require.NoError(t, err) - - pkts, err := e.Encode(ca.aus) - require.NoError(t, err) - require.Equal(t, ca.pkts, pkts) - }) - } -} diff --git a/pkg/format/rtpmpeg4audio/encoder_latm.go b/pkg/format/rtpmpeg4audio/encoder_latm.go deleted file mode 100644 index 16db07e8..00000000 --- a/pkg/format/rtpmpeg4audio/encoder_latm.go +++ /dev/null @@ -1,76 +0,0 @@ -package rtpmpeg4audio - -import ( - "github.com/pion/rtp" - - "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" -) - -func (e *Encoder) packetCountLATM(auLen int, plil int) int { - totalLen := plil + auLen - n := totalLen / e.PayloadMaxSize - if (totalLen % e.PayloadMaxSize) != 0 { - n++ - } - return n -} - -func (e *Encoder) encodeLATM(aus [][]byte) ([]*rtp.Packet, error) { - var rets []*rtp.Packet - - for i, au := range aus { - timestamp := uint32(i) * mpeg4audio.SamplesPerAccessUnit - - add, err := e.encodeLATMSingle(au, timestamp) - if err != nil { - return nil, err - } - rets = append(rets, add...) - } - - return rets, nil -} - -func (e *Encoder) encodeLATMSingle(au []byte, timestamp uint32) ([]*rtp.Packet, error) { - auLen := len(au) - plil := payloadLengthInfoEncodeSize(auLen) - packetCount := e.packetCountLATM(auLen, plil) - - ret := make([]*rtp.Packet, packetCount) - le := e.PayloadMaxSize - plil - - for i := range ret { - if i == (packetCount - 1) { - le = len(au) - } - - var payload []byte - - if i == 0 { - payload = make([]byte, plil+le) - payloadLengthInfoEncode(plil, auLen, payload) - copy(payload[plil:], au[:le]) - au = au[le:] - le = e.PayloadMaxSize - } else { - payload = au[:le] - au = au[le:] - } - - ret[i] = &rtp.Packet{ - Header: rtp.Header{ - Version: rtpVersion, - PayloadType: e.PayloadType, - SequenceNumber: e.sequenceNumber, - Timestamp: timestamp, - SSRC: *e.SSRC, - Marker: (i == packetCount-1), - }, - Payload: payload, - } - - e.sequenceNumber++ - } - - return ret, nil -} diff --git a/pkg/format/rtpmpeg4audio/encoder_latm_test.go b/pkg/format/rtpmpeg4audio/encoder_latm_test.go deleted file mode 100644 index a2cb2102..00000000 --- a/pkg/format/rtpmpeg4audio/encoder_latm_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package rtpmpeg4audio - -import ( - "bytes" - "testing" - - "github.com/pion/rtp" - "github.com/stretchr/testify/require" -) - -var casesLATM = []struct { - name string - au []byte - pkts []*rtp.Packet -}{ - { - "single", - []byte{1, 2, 3, 4}, - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 2646308882, - }, - Payload: []byte{ - 0x04, 0x01, 0x02, 0x03, 0x04, - }, - }, - }, - }, - { - "fragmented", - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 187), - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: false, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 2646308882, - }, - Payload: mergeBytes( - bytes.Repeat([]byte{0xff}, 5), - []byte{0xdd}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 124), - []byte{0, 1}, - ), - }, - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17646, - SSRC: 2646308882, - }, - Payload: mergeBytes( - []byte{2, 3, 4, 5, 6, 7}, - bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 62), - ), - }, - }, - }, - { - "fragmented to the limit", - bytes.Repeat([]byte{1}, 1992), - []*rtp.Packet{ - { - Header: rtp.Header{ - Version: 2, - Marker: false, - PayloadType: 96, - SequenceNumber: 17645, - SSRC: 2646308882, - }, - Payload: mergeBytes( - bytes.Repeat([]byte{0xff}, 7), - []byte{0xcf}, - bytes.Repeat([]byte{1}, 992), - ), - }, - { - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 96, - SequenceNumber: 17646, - SSRC: 2646308882, - }, - Payload: mergeBytes( - bytes.Repeat([]byte{1}, 1000), - ), - }, - }, - }, -} - -func TestEncodeLATM(t *testing.T) { - for _, ca := range casesLATM { - t.Run(ca.name, func(t *testing.T) { - e := &Encoder{ - LATM: true, - PayloadType: 96, - SSRC: uint32Ptr(0x9dbb7812), - InitialSequenceNumber: uint16Ptr(0x44ed), - PayloadMaxSize: 1000, - } - err := e.Init() - require.NoError(t, err) - - pkts, err := e.Encode([][]byte{ca.au}) - require.NoError(t, err) - require.Equal(t, ca.pkts, pkts) - }) - } -} diff --git a/pkg/format/rtpmpeg4audio/encoder_test.go b/pkg/format/rtpmpeg4audio/encoder_test.go index 6fa2ff0c..2692a61c 100644 --- a/pkg/format/rtpmpeg4audio/encoder_test.go +++ b/pkg/format/rtpmpeg4audio/encoder_test.go @@ -1,8 +1,10 @@ package rtpmpeg4audio import ( + "bytes" "testing" + "github.com/pion/rtp" "github.com/stretchr/testify/require" ) @@ -18,3 +20,482 @@ func TestEncodeRandomInitialState(t *testing.T) { require.NotEqual(t, nil, e.SSRC) require.NotEqual(t, nil, e.InitialSequenceNumber) } + +func uint16Ptr(v uint16) *uint16 { + return &v +} + +func uint32Ptr(v uint32) *uint32 { + return &v +} + +func mergeBytes(vals ...[]byte) []byte { + size := 0 + for _, v := range vals { + size += len(v) + } + res := make([]byte, size) + + pos := 0 + for _, v := range vals { + n := copy(res[pos:], v) + pos += n + } + + return res +} + +var casesGeneric = []struct { + name string + sizeLength int + indexLength int + indexDeltaLength int + aus [][]byte + pkts []*rtp.Packet +}{ + { + "single", + 13, + 3, + 3, + [][]byte{ + { + 0x21, 0x1a, 0xd4, 0xf5, 0x9e, 0x20, 0xc5, 0x42, + 0x89, 0x40, 0xa2, 0x9b, 0x3c, 0x94, 0xdd, 0x28, + 0x94, 0x48, 0xd5, 0x8b, 0xb0, 0x2, 0xdb, 0x1b, + 0xeb, 0xe0, 0xfa, 0x9f, 0xea, 0x91, 0xa7, 0x3, + 0xe8, 0x6b, 0xe5, 0x5, 0x95, 0x6, 0x62, 0x88, + 0x13, 0xa, 0x15, 0xa0, 0xeb, 0xef, 0x40, 0x82, + 0xdf, 0x49, 0xf2, 0xe0, 0x26, 0xfc, 0x52, 0x5b, + 0x6c, 0x2a, 0x2d, 0xe8, 0xa5, 0x70, 0xc5, 0xaf, + 0xfc, 0x98, 0x9a, 0x2f, 0x1f, 0xbb, 0xa2, 0xcb, + 0xb8, 0x26, 0xb6, 0x6e, 0x4c, 0x15, 0x6c, 0x21, + 0x3d, 0x35, 0xf6, 0xcf, 0xa4, 0x3b, 0x72, 0x26, + 0xe1, 0x3a, 0x3a, 0x99, 0xd8, 0x2d, 0x6a, 0x22, + 0xcd, 0x97, 0xa, 0xef, 0x52, 0x9c, 0x5f, 0xcd, + 0x5c, 0xd9, 0xd3, 0x12, 0x7e, 0x45, 0x45, 0xb3, + 0x24, 0xef, 0xd3, 0x4f, 0x2f, 0x96, 0xd9, 0x8b, + 0x9c, 0xc2, 0xcd, 0x54, 0xb, 0x6e, 0x19, 0x84, + 0x56, 0xeb, 0x85, 0x52, 0x63, 0x64, 0x28, 0xb2, + 0xf2, 0xcf, 0xb8, 0xa8, 0x71, 0x53, 0x6, 0x82, + 0x88, 0xf2, 0xc4, 0xe1, 0x7d, 0x65, 0x54, 0xe0, + 0x5e, 0xc8, 0x38, 0x75, 0x9d, 0xb0, 0x58, 0x65, + 0x41, 0xa2, 0xcd, 0xdb, 0x1b, 0x9e, 0xac, 0xd1, + 0xbe, 0xc9, 0x22, 0xf5, 0xe9, 0xc6, 0x6f, 0xaf, + 0xf8, 0xb1, 0x4c, 0xcb, 0xa2, 0x56, 0x11, 0xa4, + 0xd7, 0xfd, 0xe5, 0xef, 0x8e, 0xbf, 0xce, 0x4b, + 0xef, 0xe1, 0xd, 0xc0, 0x27, 0x18, 0xe2, 0x64, + 0x63, 0x5, 0x16, 0x6, 0xc, 0x34, 0xe, 0xf3, 0x62, + 0xc2, 0xd6, 0x42, 0x5d, 0x66, 0x81, 0x4, 0x65, + 0x76, 0xaa, 0xe7, 0x39, 0xdd, 0x8e, 0xfe, 0x48, + 0x23, 0x3a, 0x1, 0xc4, 0xd3, 0x65, 0x80, 0x28, + 0x6f, 0x9b, 0xc9, 0xb7, 0x4e, 0x44, 0x4c, 0x98, + 0x6a, 0x5f, 0x3b, 0x97, 0x81, 0x9b, 0xa9, 0xab, + 0xfd, 0xcf, 0x8e, 0x78, 0xbd, 0x4d, 0x70, 0x81, + 0x9b, 0x2d, 0x85, 0x94, 0x74, 0x2a, 0x3a, 0xb4, + 0xff, 0x4a, 0x13, 0x70, 0x76, 0x2c, 0x2f, 0x13, + 0x5b, 0x43, 0xf9, 0x17, 0xee, 0x26, 0x37, 0x1, + 0xbc, 0x9f, 0xb, 0xe, 0x68, 0xcb, 0x87, 0x65, + 0x86, 0xcc, 0x4c, 0x2f, 0x7a, 0x14, 0xd, 0xd1, + 0xb9, 0x57, 0xbd, 0x50, 0xb6, 0x95, 0x44, 0x1a, + 0xd, 0xc0, 0x15, 0xf, 0xd2, 0xc3, 0x72, 0x4d, + 0x6e, 0x4f, 0x8e, 0x6d, 0x64, 0xdc, 0x64, 0x1f, + 0x33, 0x53, 0x4e, 0xd8, 0xa4, 0x74, 0xf3, 0x33, + 0x4, 0x68, 0xd9, 0x92, 0xf3, 0x6e, 0xb7, 0x5b, + 0xe6, 0xf6, 0xc3, 0x55, 0x14, 0x54, 0x87, 0x0, + 0xaf, 0x7, + }, + }, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x00, 0x10, 0x0a, 0xd8, + 0x21, 0x1a, 0xd4, 0xf5, 0x9e, 0x20, 0xc5, 0x42, + 0x89, 0x40, 0xa2, 0x9b, 0x3c, 0x94, 0xdd, 0x28, + 0x94, 0x48, 0xd5, 0x8b, 0xb0, 0x02, 0xdb, 0x1b, + 0xeb, 0xe0, 0xfa, 0x9f, 0xea, 0x91, 0xa7, 0x03, + 0xe8, 0x6b, 0xe5, 0x05, 0x95, 0x06, 0x62, 0x88, + 0x13, 0x0a, 0x15, 0xa0, 0xeb, 0xef, 0x40, 0x82, + 0xdf, 0x49, 0xf2, 0xe0, 0x26, 0xfc, 0x52, 0x5b, + 0x6c, 0x2a, 0x2d, 0xe8, 0xa5, 0x70, 0xc5, 0xaf, + 0xfc, 0x98, 0x9a, 0x2f, 0x1f, 0xbb, 0xa2, 0xcb, + 0xb8, 0x26, 0xb6, 0x6e, 0x4c, 0x15, 0x6c, 0x21, + 0x3d, 0x35, 0xf6, 0xcf, 0xa4, 0x3b, 0x72, 0x26, + 0xe1, 0x3a, 0x3a, 0x99, 0xd8, 0x2d, 0x6a, 0x22, + 0xcd, 0x97, 0x0a, 0xef, 0x52, 0x9c, 0x5f, 0xcd, + 0x5c, 0xd9, 0xd3, 0x12, 0x7e, 0x45, 0x45, 0xb3, + 0x24, 0xef, 0xd3, 0x4f, 0x2f, 0x96, 0xd9, 0x8b, + 0x9c, 0xc2, 0xcd, 0x54, 0x0b, 0x6e, 0x19, 0x84, + 0x56, 0xeb, 0x85, 0x52, 0x63, 0x64, 0x28, 0xb2, + 0xf2, 0xcf, 0xb8, 0xa8, 0x71, 0x53, 0x06, 0x82, + 0x88, 0xf2, 0xc4, 0xe1, 0x7d, 0x65, 0x54, 0xe0, + 0x5e, 0xc8, 0x38, 0x75, 0x9d, 0xb0, 0x58, 0x65, + 0x41, 0xa2, 0xcd, 0xdb, 0x1b, 0x9e, 0xac, 0xd1, + 0xbe, 0xc9, 0x22, 0xf5, 0xe9, 0xc6, 0x6f, 0xaf, + 0xf8, 0xb1, 0x4c, 0xcb, 0xa2, 0x56, 0x11, 0xa4, + 0xd7, 0xfd, 0xe5, 0xef, 0x8e, 0xbf, 0xce, 0x4b, + 0xef, 0xe1, 0x0d, 0xc0, 0x27, 0x18, 0xe2, 0x64, + 0x63, 0x05, 0x16, 0x06, 0x0c, 0x34, 0x0e, 0xf3, + 0x62, 0xc2, 0xd6, 0x42, 0x5d, 0x66, 0x81, 0x04, + 0x65, 0x76, 0xaa, 0xe7, 0x39, 0xdd, 0x8e, 0xfe, + 0x48, 0x23, 0x3a, 0x01, 0xc4, 0xd3, 0x65, 0x80, + 0x28, 0x6f, 0x9b, 0xc9, 0xb7, 0x4e, 0x44, 0x4c, + 0x98, 0x6a, 0x5f, 0x3b, 0x97, 0x81, 0x9b, 0xa9, + 0xab, 0xfd, 0xcf, 0x8e, 0x78, 0xbd, 0x4d, 0x70, + 0x81, 0x9b, 0x2d, 0x85, 0x94, 0x74, 0x2a, 0x3a, + 0xb4, 0xff, 0x4a, 0x13, 0x70, 0x76, 0x2c, 0x2f, + 0x13, 0x5b, 0x43, 0xf9, 0x17, 0xee, 0x26, 0x37, + 0x01, 0xbc, 0x9f, 0x0b, 0x0e, 0x68, 0xcb, 0x87, + 0x65, 0x86, 0xcc, 0x4c, 0x2f, 0x7a, 0x14, 0x0d, + 0xd1, 0xb9, 0x57, 0xbd, 0x50, 0xb6, 0x95, 0x44, + 0x1a, 0x0d, 0xc0, 0x15, 0x0f, 0xd2, 0xc3, 0x72, + 0x4d, 0x6e, 0x4f, 0x8e, 0x6d, 0x64, 0xdc, 0x64, + 0x1f, 0x33, 0x53, 0x4e, 0xd8, 0xa4, 0x74, 0xf3, + 0x33, 0x04, 0x68, 0xd9, 0x92, 0xf3, 0x6e, 0xb7, + 0x5b, 0xe6, 0xf6, 0xc3, 0x55, 0x14, 0x54, 0x87, + 0x00, 0xaf, 0x07, + }, + }, + }, + }, + { + "aggregated", + 13, + 3, + 3, + [][]byte{ + {0x00, 0x01, 0x02, 0x03}, + {0x04, 0x05, 0x06, 0x07}, + {0x08, 0x09, 0x0A, 0x0B}, + }, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x0, 0x30, 0x0, 0x20, + 0x0, 0x20, 0x0, 0x20, 0x0, 0x1, 0x2, 0x3, + 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, + }, + }, + }, + }, + { //nolint:dupl + "fragmented", + 13, + 3, + 3, + [][]byte{ + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 187), + }, + []*rtp.Packet{ //nolint:dupl + { + Header: rtp.Header{ + Version: 2, + Marker: false, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x0, 0x10, 0x1f, 0x20}, + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 124), + []byte{0, 1, 2, 3}, + ), + }, + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17646, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x00, 0x10, 0x0f, 0xa0}, + []byte{4, 5, 6, 7}, + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 62), + ), + }, + }, + }, + { + "fragmented to the limit", + 13, + 3, + 3, + [][]byte{bytes.Repeat([]byte{1}, 1992)}, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: false, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x0, 0x10, 0x1f, 0x20}, + bytes.Repeat([]byte{1}, 996), + ), + }, + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17646, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x0, 0x10, 0x1f, 0x20}, + bytes.Repeat([]byte{1}, 996), + ), + }, + }, + }, + { + "aggregated followed by fragmented", + 13, + 3, + 3, + [][]byte{ + {0x00, 0x01, 0x02, 0x03}, + {0x04, 0x05, 0x06, 0x07}, + {0x08, 0x09, 0x0A, 0x0B}, + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 187), + }, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x0, 0x30, 0x0, 0x20, + 0x0, 0x20, 0x0, 0x20, 0x0, 0x1, 0x2, 0x3, + 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, + }, + }, + { + Header: rtp.Header{ + Version: 2, + Marker: false, + PayloadType: 96, + SequenceNumber: 17646, + Timestamp: 3072, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x0, 0x10, 0x1f, 0x20}, + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 124), + []byte{0, 1, 2, 3}, + ), + }, + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17647, + Timestamp: 3072, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x00, 0x10, 0x0f, 0xa0}, + []byte{4, 5, 6, 7}, + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 62), + ), + }, + }, + }, + { + "single, custom sized", + 6, + 2, + 2, + [][]byte{ + {0x01, 0x02, 0x03, 0x04}, + }, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x00, 0x08, 0x10, + 0x01, 0x02, 0x03, 0x04, + }, + }, + }, + }, + { + "single, custom sized, padded", + 13, + 0, + 0, + [][]byte{ + {0x01, 0x02, 0x03, 0x04}, + }, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x00, 0x0d, 0x00, 0x20, + 0x01, 0x02, 0x03, 0x04, + }, + }, + }, + }, + { + "aggregated, custom sized, padded", + 13, + 0, + 0, + [][]byte{ + {0x01, 0x02, 0x03, 0x04}, + {0x05, 0x06, 0x07, 0x08}, + }, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: []byte{ + 0x00, 0x1a, 0x00, 0x20, 0x01, 0x00, + 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, + }, + }, + }, + }, + { + "fragmented, custom sized", + 21, + 3, + 3, + [][]byte{ + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 187), + }, + []*rtp.Packet{ + { + Header: rtp.Header{ + Version: 2, + Marker: false, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x0, 0x18, 0x00, 0x1f, 0x18}, + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 124), + []byte{0, 1, 2}, + ), + }, + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17646, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x00, 0x18, 0x00, 0x0f, 0xa8}, + []byte{3, 4, 5, 6, 7}, + bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 62), + ), + }, + }, + }, + { //nolint:dupl + "fragmented, custom sized, padded", + 13, + 0, + 0, + [][]byte{ + bytes.Repeat([]byte{0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, 187), + }, + []*rtp.Packet{ //nolint:dupl + { + Header: rtp.Header{ + Version: 2, + Marker: false, + PayloadType: 96, + SequenceNumber: 17645, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x0, 0x0d, 0x1f, 0x20}, + bytes.Repeat([]byte{0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, 124), + []byte{0x08, 0x09, 0x0A, 0x0B}, + ), + }, + { + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 17646, + SSRC: 0x9dbb7812, + }, + Payload: mergeBytes( + []byte{0x0, 0x0d, 0x0f, 0xa0}, + []byte{0x0C, 0x0D, 0x0E, 0x0F}, + bytes.Repeat([]byte{0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, 62), + ), + }, + }, + }, +} + +func TestEncodeGeneric(t *testing.T) { + for _, ca := range casesGeneric { + t.Run(ca.name, func(t *testing.T) { + e := &Encoder{ + PayloadType: 96, + SSRC: uint32Ptr(0x9dbb7812), + InitialSequenceNumber: uint16Ptr(0x44ed), + SizeLength: ca.sizeLength, + IndexLength: ca.indexLength, + IndexDeltaLength: ca.indexDeltaLength, + PayloadMaxSize: 1000, + } + err := e.Init() + require.NoError(t, err) + + pkts, err := e.Encode(ca.aus) + require.NoError(t, err) + require.Equal(t, ca.pkts, pkts) + }) + } +} diff --git a/pkg/format/rtpmpeg4audio/payload_length_info.go b/pkg/format/rtpmpeg4audio/payload_length_info.go deleted file mode 100644 index ab248a7a..00000000 --- a/pkg/format/rtpmpeg4audio/payload_length_info.go +++ /dev/null @@ -1,38 +0,0 @@ -package rtpmpeg4audio - -import ( - "fmt" -) - -func payloadLengthInfoDecode(buf []byte) (int, int, error) { - lb := len(buf) - l := 0 - n := 0 - - for { - if (lb - n) == 0 { - return 0, 0, fmt.Errorf("not enough bytes") - } - - b := buf[n] - n++ - l += int(b) - - if b != 255 { - break - } - } - - return l, n, nil -} - -func payloadLengthInfoEncodeSize(auLen int) int { - return auLen/255 + 1 -} - -func payloadLengthInfoEncode(plil int, auLen int, buf []byte) { - for i := 0; i < (plil - 1); i++ { - buf[i] = 255 - } - buf[plil-1] = byte(auLen % 255) -} diff --git a/pkg/format/rtpmpeg4video/decoder.go b/pkg/format/rtpmpeg4video/decoder.go index 93630450..b1621c7a 100644 --- a/pkg/format/rtpmpeg4video/decoder.go +++ b/pkg/format/rtpmpeg4video/decoder.go @@ -1,82 +1,15 @@ package rtpmpeg4video import ( - "errors" - "fmt" - - "github.com/pion/rtp" - - "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video" + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpfragmented" ) // ErrMorePacketsNeeded is returned when more packets are needed. -var ErrMorePacketsNeeded = errors.New("need more packets") - -func joinFragments(fragments [][]byte, size int) []byte { - ret := make([]byte, size) - n := 0 - for _, p := range fragments { - n += copy(ret[n:], p) - } - return ret -} +// +// Deprecated: replaced by rtpfragmented.ErrMorePacketsNeeded +var ErrMorePacketsNeeded = rtpfragmented.ErrMorePacketsNeeded // Decoder is a RTP/MPEG-4 Video decoder. -// Specification: RFC6416 -type Decoder struct { - fragments [][]byte - fragmentsSize int - fragmentNextSeqNum uint16 -} - -// Init initializes the decoder. -func (d *Decoder) Init() error { - return nil -} - -func (d *Decoder) resetFragments() { - d.fragments = d.fragments[:0] - d.fragmentsSize = 0 -} - -// Decode decodes a frame from a RTP packet. -func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { - var frame []byte - - if d.fragmentsSize == 0 { - if pkt.Marker { - frame = pkt.Payload - } else { - d.fragmentsSize = len(pkt.Payload) - d.fragments = append(d.fragments, pkt.Payload) - d.fragmentNextSeqNum = pkt.SequenceNumber + 1 - return nil, ErrMorePacketsNeeded - } - } else { - if pkt.SequenceNumber != d.fragmentNextSeqNum { - d.resetFragments() - return nil, fmt.Errorf("discarding frame since a RTP packet is missing") - } - - d.fragmentsSize += len(pkt.Payload) - - if d.fragmentsSize > mpeg4video.MaxFrameSize { - errSize := d.fragmentsSize - d.resetFragments() - return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d", - errSize, mpeg4video.MaxFrameSize) - } - - d.fragments = append(d.fragments, pkt.Payload) - d.fragmentNextSeqNum++ - - if !pkt.Marker { - return nil, ErrMorePacketsNeeded - } - - frame = joinFragments(d.fragments, d.fragmentsSize) - d.resetFragments() - } - - return frame, nil -} +// +// Deprecated: replaced by rtpfragmented.Decoder +type Decoder = rtpfragmented.Decoder diff --git a/pkg/format/rtpmpeg4video/encoder.go b/pkg/format/rtpmpeg4video/encoder.go index ba6e6596..c7e65d2b 100644 --- a/pkg/format/rtpmpeg4video/encoder.go +++ b/pkg/format/rtpmpeg4video/encoder.go @@ -1,108 +1,10 @@ package rtpmpeg4video import ( - "crypto/rand" - - "github.com/pion/rtp" + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpfragmented" ) -const ( - rtpVersion = 2 - defaultPayloadMaxSize = 1450 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) - 10 (SRTP overhead) -) - -func randUint32() (uint32, error) { - var b [4]byte - _, err := rand.Read(b[:]) - if err != nil { - return 0, err - } - return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil -} - -func packetCount(avail, le int) int { - n := le / avail - if (le % avail) != 0 { - n++ - } - return n -} - // Encoder is a RTP/MPEG-4 Video encoder. -// Specification: RFC6416 -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 - - // maximum size of packet payloads (optional). - // It defaults to 1450. - PayloadMaxSize int - - sequenceNumber uint16 -} - -// Init initializes the encoder. -func (e *Encoder) Init() error { - if e.SSRC == nil { - v, err := randUint32() - if err != nil { - return err - } - e.SSRC = &v - } - if e.InitialSequenceNumber == nil { - v, err := randUint32() - if err != nil { - return err - } - v2 := uint16(v) - e.InitialSequenceNumber = &v2 - } - if e.PayloadMaxSize == 0 { - e.PayloadMaxSize = defaultPayloadMaxSize - } - - e.sequenceNumber = *e.InitialSequenceNumber - return nil -} - -// Encode encodes a frame into RTP packets. -func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) { - avail := e.PayloadMaxSize - le := len(frame) - packetCount := packetCount(avail, le) - - ret := make([]*rtp.Packet, packetCount) - pos := 0 - le = avail - - for i := range ret { - if i == (packetCount - 1) { - le = len(frame[pos:]) - } - - ret[i] = &rtp.Packet{ - Header: rtp.Header{ - Version: rtpVersion, - PayloadType: e.PayloadType, - SequenceNumber: e.sequenceNumber, - SSRC: *e.SSRC, - Marker: (i == packetCount-1), - }, - Payload: frame[pos : pos+le], - } - - pos += le - e.sequenceNumber++ - } - - return ret, nil -} +// +// Deprecated: replaced by rtpfragmented.Encoder +type Encoder = rtpfragmented.Encoder