Implement ICE restarts for WHIP.

This commit is contained in:
Juliusz Chroboczek
2025-03-10 13:35:52 +01:00
parent 8bfea927d3
commit 3b83044d67
4 changed files with 151 additions and 2 deletions
+57
View File
@@ -8,6 +8,9 @@ import (
"github.com/jech/galene/conn"
"github.com/jech/galene/group"
"github.com/jech/galene/sdpfrag"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
)
@@ -242,3 +245,57 @@ func (c *WhipClient) GotICECandidate(init webrtc.ICECandidateInit) error {
}
return c.connection.addICECandidate(&init)
}
func (c *WhipClient) Restart(ctx context.Context, frag sdpfrag.SDPFrag) (sdpfrag.SDPFrag, error) {
c.mu.Lock()
conn := c.connection
c.mu.Unlock()
if conn == nil {
return sdpfrag.SDPFrag{}, errors.New("no connection")
}
offer := conn.pc.RemoteDescription()
var sdpOffer sdp.SessionDescription
err := sdpOffer.Unmarshal([]byte(offer.SDP))
if err != nil {
return sdpfrag.SDPFrag{}, nil
}
sdpOffer2, _ := sdpfrag.PatchSDP(sdpOffer, frag)
offer2, err := sdpOffer2.Marshal()
if err != nil {
return sdpfrag.SDPFrag{}, nil
}
err = conn.pc.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer,
SDP: string(offer2),
})
if err != nil {
return sdpfrag.SDPFrag{}, err
}
answer, err := conn.pc.CreateAnswer(nil)
if err != nil {
return sdpfrag.SDPFrag{}, err
}
gatherComplete := webrtc.GatheringCompletePromise(conn.pc)
err = conn.pc.SetLocalDescription(answer)
if err != nil {
return sdpfrag.SDPFrag{}, err
}
select {
case <-ctx.Done():
return sdpfrag.SDPFrag{}, ctx.Err()
case <-gatherComplete:
}
sdpAnswer2 := conn.pc.LocalDescription()
var answer2 sdp.SessionDescription
err = answer2.Unmarshal([]byte(sdpAnswer2.SDP))
if err != nil {
return sdpfrag.SDPFrag{}, err
}
return sdpfrag.FromSDP(answer2), nil
}
+47
View File
@@ -226,3 +226,50 @@ func PatchSDP(s sdp.SessionDescription, f SDPFrag) (sdp.SessionDescription, bool
}
return s, override
}
func getCandidates(as []sdp.Attribute, mid *string, index *uint16, ufrag string) []webrtc.ICECandidateInit {
var cs []webrtc.ICECandidateInit
for _, a := range as {
if a.Key != "candidate" {
continue
}
c := webrtc.ICECandidateInit{
Candidate: a.Value,
SDPMid: mid,
SDPMLineIndex: index,
}
if ufrag != "" {
c.UsernameFragment = &ufrag
}
cs = append(cs, c)
}
return cs
}
// FromSDP extracts the ICE information from an [sdp.SessionDescription]
func FromSDP(s sdp.SessionDescription) SDPFrag {
var f SDPFrag
ufrag, _ := s.Attribute("ice-ufrag")
f.UsernameFragment = ufrag
pwd, _ := s.Attribute("ice-pwd")
f.Password = pwd
f.Candidates = getCandidates(s.Attributes, nil, nil, ufrag)
for index, m := range s.MediaDescriptions {
var mm MediaDescription
mm.MLine = m.MediaName.String()
mid, _ := m.Attribute("mid")
mm.Mid = mid
ufrag, _ := m.Attribute("ice-ufrag")
mm.UsernameFragment = ufrag
pwd, _ := m.Attribute("ice-pwd")
mm.Password = pwd
i := uint16(index)
mm.Candidates = getCandidates(
m.Attributes, &mid, &i, ufrag,
)
f.MediaDescriptions = append(f.MediaDescriptions, mm)
}
return f
}
+26
View File
@@ -300,3 +300,29 @@ func TestPatchSDP(t *testing.T) {
testPatchSDP(t, sdpOffer, sdpfragRestart, "HWmk", 0)
})
}
func TestFromSDP(t *testing.T) {
frag := `m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126
a=ice-ufrag:qiKa
a=ice-pwd:bcfs93hb/+ZLuUE2K50HVbkr
a=mid:0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 103 104 107 108 109 114 115 116 117 118 39 40 45 46 98 99 100 101 119 120 121
a=ice-ufrag:qiKa
a=ice-pwd:bcfs93hb/+ZLuUE2K50HVbkr
a=mid:1
`
var f SDPFrag
err := f.Unmarshal([]byte(frag))
if err != nil {
t.Fatalf("Unmarshal: %v", err)
}
var s sdp.SessionDescription
err = s.Unmarshal([]byte(sdpOffer))
if err != nil {
t.Fatalf("Unmarshal: %v", err)
}
ff := FromSDP(s)
if !reflect.DeepEqual(ff, f) {
t.Errorf("Not equal: %v %v", ff, f)
}
}
+21 -2
View File
@@ -381,11 +381,30 @@ func whipResourceHandler(w http.ResponseWriter, r *http.Request) {
}
uu, pp := frag.UFragPwd()
if uu != u || pp != p {
http.Error(w, "ICE restarts are not supported yet",
http.StatusUnprocessableEntity,
frag2, err := c.Restart(r.Context(), frag)
if err != nil {
log.Printf("WHIP restart: %v", err)
http.Error(w, "internal server error",
http.StatusInternalServerError,
)
return
}
c.SetETag("\"" + newId() + "\"")
f2, err := frag2.Marshal()
if err != nil {
log.Printf("WHIP marshal frag: %v", err)
http.Error(w, "internal server error",
http.StatusInternalServerError,
)
return
}
w.Header().Set(
"Content-Type", "application/trickle-ice-sdpfrag",
)
w.Header().Set("ETag", c.ETag())
w.Write(f2)
return
}
for _, init := range frag.AllCandidates() {
err := c.GotICECandidate(init)
if err != nil {