Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WHIP ICE Restart Support #267

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/livekit/go-rtmp v0.0.0-20230829211117-1c4f5a5c81ed
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1
github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a
github.com/livekit/protocol v1.15.0
github.com/livekit/protocol v1.16.1-0.20240514184417-5aa3d9771312
github.com/livekit/psrpc v0.5.3-0.20240426045048-8ba067a45715
github.com/livekit/server-sdk-go/v2 v2.1.2
github.com/pion/dtls/v2 v2.2.11
Expand All @@ -39,7 +39,7 @@ require (
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
Expand All @@ -55,14 +55,14 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/klauspost/compress v1.17.6 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/lithammer/shortuuid/v4 v4.0.0 // indirect
github.com/mackerelio/go-osstat v0.2.4 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-pointer v0.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/nats-io/nats.go v1.31.0 // indirect
github.com/nats-io/nats.go v1.34.1 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pion/datachannel v1.5.5 // indirect
Expand Down Expand Up @@ -90,11 +90,11 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.2.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
)
32 changes: 16 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -64,8 +64,8 @@ github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
Expand All @@ -83,8 +83,8 @@ github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkD
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a h1:ATbv0x7G5tW2HgiouQ57csFE/G4gekl2oV1cxb2Dy24=
github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE=
github.com/livekit/protocol v1.15.0 h1:JAatoWKYdFx3D0U4JBWg25ZlrY+NK26xHabFopS2Jhk=
github.com/livekit/protocol v1.15.0/go.mod h1:pnn0Dv+/0K0OFqKHX6J6SreYO1dZxl6tDuAZ1ns8L/w=
github.com/livekit/protocol v1.16.1-0.20240514184417-5aa3d9771312 h1:c7oUI6WFW8UmmzXih2Itln1SFxWN+LiyoOD4DAHKGBQ=
github.com/livekit/protocol v1.16.1-0.20240514184417-5aa3d9771312/go.mod h1:pnn0Dv+/0K0OFqKHX6J6SreYO1dZxl6tDuAZ1ns8L/w=
github.com/livekit/psrpc v0.5.3-0.20240426045048-8ba067a45715 h1:vhDMOe8fxEc/amYTFo799LySPM12Fk3vc+Nc6o4gYZQ=
github.com/livekit/psrpc v0.5.3-0.20240426045048-8ba067a45715/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0=
github.com/livekit/server-sdk-go/v2 v2.1.2 h1:3MhFqptHjzpsNAcisHYDtn77qrJ9szsAy4zJkeI3Mic=
Expand All @@ -97,8 +97,8 @@ github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
github.com/nats-io/nats.go v1.34.1 h1:syWey5xaNHZgicYBemv0nohUPPmaLteiBEUT6Q5+F/4=
github.com/nats-io/nats.go v1.34.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
Expand Down Expand Up @@ -218,10 +218,10 @@ golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIi
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
Expand Down Expand Up @@ -261,8 +261,8 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand All @@ -289,8 +289,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
Expand Down
1 change: 1 addition & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var (
ErrInvalidSimulcast = psrpc.NewErrorf(psrpc.NotAcceptable, "invalid simulcast configuration")
ErrSimulcastTranscode = psrpc.NewErrorf(psrpc.NotAcceptable, "simulcast is not supported when transcoding")
ErrRoomDisconnected = psrpc.NewErrorf(psrpc.NotAcceptable, "room disonnected")
ErrInvalidWHIPRestartRequest = psrpc.NewErrorf(psrpc.InvalidArgument, "whip restart request was invalid")
ErrRoomDisconnectedUnexpectedly = RetryableError{psrpc.NewErrorf(psrpc.Unavailable, "room disonnected unexpectedly")}
)

Expand Down
7 changes: 7 additions & 0 deletions pkg/service/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ func (h *Handler) DeleteWHIPResource(ctx context.Context, req *rpc.DeleteWHIPRes
return &google_protobuf2.Empty{}, nil
}

func (h *Handler) ICERestartWHIPResource(ctx context.Context, req *rpc.ICERestartWHIPResourceRequest) (*rpc.ICERestartWHIPResourceResponse, error) {
_, span := tracer.Start(ctx, "Handler.ICERestartWHIPResource")
defer span.End()

return &rpc.ICERestartWHIPResourceResponse{}, nil
}

func (h *Handler) GetPProf(ctx context.Context, req *ipc.PProfRequest) (*ipc.PProfResponse, error) {
ctx, span := tracer.Start(ctx, "Handler.GetPProf")
defer span.End()
Expand Down
5 changes: 5 additions & 0 deletions pkg/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,10 @@ func RegisterIngressRpcHandlers(server rpc.IngressHandlerServer, info *livekit.I
if err := server.RegisterDeleteWHIPResourceTopic(info.State.ResourceId); err != nil {
return err
}
if err := server.RegisterICERestartWHIPResourceTopic(info.State.ResourceId); err != nil {
return err
}

}

return nil
Expand All @@ -542,6 +546,7 @@ func DeregisterIngressRpcHandlers(server rpc.IngressHandlerServer, info *livekit

if info.InputType == livekit.IngressInput_WHIP_INPUT {
server.DeregisterDeleteWHIPResourceTopic(info.State.ResourceId)
server.DeregisterICERestartWHIPResourceTopic(info.State.ResourceId)
}
}

Expand Down
59 changes: 58 additions & 1 deletion pkg/whip/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"bytes"
"context"
"fmt"
"hash/crc32"
"io"
"net/http"
"strings"
Expand All @@ -31,6 +32,7 @@ import (
"github.com/livekit/ingress/pkg/stats"
"github.com/livekit/ingress/pkg/types"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"

"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
Expand Down Expand Up @@ -151,7 +153,61 @@ func (s *WHIPServer) Start(

// Trickle, ICE Restart unimplemented for now
r.HandleFunc("/{app}/{stream_key}/{resource_id}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
vars := mux.Vars(r)
streamKey := vars["stream_key"]
resourceID := vars["resource_id"]

logger.Infow("handling ICE Restart request", "resourceID", resourceID)
w.Header().Set("Access-Control-Allow-Origin", "*")

if r.Header.Get("If-Match") != "*" {
logger.Warnw("WHIP ICE Restart must have If-Match='*'", err, "streamKey", streamKey, "resourceID", resourceID)
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte("ICE Restart Request must have If-Match='*' Header"))
return
}

body, err := io.ReadAll(r.Body)
if err != nil {
logger.Warnw("WHIP ICE Restart failed to read body", err, "streamKey", streamKey, "resourceID", resourceID)
s.handleError(errors.ErrInvalidWHIPRestartRequest, w)
return
}

// Only extract the ufrag/pwd and candidates from the request
// "WHIP does not support renegotiation of non-ICE related SDP information"
//
// https://www.ietf.org/archive/id/draft-ietf-wish-whip-14.html#name-ice-restarts
userFragment, password, err := extractICEDetails(body)
if err != nil {
logger.Warnw("WHIP ICE Restart failed to unmarshal SDP", err, "streamKey", streamKey, "resourceID", resourceID)
s.handleError(errors.ErrInvalidWHIPRestartRequest, w)
return
}

if userFragment == "" || password == "" {
logger.Warnw("WHIP ICE Restart failed to extract ice-ufrag/ice-pwd", err, "streamKey", streamKey, "resourceID", resourceID)
s.handleError(errors.ErrInvalidWHIPRestartRequest, w)
return
}

resp, err := s.rpcClient.ICERestartWHIPResource(s.ctx, resourceID, &rpc.ICERestartWHIPResourceRequest{
UserFragment: userFragment,
Password: password,
ResourceId: resourceID,
StreamKey: streamKey,
}, psrpc.WithRequestTimeout(5*time.Second))
if err == psrpc.ErrNoResponse {
s.handleError(errors.ErrIngressNotFound, w)
logger.Warnw("WHIP ICE Restart failed no such session", err, "streamKey", streamKey, "resourceID", resourceID)
return
}

w.Header().Set("Content-Type", "application/trickle-ice-sdpfrag")
w.Header().Set("ETag", fmt.Sprintf("%08x", crc32.ChecksumIEEE([]byte(resp.TrickleIceSdpfrag))))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(resp.TrickleIceSdpfrag))

}).Methods("PATCH")

r.HandleFunc("/{app}/{stream_key}/{resource_id}", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -264,6 +320,7 @@ func (s *WHIPServer) handleNewWhipClient(w http.ResponseWriter, r *http.Request,
w.Header().Set("Access-Control-Expose-Headers", "Location")
w.Header().Set("Content-Type", "application/sdp")
w.Header().Set("Location", fmt.Sprintf("/%s/%s/%s", app, streamKey, resourceId))
w.Header().Set("ETag", fmt.Sprintf("%08x", crc32.ChecksumIEEE(sdpOffer.Bytes())))
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(sdp))

Expand Down
54 changes: 54 additions & 0 deletions pkg/whip/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/livekit/server-sdk-go/v2/pkg/jitter"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
)

Expand Down Expand Up @@ -64,3 +65,56 @@ func createJitterBuffer(track *webrtc.TrackRemote, logger logger.Logger, writePL

return jb, nil
}

func extractICEDetails(in []byte) (ufrag string, pwd string, err error) {
scanAttributes := func(attributes []sdp.Attribute) {
for _, a := range attributes {
if a.Key == "ice-ufrag" {
ufrag = a.Value
} else if a.Key == "ice-pwd" {
pwd = a.Value
}
}
}

var parsed sdp.SessionDescription
if err = parsed.Unmarshal(in); err != nil {
Sean-Der marked this conversation as resolved.
Show resolved Hide resolved
return
}

scanAttributes(parsed.Attributes)
for _, m := range parsed.MediaDescriptions {
scanAttributes(m.Attributes)
}

return
}

func replaceICEDetails(in, ufrag, pwd string) (string, error) {
var parsed sdp.SessionDescription
replaceAttributes := func(attributes []sdp.Attribute) {
for i := range attributes {
if attributes[i].Key == "ice-ufrag" {
attributes[i].Value = ufrag
} else if attributes[i].Key == "ice-pwd" {
attributes[i].Value = pwd
}
}
}

if err := parsed.UnmarshalString(in); err != nil {
return "", err
}

replaceAttributes(parsed.Attributes)
for _, m := range parsed.MediaDescriptions {
replaceAttributes(m.Attributes)
}

newRemoteDescription, err := parsed.Marshal()
if err != nil {
return "", err
}

return string(newRemoteDescription), nil
}
56 changes: 55 additions & 1 deletion pkg/whip/whip_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package whip

import (
"bufio"
"context"
"io"
"strings"
Expand Down Expand Up @@ -317,7 +318,7 @@ func (h *whipHandler) createPeerConnection(api *webrtc.API) (*webrtc.PeerConnect
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
h.logger.Infow("Peer Connection State changed", "state", state.String())

if state >= webrtc.PeerConnectionStateDisconnected {
if state >= webrtc.PeerConnectionStateFailed {
h.closeOnce.Do(func() {
h.sync.End()

Expand Down Expand Up @@ -696,3 +697,56 @@ func (h *whipHandler) DeleteWHIPResource(ctx context.Context, req *rpc.DeleteWHI

return &google_protobuf2.Empty{}, nil
}

func (h *whipHandler) ICERestartWHIPResource(ctx context.Context, req *rpc.ICERestartWHIPResourceRequest) (*rpc.ICERestartWHIPResourceResponse, error) {
_, span := tracer.Start(ctx, "whipHandler.ICERestartWHIPResource")
defer span.End()

if h.pc == nil {
return nil, errors.ErrIngressNotFound
}

remoteDescription := h.pc.CurrentRemoteDescription()
if remoteDescription == nil {
return nil, errors.ErrIngressNotFound
}

// Replace the current remote description with the values from remote
newRemoteDescription, err := replaceICEDetails(remoteDescription.SDP, req.UserFragment, req.Password)
if err != nil {
return nil, errors.ErrIngressNotFound
}
remoteDescription.SDP = newRemoteDescription

if err := h.pc.SetRemoteDescription(*remoteDescription); err != nil {
return nil, errors.ErrIngressNotFound
}

answer, err := h.pc.CreateAnswer(nil)
if err != nil {
return nil, errors.ErrIngressNotFound
}

gatherComplete := webrtc.GatheringCompletePromise(h.pc)
if err = h.pc.SetLocalDescription(answer); err != nil {
return nil, errors.ErrIngressNotFound
}
<-gatherComplete

// Discard all `a=` lines that aren't ICE related
// "WHIP does not support renegotiation of non-ICE related SDP information"
//
// https://www.ietf.org/archive/id/draft-ietf-wish-whip-14.html#name-ice-restarts
var trickleIceSdpfrag strings.Builder
scanner := bufio.NewScanner(strings.NewReader(h.pc.LocalDescription().SDP))
for scanner.Scan() {
l := scanner.Text()
if strings.HasPrefix(l, "a=") && !strings.HasPrefix(l, "a=ice-pwd") && !strings.HasPrefix(l, "a=ice-ufrag") && !strings.HasPrefix(l, "a=candidate") {
continue
}

trickleIceSdpfrag.WriteString(l + "\n")
}

return &rpc.ICERestartWHIPResourceResponse{TrickleIceSdpfrag: trickleIceSdpfrag.String()}, nil
}