Add opt control transceiver re-use in recvonly

SetDisableTransceiverReuseInRecvonly controls if a
transceiver is re-used when its current direction is `recvonly`.

This is useful for the following scenario
  - Remote side sends `offer` with `sendonly` media section.
  - Local side creates transceiver in `SetRemoteDescription`
    and sets direction to `recvonly.
  - Local side calls `AddTrack`.
  - As the current direction is `recvonly`, the transceiver added
    above will be re-used. That will set the direction to `sendrecv`
    and the generated `answer` will have `sendrecv` for that
    media section.
  - That answer becomes incompatible as the offerer is using
    `sendonly`.

Note that local transceiver will be in `recvonly` for both `sendrecv`
and `sendonly` directions in the media section. If the `offer` did use
`sendrecv`, it is possible to re-use that transceiver for sending.
So, disabling re-use will prohibit re-use in the `sendrecv` case also
and hence is slightly wasteful.
This commit is contained in:
boks1971
2025-08-22 17:47:52 +05:30
committed by Raja Subramanian
parent 3e84081c87
commit 2299a71701
3 changed files with 104 additions and 28 deletions

View File

@@ -2105,7 +2105,8 @@ func (pc *PeerConnection) AddTrack(track TrackLocal) (*RTPSender, error) {
// But that will cause sdp inflate. So we only check currentDirection's current value, // But that will cause sdp inflate. So we only check currentDirection's current value,
// that's worked for all browsers. // that's worked for all browsers.
if transceiver.kind == track.Kind() && transceiver.Sender() == nil && if transceiver.kind == track.Kind() && transceiver.Sender() == nil &&
currentDirection != RTPTransceiverDirectionSendrecv && currentDirection != RTPTransceiverDirectionSendonly { currentDirection != RTPTransceiverDirectionSendrecv && currentDirection != RTPTransceiverDirectionSendonly &&
(!pc.api.settingEngine.disableTransceiverReuseInRecvonly || currentDirection != RTPTransceiverDirectionRecvonly) {
sender, err := pc.api.NewRTPSender(track, pc.dtlsTransport) sender, err := pc.api.NewRTPSender(track, pc.dtlsTransport)
if err == nil { if err == nil {
err = transceiver.SetSender(sender, track) err = transceiver.SetSender(sender, track)

View File

@@ -707,41 +707,95 @@ func TestAddTransceiverFromTrackSendRecv(t *testing.T) {
} }
func TestAddTransceiverAddTrack_Reuse(t *testing.T) { func TestAddTransceiverAddTrack_Reuse(t *testing.T) {
pc, err := NewPeerConnection(Configuration{}) t.Run("reuse test", func(t *testing.T) {
assert.NoError(t, err) pc, err := NewPeerConnection(Configuration{})
tr, err := pc.AddTransceiverFromKind(
RTPCodecTypeVideo,
RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly},
)
assert.NoError(t, err)
assert.Equal(t, []*RTPTransceiver{tr}, pc.GetTransceivers())
addTrack := func() (TrackLocal, *RTPSender) {
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err) assert.NoError(t, err)
sender, err := pc.AddTrack(track) tr, err := pc.AddTransceiverFromKind(
RTPCodecTypeVideo,
RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly},
)
assert.NoError(t, err) assert.NoError(t, err)
return track, sender assert.Equal(t, []*RTPTransceiver{tr}, pc.GetTransceivers())
}
track1, sender1 := addTrack() addTrack := func() (TrackLocal, *RTPSender) {
assert.Equal(t, 1, len(pc.GetTransceivers())) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.Equal(t, sender1, tr.Sender()) assert.NoError(t, err)
assert.Equal(t, track1, tr.Sender().Track())
require.NoError(t, pc.RemoveTrack(sender1))
track2, _ := addTrack() sender, err := pc.AddTrack(track)
assert.Equal(t, 1, len(pc.GetTransceivers())) assert.NoError(t, err)
assert.Equal(t, track2, tr.Sender().Track())
addTrack() return track, sender
assert.Equal(t, 2, len(pc.GetTransceivers())) }
assert.NoError(t, pc.Close()) track1, sender1 := addTrack()
assert.Equal(t, 1, len(pc.GetTransceivers()))
assert.Equal(t, sender1, tr.Sender())
assert.Equal(t, track1, tr.Sender().Track())
require.NoError(t, pc.RemoveTrack(sender1))
track2, _ := addTrack()
assert.Equal(t, 1, len(pc.GetTransceivers()))
assert.Equal(t, track2, tr.Sender().Track())
addTrack()
assert.Equal(t, 2, len(pc.GetTransceivers()))
assert.NoError(t, pc.Close())
})
t.Run("reuse disable test", func(t *testing.T) {
se := SettingEngine{}
se.SetDisableTransceiverReuseInRecvonly(true)
pc, err := NewAPI(WithSettingEngine(se)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
tr, err := pc.AddTransceiverFromKind(
RTPCodecTypeVideo,
RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly},
)
assert.NoError(t, err)
assert.Equal(t, []*RTPTransceiver{tr}, pc.GetTransceivers())
addTrack := func() (TrackLocal, *RTPSender) {
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
sender, err := pc.AddTrack(track)
assert.NoError(t, err)
return track, sender
}
// force direction to `recvonly` and ensure SettingEngine setting disables re-use
tr.setCurrentDirection(RTPTransceiverDirectionRecvonly)
addTrack()
assert.Equal(t, 2, len(pc.GetTransceivers()))
// the newly added transceiver above will have a sender, so not re-usable
_, sender := addTrack()
assert.Equal(t, 3, len(pc.GetTransceivers()))
// remove last added track to make that transceiver re-usable
require.NoError(t, pc.RemoveTrack(sender))
track, sender := addTrack()
assert.Equal(t, 3, len(pc.GetTransceivers()))
var matchedTransceiver *RTPTransceiver
for _, tr := range pc.GetTransceivers() {
if tr.Sender() == sender {
matchedTransceiver = tr
break
}
}
assert.NotNil(t, matchedTransceiver)
assert.Equal(t, track, matchedTransceiver.Sender().Track())
assert.NoError(t, pc.Close())
})
} }
func TestAddTransceiverAddTrack_NewRTPSender_Error(t *testing.T) { func TestAddTransceiverAddTrack_NewRTPSender_Error(t *testing.T) {

View File

@@ -110,6 +110,7 @@ type SettingEngine struct {
disableCloseByDTLS bool disableCloseByDTLS bool
dataChannelBlockWrite bool dataChannelBlockWrite bool
handleUndeclaredSSRCWithoutAnswer bool handleUndeclaredSSRCWithoutAnswer bool
disableTransceiverReuseInRecvonly bool
} }
func (e *SettingEngine) getSCTPMaxMessageSize() uint32 { func (e *SettingEngine) getSCTPMaxMessageSize() uint32 {
@@ -577,3 +578,23 @@ func (e *SettingEngine) DisableCloseByDTLS(isEnabled bool) {
func (e *SettingEngine) SetHandleUndeclaredSSRCWithoutAnswer(handleUndeclaredSSRCWithoutAnswer bool) { func (e *SettingEngine) SetHandleUndeclaredSSRCWithoutAnswer(handleUndeclaredSSRCWithoutAnswer bool) {
e.handleUndeclaredSSRCWithoutAnswer = handleUndeclaredSSRCWithoutAnswer e.handleUndeclaredSSRCWithoutAnswer = handleUndeclaredSSRCWithoutAnswer
} }
// SetDisableTransceiverReuseInRecvonly controls if a transceiver is re-used
// when its current direction is `recvonly`.
//
// This is useful for the following scenario
// - Remote side sends `offer` with `sendonly` media section.
// - Local side creates transceiver in `SetRemoteDescription` and sets direction to `recvonly`.
// - Local side calls `AddTrack`.
// - As the current direction is `recvonly`, the transceiver added above will be re-used.
// That will set the direction to `sendrecv` and the generated `answer` will have `sendrecv`
// for that media section.
// - That answer becomes incompatible as the offerer is using `sendonly`.
//
// Note that local transceiver will be in `recvonly` for both `sendrecv` and `sendonly` directions
// in the media section. If the `offer` did use `sendrecv`, it is possible to re-use that transceiver
// for sending. So, disabling re-use will prohibit re-use in the `sendrecv` case also and hence is
// slightly wasteful.
func (e *SettingEngine) SetDisableTransceiverReuseInRecvonly(disableTransceiverReuseInRecvonly bool) {
e.disableTransceiverReuseInRecvonly = disableTransceiverReuseInRecvonly
}