Skip to content

Commit 5ce8e05

Browse files
committed
Resolve undeclared SSRC using the payload type
Introduces a fallback mechanism to handle undeclared SSRCs from multiple sections using the RTP stream's payload type. For legacy clients without MID extension support, it documents the existing behavior for handling undeclared SSRCs in single media sections.
1 parent c523e5a commit 5ce8e05

File tree

3 files changed

+209
-31
lines changed

3 files changed

+209
-31
lines changed

errors.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ var (
211211
"remoteDescription contained media section without mid value",
212212
)
213213
errPeerConnRemoteDescriptionNil = errors.New("remoteDescription has not been set yet")
214-
errPeerConnSingleMediaSectionHasExplicitSSRC = errors.New("single media section has an explicit SSRC")
214+
errMediaSectionHasExplictSSRCAttribute = errors.New("media section has an explicit SSRC")
215215
errPeerConnRemoteSSRCAddTransceiver = errors.New("could not add transceiver for remote SSRC")
216216
errPeerConnSimulcastMidRTPExtensionRequired = errors.New("mid RTP Extensions required for Simulcast")
217217
errPeerConnSimulcastStreamIDRTPExtensionRequired = errors.New("stream id RTP Extensions required for Simulcast")

peerconnection.go

+73-30
Original file line numberDiff line numberDiff line change
@@ -1577,22 +1577,16 @@ func (pc *PeerConnection) startSCTP(maxMessageSize uint32) {
15771577
}
15781578
}
15791579

1580-
//nolint:cyclop
15811580
func (pc *PeerConnection) handleUndeclaredSSRC(
15821581
ssrc SSRC,
1583-
remoteDescription *SessionDescription,
1582+
mediaSection *sdp.MediaDescription,
15841583
) (handled bool, err error) {
1585-
if len(remoteDescription.parsed.MediaDescriptions) != 1 {
1586-
return false, nil
1587-
}
1588-
1589-
onlyMediaSection := remoteDescription.parsed.MediaDescriptions[0]
15901584
streamID := ""
15911585
id := ""
15921586
hasRidAttribute := false
15931587
hasSSRCAttribute := false
15941588

1595-
for _, a := range onlyMediaSection.Attributes {
1589+
for _, a := range mediaSection.Attributes {
15961590
switch a.Key {
15971591
case sdp.AttrKeyMsid:
15981592
if split := strings.Split(a.Value, " "); len(split) == 2 {
@@ -1609,7 +1603,7 @@ func (pc *PeerConnection) handleUndeclaredSSRC(
16091603
if hasRidAttribute {
16101604
return false, nil
16111605
} else if hasSSRCAttribute {
1612-
return false, errPeerConnSingleMediaSectionHasExplicitSSRC
1606+
return false, errMediaSectionHasExplictSSRCAttribute
16131607
}
16141608

16151609
incoming := trackDetails{
@@ -1618,7 +1612,7 @@ func (pc *PeerConnection) handleUndeclaredSSRC(
16181612
streamID: streamID,
16191613
id: id,
16201614
}
1621-
if onlyMediaSection.MediaName.Media == RTPCodecTypeAudio.String() {
1615+
if mediaSection.MediaName.Media == RTPCodecTypeAudio.String() {
16221616
incoming.kind = RTPCodecTypeAudio
16231617
}
16241618

@@ -1636,6 +1630,38 @@ func (pc *PeerConnection) handleUndeclaredSSRC(
16361630
return true, nil
16371631
}
16381632

1633+
// For legacy clients that didn't support urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
1634+
// or urn:ietf:params:rtp-hdrext:sdes:mid extension, and didn't declare a=ssrc lines.
1635+
// Assumes that the payload type is unique across the media section.
1636+
func (pc *PeerConnection) findMediaSectionByPayloadType(
1637+
payloadType PayloadType,
1638+
remoteDescription *SessionDescription,
1639+
) (selectedMediaSection *sdp.MediaDescription, ok bool) {
1640+
for i := range remoteDescription.parsed.MediaDescriptions {
1641+
descr := remoteDescription.parsed.MediaDescriptions[i]
1642+
media := descr.MediaName.Media
1643+
if !strings.EqualFold(media, "video") && !strings.EqualFold(media, "audio") {
1644+
continue
1645+
}
1646+
1647+
formats := descr.MediaName.Formats
1648+
for _, payloadStr := range formats {
1649+
payload, err := strconv.ParseUint(payloadStr, 10, 8)
1650+
if err != nil {
1651+
continue
1652+
}
1653+
1654+
// Return the first media section that has the payload type.
1655+
// Assuming that the payload type is unique across the media section.
1656+
if PayloadType(payload) == payloadType {
1657+
return remoteDescription.parsed.MediaDescriptions[i], true
1658+
}
1659+
}
1660+
}
1661+
1662+
return nil, false
1663+
}
1664+
16391665
// Chrome sends probing traffic on SSRC 0. This reads the packets to ensure that we properly
16401666
// generate TWCC reports for it. Since this isn't actually media we don't pass this to the user.
16411667
func (pc *PeerConnection) handleNonMediaBandwidthProbe() {
@@ -1665,7 +1691,7 @@ func (pc *PeerConnection) handleNonMediaBandwidthProbe() {
16651691
}
16661692
}
16671693

1668-
func (pc *PeerConnection) handleIncomingSSRC(rtpStream io.Reader, ssrc SSRC) error { //nolint:gocognit,cyclop
1694+
func (pc *PeerConnection) handleIncomingSSRC(rtpStream io.Reader, ssrc SSRC) error { //nolint:gocyclo,gocognit,cyclop
16691695
remoteDescription := pc.RemoteDescription()
16701696
if remoteDescription == nil {
16711697
return errPeerConnRemoteDescriptionNil
@@ -1683,15 +1709,49 @@ func (pc *PeerConnection) handleIncomingSSRC(rtpStream io.Reader, ssrc SSRC) err
16831709
}
16841710
}
16851711

1686-
// If the remote SDP was only one media section the ssrc doesn't have to be explicitly declared
1687-
if handled, err := pc.handleUndeclaredSSRC(ssrc, remoteDescription); handled || err != nil {
1712+
// if the SSRC is not declared in the SDP and there is only one media section,
1713+
// we attempt to resolve it using this single section
1714+
// This applies even if the client supports RTP extensions:
1715+
// (urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id and urn:ietf:params:rtp-hdrext:sdes:mid)
1716+
// and even if the RTP stream contains an incorrect MID or RID.
1717+
// while this can be incorrect, this is done to maintain compatibility with older behavior.
1718+
if len(remoteDescription.parsed.MediaDescriptions) == 1 {
1719+
mediaSection := remoteDescription.parsed.MediaDescriptions[0]
1720+
if handled, err := pc.handleUndeclaredSSRC(ssrc, mediaSection); handled || err != nil {
1721+
return err
1722+
}
1723+
}
1724+
1725+
// We read the RTP packet to determine the payload type
1726+
b := make([]byte, pc.api.settingEngine.getReceiveMTU())
1727+
1728+
i, err := rtpStream.Read(b)
1729+
if err != nil {
1730+
return err
1731+
}
1732+
1733+
if i < 4 {
1734+
return errRTPTooShort
1735+
}
1736+
1737+
payloadType := PayloadType(b[1] & 0x7f)
1738+
params, err := pc.api.mediaEngine.getRTPParametersByPayloadType(payloadType)
1739+
if err != nil {
16881740
return err
16891741
}
16901742

16911743
midExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.getHeaderExtensionID(
16921744
RTPHeaderExtensionCapability{sdp.SDESMidURI},
16931745
)
16941746
if !audioSupported && !videoSupported {
1747+
// try to find media section by payload type as a last resort for legacy clients.
1748+
mediaSection, ok := pc.findMediaSectionByPayloadType(payloadType, remoteDescription)
1749+
if ok {
1750+
if ok, err = pc.handleUndeclaredSSRC(ssrc, mediaSection); ok || err != nil {
1751+
return err
1752+
}
1753+
}
1754+
16951755
return errPeerConnSimulcastMidRTPExtensionRequired
16961756
}
16971757

@@ -1706,23 +1766,6 @@ func (pc *PeerConnection) handleIncomingSSRC(rtpStream io.Reader, ssrc SSRC) err
17061766
RTPHeaderExtensionCapability{sdp.SDESRepairRTPStreamIDURI},
17071767
)
17081768

1709-
b := make([]byte, pc.api.settingEngine.getReceiveMTU())
1710-
1711-
i, err := rtpStream.Read(b)
1712-
if err != nil {
1713-
return err
1714-
}
1715-
1716-
if i < 4 {
1717-
return errRTPTooShort
1718-
}
1719-
1720-
payloadType := PayloadType(b[1] & 0x7f)
1721-
params, err := pc.api.mediaEngine.getRTPParametersByPayloadType(payloadType)
1722-
if err != nil {
1723-
return err
1724-
}
1725-
17261769
streamInfo := createStreamInfo(
17271770
"",
17281771
ssrc,

peerconnection_media_test.go

+135
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,20 @@ func filterSsrc(offer string) (filteredSDP string) {
441441
return
442442
}
443443

444+
func filterSDPExtensions(offer string) (filteredSDP string) {
445+
scanner := bufio.NewScanner(strings.NewReader(offer))
446+
for scanner.Scan() {
447+
l := scanner.Text()
448+
if strings.HasPrefix(l, "a=extmap") {
449+
continue
450+
}
451+
452+
filteredSDP += l + "\n"
453+
}
454+
455+
return
456+
}
457+
444458
// If a SessionDescription has a single media section and no SSRC
445459
// assume that it is meant to handle all RTP packets.
446460
func TestUndeclaredSSRC(t *testing.T) {
@@ -530,6 +544,127 @@ func TestUndeclaredSSRC(t *testing.T) {
530544
sendVideoUntilDone(t, unhandledSimulcastError, []*TrackLocalStaticSample{vp8Writer})
531545
closePairNow(t, pcOffer, pcAnswer)
532546
})
547+
548+
t.Run("multiple media sections, no sdp extensions", func(t *testing.T) {
549+
pcOffer, pcAnswer, err := newPair()
550+
assert.NoError(t, err)
551+
552+
vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
553+
assert.NoError(t, err)
554+
555+
_, err = pcOffer.CreateDataChannel("data", nil)
556+
assert.NoError(t, err)
557+
558+
_, err = pcOffer.AddTrack(vp8Writer)
559+
assert.NoError(t, err)
560+
561+
opusWriter, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "audio", "pion")
562+
assert.NoError(t, err)
563+
564+
_, err = pcOffer.AddTrack(opusWriter)
565+
assert.NoError(t, err)
566+
567+
onVideoTrackFired := make(chan struct{})
568+
onAudioTrackFired := make(chan struct{})
569+
570+
gotVideo, gotAudio := false, false
571+
pcAnswer.OnTrack(func(trackRemote *TrackRemote, _ *RTPReceiver) {
572+
switch trackRemote.Kind() {
573+
case RTPCodecTypeVideo:
574+
assert.False(t, gotVideo, "already got video track")
575+
assert.Equal(t, trackRemote.StreamID(), vp8Writer.StreamID())
576+
assert.Equal(t, trackRemote.ID(), vp8Writer.ID())
577+
gotVideo = true
578+
onVideoTrackFired <- struct{}{}
579+
case RTPCodecTypeAudio:
580+
assert.False(t, gotAudio, "already got audio track")
581+
assert.Equal(t, trackRemote.StreamID(), opusWriter.StreamID())
582+
assert.Equal(t, trackRemote.ID(), opusWriter.ID())
583+
gotAudio = true
584+
onAudioTrackFired <- struct{}{}
585+
default:
586+
assert.Fail(t, "unexpected track kind", trackRemote.Kind())
587+
}
588+
})
589+
590+
offer, err := pcOffer.CreateOffer(nil)
591+
assert.NoError(t, err)
592+
593+
offerGatheringComplete := GatheringCompletePromise(pcOffer)
594+
assert.NoError(t, pcOffer.SetLocalDescription(offer))
595+
<-offerGatheringComplete
596+
597+
offer.SDP = filterSDPExtensions(filterSsrc(pcOffer.LocalDescription().SDP))
598+
assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
599+
600+
answer, err := pcAnswer.CreateAnswer(nil)
601+
assert.NoError(t, err)
602+
603+
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
604+
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
605+
<-answerGatheringComplete
606+
607+
assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
608+
609+
wait := sync.WaitGroup{}
610+
wait.Add(2)
611+
go func() {
612+
sendVideoUntilDone(t, onVideoTrackFired, []*TrackLocalStaticSample{vp8Writer})
613+
wait.Done()
614+
}()
615+
go func() {
616+
sendVideoUntilDone(t, onAudioTrackFired, []*TrackLocalStaticSample{opusWriter})
617+
wait.Done()
618+
}()
619+
620+
wait.Wait()
621+
closePairNow(t, pcOffer, pcAnswer)
622+
})
623+
624+
t.Run("findMediaSectionByPayloadType test", func(t *testing.T) {
625+
parsed := &SessionDescription{
626+
parsed: &sdp.SessionDescription{
627+
MediaDescriptions: []*sdp.MediaDescription{
628+
{
629+
MediaName: sdp.MediaName{
630+
Media: "video",
631+
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
632+
Formats: []string{"96", "97", "98", "99", "BAD", "100", "101", "102"},
633+
},
634+
},
635+
{
636+
MediaName: sdp.MediaName{
637+
Media: "audio",
638+
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
639+
Formats: []string{"8", "9", "101"},
640+
},
641+
},
642+
{
643+
MediaName: sdp.MediaName{
644+
Media: "application",
645+
Protos: []string{"UDP", "DTLS", "SCTP"},
646+
Formats: []string{"webrtc-datachannel"},
647+
},
648+
},
649+
},
650+
},
651+
}
652+
peer := &PeerConnection{}
653+
654+
video, ok := peer.findMediaSectionByPayloadType(96, parsed)
655+
assert.True(t, ok)
656+
assert.NotNil(t, video)
657+
assert.Equal(t, "video", video.MediaName.Media)
658+
659+
audio, ok := peer.findMediaSectionByPayloadType(8, parsed)
660+
assert.True(t, ok)
661+
assert.NotNil(t, audio)
662+
assert.Equal(t, "audio", audio.MediaName.Media)
663+
664+
missing, ok := peer.findMediaSectionByPayloadType(42, parsed)
665+
assert.False(t, ok)
666+
assert.Nil(t, missing)
667+
})
533668
}
534669

535670
func TestAddTransceiverFromTrackSendOnly(t *testing.T) {

0 commit comments

Comments
 (0)