Support advertising ICE trickling (#3097)

As defined in [RFC8839], when an ICE restart occurs, a new SDP offer/answer exchange is triggered. However, as WHIP does not support renegotiation of non-ICE-related SDP information, a WHIP client will not send a new offer when an ICE restart occurs. Instead, the WHIP client and WHIP session will only exchange the relevant ICE information via an HTTP PATCH request as defined in Section 4.3.1 and MUST assume that the previously negotiated non-ICE-related SDP information still applies after the ICE restart.

When performing an ICE restart, the WHIP client MUST include the updated "ice-pwd" and "ice-ufrag" in the SDP fragment of the HTTP PATCH request body as well as the new set of gathered ICE candidates as defined in [RFC8840]. Similar to what is defined in Section 4.3.2, as per [RFC9429], only "m=" sections not marked as bundle-only can gather ICE candidates, so given that the "max-bundle" policy is being used, the SDP fragment will contain only the offerer-tagged "m=" line of the bundle group. A WHIP client sending a PATCH request for performing ICE restart MUST contain an If-Match header field with a field-value of "*" as per Section 13.1.1 of [RFC9110].



Co-authored-by: Joe Turki <git@joeturki.com>
This commit is contained in:
Srayan Jana
2025-12-02 16:25:07 -08:00
committed by GitHub
parent 71b8a13dc9
commit 4b59cf9986
3 changed files with 65 additions and 1 deletions

View File

@@ -9,6 +9,10 @@ type OfferAnswerOptions struct {
// VoiceActivityDetection allows the application to provide information
// about whether it wishes voice detection feature to be enabled or disabled.
VoiceActivityDetection bool
// ICETricklingSupported indicates whether the ICE agent should use trickle ICE
// If set, the "a=ice-options:trickle" attribute is added to the generated SDP payload.
// (See https://datatracker.ietf.org/doc/html/rfc9725#section-4.3.3)
ICETricklingSupported bool
}
// AnswerOptions structure describes the options used to control the answer

View File

@@ -718,6 +718,10 @@ func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription
return SessionDescription{}, err
}
if options != nil && options.ICETricklingSupported {
descr.WithICETrickleAdvertised()
}
updateSDPOrigin(&pc.sdpOrigin, descr)
sdpBytes, err := descr.Marshal()
if err != nil {
@@ -842,7 +846,7 @@ func (pc *PeerConnection) createICETransport() *ICETransport {
// CreateAnswer starts the PeerConnection and generates the localDescription.
//
//nolint:cyclop
func (pc *PeerConnection) CreateAnswer(*AnswerOptions) (SessionDescription, error) {
func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescription, error) {
useIdentity := pc.idpLoginURL != nil
remoteDesc := pc.RemoteDescription()
switch {
@@ -876,6 +880,10 @@ func (pc *PeerConnection) CreateAnswer(*AnswerOptions) (SessionDescription, erro
return SessionDescription{}, err
}
if options != nil && options.ICETricklingSupported {
descr.WithICETrickleAdvertised()
}
updateSDPOrigin(&pc.sdpOrigin, descr)
sdpBytes, err := descr.Marshal()
if err != nil {

View File

@@ -1787,6 +1787,58 @@ func TestPeerConnectionNoNULLCipherDefault(t *testing.T) {
closePairNow(t, offerPC, answerPC)
}
func TestICETricklingSupported(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
pc, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
offer, err := pc.CreateOffer(&OfferOptions{
OfferAnswerOptions: OfferAnswerOptions{ICETricklingSupported: true},
})
assert.NoError(t, err)
assert.Contains(t, offer.SDP, "a=ice-options:trickle")
assert.NoError(t, pc.Close())
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
offerSDP := strings.Join([]string{
"v=0",
"o=- 0 0 IN IP4 127.0.0.1",
"s=-",
"t=0 0",
"a=group:BUNDLE 0",
"a=ice-ufrag:someufrag",
"a=ice-pwd:somepwd",
"a=fingerprint:sha-256 " +
"F7:BF:B4:42:5B:44:C0:B9:49:70:6D:26:D7:3E:E6:08:B1:5B:25:2E:32:88:50:B6:3C:BE:4E:18:A7:2C:85:7C",
"a=msid-semantic: WMS *",
"m=audio 9 UDP/TLS/RTP/SAVPF 111",
"c=IN IP4 0.0.0.0",
"a=rtcp:9 IN IP4 0.0.0.0",
"a=mid:0",
"a=rtcp-mux",
"a=setup:actpass",
"a=rtpmap:111 opus/48000/2",
"",
}, "\r\n")
assert.NoError(t, pcAnswer.SetRemoteDescription(SessionDescription{
Type: SDPTypeOffer,
SDP: offerSDP,
}))
answer, err := pcAnswer.CreateAnswer(&AnswerOptions{
OfferAnswerOptions: OfferAnswerOptions{ICETricklingSupported: true},
})
assert.NoError(t, err)
assert.Contains(t, answer.SDP, "a=ice-options:trickle")
assert.NoError(t, pcAnswer.Close())
}
// https://github.com/pion/webrtc/issues/2690
func TestPeerConnectionTrickleMediaStreamIdentification(t *testing.T) {
const remoteSdp = `v=0