From cce968ce5b2699f0aa907f421bd3c9e12ca836e9 Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:07:55 +0100 Subject: [PATCH] fix medias wrongly marked as back channels (bluenviron/mediamtx#5074) Some cameras mark medias as back channels even though they are not. Try to detect this by checking whether in full the SDP there are both back channels and standard channels, unmarking back channels otherwise. --- pkg/description/session.go | 22 ++++- pkg/description/session_test.go | 159 ++++++++++++++++++++++++++++++-- 2 files changed, 171 insertions(+), 10 deletions(-) diff --git a/pkg/description/session.go b/pkg/description/session.go index 4116ab74..868a7088 100644 --- a/pkg/description/session.go +++ b/pkg/description/session.go @@ -21,7 +21,7 @@ func atLeastOneHasMID(medias []*Media) bool { return false } -func atLeastOneDoesntHaveMID(medias []*Media) bool { +func atLeastOneDoesNotHaveMID(medias []*Media) bool { for _, media := range medias { if media.ID == "" { return true @@ -30,6 +30,15 @@ func atLeastOneDoesntHaveMID(medias []*Media) bool { return false } +func atLeastOneIsNotBackChannel(medias []*Media) bool { + for _, media := range medias { + if !media.IsBackChannel { + return true + } + } + return false +} + func hasMediaWithID(medias []*Media, id string) bool { for _, media := range medias { if media.ID == id { @@ -119,10 +128,19 @@ func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error { d.Medias[i] = &m } - if atLeastOneHasMID(d.Medias) && atLeastOneDoesntHaveMID(d.Medias) { + if atLeastOneHasMID(d.Medias) && atLeastOneDoesNotHaveMID(d.Medias) { return fmt.Errorf("media IDs sent partially") } + // Some cameras mark medias as back channels even though they are not. + // Try to detect this by checking whether in the full SDP there are both + // back channels and standard channels, unmarking back channels otherwise. + if !atLeastOneIsNotBackChannel(d.Medias) { + for _, m := range d.Medias { + m.IsBackChannel = false + } + } + for _, attr := range ssd.Attributes { if attr.Key == "group" && strings.HasPrefix(attr.Value, "FEC ") { group := SessionFECGroup(strings.Split(attr.Value[len("FEC "):], " ")) diff --git a/pkg/description/session_test.go b/pkg/description/session_test.go index 0d1a5e5c..9ac3d470 100644 --- a/pkg/description/session_test.go +++ b/pkg/description/session_test.go @@ -269,7 +269,6 @@ var casesSession = []struct { "t=0 0\r\n" + "m=audio 0 RTP/AVP 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" + "a=mid:audio\r\n" + - "a=sendonly\r\n" + "a=control\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=fmtp:111 sprop-stereo=0\r\n" + @@ -288,7 +287,6 @@ var casesSession = []struct { "a=rtpmap:126 telephone-event/8000\r\n" + "m=video 0 RTP/AVP 96 97 98 99 100 101 127 124 125\r\n" + "a=mid:video\r\n" + - "a=sendonly\r\n" + "a=control\r\n" + "a=rtpmap:96 VP8/90000\r\n" + "a=rtpmap:97 rtx/90000\r\n" + @@ -307,9 +305,8 @@ var casesSession = []struct { Title: ``, Medias: []*Media{ { - ID: "audio", - Type: MediaTypeAudio, - IsBackChannel: true, + ID: "audio", + Type: MediaTypeAudio, Formats: []format.Format{ &format.Opus{ PayloadTyp: 111, @@ -381,9 +378,8 @@ var casesSession = []struct { }, }, { - ID: "video", - Type: MediaTypeVideo, - IsBackChannel: true, + ID: "video", + Type: MediaTypeVideo, Formats: []format.Format{ &format.VP8{ PayloadTyp: 96, @@ -827,6 +823,153 @@ var casesSession = []struct { }, }, }, + { + "issue mediamtx/5074 (fake back channel)", + "v=0\n" + + "o=- 1645677566 1 IN IP4 *****\n" + + "s=Streamed by \"IDS uEye Live RTSP Server\"\n" + + "i=session information\n" + + "t=0 0\n" + + "a=tool:IDS uEye Live RTSP Server:Jul 17 2025 - 13:23:42\n" + + "a=type:broadcast\n" + + "a=control:*\n" + + "a=range:npt=0-\n" + + "m=video 0 RTP/AVP 96\n" + + "c=IN IP4 *****\n" + + "a=sendonly\n" + + "a=rtpmap:96 H264/90000\n" + + "a=fmtp:96 packetization-mode=1; profile-level-id=4D4028; " + + "sprop-parameter-sets=Z01AKI2NQDwBE/LgLcBAQFAAAD6AAARlDoYAUVAABfXgu8uNDACioAAL68F3lwo=,aO44gA==;\n" + + "a=cliprect:0,0,1080,1920\n" + + "a=control:rtsp://*****/video=video1\n", + "v=0\r\n" + + "o=- 0 0 IN IP4 127.0.0.1\r\n" + + "s=Streamed by \"IDS uEye Live RTSP Server\"\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "t=0 0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=control:rtsp://*****/video=video1\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=fmtp:96 packetization-mode=1; profile-level-id=4D4028; " + + "sprop-parameter-sets=Z01AKI2NQDwBE/LgLcBAQFAAAD6AAARlDoYAUVAABfXgu8uNDACioAAL68F3lwo=,aO44gA==\r\n", + Session{ + Title: `Streamed by "IDS uEye Live RTSP Server"`, + Medias: []*Media{ + { + Type: "video", + Control: "rtsp://*****/video=video1", + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + // ProfileLevelID: "4D4028", + SPS: []byte{ + 0x67, 0x4d, 0x40, 0x28, 0x8d, 0x8d, 0x40, 0x3c, + 0x01, 0x13, 0xf2, 0xe0, 0x2d, 0xc0, 0x40, 0x40, + 0x50, 0x00, 0x00, 0x3e, 0x80, 0x00, 0x04, 0x65, + 0x0e, 0x86, 0x00, 0x51, 0x50, 0x00, 0x05, 0xf5, + 0xe0, 0xbb, 0xcb, 0x8d, 0x0c, 0x00, 0xa2, 0xa0, + 0x00, 0x0b, 0xeb, 0xc1, 0x77, 0x97, 0x0a, + }, + PPS: []byte{0x68, 0xee, 0x38, 0x80}, + }}, + }, + }, + }, + }, + { + "issue mediamtx/5074 (real back channel)", + "v=0\n" + + "o=- 1 1 IN IP4 192.168.81.194:554\n" + + "s=L10013/video1 - ACES Server(SSDRTSPServer)\n" + + "t=0 0\n" + + "c=IN IP4 0.0.0.0\n" + + "a=control:*\n" + + "a=range:npt=now-\n" + + "m=video 0 RTP/AVP 96\n" + + "a=control:video\n" + + "a=rtpmap:96 H264/90000\n" + + "a=fmtp:96 sprop-parameter-sets=Z0IAHp2oKAv+WbgICAgQ,aM48gA==\n" + + "a=framerate:30\n" + + "a=recvonly\n" + + "m=audio 0 RTP/AVP 0\n" + + "a=control:audio\n" + + "a=rtpmap:0 PCMU/8000\n" + + "a=recvonly\n" + + "m=audio 0 RTP/AVP 0\n" + + "a=control:backchannel\n" + + "a=rtpmap:0 PCMU/8000\n" + + "a=sendonly\n" + + "m=application 0 RTP/AVP 98\n" + + "a=control:meta\n" + + "a=rtpmap:98 vnd.onvif.metadata/90000\n" + + "a=recvonly\n", + "v=0\r\n" + + "o=- 0 0 IN IP4 127.0.0.1\r\n" + + "s=L10013/video1 - ACES Server(SSDRTSPServer)\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "t=0 0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=control:video\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=fmtp:96 profile-level-id=42001E; sprop-parameter-sets=Z0IAHp2oKAv+WbgICAgQ,aM48gA==\r\n" + + "m=audio 0 RTP/AVP 0\r\n" + + "a=control:audio\r\n" + + "a=rtpmap:0 PCMU/8000\r\n" + + "m=audio 0 RTP/AVP 0\r\n" + + "a=sendonly\r\n" + + "a=control:backchannel\r\n" + + "a=rtpmap:0 PCMU/8000\r\n" + + "m=application 0 RTP/AVP 98\r\n" + + "a=control:meta\r\n" + + "a=rtpmap:98 vnd.onvif.metadata/90000\r\n", + Session{ + Title: `L10013/video1 - ACES Server(SSDRTSPServer)`, + Medias: []*Media{ + { + Type: "video", + Control: "video", + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + SPS: []byte{ + 0x67, 0x42, 0x00, 0x1e, 0x9d, 0xa8, 0x28, 0x0b, + 0xfe, 0x59, 0xb8, 0x08, 0x08, 0x08, 0x10, + }, + PPS: []byte{0x68, 0xce, 0x3c, 0x80}, + }}, + }, + { + Type: "audio", + Control: "audio", + Formats: []format.Format{&format.G711{ + PayloadTyp: 0, + MULaw: true, + SampleRate: 8000, + ChannelCount: 1, + }}, + }, + { + Type: "audio", + IsBackChannel: true, + Control: "backchannel", + Formats: []format.Format{&format.G711{ + PayloadTyp: 0, + MULaw: true, + SampleRate: 8000, + ChannelCount: 1, + }}, + }, + { + Type: "application", + Control: "meta", + Formats: []format.Format{&format.Generic{ + PayloadTyp: 98, + RTPMa: "vnd.onvif.metadata/90000", + ClockRat: 90000, + }}, + }, + }, + }, + }, } func TestSessionUnmarshal(t *testing.T) {