add additional checks on always-available streams (#5408)

make sure that published streams match MPEG-4 audio, G711 and LPCM
parameters declared in configuration or used by the offline file.
This commit is contained in:
Alessandro Ros
2026-02-06 16:06:24 +01:00
committed by GitHub
parent e0fc61ad16
commit ae21240c76
3 changed files with 173 additions and 8 deletions
+1
View File
@@ -170,6 +170,7 @@ func mediasFromAlwaysAvailableTracks(alwaysAvailableTracks []conf.AlwaysAvailabl
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: track.SampleRate,
ChannelConfig: uint8(track.ChannelCount),
ChannelCount: track.ChannelCount,
},
}},
})
+111
View File
@@ -6,6 +6,7 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -1391,3 +1392,113 @@ func TestStreamAlwaysAvailable(t *testing.T) {
<-recv2
}
func TestStreamAlwaysAvailableErrors(t *testing.T) {
for _, ca := range []struct {
name string
tracks []conf.AlwaysAvailableTrack
desc *description.Session
err string
}{
{
"wrong tracks",
[]conf.AlwaysAvailableTrack{
{Codec: "H264"},
{Codec: "H265"},
},
&description.Session{
Medias: []*description.Media{
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{}},
},
},
},
"wants to publish [H264], but stream expects [H264 H265]",
},
{
"wrong mpeg-4 audio config",
[]conf.AlwaysAvailableTrack{
{Codec: "MPEG4Audio", SampleRate: 44100, ChannelCount: 2},
},
&description.Session{
Medias: []*description.Media{
{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG4Audio{
Config: &mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 48000,
ChannelConfig: 1,
},
}},
},
},
},
"MPEG-4 audio configuration does not match, is type=2, sampleRate=48000, " +
"channelCount=1, but stream expects type=2, sampleRate=44100, channelCount=2",
},
{
"wrong g711 config",
[]conf.AlwaysAvailableTrack{
{Codec: "G711", MULaw: true, SampleRate: 8000, ChannelCount: 2},
},
&description.Session{
Medias: []*description.Media{
{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.G711{
MULaw: false,
SampleRate: 8000,
ChannelCount: 2,
}},
},
},
},
"G711 configuration does not match, is MULaw=false, sampleRate=8000, " +
"channelCount=2, but stream expects MULaw=true, sampleRate=8000, channelCount=2",
},
{
"wrong lpcm config",
[]conf.AlwaysAvailableTrack{
{Codec: "LPCM", SampleRate: 44100, ChannelCount: 2},
},
&description.Session{
Medias: []*description.Media{
{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.LPCM{
BitDepth: 16,
SampleRate: 48000,
ChannelCount: 2,
}},
},
},
},
"LPCM configuration does not match, is bitDepth=16, sampleRate=48000, " +
"channelCount=2, but stream expects bitDepth=16, sampleRate=44100, channelCount=2",
},
} {
t.Run(ca.name, func(t *testing.T) {
strm := &Stream{
AlwaysAvailable: true,
AlwaysAvailableTracks: ca.tracks,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
ReplaceNTP: true,
Parent: &nilLogger{},
}
err := strm.Initialize()
require.NoError(t, err)
defer strm.Close()
subStream := &SubStream{
Stream: strm,
CurDesc: ca.desc,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.EqualError(t, err, ca.err)
})
}
}
+61 -8
View File
@@ -6,6 +6,7 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
)
@@ -43,24 +44,76 @@ func mediasToCodecs(medias []*description.Media) []string {
return FormatsToCodecs(gatherFormats(medias))
}
func mediasAreCompatible(medias1 []*description.Media, medias2 []*description.Media) bool {
func formatMPEG4AudioConfig(asc *mpeg4audio.AudioSpecificConfig) string {
return fmt.Sprintf("type=%d, sampleRate=%d, channelCount=%d",
asc.Type, asc.SampleRate, asc.ChannelConfig)
}
func formatG711Config(f *format.G711) string {
return fmt.Sprintf("MULaw=%v, sampleRate=%d, channelCount=%d",
f.MULaw, f.SampleRate, f.ChannelCount)
}
func formatLPCMConfig(f *format.LPCM) string {
return fmt.Sprintf("bitDepth=%d, sampleRate=%d, channelCount=%d",
f.BitDepth, f.SampleRate, f.ChannelCount)
}
func mediasAreCompatible(medias1 []*description.Media, medias2 []*description.Media) error {
if len(medias1) != len(medias2) {
return false
return fmt.Errorf("wants to publish %v, but stream expects %v",
mediasToCodecs(medias2), mediasToCodecs(medias1))
}
for i := range medias1 {
if len(medias1[i].Formats) != len(medias2[i].Formats) {
return false
return fmt.Errorf("wants to publish %v, but stream expects %v",
mediasToCodecs(medias2), mediasToCodecs(medias1))
}
for j := range medias1[i].Formats {
if reflect.TypeOf(medias1[i].Formats[j]) != reflect.TypeOf(medias2[i].Formats[j]) {
return false
return fmt.Errorf("wants to publish %v, but stream expects %v",
mediasToCodecs(medias2), mediasToCodecs(medias1))
}
}
}
return true
for i := range medias1 {
for j := range medias1[i].Formats {
switch format1 := medias1[i].Formats[j].(type) {
case *format.MPEG4Audio:
format2 := medias2[i].Formats[j].(*format.MPEG4Audio)
if !reflect.DeepEqual(format1.Config, format2.Config) {
return fmt.Errorf("MPEG-4 audio configuration does not match, is %s, but stream expects %s",
formatMPEG4AudioConfig(format2.Config), formatMPEG4AudioConfig(format1.Config))
}
case *format.G711:
format2 := medias2[i].Formats[j].(*format.G711)
if format1.MULaw != format2.MULaw ||
format1.SampleRate != format2.SampleRate ||
format1.ChannelCount != format2.ChannelCount {
return fmt.Errorf("G711 configuration does not match, is %s, but stream expects %s",
formatG711Config(format2), formatG711Config(format1))
}
case *format.LPCM:
format2 := medias2[i].Formats[j].(*format.LPCM)
if format1.BitDepth != format2.BitDepth ||
format1.SampleRate != format2.SampleRate ||
format1.ChannelCount != format2.ChannelCount {
return fmt.Errorf("LPCM configuration does not match, is %s, but stream expects %s",
formatLPCMConfig(format2), formatLPCMConfig(format1))
}
}
}
}
return nil
}
// SubStream is a Stream without interruptions.
@@ -87,9 +140,9 @@ func (ss *SubStream) Initialize() error {
panic("should not happen")
}
if !mediasAreCompatible(ss.Stream.Desc.Medias, ss.CurDesc.Medias) {
return fmt.Errorf("want to publish %v, but stream expects %v",
mediasToCodecs(ss.CurDesc.Medias), mediasToCodecs(ss.Stream.Desc.Medias))
err := mediasAreCompatible(ss.Stream.Desc.Medias, ss.CurDesc.Medias)
if err != nil {
return err
}
}