From 898beb369a8be32686dcf656ec4f842b1cd432aa Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Sat, 20 Dec 2025 09:17:57 +0200 Subject: [PATCH] Add ice renomination option --- jsep.go | 26 +++++++++++++++++- jsep_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/jsep.go b/jsep.go index ce0ba53..a0960f3 100644 --- a/jsep.go +++ b/jsep.go @@ -117,10 +117,34 @@ func (s *SessionDescription) WithValueAttribute(key, value string) *SessionDescr return s } +// addOrUpdateICEOption adds or updates the ice-options attribute with the given value. +func (s *SessionDescription) addOrUpdateICEOption(value string) *SessionDescription { + for i := range s.Attributes { + if s.Attributes[i].Key == AttrKeyICEOptions { + prefix := " " + if s.Attributes[i].Value == "" { + prefix = "" + } + + s.Attributes[i].Value += prefix + value + + return s + } + } + + return s.WithValueAttribute(AttrKeyICEOptions, value) +} + // WithICETrickleAdvertised advertises ICE trickle support in the session description. // See https://datatracker.ietf.org/doc/html/rfc9429#section-5.2.1 func (s *SessionDescription) WithICETrickleAdvertised() *SessionDescription { - return s.WithValueAttribute(AttrKeyICEOptions, "trickle") + return s.addOrUpdateICEOption("trickle") +} + +// WithICERenomination advertises ICE renomination support in the session description. +// See https://datatracker.ietf.org/doc/html/draft-thatcher-ice-renomination-01#section-3 +func (s *SessionDescription) WithICERenomination() *SessionDescription { + return s.addOrUpdateICEOption("renomination") } // WithFingerprint adds a fingerprint to the session description. diff --git a/jsep_test.go b/jsep_test.go index 2478301..e21a7a8 100644 --- a/jsep_test.go +++ b/jsep_test.go @@ -37,34 +37,88 @@ func TestNewJSEPSessionDescription(t *testing.T) { } func TestSessionDescriptionAttributes(t *testing.T) { - sd, err := NewJSEPSessionDescription(false) - assert.NoError(t, err) - t.Run("WithPropertyAttribute", func(t *testing.T) { + sd, err := NewJSEPSessionDescription(false) + assert.NoError(t, err) sd = sd.WithPropertyAttribute(AttrKeyRTCPMux) assert.Len(t, sd.Attributes, 1) assert.Equal(t, AttrKeyRTCPMux, sd.Attributes[0].Key) }) t.Run("WithValueAttribute", func(t *testing.T) { + sd, err := NewJSEPSessionDescription(false) + assert.NoError(t, err) sd = sd.WithValueAttribute(AttrKeyMID, "video") - assert.Len(t, sd.Attributes, 2) - assert.Equal(t, AttrKeyMID, sd.Attributes[1].Key) - assert.Equal(t, "video", sd.Attributes[1].Value) + assert.Len(t, sd.Attributes, 1) + assert.Equal(t, AttrKeyMID, sd.Attributes[0].Key) + assert.Equal(t, "video", sd.Attributes[0].Value) }) t.Run("WithICETrickleAdvertised", func(t *testing.T) { + sd, err := NewJSEPSessionDescription(false) + assert.NoError(t, err) sd = sd.WithICETrickleAdvertised() - assert.Len(t, sd.Attributes, 3) - assert.Equal(t, AttrKeyICEOptions, sd.Attributes[2].Key) - assert.Equal(t, "trickle", sd.Attributes[2].Value) + assert.Len(t, sd.Attributes, 1) + assert.Equal(t, AttrKeyICEOptions, sd.Attributes[0].Key) + assert.Equal(t, "trickle", sd.Attributes[0].Value) + }) + + t.Run("WithICERenomination", func(t *testing.T) { + sd, err := NewJSEPSessionDescription(false) + assert.NoError(t, err) + sd = sd.WithICETrickleAdvertised().WithICERenomination() + assert.Len(t, sd.Attributes, 1) + assert.Equal(t, AttrKeyICEOptions, sd.Attributes[0].Key) + assert.Equal(t, "trickle renomination", sd.Attributes[0].Value) }) t.Run("WithFingerprint", func(t *testing.T) { + sd, err := NewJSEPSessionDescription(false) + assert.NoError(t, err) sd = sd.WithFingerprint("sha-256", "test-fingerprint") - assert.Len(t, sd.Attributes, 4) - assert.Equal(t, "fingerprint", sd.Attributes[3].Key) - assert.Equal(t, "sha-256 test-fingerprint", sd.Attributes[3].Value) + assert.Len(t, sd.Attributes, 1) + assert.Equal(t, "fingerprint", sd.Attributes[0].Key) + assert.Equal(t, "sha-256 test-fingerprint", sd.Attributes[0].Value) + }) +} + +func TestSessionDescription_ICEOptions_Combined(t *testing.T) { + t.Run("WithICETrickleAdvertised and WithICERenominationAdvertised", func(t *testing.T) { + sd, err := NewJSEPSessionDescription(false) + assert.NoError(t, err) + + sd = sd.WithICETrickleAdvertised().WithICERenomination() + + iceOptionsCount := 0 + var iceOptionsValue string + for _, attr := range sd.Attributes { + if attr.Key == AttrKeyICEOptions { + iceOptionsCount++ + iceOptionsValue = attr.Value + } + } + + assert.Equal(t, 1, iceOptionsCount, "Should have exactly one ice-options attribute") + assert.Equal(t, "trickle renomination", iceOptionsValue, "Should combine both values with space") + }) + + t.Run("WithICERenominationAdvertised and WithICETrickleAdvertised (reverse order)", func(t *testing.T) { + sd, err := NewJSEPSessionDescription(false) + assert.NoError(t, err) + + sd = sd.WithICERenomination().WithICETrickleAdvertised() + + iceOptionsCount := 0 + var iceOptionsValue string + for _, attr := range sd.Attributes { + if attr.Key == AttrKeyICEOptions { + iceOptionsCount++ + iceOptionsValue = attr.Value + } + } + + assert.Equal(t, 1, iceOptionsCount, "Should have exactly one ice-options attribute") + assert.Equal(t, "renomination trickle", iceOptionsValue, "Should combine both values with space") }) }