From 29d70791641eed2db6d269a9cb4c7ae3bf5e0d66 Mon Sep 17 00:00:00 2001 From: Ramlal <132562384+ram-lal0234@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:30:22 +0530 Subject: [PATCH] rtc: apply simulcast rids from SDP for WHIP (one-shot) publishers (#5004) * rtc: apply simulcast rids from SDP for WHIP (one-shot) publishers In one-shot signalling mode HandleRemoteDescription set the remote offer directly and never called OnSetRemoteDescriptionOffer, so updateRidsFromSDP did not run for WHIP participants. Every simulcast layer then mapped to spatial layer 0 and all but the first were rejected with 'duplicate layer'. * rtc: test that a one-shot simulcast offer applies rids to the pending track * rtc: use h/q rids in one-shot simulcast test, cover single-layer offer Name the simulcast test layers medium/h and low/q per convention, and add TestOneShotSingleLayerOfferUnchanged to check a one-shot offer without rids still yields a single layer with rid "" at spatial layer 0. --- pkg/rtc/participant_internal_test.go | 123 ++++++++++++++++++++++----- pkg/rtc/transport.go | 3 + 2 files changed, 107 insertions(+), 19 deletions(-) diff --git a/pkg/rtc/participant_internal_test.go b/pkg/rtc/participant_internal_test.go index c32b8a643..68cbffa2a 100644 --- a/pkg/rtc/participant_internal_test.go +++ b/pkg/rtc/participant_internal_test.go @@ -824,6 +824,89 @@ func TestPreferMediaCodecForPublisher(t *testing.T) { } } +func TestOneShotSimulcastOfferAppliesRids(t *testing.T) { + participant := newParticipantForTestWithOpts("whip", &participantOpts{ + publisher: true, + oneShot: true, + }) + participant.SetMigrateState(types.MigrateStateComplete) + + pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) + require.NoError(t, err) + defer pc.Close() + + codec := webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()} + medium, err := webrtc.NewTrackLocalStaticRTP(codec, "video", "stream", webrtc.WithRTPStreamID("h")) + require.NoError(t, err) + low, err := webrtc.NewTrackLocalStaticRTP(codec, "video", "stream", webrtc.WithRTPStreamID("q")) + require.NoError(t, err) + sender, err := pc.AddTrack(medium) + require.NoError(t, err) + require.NoError(t, sender.AddEncoding(low)) + + offer, err := pc.CreateOffer(nil) + require.NoError(t, err) + require.NoError(t, pc.SetLocalDescription(offer)) + require.Contains(t, offer.SDP, "a=simulcast:send h;q") + + require.NoError(t, participant.HandleOffer(&livekit.SessionDescription{ + Type: webrtc.SDPTypeOffer.String(), + Sdp: offer.SDP, + })) + + participant.pendingTracksLock.RLock() + pti := participant.pendingTracks["video"] + participant.pendingTracksLock.RUnlock() + require.NotNil(t, pti) + + layers := pti.trackInfos[0].Codecs[0].Layers + require.Len(t, layers, 2) + require.Equal(t, livekit.VideoQuality_LOW, layers[0].Quality) + require.Equal(t, "q", layers[0].Rid) + require.EqualValues(t, 0, layers[0].SpatialLayer) + require.Equal(t, livekit.VideoQuality_MEDIUM, layers[1].Quality) + require.Equal(t, "h", layers[1].Rid) + require.EqualValues(t, 1, layers[1].SpatialLayer) +} + +func TestOneShotSingleLayerOfferUnchanged(t *testing.T) { + participant := newParticipantForTestWithOpts("whip", &participantOpts{ + publisher: true, + oneShot: true, + }) + participant.SetMigrateState(types.MigrateStateComplete) + + pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) + require.NoError(t, err) + defer pc.Close() + + track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()}, "video", "stream") + require.NoError(t, err) + _, err = pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}) + require.NoError(t, err) + + offer, err := pc.CreateOffer(nil) + require.NoError(t, err) + require.NoError(t, pc.SetLocalDescription(offer)) + require.NotContains(t, offer.SDP, "a=simulcast") + require.NotContains(t, offer.SDP, "a=rid") + + require.NoError(t, participant.HandleOffer(&livekit.SessionDescription{ + Type: webrtc.SDPTypeOffer.String(), + Sdp: offer.SDP, + })) + + participant.pendingTracksLock.RLock() + pti := participant.pendingTracks["video"] + participant.pendingTracksLock.RUnlock() + require.NotNil(t, pti) + + layers := pti.trackInfos[0].Codecs[0].Layers + require.Len(t, layers, 1) + require.Equal(t, "", layers[0].Rid) + require.EqualValues(t, 0, layers[0].SpatialLayer) +} + func TestPreferAudioCodecForRed(t *testing.T) { participant := newParticipantForTestWithOpts("123", &participantOpts{ publisher: true, @@ -969,6 +1052,7 @@ type participantOpts struct { clientConf *livekit.ClientConfiguration clientInfo *livekit.ClientInfo migration bool + oneShot bool } func newParticipantForTestWithOpts(identity livekit.ParticipantIdentity, opts *participantOpts) *ParticipantImpl { @@ -1005,25 +1089,26 @@ func newParticipantForTestWithOpts(identity livekit.ParticipantIdentity, opts *p } sid := livekit.ParticipantID(guid.New(utils.ParticipantPrefix)) p, _ := NewParticipant(ParticipantParams{ - SID: sid, - Identity: identity, - Config: rtcConf, - Sink: &routingfakes.FakeMessageSink{}, - ProtocolVersion: opts.protocolVersion, - SessionStartTime: time.Now(), - PLIThrottleConfig: conf.RTC.PLIThrottle, - Grants: grants, - PublishEnabledCodecs: enabledCodecs, - SubscribeEnabledCodecs: enabledCodecs, - ClientConf: opts.clientConf, - ClientInfo: ClientInfo{ClientInfo: opts.clientInfo}, - Logger: LoggerWithParticipant(logger.GetLogger(), identity, sid, false), - Reporter: roomobs.NewNoopParticipantSessionReporter(), - TelemetryListener: &typesfakes.FakeParticipantTelemetryListener{}, - VersionGenerator: utils.NewDefaultTimedVersionGenerator(), - ParticipantListener: &typesfakes.FakeLocalParticipantListener{}, - ParticipantHelper: &typesfakes.FakeLocalParticipantHelper{}, - Migration: opts.migration, + SID: sid, + Identity: identity, + Config: rtcConf, + Sink: &routingfakes.FakeMessageSink{}, + ProtocolVersion: opts.protocolVersion, + SessionStartTime: time.Now(), + PLIThrottleConfig: conf.RTC.PLIThrottle, + Grants: grants, + PublishEnabledCodecs: enabledCodecs, + SubscribeEnabledCodecs: enabledCodecs, + ClientConf: opts.clientConf, + ClientInfo: ClientInfo{ClientInfo: opts.clientInfo}, + Logger: LoggerWithParticipant(logger.GetLogger(), identity, sid, false), + Reporter: roomobs.NewNoopParticipantSessionReporter(), + TelemetryListener: &typesfakes.FakeParticipantTelemetryListener{}, + VersionGenerator: utils.NewDefaultTimedVersionGenerator(), + ParticipantListener: &typesfakes.FakeLocalParticipantListener{}, + ParticipantHelper: &typesfakes.FakeLocalParticipantHelper{}, + Migration: opts.migration, + UseOneShotSignallingMode: opts.oneShot, }) p.isPublisher.Store(opts.publisher) p.updateState(livekit.ParticipantInfo_ACTIVE) diff --git a/pkg/rtc/transport.go b/pkg/rtc/transport.go index 350fffe43..c2db48f92 100644 --- a/pkg/rtc/transport.go +++ b/pkg/rtc/transport.go @@ -1709,6 +1709,9 @@ func (t *PCTransport) HandleRemoteDescription(sd webrtc.SessionDescription, remo return err } t.setRemoteOfferParsed(sd.Type, parsed) + if sd.Type == webrtc.SDPTypeOffer { + t.params.Handler.OnSetRemoteDescriptionOffer() + } rtxRepairs := nonSimulcastRTXRepairsFromSDP(parsed, t.params.Logger) if len(rtxRepairs) > 0 {