webrtc icon indicating copy to clipboard operation
webrtc copied to clipboard

Question about Multiple Video Codec Support in SFU Scenario

Open San9H0 opened this issue 9 months ago • 2 comments

Description

We are implementing an SFU (Selective Forwarding Unit) that needs to handle multiple streamers using different video codecs.

Our Use Case

  • SFU acts as an offerer
  • Streamer1 connects using VP8
  • Viewer connects and receives VP8 stream successfully
  • Streamer2 tries to connect using H264
  • SFU attempts to add new transceiver with H264 codec
  • Renegotiation fails

Question

Is this limitation (not being able to add a different codec during renegotiation) intentional? If so, what would be the recommended approach for handling multiple codecs in an SFU scenario?

Current Workaround

We currently maintain a fork with modifications to handle this case, but we'd prefer to use the upstream version if possible. we've added an interface function to push codecs into MediaEngine's negotiatedVideoCodecs and negotiatedAudioCodecs to handle this scenario.

Scenario


func TestPeerConnection_Renegotiation_AddTransceiver_With_Different_Codec(t *testing.T) {
	lim := test.TimeOut(time.Second * 30)
	defer lim.Stop()

	report := test.CheckRoutines(t)
	defer report()

	// Our SFU server is on the offer side. Our SFU server supports VP8 and H264
	pcOffer, err := NewPeerConnection(Configuration{})
	assert.NoError(t, err)

	// Viewer is on the answer side
	pcAnswer, err := NewPeerConnection(Configuration{})
	assert.NoError(t, err)

	tracksCh := make(chan *TrackRemote)
	pcAnswer.OnTrack(func(track *TrackRemote, _ *RTPReceiver) {
		tracksCh <- track
		for {
			if _, _, readErr := track.ReadRTP(); errors.Is(readErr, io.EOF) {
				return
			}
		}
	})
	connected := make(chan struct{})
	pcOffer.OnConnectionStateChange(func(state PeerConnectionState) {
		if state == PeerConnectionStateConnected {
			close(connected)
		}
	})

	err = signalPair(pcOffer, pcAnswer)
	<-connected
	require.NoError(t, err)

	track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video1", "pion1")
	require.NoError(t, err)

	// First streamer uses VP8
	sender1, err := pcOffer.AddTransceiverFromTrack(track1)
	_ = sender1
	require.NoError(t, err)
	err = sender1.SetCodecPreferences([]RTPCodecParameters{
		{
			RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8, ClockRate: 90000},
			PayloadType:        96,
		},
	})
	require.NoError(t, err)

	err = signalPair(pcOffer, pcAnswer)

	transceivers := pcOffer.GetTransceivers()
	require.Equal(t, 1, len(transceivers))
	require.Equal(t, "1", transceivers[0].Mid())

	transceivers = pcAnswer.GetTransceivers()
	require.Equal(t, 1, len(transceivers))
	require.Equal(t, "1", transceivers[0].Mid())

	ctx, cancel := context.WithCancel(context.Background())
	go sendVideoUntilDone(t, ctx.Done(), []*TrackLocalStaticSample{track1})
	remoteTrack1 := <-tracksCh
	cancel()

	assert.Equal(t, "video1", remoteTrack1.ID())
	assert.Equal(t, "pion1", remoteTrack1.StreamID())

	// Second streamer is created
	track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264}, "video2", "pion2")
	// track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video2", "pion2")
	require.NoError(t, err)

	// Second streamer uses H264
	sender2, err := pcOffer.AddTransceiverFromTrack(track2)
	_ = sender2
	require.NoError(t, err)
	err = sender2.SetCodecPreferences([]RTPCodecParameters{
		{
			RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f"},
			// RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8, ClockRate: 90000, SDPFmtpLine: ""},
			PayloadType: 112,
		},
	})
	require.NoError(t, err) // Error occurs here

	require.NoError(t, signalPair(pcOffer, pcAnswer))
	ctx, cancel = context.WithCancel(context.Background())
	go sendVideoUntilDone(t, ctx.Done(), []*TrackLocalStaticSample{track2})

	remoteTrack2 := <-tracksCh
	cancel()

	assert.Equal(t, "video2", remoteTrack2.ID())
	assert.Equal(t, "pion2", remoteTrack2.StreamID())

	closePairNow(t, pcOffer, pcAnswer)
}

San9H0 avatar Apr 11 '25 11:04 San9H0

Hello there is a working PR to add multiple codec negotiation https://github.com/pion/webrtc/pull/3018 Expect it to be merged soon 👍

JoTurk avatar Apr 11 '25 11:04 JoTurk

Thank you for the quick response and pointing to PR #3018.

However, looking at the PR, it seems to focus on the answer side (RemoteDescription). In our SFU scenario, we are on the offer side and need to support multiple codecs in the offer SDP during CreateOffer.

Our SFU acts as an offerer and needs to generate an SDP that includes both VP8 and H264 codecs when creating the offer. The current PR doesn't seem to address this specific use case.

San9H0 avatar Apr 11 '25 12:04 San9H0