move MPEG-4 audio LATM to dedicated format (bluenviron/mediamtx#4403) (#834)

This commit is contained in:
Alessandro Ros
2025-07-20 17:28:52 +02:00
committed by GitHub
parent 1db87f6bf6
commit 800dd9c850
31 changed files with 1620 additions and 1800 deletions

View File

@@ -106,11 +106,11 @@ Features:
## RTP Payload Formats ## 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 ### 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:| |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:| |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 ### 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:| |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)|| |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 (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:| |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:| |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)|| |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 ### 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)|| |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:| |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| |[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| |[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| |[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| |[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| |[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| |[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|

View File

@@ -122,7 +122,7 @@ func TestClientPlayFormats(t *testing.T) {
Type: description.MediaTypeAudio, Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG4Audio{ Formats: []format.Format{&format.MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 44100, SampleRate: 44100,
ChannelCount: 2, ChannelCount: 2,
@@ -137,7 +137,7 @@ func TestClientPlayFormats(t *testing.T) {
Type: description.MediaTypeAudio, Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG4Audio{ Formats: []format.Format{&format.MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 96000, SampleRate: 96000,
ChannelCount: 2, ChannelCount: 2,

View File

@@ -10,7 +10,6 @@ import (
"os" "os"
"time" "time"
"github.com/asticode/go-astits"
"github.com/bluenviron/gortsplib/v4" "github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
@@ -149,7 +148,7 @@ func main() {
err := r.Read() err := r.Read()
if err != nil { if err != nil {
// file has ended // file has ended
if errors.Is(err, astits.ErrNoMorePackets) { if errors.Is(err, io.EOF) {
log.Printf("file has ended, rewinding") log.Printf("file has ended, rewinding")
// rewind to start position // rewind to start position

View File

@@ -9,7 +9,6 @@ import (
"os" "os"
"time" "time"
"github.com/asticode/go-astits"
"github.com/bluenviron/gortsplib/v4" "github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts" "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts"
@@ -138,7 +137,7 @@ func (r *fileStreamer) run() {
err := mr.Read() err := mr.Read()
if err != nil { if err != nil {
// file has ended // file has ended
if errors.Is(err, astits.ErrNoMorePackets) { if errors.Is(err, io.EOF) {
log.Printf("file has ended, rewinding") log.Printf("file has ended, rewinding")
// rewind to start position // rewind to start position

4
go.mod
View File

@@ -3,8 +3,7 @@ module github.com/bluenviron/gortsplib/v4
go 1.23.0 go 1.23.0
require ( require (
github.com/asticode/go-astits v1.13.0 github.com/bluenviron/mediacommon/v2 v2.3.1-0.20250720151422-8fb6595e0eac
github.com/bluenviron/mediacommon/v2 v2.3.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/pion/rtcp v1.2.15 github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.21 github.com/pion/rtp v1.8.21
@@ -16,6 +15,7 @@ require (
require ( require (
github.com/asticode/go-astikit v0.30.0 // indirect 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/davecgh/go-spew v1.1.1 // indirect
github.com/pion/logging v0.2.3 // indirect github.com/pion/logging v0.2.3 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect

4
go.sum
View File

@@ -2,8 +2,8 @@ github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflx
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= 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/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.1-0.20250720151422-8fb6595e0eac h1:EgZce5Y9ZV6lcvc6p9ZqywjJKkCdpx4V0h5sAgHrLGo=
github.com/bluenviron/mediacommon/v2 v2.3.0/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GYLLrX2R7ZkUE+1pwV0= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@@ -160,9 +160,12 @@ func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error)
case codec == "vorbis" && payloadType >= 96 && payloadType <= 127: case codec == "vorbis" && payloadType >= 96 && payloadType <= 127:
return &Vorbis{} return &Vorbis{}
case (codec == "mpeg4-generic" || codec == "mp4a-latm") && payloadType >= 96 && payloadType <= 127: case codec == "mpeg4-generic" && payloadType >= 96 && payloadType <= 127:
return &MPEG4Audio{} return &MPEG4Audio{}
case codec == "mp4a-latm" && payloadType >= 96 && payloadType <= 127:
return &MPEG4AudioLATM{}
case codec == "ac3" && payloadType >= 96 && payloadType <= 127: case codec == "ac3" && payloadType >= 96 && payloadType <= 127:
return &AC3{} return &AC3{}

View File

@@ -282,7 +282,7 @@ var casesFormat = []struct {
&MPEG4Audio{ &MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
ProfileLevelID: 1, ProfileLevelID: 1,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000, SampleRate: 48000,
ChannelCount: 2, ChannelCount: 2,
@@ -314,7 +314,7 @@ var casesFormat = []struct {
&MPEG4Audio{ &MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
ProfileLevelID: 1, ProfileLevelID: 1,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000, SampleRate: 48000,
ChannelCount: 2, ChannelCount: 2,
@@ -346,7 +346,7 @@ var casesFormat = []struct {
&MPEG4Audio{ &MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
ProfileLevelID: 14, ProfileLevelID: 14,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000, SampleRate: 48000,
ChannelCount: 2, ChannelCount: 2,
@@ -374,7 +374,7 @@ var casesFormat = []struct {
&MPEG4Audio{ &MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
ProfileLevelID: 48, ProfileLevelID: 48,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: 2, Type: 2,
ExtensionType: 29, ExtensionType: 29,
ExtensionSampleRate: 48000, ExtensionSampleRate: 48000,
@@ -401,8 +401,7 @@ var casesFormat = []struct {
"a=rtpmap:96 MP4A-LATM/24000/2\n" + "a=rtpmap:96 MP4A-LATM/24000/2\n" +
"a=fmtp:96 profile-level-id=1; " + "a=fmtp:96 profile-level-id=1; " +
"bitrate=64000; cpresent=0; object=2; config=400026203fc0\n", "bitrate=64000; cpresent=0; object=2; config=400026203fc0\n",
&MPEG4Audio{ &MPEG4AudioLATM{
LATM: true,
PayloadTyp: 96, PayloadTyp: 96,
ProfileLevelID: 1, ProfileLevelID: 1,
Bitrate: intPtr(64000), Bitrate: intPtr(64000),
@@ -410,7 +409,7 @@ var casesFormat = []struct {
StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ StreamMuxConfig: &mpeg4audio.StreamMuxConfig{
Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{
Layers: []*mpeg4audio.StreamMuxConfigLayer{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{
AudioSpecificConfig: &mpeg4audio.Config{ AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{
Type: 2, Type: 2,
SampleRate: 24000, SampleRate: 24000,
ChannelCount: 2, ChannelCount: 2,
@@ -438,8 +437,7 @@ var casesFormat = []struct {
"a=rtpmap:110 MP4A-LATM/24000/1\n" + "a=rtpmap:110 MP4A-LATM/24000/1\n" +
"a=fmtp:110 profile-level-id=15; " + "a=fmtp:110 profile-level-id=15; " +
"cpresent=0; object=2; config=400026103fc0; sbr-enabled=1\n", "cpresent=0; object=2; config=400026103fc0; sbr-enabled=1\n",
&MPEG4Audio{ &MPEG4AudioLATM{
LATM: true,
PayloadTyp: 110, PayloadTyp: 110,
ProfileLevelID: 15, ProfileLevelID: 15,
CPresent: false, CPresent: false,
@@ -447,7 +445,7 @@ var casesFormat = []struct {
StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ StreamMuxConfig: &mpeg4audio.StreamMuxConfig{
Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{
Layers: []*mpeg4audio.StreamMuxConfigLayer{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{
AudioSpecificConfig: &mpeg4audio.Config{ AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{
Type: 2, Type: 2,
SampleRate: 24000, SampleRate: 24000,
ChannelCount: 1, ChannelCount: 1,
@@ -475,8 +473,7 @@ var casesFormat = []struct {
"a=rtpmap:110 MP4A-LATM/48000/2\n" + "a=rtpmap:110 MP4A-LATM/48000/2\n" +
"a=fmtp:110 profile-level-id=44; " + "a=fmtp:110 profile-level-id=44; " +
"bitrate=64000; cpresent=0; config=40005623101fe0; sbr-enabled=1\n", "bitrate=64000; cpresent=0; config=40005623101fe0; sbr-enabled=1\n",
&MPEG4Audio{ &MPEG4AudioLATM{
LATM: true,
PayloadTyp: 110, PayloadTyp: 110,
ProfileLevelID: 44, ProfileLevelID: 44,
CPresent: false, CPresent: false,
@@ -485,7 +482,7 @@ var casesFormat = []struct {
StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ StreamMuxConfig: &mpeg4audio.StreamMuxConfig{
Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{
Layers: []*mpeg4audio.StreamMuxConfigLayer{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{
AudioSpecificConfig: &mpeg4audio.Config{ AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{
Type: 2, Type: 2,
ExtensionType: 5, ExtensionType: 5,
ExtensionSampleRate: 48000, ExtensionSampleRate: 48000,
@@ -516,8 +513,7 @@ var casesFormat = []struct {
"a=rtpmap:110 MP4A-LATM/48000/2\n" + "a=rtpmap:110 MP4A-LATM/48000/2\n" +
"a=fmtp:110 profile-level-id=48; " + "a=fmtp:110 profile-level-id=48; " +
"bitrate=64000; cpresent=0; config=4001d613101fe0\n", "bitrate=64000; cpresent=0; config=4001d613101fe0\n",
&MPEG4Audio{ &MPEG4AudioLATM{
LATM: true,
PayloadTyp: 110, PayloadTyp: 110,
ProfileLevelID: 48, ProfileLevelID: 48,
Bitrate: intPtr(64000), Bitrate: intPtr(64000),
@@ -525,7 +521,7 @@ var casesFormat = []struct {
StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ StreamMuxConfig: &mpeg4audio.StreamMuxConfig{
Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{
Layers: []*mpeg4audio.StreamMuxConfigLayer{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{
AudioSpecificConfig: &mpeg4audio.Config{ AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{
Type: 2, Type: 2,
ExtensionType: 29, ExtensionType: 29,
ExtensionSampleRate: 48000, ExtensionSampleRate: 48000,
@@ -555,15 +551,14 @@ var casesFormat = []struct {
"a=rtpmap:110 MP4A-LATM/48000\n" + "a=rtpmap:110 MP4A-LATM/48000\n" +
"a=fmtp:110 profile-level-id=30; " + "a=fmtp:110 profile-level-id=30; " +
"cpresent=0; config=40002310\n", "cpresent=0; config=40002310\n",
&MPEG4Audio{ &MPEG4AudioLATM{
LATM: true,
PayloadTyp: 110, PayloadTyp: 110,
ProfileLevelID: 30, ProfileLevelID: 30,
CPresent: false, CPresent: false,
StreamMuxConfig: &mpeg4audio.StreamMuxConfig{ StreamMuxConfig: &mpeg4audio.StreamMuxConfig{
Programs: []*mpeg4audio.StreamMuxConfigProgram{{ Programs: []*mpeg4audio.StreamMuxConfigProgram{{
Layers: []*mpeg4audio.StreamMuxConfigLayer{{ Layers: []*mpeg4audio.StreamMuxConfigLayer{{
AudioSpecificConfig: &mpeg4audio.Config{ AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{
Type: 2, Type: 2,
SampleRate: 48000, SampleRate: 48000,
ChannelCount: 1, ChannelCount: 1,
@@ -587,16 +582,15 @@ var casesFormat = []struct {
"v=0\n" + "v=0\n" +
"s=\n" + "s=\n" +
"m=audio 0 RTP/AVP 96\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", "a=fmtp:96 cpresent=1\n",
&MPEG4Audio{ &MPEG4AudioLATM{
PayloadTyp: 96, PayloadTyp: 96,
LATM: true,
ProfileLevelID: 30, ProfileLevelID: 30,
CPresent: true, CPresent: true,
}, },
96, 96,
"MP4A-LATM/16000/1", "MP4A-LATM/90000/1",
map[string]string{ map[string]string{
"cpresent": "1", "cpresent": "1",
"profile-level-id": "30", "profile-level-id": "30",

View File

@@ -14,35 +14,20 @@ import (
// MPEG4Audio is the RTP format for a MPEG-4 Audio codec. // MPEG4Audio is the RTP format for a MPEG-4 Audio codec.
// Specification: RFC3640 // Specification: RFC3640
// Specification: RFC6416, section 7.3
type MPEG4Audio struct { type MPEG4Audio struct {
// payload type of packets. // payload type of packets.
PayloadTyp uint8 PayloadTyp uint8
// use LATM format (RFC6416) instead of generic format (RFC3640).
LATM bool
// profile level ID.
ProfileLevelID int ProfileLevelID int
Config *mpeg4audio.AudioSpecificConfig
// generic only
Config *mpeg4audio.Config
SizeLength int SizeLength int
IndexLength int IndexLength int
IndexDeltaLength int IndexDeltaLength int
// LATM only
Bitrate *int
CPresent bool
StreamMuxConfig *mpeg4audio.StreamMuxConfig
SBREnabled *bool
} }
func (f *MPEG4Audio) unmarshal(ctx *unmarshalContext) error { func (f *MPEG4Audio) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType f.PayloadTyp = ctx.payloadType
f.LATM = (ctx.codec != "mpeg4-generic")
if !f.LATM {
for key, val := range ctx.fmtp { for key, val := range ctx.fmtp {
switch key { switch key {
case "streamtype": case "streamtype":
@@ -66,13 +51,13 @@ func (f *MPEG4Audio) unmarshal(ctx *unmarshalContext) error {
case "config": case "config":
enc, err := hex.DecodeString(val) enc, err := hex.DecodeString(val)
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC config: %w", err) return fmt.Errorf("invalid AAC config: %v", val)
} }
f.Config = &mpeg4audio.Config{} f.Config = &mpeg4audio.AudioSpecificConfig{}
err = f.Config.Unmarshal(enc) err = f.Config.Unmarshal(enc)
if err != nil { if err != nil {
return fmt.Errorf("invalid AAC config: %w", err) return fmt.Errorf("invalid AAC config: %v", val)
} }
case "sizelength": case "sizelength":
@@ -105,61 +90,6 @@ func (f *MPEG4Audio) unmarshal(ctx *unmarshalContext) error {
if f.SizeLength == 0 { if f.SizeLength == 0 {
return fmt.Errorf("sizelength is missing") 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
}
}
if f.CPresent {
if f.StreamMuxConfig != nil {
return fmt.Errorf("config and cpresent can't be used at the same time")
}
} else {
if f.StreamMuxConfig == nil {
return fmt.Errorf("config is missing")
}
}
}
return nil return nil
} }
@@ -171,13 +101,7 @@ func (f *MPEG4Audio) Codec() string {
// ClockRate implements Format. // ClockRate implements Format.
func (f *MPEG4Audio) ClockRate() int { func (f *MPEG4Audio) ClockRate() int {
if !f.LATM {
return f.Config.SampleRate return f.Config.SampleRate
}
if f.CPresent {
return 16000
}
return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.SampleRate
} }
// PayloadType implements Format. // PayloadType implements Format.
@@ -187,7 +111,6 @@ func (f *MPEG4Audio) PayloadType() uint8 {
// RTPMap implements Format. // RTPMap implements Format.
func (f *MPEG4Audio) RTPMap() string { func (f *MPEG4Audio) RTPMap() string {
if !f.LATM {
sampleRate := f.Config.SampleRate sampleRate := f.Config.SampleRate
if f.Config.ExtensionSampleRate != 0 { if f.Config.ExtensionSampleRate != 0 {
sampleRate = f.Config.ExtensionSampleRate sampleRate = f.Config.ExtensionSampleRate
@@ -200,31 +123,10 @@ func (f *MPEG4Audio) RTPMap() string {
return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) +
"/" + strconv.FormatInt(int64(channelCount), 10) "/" + strconv.FormatInt(int64(channelCount), 10)
}
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 = 2
}
return "MP4A-LATM/" + strconv.FormatInt(int64(sampleRate), 10) +
"/" + strconv.FormatInt(int64(channelCount), 10)
} }
// FMTP implements Format. // FMTP implements Format.
func (f *MPEG4Audio) FMTP() map[string]string { func (f *MPEG4Audio) FMTP() map[string]string {
if !f.LATM {
enc, err := f.Config.Marshal() enc, err := f.Config.Marshal()
if err != nil { if err != nil {
return nil return nil
@@ -255,39 +157,6 @@ func (f *MPEG4Audio) FMTP() map[string]string {
fmtp["config"] = hex.EncodeToString(enc) fmtp["config"] = hex.EncodeToString(enc)
return fmtp
}
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 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. // CreateDecoder creates a decoder able to decode the content of the format.
func (f *MPEG4Audio) CreateDecoder() (*rtpmpeg4audio.Decoder, error) { func (f *MPEG4Audio) CreateDecoder() (*rtpmpeg4audio.Decoder, error) {
d := &rtpmpeg4audio.Decoder{ d := &rtpmpeg4audio.Decoder{
LATM: f.LATM,
SizeLength: f.SizeLength, SizeLength: f.SizeLength,
IndexLength: f.IndexLength, IndexLength: f.IndexLength,
IndexDeltaLength: f.IndexDeltaLength, 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. // CreateEncoder creates an encoder able to encode the content of the format.
func (f *MPEG4Audio) CreateEncoder() (*rtpmpeg4audio.Encoder, error) { func (f *MPEG4Audio) CreateEncoder() (*rtpmpeg4audio.Encoder, error) {
e := &rtpmpeg4audio.Encoder{ e := &rtpmpeg4audio.Encoder{
LATM: f.LATM,
PayloadType: f.PayloadTyp, PayloadType: f.PayloadTyp,
SizeLength: f.SizeLength, SizeLength: f.SizeLength,
IndexLength: f.IndexLength, IndexLength: f.IndexLength,
@@ -332,12 +199,8 @@ func (f *MPEG4Audio) CreateEncoder() (*rtpmpeg4audio.Encoder, error) {
} }
// GetConfig returns the MPEG-4 Audio configuration. // GetConfig returns the MPEG-4 Audio configuration.
//
// Deprecated: redundant. Use f.Config.
func (f *MPEG4Audio) GetConfig() *mpeg4audio.Config { func (f *MPEG4Audio) GetConfig() *mpeg4audio.Config {
if !f.LATM {
return f.Config return f.Config
}
if f.CPresent {
return nil
}
return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig
} }

View File

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

View File

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

View File

@@ -10,10 +10,9 @@ import (
) )
func TestMPEG4AudioAttributes(t *testing.T) { func TestMPEG4AudioAttributes(t *testing.T) {
t.Run("generic", func(t *testing.T) {
format := &MPEG4Audio{ format := &MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000, SampleRate: 48000,
ChannelCount: 2, ChannelCount: 2,
@@ -25,47 +24,17 @@ func TestMPEG4AudioAttributes(t *testing.T) {
require.Equal(t, "MPEG-4 Audio", format.Codec()) require.Equal(t, "MPEG-4 Audio", format.Codec())
require.Equal(t, 48000, format.ClockRate()) require.Equal(t, 48000, format.ClockRate())
require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{})) require.Equal(t, true, format.PTSEqualsDTS(&rtp.Packet{}))
require.Equal(t, &mpeg4audio.Config{ require.Equal(t, &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000, SampleRate: 48000,
ChannelCount: 2, ChannelCount: 2,
}, format.GetConfig()) }, 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())
})
} }
func TestMPEG4AudioDecEncoder(t *testing.T) { func TestMPEG4AudioDecEncoder(t *testing.T) {
t.Run("generic", func(t *testing.T) {
format := &MPEG4Audio{ format := &MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
Config: &mpeg4audio.Config{ Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC, Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000, SampleRate: 48000,
ChannelCount: 2, ChannelCount: 2,
@@ -88,39 +57,4 @@ func TestMPEG4AudioDecEncoder(t *testing.T) {
byts, err := dec.Decode(pkts[0]) byts, err := dec.Decode(pkts[0])
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, [][]byte{{0x01, 0x02, 0x03, 0x04}}, byts) 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)
})
} }

View File

@@ -10,7 +10,7 @@ import (
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video" "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video"
"github.com/pion/rtp" "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. // 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. // CreateDecoder creates a decoder able to decode the content of the format.
func (f *MPEG4Video) CreateDecoder() (*rtpmpeg4video.Decoder, error) { func (f *MPEG4Video) CreateDecoder() (*rtpfragmented.Decoder, error) {
d := &rtpmpeg4video.Decoder{} d := &rtpfragmented.Decoder{}
err := d.Init() err := d.Init()
if err != nil { 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. // CreateEncoder creates an encoder able to encode the content of the format.
func (f *MPEG4Video) CreateEncoder() (*rtpmpeg4video.Encoder, error) { func (f *MPEG4Video) CreateEncoder() (*rtpfragmented.Encoder, error) {
e := &rtpmpeg4video.Encoder{ e := &rtpfragmented.Encoder{
PayloadType: f.PayloadTyp, PayloadType: f.PayloadTyp,
} }

View File

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

View File

@@ -1,7 +1,6 @@
package rtpmpeg4audio package rtpfragmented
import ( import (
"bytes"
"errors" "errors"
"testing" "testing"
@@ -9,25 +8,17 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDecodeLATM(t *testing.T) { func TestDecode(t *testing.T) {
for _, ca := range casesLATM { for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) { t.Run(ca.name, func(t *testing.T) {
d := &Decoder{ d := &Decoder{}
LATM: true,
}
err := d.Init() err := d.Init()
require.NoError(t, err) require.NoError(t, err)
var aus [][]byte var frame []byte
for _, pkt := range ca.pkts { for _, pkt := range ca.pkts {
clone := pkt.Clone() frame, err = d.Decode(pkt)
aus, err = d.Decode(pkt)
// test input integrity
require.Equal(t, clone, pkt)
if errors.Is(err, ErrMorePacketsNeeded) { if errors.Is(err, ErrMorePacketsNeeded) {
continue continue
} }
@@ -35,37 +26,13 @@ func TestDecodeLATM(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
require.Equal(t, ca.au, aus[0]) require.Equal(t, ca.frame, frame)
}) })
} }
} }
func TestDecodeLATMOtherData(t *testing.T) { func TestDecodeErrorMissingPacket(t *testing.T) {
d := &Decoder{ 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}
err := d.Init() err := d.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -77,12 +44,7 @@ func TestDecodeLATMErrorMissingPacket(t *testing.T) {
SequenceNumber: 17645, SequenceNumber: 17645,
SSRC: 0x9dbb7812, SSRC: 0x9dbb7812,
}, },
Payload: mergeBytes( Payload: []byte{0x01, 0x02, 0x03, 0x04},
bytes.Repeat([]byte{0xff}, 16),
[]byte{0x10},
bytes.Repeat([]byte{0, 1, 2, 3, 4, 5, 6, 7}, 180),
[]byte{0, 1, 2},
),
}) })
require.Equal(t, ErrMorePacketsNeeded, err) require.Equal(t, ErrMorePacketsNeeded, err)
@@ -94,20 +56,14 @@ func TestDecodeLATMErrorMissingPacket(t *testing.T) {
SequenceNumber: 17647, SequenceNumber: 17647,
SSRC: 0x9dbb7812, SSRC: 0x9dbb7812,
}, },
Payload: mergeBytes( Payload: []byte{0x01, 0x02, 0x03, 0x04},
[]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},
),
}) })
require.EqualError(t, err, "discarding frame since a RTP packet is missing") 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) { f.Fuzz(func(t *testing.T, a []byte, am bool, b []byte, bm bool) {
d := &Decoder{ d := &Decoder{}
LATM: true,
}
err := d.Init() err := d.Init()
require.NoError(t, err) require.NoError(t, err)

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
// Package rtpfragmented contains a RTP decoder and encoder for codecs with access units that can be fragmented.
package rtpfragmented

View File

@@ -2,7 +2,10 @@ package rtpmpeg4audio
import ( import (
"errors" "errors"
"fmt"
"github.com/bluenviron/mediacommon/v2/pkg/bits"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
@@ -20,16 +23,13 @@ func joinFragments(fragments [][]byte, size int) []byte {
// Decoder is a RTP/MPEG-4 Audio decoder. // Decoder is a RTP/MPEG-4 Audio decoder.
// Specification: RFC3640 // Specification: RFC3640
// Specification: RFC6416, section 7.3
type Decoder struct { 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. // The number of bits in which the AU-size field is encoded in the AU-header.
SizeLength int SizeLength int
// The number of bits in which the AU-Index is encoded in the first AU-header. // The number of bits in which the AU-Index is encoded in the first AU-header.
IndexLength int IndexLength int
// The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header. // The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header.
IndexDeltaLength int IndexDeltaLength int
@@ -37,7 +37,6 @@ type Decoder struct {
adtsMode bool adtsMode bool
fragments [][]byte fragments [][]byte
fragmentsSize int fragmentsSize int
fragmentsExpected int
fragmentNextSeqNum uint16 fragmentNextSeqNum uint16
} }
@@ -51,10 +50,195 @@ func (d *Decoder) resetFragments() {
d.fragmentsSize = 0 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) { func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) {
if !d.LATM { if len(pkt.Payload) < 2 {
return d.decodeGeneric(pkt) 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
} }

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package rtpmpeg4audio
import ( import (
"crypto/rand" "crypto/rand"
"github.com/bluenviron/mediacommon/v2/pkg/bits"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/pion/rtp" "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 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. // Encoder is a RTP/MPEG-4 audio encoder.
// Specification: RFC3640 // Specification: RFC3640
// Specification: RFC6416, section 7.3
type Encoder struct { type Encoder struct {
// payload type of packets. // payload type of packets.
PayloadType uint8 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. // The number of bits in which the AU-size field is encoded in the AU-header.
SizeLength int SizeLength int
@@ -81,8 +87,181 @@ func (e *Encoder) Init() error {
// Encode encodes AUs (non-LATM) or AudioMuxElements (LATM) into RTP packets. // Encode encodes AUs (non-LATM) or AudioMuxElements (LATM) into RTP packets.
func (e *Encoder) Encode(aus [][]byte) ([]*rtp.Packet, error) { func (e *Encoder) Encode(aus [][]byte) ([]*rtp.Packet, error) {
if !e.LATM { var rets []*rtp.Packet
return e.encodeGeneric(aus) 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
} }
return e.encodeLATM(aus) 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
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
package rtpmpeg4audio package rtpmpeg4audio
import ( import (
"bytes"
"testing" "testing"
"github.com/pion/rtp"
"github.com/stretchr/testify/require" "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.SSRC)
require.NotEqual(t, nil, e.InitialSequenceNumber) 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)
})
}
}

View File

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

View File

@@ -1,82 +1,15 @@
package rtpmpeg4video package rtpmpeg4video
import ( import (
"errors" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpfragmented"
"fmt"
"github.com/pion/rtp"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video"
) )
// ErrMorePacketsNeeded is returned when more packets are needed. // ErrMorePacketsNeeded is returned when more packets are needed.
var ErrMorePacketsNeeded = errors.New("need more packets") //
// Deprecated: replaced by rtpfragmented.ErrMorePacketsNeeded
func joinFragments(fragments [][]byte, size int) []byte { var ErrMorePacketsNeeded = rtpfragmented.ErrMorePacketsNeeded
ret := make([]byte, size)
n := 0
for _, p := range fragments {
n += copy(ret[n:], p)
}
return ret
}
// Decoder is a RTP/MPEG-4 Video decoder. // Decoder is a RTP/MPEG-4 Video decoder.
// Specification: RFC6416 //
type Decoder struct { // Deprecated: replaced by rtpfragmented.Decoder
fragments [][]byte type Decoder = rtpfragmented.Decoder
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
}

View File

@@ -1,108 +1,10 @@
package rtpmpeg4video package rtpmpeg4video
import ( import (
"crypto/rand" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpfragmented"
"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/MPEG-4 Video encoder. // Encoder is a RTP/MPEG-4 Video encoder.
// Specification: RFC6416 //
type Encoder struct { // Deprecated: replaced by rtpfragmented.Encoder
// payload type of packets. type Encoder = rtpfragmented.Encoder
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
}