From eb6f46b15b45c4c97f68c9640b204f63bed9ea62 Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Thu, 30 Jan 2025 14:20:11 -0600 Subject: [PATCH] Preserve ICE candidate Extensions Ensure that ICE candidates are retained when parsed and stringified, and converted to ICE.candidate. --- icecandidate.go | 70 ++++++++++++++++++++++++++-- icecandidate_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 5 deletions(-) diff --git a/icecandidate.go b/icecandidate.go index 91cf6a1f361..fb40f33db39 100644 --- a/icecandidate.go +++ b/icecandidate.go @@ -24,6 +24,7 @@ type ICECandidate struct { TCPType string `json:"tcpType"` SDPMid string `json:"sdpMid"` SDPMLineIndex uint16 `json:"sdpMLineIndex"` + extensions string } // Conversion for package ice. @@ -69,6 +70,8 @@ func newICECandidateFromICE(candidate ice.Candidate, sdpMid string, sdpMLineInde SDPMLineIndex: sdpMLineIndex, } + newCandidate.setExtensions(candidate.Extensions()) + if candidate.RelatedAddress() != nil { newCandidate.RelatedAddress = candidate.RelatedAddress().Address newCandidate.RelatedPort = uint16(candidate.RelatedAddress().Port) //nolint:gosec // G115 @@ -77,7 +80,7 @@ func newICECandidateFromICE(candidate ice.Candidate, sdpMid string, sdpMLineInde return newCandidate, nil } -func (c ICECandidate) toICE() (ice.Candidate, error) { +func (c ICECandidate) toICE() (cand ice.Candidate, err error) { candidateID := c.statsID switch c.Typ { case ICECandidateTypeHost: @@ -92,7 +95,7 @@ func (c ICECandidate) toICE() (ice.Candidate, error) { Priority: c.Priority, } - return ice.NewCandidateHost(&config) + cand, err = ice.NewCandidateHost(&config) case ICECandidateTypeSrflx: config := ice.CandidateServerReflexiveConfig{ CandidateID: candidateID, @@ -106,7 +109,7 @@ func (c ICECandidate) toICE() (ice.Candidate, error) { RelPort: int(c.RelatedPort), } - return ice.NewCandidateServerReflexive(&config) + cand, err = ice.NewCandidateServerReflexive(&config) case ICECandidateTypePrflx: config := ice.CandidatePeerReflexiveConfig{ CandidateID: candidateID, @@ -120,7 +123,7 @@ func (c ICECandidate) toICE() (ice.Candidate, error) { RelPort: int(c.RelatedPort), } - return ice.NewCandidatePeerReflexive(&config) + cand, err = ice.NewCandidatePeerReflexive(&config) case ICECandidateTypeRelay: config := ice.CandidateRelayConfig{ CandidateID: candidateID, @@ -134,10 +137,67 @@ func (c ICECandidate) toICE() (ice.Candidate, error) { RelPort: int(c.RelatedPort), } - return ice.NewCandidateRelay(&config) + cand, err = ice.NewCandidateRelay(&config) default: return nil, fmt.Errorf("%w: %s", errICECandidateTypeUnknown, c.Typ) } + + if cand != nil && err == nil { + err = c.exportExtensions(cand) + } + + return cand, err +} + +func (c *ICECandidate) setExtensions(ext []ice.CandidateExtension) { + var extensions string + + for i := range ext { + if i > 0 { + extensions += " " + } + + extensions += ext[i].Key + " " + ext[i].Value + } + + c.extensions = extensions +} + +func (c *ICECandidate) exportExtensions(cand ice.Candidate) error { + extensions := c.extensions + var ext ice.CandidateExtension + var field string + + for i, start := 0, 0; i < len(extensions); i++ { + switch { + case extensions[i] == ' ': + field = extensions[start:i] + start = i + 1 + case i == len(extensions)-1: + field = extensions[start:] + default: + continue + } + + // Extension keys can't be empty + hasKey := ext.Key != "" + if !hasKey { + ext.Key = field + } else { + ext.Value = field + } + + // Extension value can be empty + if hasKey || i == len(extensions)-1 { + if err := cand.AddExtension(ext); err != nil { + return err + } + + ext = ice.CandidateExtension{} + } + } + + return nil } func convertTypeFromICE(t ice.CandidateType) (ICECandidateType, error) { diff --git a/icecandidate_test.go b/icecandidate_test.go index c553f2c1882..960fed8698a 100644 --- a/icecandidate_test.go +++ b/icecandidate_test.go @@ -248,3 +248,110 @@ func TestICECandidateSDPMid_ToJSON(t *testing.T) { assert.Equal(t, candidate.SDPMid, "0") assert.Equal(t, candidate.SDPMLineIndex, uint16(1)) } + +func TestICECandidateExtensions_ToJSON(t *testing.T) { + candidates := []struct { + candidate string + extensions []ice.CandidateExtension + }{ + { + "2637185494 1 udp 2121932543 192.168.1.4 50723 typ host generation 1 ufrag Jzd0 network-id 1", + []ice.CandidateExtension{ + { + Key: "generation", + Value: "1", + }, + { + Key: "ufrag", + Value: "Jzd0", + }, + { + Key: "network-id", + Value: "1", + }, + }, + }, + { + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active ufrag Jzd0 network-id 1", + []ice.CandidateExtension{ + { + Key: "tcptype", + Value: "active", + }, + { + Key: "ufrag", + Value: "Jzd0", + }, + { + Key: "network-id", + Value: "1", + }, + }, + }, + { + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active ufrag Jzd0 network-id 1 empty-ext ", + []ice.CandidateExtension{ + { + Key: "tcptype", + Value: "active", + }, + { + Key: "ufrag", + Value: "Jzd0", + }, + { + Key: "network-id", + Value: "1", + }, + { + Key: "empty-ext", + Value: "", + }, + }, + }, + { + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active ufrag Jzd0 empty-ext network-id 1", + []ice.CandidateExtension{ + { + Key: "tcptype", + Value: "active", + }, + { + Key: "ufrag", + Value: "Jzd0", + }, + { + Key: "empty-ext", + Value: "", + }, + { + Key: "network-id", + Value: "1", + }, + }, + }, + } + + for _, cand := range candidates { + cand := cand + candidate, err := ice.UnmarshalCandidate(cand.candidate) + assert.NoError(t, err) + + sdpMid := "1" + sdpMLineIndex := uint16(2) + + iceCandidate, err := newICECandidateFromICE(candidate, sdpMid, sdpMLineIndex) + assert.NoError(t, err) + + candidateInit := iceCandidate.ToJSON() + + assert.Equal(t, sdpMLineIndex, *candidateInit.SDPMLineIndex) + assert.Equal(t, "candidate:"+cand.candidate, candidateInit.Candidate) + + iceBack, err := iceCandidate.toICE() + + assert.NoError(t, err) + + assert.Equal(t, cand.extensions, iceBack.Extensions()) + } +}