Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support multichannel Opus (https://github.com/bluenviron/mediamtx/issues/3355) #572

Merged
merged 1 commit into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ In RTSP, media streams are routed between server and clients by using RTP packet
|[RFC2250, RTP Payload Format for MPEG1/MPEG2 Video](https://datatracker.ietf.org/doc/html/rfc2250)|MPEG-1 video, MPEG-2 audio, MPEG-TS payload formats|
|[RFC2435, RTP Payload Format for JPEG-compressed Video](https://datatracker.ietf.org/doc/html/rfc2435)|M-JPEG payload format|
|[RFC7587, RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)|Opus payload format|
|[Multiopus in libwebrtc](https://webrtc-review.googlesource.com/c/src/+/129768)|Opus payload format|
|[RFC5215, RTP Payload Format for Vorbis Encoded Audio](https://datatracker.ietf.org/doc/html/rfc5215)|Vorbis payload format|
|[RFC4184, RTP Payload Format for AC-3 Audio](https://datatracker.ietf.org/doc/html/rfc4184)|AC-3 payload format|
|[RFC6416, RTP Payload Format for MPEG-4 Audio/Visual Streams](https://datatracker.ietf.org/doc/html/rfc6416)|MPEG-4 audio payload format|
Expand Down
10 changes: 6 additions & 4 deletions pkg/description/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,9 @@ var casesSession = []struct {
IsBackChannel: true,
Formats: []format.Format{
&format.Opus{
PayloadTyp: 111,
IsStereo: false,
PayloadTyp: 111,
IsStereo: false,
ChannelCount: 1,
},
&format.Generic{
PayloadTyp: 103,
Expand Down Expand Up @@ -820,8 +821,9 @@ func TestSessionFindFormat(t *testing.T) {
Type: MediaTypeAudio,
Formats: []format.Format{
&format.Opus{
PayloadTyp: 111,
IsStereo: true,
PayloadTyp: 111,
IsStereo: true,
ChannelCount: 2,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func Unmarshal(mediaType string, payloadType uint8, rtpMap string, fmtp map[stri

// audio

case codec == "opus":
case codec == "opus", codec == "multiopus":
return &Opus{}

case codec == "vorbis":
Expand Down
35 changes: 33 additions & 2 deletions pkg/format/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,14 +645,37 @@ var casesFormat = []struct {
"sprop-stereo": "1",
},
&Opus{
PayloadTyp: 96,
IsStereo: true,
PayloadTyp: 96,
IsStereo: true,
ChannelCount: 2,
},
"opus/48000/2",
map[string]string{
"sprop-stereo": "1",
},
},
{
"audio opus 5.1",
"audio",
96,
"multiopus/48000/6",
map[string]string{
"num_streams": "4",
"coupled_streams": "2",
"channel_mapping": "0,4,1,2,3,5",
},
&Opus{
PayloadTyp: 96,
ChannelCount: 6,
},
"multiopus/48000/6",
map[string]string{
"channel_mapping": "0,4,1,2,3,5",
"coupled_streams": "2",
"num_streams": "4",
"sprop-maxcapturerate": "48000",
},
},
{
"audio ac3",
"audio",
Expand Down Expand Up @@ -1250,6 +1273,14 @@ func FuzzUnmarshalOpus(f *testing.F) {
})
}

func FuzzUnmarshalOpusMulti(f *testing.F) {
f.Add("48000/a")

f.Fuzz(func(_ *testing.T, a string) {
Unmarshal("audio", 96, "multiopus/"+a, nil) //nolint:errcheck
})
}

func FuzzUnmarshalVorbis(f *testing.F) {
f.Fuzz(func(_ *testing.T, a, b string) {
Unmarshal("audio", 96, "Vorbis/"+a, map[string]string{ //nolint:errcheck
Expand Down
141 changes: 113 additions & 28 deletions pkg/format/opus.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,63 @@

// Opus is the RTP format for the Opus codec.
// Specification: https://datatracker.ietf.org/doc/html/rfc7587
// Specification: https://webrtc-review.googlesource.com/c/src/+/129768
type Opus struct {
PayloadTyp uint8
IsStereo bool
PayloadTyp uint8
ChannelCount int

// Deprecated: replaced by ChannelCount.
IsStereo bool
}

func (f *Opus) unmarshal(ctx *unmarshalContext) error {
f.PayloadTyp = ctx.payloadType

tmp := strings.SplitN(ctx.clock, "/", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid clock (%v)", ctx.clock)
}
if ctx.codec == "opus" {
tmp := strings.SplitN(ctx.clock, "/", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid clock (%v)", ctx.clock)
}

sampleRate, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil || sampleRate != 48000 {
return fmt.Errorf("invalid sample rate: %d", sampleRate)
}
sampleRate, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil || sampleRate != 48000 {
return fmt.Errorf("invalid sample rate: '%s", tmp[0])
}

channelCount, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil || channelCount != 2 {
return fmt.Errorf("invalid channel count: %d", channelCount)
}
channelCount, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil || channelCount != 2 {
return fmt.Errorf("invalid channel count: '%s'", tmp[1])
}

// assume mono
f.ChannelCount = 1
f.IsStereo = false

for key, val := range ctx.fmtp {
if key == "sprop-stereo" {
f.IsStereo = (val == "1")
for key, val := range ctx.fmtp {
if key == "sprop-stereo" {
if val == "1" {
f.ChannelCount = 2
f.IsStereo = true
}
}
}
} else {
tmp := strings.SplitN(ctx.clock, "/", 2)
if len(tmp) != 2 {
return fmt.Errorf("invalid clock (%v)", ctx.clock)
}

sampleRate, err := strconv.ParseUint(tmp[0], 10, 31)
if err != nil || sampleRate != 48000 {
return fmt.Errorf("invalid sample rate: '%s'", tmp[0])
}

channelCount, err := strconv.ParseUint(tmp[1], 10, 31)
if err != nil {
return fmt.Errorf("invalid channel count: '%s'", tmp[1])
}

f.ChannelCount = int(channelCount)
}

return nil
Expand All @@ -63,22 +93,77 @@

// RTPMap implements Format.
func (f *Opus) RTPMap() string {
// RFC7587: The RTP clock rate in "a=rtpmap" MUST be 48000, and the
// number of channels MUST be 2.
return "opus/48000/2"
if f.ChannelCount <= 2 {
// RFC7587: The RTP clock rate in "a=rtpmap" MUST be 48000, and the
// number of channels MUST be 2.
return "opus/48000/2"
}

return "multiopus/48000/" + strconv.FormatUint(uint64(f.ChannelCount), 10)
}

// FMTP implements Format.
func (f *Opus) FMTP() map[string]string {
fmtp := map[string]string{
"sprop-stereo": func() string {
if f.IsStereo {
return "1"
}
return "0"
}(),
if f.ChannelCount <= 2 {
return map[string]string{
"sprop-stereo": func() string {
if f.ChannelCount == 2 || (f.ChannelCount == 0 && f.IsStereo) {
return "1"
}
return "0"

Check warning on line 113 in pkg/format/opus.go

View check run for this annotation

Codecov / codecov/patch

pkg/format/opus.go#L113

Added line #L113 was not covered by tests
}(),
}
}

switch f.ChannelCount {
case 3:
return map[string]string{
"num_streams": "2",
"coupled_streams": "1",
"channel_mapping": "0,2,1",
"sprop-maxcapturerate": "48000",
}

Check warning on line 125 in pkg/format/opus.go

View check run for this annotation

Codecov / codecov/patch

pkg/format/opus.go#L119-L125

Added lines #L119 - L125 were not covered by tests

case 4:
return map[string]string{
"num_streams": "2",
"coupled_streams": "2",
"channel_mapping": "0,1,2,3",
"sprop-maxcapturerate": "48000",
}

Check warning on line 133 in pkg/format/opus.go

View check run for this annotation

Codecov / codecov/patch

pkg/format/opus.go#L127-L133

Added lines #L127 - L133 were not covered by tests

case 5:
return map[string]string{
"num_streams": "3",
"coupled_streams": "2",
"channel_mapping": "0,4,1,2,3",
"sprop-maxcapturerate": "48000",
}

Check warning on line 141 in pkg/format/opus.go

View check run for this annotation

Codecov / codecov/patch

pkg/format/opus.go#L135-L141

Added lines #L135 - L141 were not covered by tests

case 6:
return map[string]string{
"num_streams": "4",
"coupled_streams": "2",
"channel_mapping": "0,4,1,2,3,5",
"sprop-maxcapturerate": "48000",
}

case 7:
return map[string]string{
"num_streams": "4",
"coupled_streams": "3",
"channel_mapping": "0,4,1,2,3,5,6",
"sprop-maxcapturerate": "48000",
}

Check warning on line 157 in pkg/format/opus.go

View check run for this annotation

Codecov / codecov/patch

pkg/format/opus.go#L151-L157

Added lines #L151 - L157 were not covered by tests

default: // assume 8
return map[string]string{
"num_streams": "5",
"coupled_streams": "3",
"channel_mapping": "0,6,1,4,5,2,3,7",
"sprop-maxcapturerate": "48000",
}

Check warning on line 165 in pkg/format/opus.go

View check run for this annotation

Codecov / codecov/patch

pkg/format/opus.go#L159-L165

Added lines #L159 - L165 were not covered by tests
}
return fmtp
}

// PTSEqualsDTS implements Format.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("0")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("/")
Loading