From cab3fe270e00e4ad0f59f06b03880e8a23f4f40d Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Sat, 30 Oct 2021 15:45:13 +0200 Subject: [PATCH] client: support publishing with opus --- README.md | 1 + examples/client-publish-aac/main.go | 2 +- examples/client-publish-h264/main.go | 4 +- examples/client-publish-options/main.go | 2 +- examples/client-publish-opus/main.go | 64 +++ examples/client-publish-pause/main.go | 2 +- track.go | 234 --------- track_aac.go | 130 +++++ track_aac_test.go | 286 +++++++++++ track_h264.go | 124 +++++ track_h264_test.go | 348 +++++++++++++ track_opus.go | 93 ++++ track_opus_test.go | 157 ++++++ track_test.go | 616 ------------------------ 14 files changed, 1208 insertions(+), 855 deletions(-) create mode 100644 examples/client-publish-opus/main.go create mode 100644 track_aac.go create mode 100644 track_aac_test.go create mode 100644 track_h264.go create mode 100644 track_h264_test.go create mode 100644 track_opus.go create mode 100644 track_opus_test.go diff --git a/README.md b/README.md index 3188f017..b9a8c050 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Features: * [client-read-save-to-disk](examples/client-read-save-to-disk/main.go) * [client-publish-h264](examples/client-publish-h264/main.go) * [client-publish-aac](examples/client-publish-aac/main.go) +* [client-publish-opus](examples/client-publish-opus/main.go) * [client-publish-options](examples/client-publish-options/main.go) * [client-publish-pause](examples/client-publish-pause/main.go) * [server](examples/server/main.go) diff --git a/examples/client-publish-aac/main.go b/examples/client-publish-aac/main.go index 106776d3..f93574b6 100644 --- a/examples/client-publish-aac/main.go +++ b/examples/client-publish-aac/main.go @@ -55,7 +55,7 @@ func main() { panic(err) } - // write RTP packets + // route RTP packets to the server err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) if err != nil { panic(err) diff --git a/examples/client-publish-h264/main.go b/examples/client-publish-h264/main.go index 6faad74d..e8697ab7 100644 --- a/examples/client-publish-h264/main.go +++ b/examples/client-publish-h264/main.go @@ -21,7 +21,7 @@ func main() { } defer pc.Close() - fmt.Println("Waiting for a RTP/h264 stream on UDP port 9000 - you can send one with Gstreamer:\n" + + fmt.Println("Waiting for a RTP/H264 stream on UDP port 9000 - you can send one with Gstreamer:\n" + "gst-launch-1.0 videotestsrc ! video/x-raw,width=1920,height=1080" + " ! x264enc speed-preset=veryfast tune=zerolatency bitrate=600000" + " ! rtph264pay ! udpsink host=127.0.0.1 port=9000") @@ -56,7 +56,7 @@ func main() { panic(err) } - // write RTP packets + // route RTP packets to the server err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) if err != nil { panic(err) diff --git a/examples/client-publish-options/main.go b/examples/client-publish-options/main.go index c9d2ceb4..cd859aad 100644 --- a/examples/client-publish-options/main.go +++ b/examples/client-publish-options/main.go @@ -67,7 +67,7 @@ func main() { panic(err) } - // write RTP packets + // route RTP packets to the server err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) if err != nil { panic(err) diff --git a/examples/client-publish-opus/main.go b/examples/client-publish-opus/main.go new file mode 100644 index 00000000..4231fafb --- /dev/null +++ b/examples/client-publish-opus/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "net" + + "github.com/aler9/gortsplib" +) + +// This example shows how to +// 1. generate RTP/Opus packets with Gstreamer +// 2. connect to a RTSP server, announce an Opus track +// 3. route the packets from Gstreamer to the server + +func main() { + // open a listener to receive RTP/Opus packets + pc, err := net.ListenPacket("udp", "localhost:9000") + if err != nil { + panic(err) + } + defer pc.Close() + + fmt.Println("Waiting for a RTP/Opus stream on UDP port 9000 - you can send one with Gstreamer:\n" + + "gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=48000" + + " ! opusenc" + + " ! rtpopuspay ! udpsink host=127.0.0.1 port=9000") + + // wait for first packet + buf := make([]byte, 2048) + _, _, err = pc.ReadFrom(buf) + if err != nil { + panic(err) + } + fmt.Println("stream connected") + + // create an Opus track + track, err := gortsplib.NewTrackOpus(96, &gortsplib.TrackConfigOpus{SampleRate: 48000, ChannelCount: 2}) + if err != nil { + panic(err) + } + + // connect to the server and start publishing the track + conn, err := gortsplib.DialPublish("rtsp://localhost:8554/mystream", + gortsplib.Tracks{track}) + if err != nil { + panic(err) + } + defer conn.Close() + + buf = make([]byte, 2048) + for { + // read RTP packets from the source + n, _, err := pc.ReadFrom(buf) + if err != nil { + panic(err) + } + + // route RTP packets to the server + err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) + if err != nil { + panic(err) + } + } +} diff --git a/examples/client-publish-pause/main.go b/examples/client-publish-pause/main.go index 4d2af4a2..bf2cd0d8 100644 --- a/examples/client-publish-pause/main.go +++ b/examples/client-publish-pause/main.go @@ -63,7 +63,7 @@ func main() { break } - // write RTP packets + // route RTP packets to the server err = conn.WriteFrame(0, gortsplib.StreamTypeRTP, buf[:n]) if err != nil { break diff --git a/track.go b/track.go index 7711f7d1..edfcdd8f 100644 --- a/track.go +++ b/track.go @@ -1,15 +1,12 @@ package gortsplib import ( - "encoding/base64" - "encoding/hex" "fmt" "strconv" "strings" psdp "github.com/pion/sdp/v3" - "github.com/aler9/gortsplib/pkg/aac" "github.com/aler9/gortsplib/pkg/base" "github.com/aler9/gortsplib/pkg/sdp" ) @@ -20,20 +17,6 @@ type Track struct { Media *psdp.MediaDescription } -// TrackConfigH264 is the configuration of an H264 track. -type TrackConfigH264 struct { - SPS []byte - PPS []byte -} - -// TrackConfigAAC is the configuration of an AAC track. -type TrackConfigAAC struct { - Type int - SampleRate int - ChannelCount int - AOTSpecificConfig []byte -} - func (t *Track) hasControlAttribute() bool { for _, attr := range t.Media.Attributes { if attr.Key == "control" { @@ -140,223 +123,6 @@ func (t *Track) ClockRate() (int, error) { return 0, fmt.Errorf("attribute 'rtpmap' not found") } -// NewTrackH264 initializes an H264 track. -func NewTrackH264(payloadType uint8, conf *TrackConfigH264) (*Track, error) { - if len(conf.SPS) < 4 { - return nil, fmt.Errorf("invalid SPS") - } - - spropParameterSets := base64.StdEncoding.EncodeToString(conf.SPS) + - "," + base64.StdEncoding.EncodeToString(conf.PPS) - profileLevelID := strings.ToUpper(hex.EncodeToString(conf.SPS[1:4])) - - typ := strconv.FormatInt(int64(payloadType), 10) - - return &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " H264/90000", - }, - { - Key: "fmtp", - Value: typ + " packetization-mode=1; " + - "sprop-parameter-sets=" + spropParameterSets + "; " + - "profile-level-id=" + profileLevelID, - }, - }, - }, - }, nil -} - -// IsH264 checks whether the track is an H264 track. -func (t *Track) IsH264() bool { - if t.Media.MediaName.Media != "video" { - return false - } - - v, ok := t.Media.Attribute("rtpmap") - if !ok { - return false - } - - v = strings.TrimSpace(v) - vals := strings.Split(v, " ") - if len(vals) != 2 { - return false - } - - return vals[1] == "H264/90000" -} - -// ExtractConfigH264 extracts the configuration of an H264 track. -func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) { - v, ok := t.Media.Attribute("fmtp") - if !ok { - return nil, fmt.Errorf("fmtp attribute is missing") - } - - tmp := strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - if tmp[0] == "sprop-parameter-sets" { - tmp := strings.SplitN(tmp[1], ",", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) - } - - sps, err := base64.StdEncoding.DecodeString(tmp[0]) - if err != nil { - return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) - } - - pps, err := base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) - } - - conf := &TrackConfigH264{ - SPS: sps, - PPS: pps, - } - - return conf, nil - } - } - - return nil, fmt.Errorf("sprop-parameter-sets is missing (%v)", v) -} - -// NewTrackAAC initializes an AAC track. -func NewTrackAAC(payloadType uint8, conf *TrackConfigAAC) (*Track, error) { - mpegConf, err := aac.MPEG4AudioConfig{ - Type: aac.MPEG4AudioType(conf.Type), - SampleRate: conf.SampleRate, - ChannelCount: conf.ChannelCount, - AOTSpecificConfig: conf.AOTSpecificConfig, - }.Encode() - if err != nil { - return nil, err - } - - typ := strconv.FormatInt(int64(payloadType), 10) - - return &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(conf.SampleRate), 10) + - "/" + strconv.FormatInt(int64(conf.ChannelCount), 10), - }, - { - Key: "fmtp", - Value: typ + " profile-level-id=1; " + - "mode=AAC-hbr; " + - "sizelength=13; " + - "indexlength=3; " + - "indexdeltalength=3; " + - "config=" + hex.EncodeToString(mpegConf), - }, - }, - }, - }, nil -} - -// IsAAC checks whether the track is an AAC track. -func (t *Track) IsAAC() bool { - if t.Media.MediaName.Media != "audio" { - return false - } - - v, ok := t.Media.Attribute("rtpmap") - if !ok { - return false - } - - vals := strings.Split(v, " ") - if len(vals) != 2 { - return false - } - - return strings.HasPrefix(strings.ToLower(vals[1]), "mpeg4-generic/") -} - -// ExtractConfigAAC extracts the configuration of an AAC track. -func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) { - v, ok := t.Media.Attribute("fmtp") - if !ok { - return nil, fmt.Errorf("fmtp attribute is missing") - } - - tmp := strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp (%v)", v) - } - - if tmp[0] == "config" { - enc, err := hex.DecodeString(tmp[1]) - if err != nil { - return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - var mpegConf aac.MPEG4AudioConfig - err = mpegConf.Decode(enc) - if err != nil { - return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - conf := &TrackConfigAAC{ - Type: int(mpegConf.Type), - SampleRate: mpegConf.SampleRate, - ChannelCount: mpegConf.ChannelCount, - AOTSpecificConfig: mpegConf.AOTSpecificConfig, - } - - return conf, nil - } - } - - return nil, fmt.Errorf("config is missing (%v)", v) -} - // Tracks is a list of tracks. type Tracks []*Track diff --git a/track_aac.go b/track_aac.go new file mode 100644 index 00000000..e7f46aa4 --- /dev/null +++ b/track_aac.go @@ -0,0 +1,130 @@ +package gortsplib + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/pkg/aac" +) + +// TrackConfigAAC is the configuration of an AAC track. +type TrackConfigAAC struct { + Type int + SampleRate int + ChannelCount int + AOTSpecificConfig []byte +} + +// NewTrackAAC initializes an AAC track. +func NewTrackAAC(payloadType uint8, conf *TrackConfigAAC) (*Track, error) { + mpegConf, err := aac.MPEG4AudioConfig{ + Type: aac.MPEG4AudioType(conf.Type), + SampleRate: conf.SampleRate, + ChannelCount: conf.ChannelCount, + AOTSpecificConfig: conf.AOTSpecificConfig, + }.Encode() + if err != nil { + return nil, err + } + + typ := strconv.FormatInt(int64(payloadType), 10) + + return &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{typ}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(conf.SampleRate), 10) + + "/" + strconv.FormatInt(int64(conf.ChannelCount), 10), + }, + { + Key: "fmtp", + Value: typ + " profile-level-id=1; " + + "mode=AAC-hbr; " + + "sizelength=13; " + + "indexlength=3; " + + "indexdeltalength=3; " + + "config=" + hex.EncodeToString(mpegConf), + }, + }, + }, + }, nil +} + +// IsAAC checks whether the track is an AAC track. +func (t *Track) IsAAC() bool { + if t.Media.MediaName.Media != "audio" { + return false + } + + v, ok := t.Media.Attribute("rtpmap") + if !ok { + return false + } + + vals := strings.Split(v, " ") + if len(vals) != 2 { + return false + } + + return strings.HasPrefix(strings.ToLower(vals[1]), "mpeg4-generic/") +} + +// ExtractConfigAAC extracts the configuration of an AAC track. +func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) { + v, ok := t.Media.Attribute("fmtp") + if !ok { + return nil, fmt.Errorf("fmtp attribute is missing") + } + + tmp := strings.SplitN(v, " ", 2) + if len(tmp) != 2 { + return nil, fmt.Errorf("invalid fmtp (%v)", v) + } + + for _, kv := range strings.Split(tmp[1], ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return nil, fmt.Errorf("invalid fmtp (%v)", v) + } + + if tmp[0] == "config" { + enc, err := hex.DecodeString(tmp[1]) + if err != nil { + return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) + } + + var mpegConf aac.MPEG4AudioConfig + err = mpegConf.Decode(enc) + if err != nil { + return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) + } + + conf := &TrackConfigAAC{ + Type: int(mpegConf.Type), + SampleRate: mpegConf.SampleRate, + ChannelCount: mpegConf.ChannelCount, + AOTSpecificConfig: mpegConf.AOTSpecificConfig, + } + + return conf, nil + } + } + + return nil, fmt.Errorf("config is missing (%v)", v) +} diff --git a/track_aac_test.go b/track_aac_test.go new file mode 100644 index 00000000..8d3d1106 --- /dev/null +++ b/track_aac_test.go @@ -0,0 +1,286 @@ +package gortsplib + +import ( + "testing" + + psdp "github.com/pion/sdp/v3" + "github.com/stretchr/testify/require" +) + +func TestTrackAACNew(t *testing.T) { + track, err := NewTrackAAC(96, &TrackConfigAAC{ + Type: 2, + SampleRate: 48000, + ChannelCount: 2, + }) + require.NoError(t, err) + require.Equal(t, &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", + }, + }, + }, + }, track) +} + +func TestTrackIsAAC(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + }{ + { + "standard", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", + }, + }, + }, + }, + }, + { + "uppercase", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 MPEG4-GENERIC/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", + }, + }, + }, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + require.Equal(t, true, ca.track.IsAAC()) + }) + } +} + +func TestTrackExtractConfigAAC(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + conf *TrackConfigAAC + }{ + { + "generic", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", + }, + }, + }, + }, + &TrackConfigAAC{ + Type: 2, + SampleRate: 48000, + ChannelCount: 2, + }, + }, + { + "vlc rtsp server", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190;", + }, + }, + }, + }, + &TrackConfigAAC{ + Type: 2, + SampleRate: 48000, + ChannelCount: 2, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + conf, err := ca.track.ExtractConfigAAC() + require.NoError(t, err) + require.Equal(t, ca.conf, conf) + }) + } +} + +func TestTrackConfigAACErrors(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + err string + }{ + { + "missing fmtp", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + }, + }, + }, + "fmtp attribute is missing", + }, + { + "invalid fmtp", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96", + }, + }, + }, + }, + "invalid fmtp (96)", + }, + { + "fmtp without key", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id", + }, + }, + }, + }, + "invalid fmtp (96 profile-level-id)", + }, + { + "missing config", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1", + }, + }, + }, + }, + "config is missing (96 profile-level-id=1)", + }, + { + "invalid config", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; config=zz", + }, + }, + }, + }, + "invalid AAC config (zz)", + }, + } { + t.Run(ca.name, func(t *testing.T) { + _, err := ca.track.ExtractConfigAAC() + require.Equal(t, ca.err, err.Error()) + }) + } +} diff --git a/track_h264.go b/track_h264.go new file mode 100644 index 00000000..5844c0cc --- /dev/null +++ b/track_h264.go @@ -0,0 +1,124 @@ +package gortsplib + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "strconv" + "strings" + + psdp "github.com/pion/sdp/v3" +) + +// TrackConfigH264 is the configuration of an H264 track. +type TrackConfigH264 struct { + SPS []byte + PPS []byte +} + +// NewTrackH264 initializes an H264 track. +func NewTrackH264(payloadType uint8, conf *TrackConfigH264) (*Track, error) { + if len(conf.SPS) < 4 { + return nil, fmt.Errorf("invalid SPS") + } + + spropParameterSets := base64.StdEncoding.EncodeToString(conf.SPS) + + "," + base64.StdEncoding.EncodeToString(conf.PPS) + profileLevelID := strings.ToUpper(hex.EncodeToString(conf.SPS[1:4])) + + typ := strconv.FormatInt(int64(payloadType), 10) + + return &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{typ}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: typ + " H264/90000", + }, + { + Key: "fmtp", + Value: typ + " packetization-mode=1; " + + "sprop-parameter-sets=" + spropParameterSets + "; " + + "profile-level-id=" + profileLevelID, + }, + }, + }, + }, nil +} + +// IsH264 checks whether the track is an H264 track. +func (t *Track) IsH264() bool { + if t.Media.MediaName.Media != "video" { + return false + } + + v, ok := t.Media.Attribute("rtpmap") + if !ok { + return false + } + + v = strings.TrimSpace(v) + vals := strings.Split(v, " ") + if len(vals) != 2 { + return false + } + + return vals[1] == "H264/90000" +} + +// ExtractConfigH264 extracts the configuration of an H264 track. +func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) { + v, ok := t.Media.Attribute("fmtp") + if !ok { + return nil, fmt.Errorf("fmtp attribute is missing") + } + + tmp := strings.SplitN(v, " ", 2) + if len(tmp) != 2 { + return nil, fmt.Errorf("invalid fmtp attribute (%v)", v) + } + + for _, kv := range strings.Split(tmp[1], ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return nil, fmt.Errorf("invalid fmtp attribute (%v)", v) + } + + if tmp[0] == "sprop-parameter-sets" { + tmp := strings.SplitN(tmp[1], ",", 2) + if len(tmp) != 2 { + return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) + } + + sps, err := base64.StdEncoding.DecodeString(tmp[0]) + if err != nil { + return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) + } + + pps, err := base64.StdEncoding.DecodeString(tmp[1]) + if err != nil { + return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) + } + + conf := &TrackConfigH264{ + SPS: sps, + PPS: pps, + } + + return conf, nil + } + } + + return nil, fmt.Errorf("sprop-parameter-sets is missing (%v)", v) +} diff --git a/track_h264_test.go b/track_h264_test.go new file mode 100644 index 00000000..08a62cb2 --- /dev/null +++ b/track_h264_test.go @@ -0,0 +1,348 @@ +package gortsplib + +import ( + "testing" + + psdp "github.com/pion/sdp/v3" + "github.com/stretchr/testify/require" +) + +func TestTrackH264New(t *testing.T) { + tr, err := NewTrackH264(96, &TrackConfigH264{ + SPS: []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + PPS: []byte{ + 0x68, 0xee, 0x3c, 0x80, + }, + }) + require.NoError(t, err) + require.Equal(t, &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1; " + + "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", + }, + }, + }, + }, tr) +} + +func TestTrackIsH264(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + }{ + { + "standard", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1; " + + "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", + }, + }, + }, + }, + }, + { + "space at the end rtpmap", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000 ", + }, + }, + }, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + require.Equal(t, true, ca.track.IsH264()) + }) + } +} + +func TestTrackExtractConfigH264(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + conf *TrackConfigH264 + }{ + { + "generic", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1; " + + "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", + }, + }, + }, + }, + &TrackConfigH264{ + SPS: []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + PPS: []byte{ + 0x68, 0xee, 0x3c, 0x80, + }, + }, + }, + { + "vlc rtsp server", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1;profile-level-id=64001f;" + + "sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,aOvjyyLA;", + }, + }, + }, + }, + &TrackConfigH264{ + SPS: []byte{ + 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50, + 0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03, + 0x00, 0x80, 0x00, 0x00, 0x1e, 0x07, 0x8c, 0x18, + 0xcb, + }, + PPS: []byte{ + 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, + }, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + conf, err := ca.track.ExtractConfigH264() + require.NoError(t, err) + require.Equal(t, ca.conf, conf) + }) + } +} + +func TestTrackConfigH264Errors(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + err string + }{ + { + "missing fmtp", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + }, + }, + }, + "fmtp attribute is missing", + }, + { + "invalid fmtp", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96", + }, + }, + }, + }, + "invalid fmtp attribute (96)", + }, + { + "fmtp without key", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode", + }, + }, + }, + }, + "invalid fmtp attribute (96 packetization-mode)", + }, + { + "missing sprop-parameter-set", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1", + }, + }, + }, + }, + "sprop-parameter-sets is missing (96 packetization-mode=1)", + }, + { + "invalid sprop-parameter-set 1", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 sprop-parameter-sets=aaaaaa", + }, + }, + }, + }, + "invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa)", + }, + { + "invalid sprop-parameter-set 2", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 sprop-parameter-sets=aaaaaa,bbb", + }, + }, + }, + }, + "invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa,bbb)", + }, + { + "invalid sprop-parameter-set 3", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb", + }, + }, + }, + }, + "invalid sprop-parameter-sets (96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb)", + }, + } { + t.Run(ca.name, func(t *testing.T) { + _, err := ca.track.ExtractConfigH264() + require.Equal(t, ca.err, err.Error()) + }) + } +} diff --git a/track_opus.go b/track_opus.go new file mode 100644 index 00000000..67114930 --- /dev/null +++ b/track_opus.go @@ -0,0 +1,93 @@ +package gortsplib + +import ( + "fmt" + "strconv" + "strings" + + psdp "github.com/pion/sdp/v3" +) + +// TrackConfigOpus is the configuration of an Opus track. +type TrackConfigOpus struct { + SampleRate int + ChannelCount int +} + +// NewTrackOpus initializes an Opus track. +func NewTrackOpus(payloadType uint8, conf *TrackConfigOpus) (*Track, error) { + typ := strconv.FormatInt(int64(payloadType), 10) + + return &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{typ}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: typ + " opus/" + strconv.FormatInt(int64(conf.SampleRate), 10) + + "/" + strconv.FormatInt(int64(conf.ChannelCount), 10), + }, + { + Key: "fmtp", + Value: typ + " sprop-stereo=" + func() string { + if conf.ChannelCount == 2 { + return "1" + } + return "0" + }(), + }, + }, + }, + }, nil +} + +// IsOpus checks whether the track is an Opus track. +func (t *Track) IsOpus() bool { + if t.Media.MediaName.Media != "audio" { + return false + } + + v, ok := t.Media.Attribute("rtpmap") + if !ok { + return false + } + + vals := strings.Split(v, " ") + if len(vals) != 2 { + return false + } + + return strings.HasPrefix(vals[1], "opus/") +} + +// ExtractConfigOpus extracts the configuration of an Opus track. +func (t *Track) ExtractConfigOpus() (*TrackConfigOpus, error) { + v, ok := t.Media.Attribute("rtpmap") + if !ok { + return nil, fmt.Errorf("rtpmap attribute is missing") + } + + tmp := strings.SplitN(v, "/", 3) + if len(tmp) != 3 { + return nil, fmt.Errorf("invalid rtpmap (%v)", v) + } + + sampleRate, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return nil, err + } + + channelCount, err := strconv.ParseInt(tmp[2], 10, 64) + if err != nil { + return nil, err + } + + return &TrackConfigOpus{ + SampleRate: int(sampleRate), + ChannelCount: int(channelCount), + }, nil +} diff --git a/track_opus_test.go b/track_opus_test.go new file mode 100644 index 00000000..731ebf8b --- /dev/null +++ b/track_opus_test.go @@ -0,0 +1,157 @@ +package gortsplib + +import ( + "testing" + + psdp "github.com/pion/sdp/v3" + "github.com/stretchr/testify/require" +) + +func TestTrackOpusNew(t *testing.T) { + track, err := NewTrackOpus(96, &TrackConfigOpus{ + SampleRate: 48000, + ChannelCount: 2, + }) + require.NoError(t, err) + require.Equal(t, &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 opus/48000/2", + }, + { + Key: "fmtp", + Value: "96 sprop-stereo=1", + }, + }, + }, + }, track) +} + +func TestTrackIsOpus(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + }{ + { + "standard", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 opus/48000/2", + }, + { + Key: "fmtp", + Value: "96 sprop-stereo=1", + }, + }, + }, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + require.Equal(t, true, ca.track.IsOpus()) + }) + } +} + +func TestTrackExtractConfigOpus(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + conf *TrackConfigOpus + }{ + { + "generic", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 opus/48000/2", + }, + { + Key: "fmtp", + Value: "96 sprop-stereo=1", + }, + }, + }, + }, + &TrackConfigOpus{ + SampleRate: 48000, + ChannelCount: 2, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + conf, err := ca.track.ExtractConfigOpus() + require.NoError(t, err) + require.Equal(t, ca.conf, conf) + }) + } +} + +func TestTrackConfigOpusErrors(t *testing.T) { + for _, ca := range []struct { + name string + track *Track + err string + }{ + { + "missing rtpmap", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{}, + }, + }, + "rtpmap attribute is missing", + }, + { + "invalid rtpmap", + &Track{ + Media: &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96", + }, + }, + }, + }, + "invalid rtpmap (96)", + }, + } { + t.Run(ca.name, func(t *testing.T) { + _, err := ca.track.ExtractConfigOpus() + require.Equal(t, ca.err, err.Error()) + }) + } +} diff --git a/track_test.go b/track_test.go index 9410bbde..b852d841 100644 --- a/track_test.go +++ b/track_test.go @@ -3,7 +3,6 @@ package gortsplib import ( "testing" - psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" "github.com/aler9/gortsplib/pkg/base" @@ -219,618 +218,3 @@ func TestTrackClockRate(t *testing.T) { }) } } - -func TestTrackH264New(t *testing.T) { - sps := []byte{ - 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, - 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, - 0x00, 0x03, 0x00, 0x3d, 0x08, - } - - pps := []byte{ - 0x68, 0xee, 0x3c, 0x80, - } - - tr, err := NewTrackH264(96, &TrackConfigH264{sps, pps}) - require.NoError(t, err) - require.Equal(t, &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1; " + - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", - }, - }, - }, - }, tr) -} - -func TestTrackIsH264(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - }{ - { - "standard", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1; " + - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", - }, - }, - }, - }, - }, - { - "standard with a space at the end rtpmap", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000 ", - }, - }, - }, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - require.Equal(t, true, ca.track.IsH264()) - }) - } -} - -func TestTrackExtractConfigH264(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - conf *TrackConfigH264 - }{ - { - "generic", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1; " + - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", - }, - }, - }, - }, - &TrackConfigH264{ - SPS: []byte{ - 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, - 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, - 0x00, 0x03, 0x00, 0x3d, 0x08, - }, - PPS: []byte{ - 0x68, 0xee, 0x3c, 0x80, - }, - }, - }, - { - "vlc rtsp server", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1;profile-level-id=64001f;" + - "sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,aOvjyyLA;", - }, - }, - }, - }, - &TrackConfigH264{ - SPS: []byte{ - 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50, - 0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03, - 0x00, 0x80, 0x00, 0x00, 0x1e, 0x07, 0x8c, 0x18, - 0xcb, - }, - PPS: []byte{ - 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, - }, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - conf, err := ca.track.ExtractConfigH264() - require.NoError(t, err) - require.Equal(t, ca.conf, conf) - }) - } -} - -func TestTrackConfigH264Errors(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - err string - }{ - { - "missing fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - }, - }, - }, - "fmtp attribute is missing", - }, - { - "invalid fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96", - }, - }, - }, - }, - "invalid fmtp attribute (96)", - }, - { - "fmtp without key", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode", - }, - }, - }, - }, - "invalid fmtp attribute (96 packetization-mode)", - }, - { - "missing sprop-parameter-set", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1", - }, - }, - }, - }, - "sprop-parameter-sets is missing (96 packetization-mode=1)", - }, - { - "invalid sprop-parameter-set 1", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=aaaaaa", - }, - }, - }, - }, - "invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa)", - }, - { - "invalid sprop-parameter-set 2", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=aaaaaa,bbb", - }, - }, - }, - }, - "invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa,bbb)", - }, - { - "invalid sprop-parameter-set 3", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb", - }, - }, - }, - }, - "invalid sprop-parameter-sets (96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb)", - }, - } { - t.Run(ca.name, func(t *testing.T) { - _, err := ca.track.ExtractConfigH264() - require.Equal(t, ca.err, err.Error()) - }) - } -} - -func TestTrackAACNew(t *testing.T) { - track, err := NewTrackAAC(96, &TrackConfigAAC{Type: 2, SampleRate: 48000, ChannelCount: 2}) - require.NoError(t, err) - require.Equal(t, &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - }, - }, - }, track) -} - -func TestTrackIsAAC(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - }{ - { - "standard", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - }, - }, - }, - }, - { - "uppercase", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 MPEG4-GENERIC/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - }, - }, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - require.Equal(t, true, ca.track.IsAAC()) - }) - } -} - -func TestTrackExtractConfigAAC(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - conf *TrackConfigAAC - }{ - { - "generic", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - }, - }, - }, - &TrackConfigAAC{ - Type: 2, - SampleRate: 48000, - ChannelCount: 2, - }, - }, - { - "vlc rtsp server", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190;", - }, - }, - }, - }, - &TrackConfigAAC{ - Type: 2, - SampleRate: 48000, - ChannelCount: 2, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - conf, err := ca.track.ExtractConfigAAC() - require.NoError(t, err) - require.Equal(t, ca.conf, conf) - }) - } -} - -func TestTrackConfigAACErrors(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - err string - }{ - { - "missing fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - }, - }, - }, - "fmtp attribute is missing", - }, - { - "invalid fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96", - }, - }, - }, - }, - "invalid fmtp (96)", - }, - { - "fmtp without key", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id", - }, - }, - }, - }, - "invalid fmtp (96 profile-level-id)", - }, - { - "missing config", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1", - }, - }, - }, - }, - "config is missing (96 profile-level-id=1)", - }, - { - "invalid config", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; config=zz", - }, - }, - }, - }, - "invalid AAC config (zz)", - }, - } { - t.Run(ca.name, func(t *testing.T) { - _, err := ca.track.ExtractConfigAAC() - require.Equal(t, ca.err, err.Error()) - }) - } -}