mirror of
https://github.com/livekit/livekit.git
synced 2026-06-03 19:41:43 +00:00
Single peer connection mode (#3873)
* WIP * check using protocol version * revert * clean up * sdp cid argument * WIP * WIP * test * clean up * clean up * fixes * clean up * clean up * clean up * conditional checks * tests for both dual and single peer connection * test * test * test * type check * test * todo * munges * combined config * populate mid * limit to receive only * clean up * clean up * clean up * older test * clean up * alternative audio codec * dtx * don't need to copy * Anunay feedback * use the available peer connection * publisher check * WIP * WIP * WIP * no mid * media sections requirement * mage generate * WIP * WIP * set data channel receive size for test * handle early media better * WIP * do not do ICERestart if no subscriber * WIP * WIP * WIP * WIP * WIP * WIP * start up subscriber RTCP worker * WIP * WIP * clean up * clean up * flag to indicate use of single peer connection * remove unused interface method * clean up * clean up * Jie feedback #1 * deps * do not access subscriber in one shot mode * more places for one shot mode * more one shot fixes * deps * deps * test
This commit is contained in:
@@ -7,7 +7,7 @@ toolchain go1.24.6
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/d5/tengo/v2 v2.17.0
|
||||
github.com/dennwc/iters v1.1.0
|
||||
github.com/dennwc/iters v1.2.2
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elliotchance/orderedmap/v2 v2.7.0
|
||||
github.com/florianl/go-tc v0.4.5
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
github.com/jxskiss/base62 v1.1.0
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
|
||||
github.com/livekit/mediatransportutil v0.0.0-20250825135402-7bc31f107ade
|
||||
github.com/livekit/protocol v1.40.0
|
||||
github.com/livekit/protocol v1.40.1-0.20250826073447-c714707269e5
|
||||
github.com/livekit/psrpc v0.6.1-0.20250726180611-3915e005e741
|
||||
github.com/mackerelio/go-osstat v0.2.6
|
||||
github.com/magefile/mage v1.15.0
|
||||
@@ -41,12 +41,12 @@ require (
|
||||
github.com/pion/sdp/v3 v3.0.15
|
||||
github.com/pion/transport/v3 v3.0.7
|
||||
github.com/pion/turn/v4 v4.1.1
|
||||
github.com/pion/webrtc/v4 v4.1.3
|
||||
github.com/pion/webrtc/v4 v4.1.5-0.20250828044558-c376d0edf977
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/redis/go-redis/v9 v9.12.1
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/thoas/go-funk v0.9.3
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible
|
||||
@@ -55,10 +55,10 @@ require (
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/mod v0.27.0
|
||||
golang.org/x/sync v0.16.0
|
||||
google.golang.org/protobuf v1.36.7
|
||||
google.golang.org/protobuf v1.36.8
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.7-20250717185734-6c6e0d3c608e.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1 // indirect
|
||||
buf.build/go/protovalidate v0.14.0 // indirect
|
||||
buf.build/go/protoyaml v0.6.0 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
@@ -109,7 +109,7 @@ require (
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/nats.go v1.44.0 // indirect
|
||||
github.com/nats-io/nats.go v1.45.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@@ -139,8 +139,8 @@ require (
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/grpc v1.75.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.7-20250717185734-6c6e0d3c608e.1 h1:/AZH8sVB6LHv8G+hZlAMCP31NevnesHwYgnlgS5Vt14=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.7-20250717185734-6c6e0d3c608e.1/go.mod h1:eva/VCrd8X7xuJw+JtwCEyrCKiRRASukFqmirnWBvFU=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1 h1:sjY1k5uszbIZfv11HO2keV4SLhNA47SabPO886v7Rvo=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1/go.mod h1:8EQ5GzyGJQ5tEIwMSxCl8RKJYsjCpAwkdcENoioXT6g=
|
||||
buf.build/go/protovalidate v0.14.0 h1:kr/rC/no+DtRyYX+8KXLDxNnI1rINz0imk5K44ZpZ3A=
|
||||
buf.build/go/protovalidate v0.14.0/go.mod h1:+F/oISho9MO7gJQNYC2VWLzcO1fTPmaTA08SDYJZncA=
|
||||
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
||||
@@ -47,8 +47,8 @@ github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dennwc/iters v1.1.0 h1:PsS3DbOU7GxSUQO0e7SGmzHkPhtwOlwbqggJ++Bgnr8=
|
||||
github.com/dennwc/iters v1.1.0/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
|
||||
github.com/dennwc/iters v1.2.2 h1:XH2/Etihiy9ZvPOVCR+icQXeYlhbvS7k0qro4x/2qQo=
|
||||
github.com/dennwc/iters v1.2.2/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
@@ -170,8 +170,8 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
|
||||
github.com/livekit/mediatransportutil v0.0.0-20250825135402-7bc31f107ade h1:lpxPcglwzUWNB4J0S2qZuyMehzmR7vW9whzSwV4IGoI=
|
||||
github.com/livekit/mediatransportutil v0.0.0-20250825135402-7bc31f107ade/go.mod h1:mSNtYzSf6iY9xM3UX42VEI+STHvMgHmrYzEHPcdhB8A=
|
||||
github.com/livekit/protocol v1.40.0 h1:FzCv17ivkxI4ySE0uFXSPPVPBVepf736Cgnmi054z2o=
|
||||
github.com/livekit/protocol v1.40.0/go.mod h1:YlgUxAegtU8jZ0tVXoIV/4fHeHqqLvS+6JnPKDbpFPU=
|
||||
github.com/livekit/protocol v1.40.1-0.20250826073447-c714707269e5 h1:aBqHlrgCI3qzVUAoOx7n5Kt8GQ8g7UbNp69fUt2gO8I=
|
||||
github.com/livekit/protocol v1.40.1-0.20250826073447-c714707269e5/go.mod h1:YlgUxAegtU8jZ0tVXoIV/4fHeHqqLvS+6JnPKDbpFPU=
|
||||
github.com/livekit/psrpc v0.6.1-0.20250726180611-3915e005e741 h1:KKL1u94l6dF9u4cBwnnfozk27GH1txWy2SlvkfgmzoY=
|
||||
github.com/livekit/psrpc v0.6.1-0.20250726180611-3915e005e741/go.mod h1:AuDC5uOoEjQJEc69v4Li3t77Ocz0e0NdjQEuFfO+vfk=
|
||||
github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0=
|
||||
@@ -215,8 +215,8 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
|
||||
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nats.go v1.45.0 h1:/wGPbnYXDM0pLKFjZTX+2JOw9TQPoIgTFrUaH97giwA=
|
||||
github.com/nats-io/nats.go v1.45.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
@@ -263,8 +263,8 @@ github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
|
||||
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
|
||||
github.com/pion/webrtc/v4 v4.1.5-0.20250828044558-c376d0edf977 h1:skeDG50xusAciSuWumr6OT/PvlyKdIC/E7OYmysehWw=
|
||||
github.com/pion/webrtc/v4 v4.1.5-0.20250828044558-c376d0edf977/go.mod h1:L+kyaW50BzPT8fQQyllbJCz+a6T3JsFTQfJs8zbWuAI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -303,8 +303,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
|
||||
github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
|
||||
@@ -339,10 +339,10 @@ go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
@@ -364,8 +364,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4=
|
||||
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -474,14 +474,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a/go.mod h1:y2yVLIE/CSMCPXaHnSKXxu1spLPnglFLegmgdY23uuE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -365,8 +365,8 @@ var DefaultConfig = Config{
|
||||
{Mime: mime.MimeTypeH264.String()},
|
||||
{Mime: mime.MimeTypeVP9.String()},
|
||||
{Mime: mime.MimeTypeAV1.String()},
|
||||
{Mime: mime.MimeTypeRTX.String()},
|
||||
{Mime: mime.MimeTypeH265.String()},
|
||||
{Mime: mime.MimeTypeRTX.String()},
|
||||
},
|
||||
EmptyTimeout: 5 * 60,
|
||||
DepartureTimeout: 20,
|
||||
|
||||
+51
-47
@@ -184,22 +184,23 @@ func CreateRouter(
|
||||
// ------------------------------------------------
|
||||
|
||||
type ParticipantInit struct {
|
||||
Identity livekit.ParticipantIdentity
|
||||
Name livekit.ParticipantName
|
||||
Reconnect bool
|
||||
ReconnectReason livekit.ReconnectReason
|
||||
AutoSubscribe bool
|
||||
Client *livekit.ClientInfo
|
||||
Grants *auth.ClaimGrants
|
||||
Region string
|
||||
AdaptiveStream bool
|
||||
ID livekit.ParticipantID
|
||||
SubscriberAllowPause *bool
|
||||
DisableICELite bool
|
||||
CreateRoom *livekit.CreateRoomRequest
|
||||
AddTrackRequests []*livekit.AddTrackRequest
|
||||
PublisherOffer *livekit.SessionDescription
|
||||
SyncState *livekit.SyncState
|
||||
Identity livekit.ParticipantIdentity
|
||||
Name livekit.ParticipantName
|
||||
Reconnect bool
|
||||
ReconnectReason livekit.ReconnectReason
|
||||
AutoSubscribe bool
|
||||
Client *livekit.ClientInfo
|
||||
Grants *auth.ClaimGrants
|
||||
Region string
|
||||
AdaptiveStream bool
|
||||
ID livekit.ParticipantID
|
||||
SubscriberAllowPause *bool
|
||||
DisableICELite bool
|
||||
CreateRoom *livekit.CreateRoomRequest
|
||||
AddTrackRequests []*livekit.AddTrackRequest
|
||||
PublisherOffer *livekit.SessionDescription
|
||||
SyncState *livekit.SyncState
|
||||
UseSinglePeerConnection bool
|
||||
}
|
||||
|
||||
func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
@@ -230,6 +231,7 @@ func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
e.AddArray("AddTrackRequests", logger.ProtoSlice(pi.AddTrackRequests))
|
||||
e.AddObject("PublisherOffer", logger.Proto(pi.PublisherOffer))
|
||||
e.AddObject("SyncState", logger.Proto(pi.SyncState))
|
||||
logBoolPtr("UseSinglePeerConnection", &pi.UseSinglePeerConnection)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -240,22 +242,23 @@ func (pi *ParticipantInit) ToStartSession(roomName livekit.RoomName, connectionI
|
||||
}
|
||||
|
||||
ss := &livekit.StartSession{
|
||||
RoomName: string(roomName),
|
||||
Identity: string(pi.Identity),
|
||||
Name: string(pi.Name),
|
||||
ConnectionId: string(connectionID),
|
||||
Reconnect: pi.Reconnect,
|
||||
ReconnectReason: pi.ReconnectReason,
|
||||
AutoSubscribe: pi.AutoSubscribe,
|
||||
Client: pi.Client,
|
||||
GrantsJson: string(claims),
|
||||
AdaptiveStream: pi.AdaptiveStream,
|
||||
ParticipantId: string(pi.ID),
|
||||
DisableIceLite: pi.DisableICELite,
|
||||
CreateRoom: pi.CreateRoom,
|
||||
AddTrackRequests: pi.AddTrackRequests,
|
||||
PublisherOffer: pi.PublisherOffer,
|
||||
SyncState: pi.SyncState,
|
||||
RoomName: string(roomName),
|
||||
Identity: string(pi.Identity),
|
||||
Name: string(pi.Name),
|
||||
ConnectionId: string(connectionID),
|
||||
Reconnect: pi.Reconnect,
|
||||
ReconnectReason: pi.ReconnectReason,
|
||||
AutoSubscribe: pi.AutoSubscribe,
|
||||
Client: pi.Client,
|
||||
GrantsJson: string(claims),
|
||||
AdaptiveStream: pi.AdaptiveStream,
|
||||
ParticipantId: string(pi.ID),
|
||||
DisableIceLite: pi.DisableICELite,
|
||||
CreateRoom: pi.CreateRoom,
|
||||
AddTrackRequests: pi.AddTrackRequests,
|
||||
PublisherOffer: pi.PublisherOffer,
|
||||
SyncState: pi.SyncState,
|
||||
UseSinglePeerConnection: pi.UseSinglePeerConnection,
|
||||
}
|
||||
if pi.SubscriberAllowPause != nil {
|
||||
subscriberAllowPause := *pi.SubscriberAllowPause
|
||||
@@ -272,21 +275,22 @@ func ParticipantInitFromStartSession(ss *livekit.StartSession, region string) (*
|
||||
}
|
||||
|
||||
pi := &ParticipantInit{
|
||||
Identity: livekit.ParticipantIdentity(ss.Identity),
|
||||
Name: livekit.ParticipantName(ss.Name),
|
||||
Reconnect: ss.Reconnect,
|
||||
ReconnectReason: ss.ReconnectReason,
|
||||
Client: ss.Client,
|
||||
AutoSubscribe: ss.AutoSubscribe,
|
||||
Grants: claims,
|
||||
Region: region,
|
||||
AdaptiveStream: ss.AdaptiveStream,
|
||||
ID: livekit.ParticipantID(ss.ParticipantId),
|
||||
DisableICELite: ss.DisableIceLite,
|
||||
CreateRoom: ss.CreateRoom,
|
||||
AddTrackRequests: ss.AddTrackRequests,
|
||||
PublisherOffer: ss.PublisherOffer,
|
||||
SyncState: ss.SyncState,
|
||||
Identity: livekit.ParticipantIdentity(ss.Identity),
|
||||
Name: livekit.ParticipantName(ss.Name),
|
||||
Reconnect: ss.Reconnect,
|
||||
ReconnectReason: ss.ReconnectReason,
|
||||
Client: ss.Client,
|
||||
AutoSubscribe: ss.AutoSubscribe,
|
||||
Grants: claims,
|
||||
Region: region,
|
||||
AdaptiveStream: ss.AdaptiveStream,
|
||||
ID: livekit.ParticipantID(ss.ParticipantId),
|
||||
DisableICELite: ss.DisableIceLite,
|
||||
CreateRoom: ss.CreateRoom,
|
||||
AddTrackRequests: ss.AddTrackRequests,
|
||||
PublisherOffer: ss.PublisherOffer,
|
||||
SyncState: ss.SyncState,
|
||||
UseSinglePeerConnection: ss.UseSinglePeerConnection,
|
||||
}
|
||||
if ss.SubscriberAllowPause != nil {
|
||||
subscriberAllowPause := *ss.SubscriberAllowPause
|
||||
|
||||
+65
-25
@@ -25,8 +25,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
frameMarking = "urn:ietf:params:rtp-hdrext:framemarking"
|
||||
repairedRTPStreamID = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
|
||||
frameMarkingURI = "urn:ietf:params:rtp-hdrext:framemarking"
|
||||
repairedRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
|
||||
)
|
||||
|
||||
type WebRTCConfig struct {
|
||||
@@ -79,8 +79,67 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
|
||||
rtcConf.PacketBufferSizeAudio = rtcConf.PacketBufferSize
|
||||
}
|
||||
|
||||
// publisher configuration
|
||||
publisherConfig := DirectionConfig{
|
||||
return &WebRTCConfig{
|
||||
WebRTCConfig: *webRTCConfig,
|
||||
Receiver: ReceiverConfig{
|
||||
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
|
||||
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
|
||||
},
|
||||
Publisher: getPublisherConfig(false),
|
||||
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdatePublisherConfig(consolidated bool) {
|
||||
c.Publisher = getPublisherConfig(consolidated)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdateSubscriberConfig(ccConf config.CongestionControlConfig) {
|
||||
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
|
||||
c.BufferFactory = factory
|
||||
c.SettingEngine.BufferFactory = factory.GetOrNew
|
||||
}
|
||||
|
||||
func getPublisherConfig(consolidated bool) DirectionConfig {
|
||||
if consolidated {
|
||||
return DirectionConfig{
|
||||
RTPHeaderExtension: RTPHeaderExtensionConfig{
|
||||
Audio: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.AudioLevelURI,
|
||||
//act.AbsCaptureTimeURI,
|
||||
},
|
||||
Video: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.TransportCCURI,
|
||||
sdp.ABSSendTimeURI,
|
||||
frameMarkingURI,
|
||||
dd.ExtensionURI,
|
||||
repairedRTPStreamIDURI,
|
||||
//act.AbsCaptureTimeURI,
|
||||
},
|
||||
},
|
||||
RTCPFeedback: RTCPFeedbackConfig{
|
||||
Audio: []webrtc.RTCPFeedback{
|
||||
{Type: webrtc.TypeRTCPFBNACK},
|
||||
},
|
||||
Video: []webrtc.RTCPFeedback{
|
||||
{Type: webrtc.TypeRTCPFBTransportCC},
|
||||
{Type: webrtc.TypeRTCPFBGoogREMB},
|
||||
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
|
||||
{Type: webrtc.TypeRTCPFBNACK},
|
||||
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return DirectionConfig{
|
||||
RTPHeaderExtension: RTPHeaderExtensionConfig{
|
||||
Audio: []string{
|
||||
sdp.SDESMidURI,
|
||||
@@ -92,9 +151,9 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.TransportCCURI,
|
||||
frameMarking,
|
||||
frameMarkingURI,
|
||||
dd.ExtensionURI,
|
||||
repairedRTPStreamID,
|
||||
repairedRTPStreamIDURI,
|
||||
//act.AbsCaptureTimeURI,
|
||||
},
|
||||
},
|
||||
@@ -110,25 +169,6 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &WebRTCConfig{
|
||||
WebRTCConfig: *webRTCConfig,
|
||||
Receiver: ReceiverConfig{
|
||||
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
|
||||
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
|
||||
},
|
||||
Publisher: publisherConfig,
|
||||
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdateCongestionControl(ccConf config.CongestionControlConfig) {
|
||||
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
|
||||
c.BufferFactory = factory
|
||||
c.SettingEngine.BufferFactory = factory.GetOrNew
|
||||
}
|
||||
|
||||
func getSubscriberConfig(enableTWCC bool) DirectionConfig {
|
||||
|
||||
+217
-144
@@ -24,169 +24,188 @@ import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
var OpusCodecCapability = webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeOpus.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "minptime=10;useinbandfec=1",
|
||||
}
|
||||
var RedCodecCapability = webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRED.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "111/111",
|
||||
}
|
||||
var videoRTX = webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRTX.String(),
|
||||
ClockRate: 90000,
|
||||
}
|
||||
var (
|
||||
OpusCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeOpus.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "minptime=10;useinbandfec=1",
|
||||
},
|
||||
PayloadType: 111,
|
||||
}
|
||||
|
||||
RedCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRED.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "111/111",
|
||||
},
|
||||
PayloadType: 63,
|
||||
}
|
||||
|
||||
pcmuCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypePCMU.String(),
|
||||
ClockRate: 8000,
|
||||
},
|
||||
PayloadType: 0,
|
||||
}
|
||||
|
||||
pcmaCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypePCMA.String(),
|
||||
ClockRate: 8000,
|
||||
},
|
||||
PayloadType: 8,
|
||||
}
|
||||
|
||||
videoRTXCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRTX.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
}
|
||||
|
||||
vp8CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
PayloadType: 96,
|
||||
}
|
||||
|
||||
vp9ProfileId0CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=0",
|
||||
},
|
||||
PayloadType: 98,
|
||||
}
|
||||
|
||||
vp9ProfileId1CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=1",
|
||||
},
|
||||
PayloadType: 100,
|
||||
}
|
||||
|
||||
h264ProfileLevelId42e01fPacketizationMode0CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||
},
|
||||
PayloadType: 125,
|
||||
}
|
||||
|
||||
h264ProfileLevelId42e01fPacketizationMode1CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
|
||||
},
|
||||
PayloadType: 108,
|
||||
}
|
||||
|
||||
h264HighProfileFmtp = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
|
||||
h264HighProfileCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: h264HighProfileFmtp,
|
||||
},
|
||||
PayloadType: 123,
|
||||
}
|
||||
|
||||
av1CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeAV1.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
PayloadType: 35,
|
||||
}
|
||||
|
||||
h265CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH265.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
PayloadType: 116,
|
||||
}
|
||||
|
||||
videoCodecsParameters = []webrtc.RTPCodecParameters{
|
||||
vp8CodecParameters,
|
||||
vp9ProfileId0CodecParameters,
|
||||
vp9ProfileId1CodecParameters,
|
||||
h264ProfileLevelId42e01fPacketizationMode0CodecParameters,
|
||||
h264ProfileLevelId42e01fPacketizationMode1CodecParameters,
|
||||
h264HighProfileCodecParameters,
|
||||
av1CodecParameters,
|
||||
h265CodecParameters,
|
||||
}
|
||||
)
|
||||
|
||||
func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedback RTCPFeedbackConfig, filterOutH264HighProfile bool) error {
|
||||
opusCodec := OpusCodecCapability
|
||||
opusCodec.RTCPFeedback = rtcpFeedback.Audio
|
||||
var opusPayload webrtc.PayloadType
|
||||
if IsCodecEnabled(codecs, opusCodec) {
|
||||
opusPayload = 111
|
||||
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: opusCodec,
|
||||
PayloadType: opusPayload,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
// audio codecs
|
||||
if IsCodecEnabled(codecs, OpusCodecParameters.RTPCodecCapability) {
|
||||
cp := OpusCodecParameters
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if IsCodecEnabled(codecs, RedCodecCapability) {
|
||||
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: RedCodecCapability,
|
||||
PayloadType: 63,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
if IsCodecEnabled(codecs, RedCodecParameters.RTPCodecCapability) {
|
||||
if err := me.RegisterCodec(RedCodecParameters, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypePCMU.String(),
|
||||
ClockRate: 8000,
|
||||
Channels: 1,
|
||||
RTCPFeedback: rtcpFeedback.Audio,
|
||||
},
|
||||
PayloadType: 0,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypePCMA.String(),
|
||||
ClockRate: 8000,
|
||||
Channels: 1,
|
||||
RTCPFeedback: rtcpFeedback.Audio,
|
||||
},
|
||||
PayloadType: 8,
|
||||
},
|
||||
} {
|
||||
if IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, codec := range []webrtc.RTPCodecParameters{pcmuCodecParameters, pcmaCodecParameters} {
|
||||
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
continue
|
||||
}
|
||||
|
||||
cp := codec
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rtxEnabled := IsCodecEnabled(codecs, videoRTX)
|
||||
|
||||
h264HighProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
ClockRate: 90000,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 96,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=0",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 98,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=1",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 100,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 125,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 108,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: h264HighProfileFmtp,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 123,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeAV1.String(),
|
||||
ClockRate: 90000,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 35,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH265.String(),
|
||||
ClockRate: 90000,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 116,
|
||||
},
|
||||
} {
|
||||
// video codecs
|
||||
rtxEnabled := IsCodecEnabled(codecs, videoRTXCodecParameters.RTPCodecCapability)
|
||||
for _, codec := range videoCodecsParameters {
|
||||
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == h264HighProfileFmtp {
|
||||
continue
|
||||
}
|
||||
if mime.IsMimeTypeStringRTX(codec.MimeType) {
|
||||
continue
|
||||
}
|
||||
if IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
if rtxEnabled {
|
||||
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRTX.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: fmt.Sprintf("apt=%d", codec.PayloadType),
|
||||
},
|
||||
PayloadType: codec.PayloadType + 1,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
continue
|
||||
}
|
||||
|
||||
cp := codec
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Video
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !rtxEnabled {
|
||||
continue
|
||||
}
|
||||
|
||||
cp = videoRTXCodecParameters
|
||||
cp.RTPCodecCapability.SDPFmtpLine = fmt.Sprintf("apt=%d", codec.PayloadType)
|
||||
cp.PayloadType = codec.PayloadType + 1
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -242,3 +261,57 @@ func selectAlternativeVideoCodec(enabledCodecs []*livekit.Codec) string {
|
||||
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
|
||||
return mime.MimeTypeVP8.String()
|
||||
}
|
||||
|
||||
func selectAlternativeAudioCodec(enabledCodecs []*livekit.Codec) string {
|
||||
for _, c := range enabledCodecs {
|
||||
if mime.IsMimeTypeStringAudio(c.Mime) {
|
||||
return c.Mime
|
||||
}
|
||||
}
|
||||
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
|
||||
return mime.MimeTypeOpus.String()
|
||||
}
|
||||
|
||||
func filterCodecs(
|
||||
codecs []webrtc.RTPCodecParameters,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
filterOutH264HighProfile bool,
|
||||
) []webrtc.RTPCodecParameters {
|
||||
filteredCodecs := make([]webrtc.RTPCodecParameters, 0, len(codecs))
|
||||
for _, c := range codecs {
|
||||
if filterOutH264HighProfile && isH264HighProfile(c.RTPCodecCapability.SDPFmtpLine) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, enabledCodec := range enabledCodecs {
|
||||
if mime.NormalizeMimeType(enabledCodec.Mime) == mime.NormalizeMimeType(c.RTPCodecCapability.MimeType) {
|
||||
// SINGLE-PEER-CONNECTION-TOOD: remove `nack` for RED?
|
||||
if mime.IsMimeTypeStringVideo(c.RTPCodecCapability.MimeType) {
|
||||
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Video
|
||||
} else {
|
||||
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Audio
|
||||
}
|
||||
filteredCodecs = append(filteredCodecs, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredCodecs
|
||||
}
|
||||
|
||||
func isH264HighProfile(fmtp string) bool {
|
||||
params := strings.Split(fmtp, ";")
|
||||
for _, param := range params {
|
||||
parts := strings.Split(param, "=")
|
||||
if len(parts) == 2 {
|
||||
if parts[0] == "profile-level-id" {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6184#section-8.1
|
||||
// hex value 0x64 for profile_idc is high profile
|
||||
return strings.HasPrefix(parts[1], "64")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package rtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/rtcp"
|
||||
@@ -309,7 +310,7 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
|
||||
if transceiver == nil {
|
||||
info := t.params.MediaTrack.ToProto()
|
||||
addTrackParams := types.AddTrackParams{
|
||||
Stereo: info.Stereo,
|
||||
Stereo: slices.Contains(info.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO),
|
||||
Red: !info.DisableRed,
|
||||
}
|
||||
if addTrackParams.Red && (len(codecs) == 1 && mime.IsMimeTypeStringOpus(codecs[0].MimeType)) {
|
||||
|
||||
+94
-14
@@ -209,6 +209,7 @@ type ParticipantParams struct {
|
||||
LastPubReliableSeq uint32
|
||||
Country string
|
||||
PreferVideoSizeFromMedia bool
|
||||
UseSinglePeerConnection bool
|
||||
}
|
||||
|
||||
type ParticipantImpl struct {
|
||||
@@ -1098,7 +1099,7 @@ func (p *ParticipantImpl) updateRidsFromSDP(parsed *sdp.SessionDescription, unma
|
||||
}
|
||||
|
||||
p.pendingTracksLock.Lock()
|
||||
pti := p.getPendingTrackPrimaryBySdpCid(mst, true)
|
||||
pti := p.getPendingTrackPrimaryBySdpCid(mst)
|
||||
if pti != nil {
|
||||
pti.sdpRids = getRids(pti.sdpRids)
|
||||
p.pubLogger.Debugw(
|
||||
@@ -1201,6 +1202,7 @@ func (p *ParticipantImpl) HandleOffer(offer webrtc.SessionDescription, offerId u
|
||||
}
|
||||
}
|
||||
|
||||
p.handlePendingRemoteTracks()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1463,6 +1465,10 @@ func (p *ParticipantImpl) clearMigrationTimer() {
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setupMigrationTimerLocked() {
|
||||
if p.params.UseSinglePeerConnection {
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// On subscriber peer connection, remote side will try ICE on both
|
||||
// pre- and post-migration ICE candidates as the migrating out
|
||||
@@ -1841,6 +1847,10 @@ func (h PublisherTransportHandler) OnDataSendError(err error) {
|
||||
h.p.onDataSendError(err)
|
||||
}
|
||||
|
||||
func (h PublisherTransportHandler) OnUnmatchedMedia(numAudios uint32, numVideos uint32) error {
|
||||
return h.p.sendMediaSectionsRequirement(numAudios, numVideos)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type SubscriberTransportHandler struct {
|
||||
@@ -1879,6 +1889,8 @@ func (h PrimaryTransportHandler) OnFullyEstablished() {
|
||||
h.p.onPrimaryTransportFullyEstablished()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func (p *ParticipantImpl) setupSignalling() {
|
||||
p.signalling = signalling.NewSignalling(signalling.SignallingParams{
|
||||
Logger: p.params.Logger,
|
||||
@@ -1902,7 +1914,7 @@ func (p *ParticipantImpl) setupTransportManager() error {
|
||||
var pth transport.Handler = PublisherTransportHandler{ath}
|
||||
var sth transport.Handler = SubscriberTransportHandler{ath}
|
||||
|
||||
subscriberAsPrimary := p.ProtocolVersion().SubscriberAsPrimary() && p.CanSubscribe() && !p.params.UseOneShotSignallingMode
|
||||
subscriberAsPrimary := !p.params.UseOneShotSignallingMode && (p.ProtocolVersion().SubscriberAsPrimary() && p.CanSubscribe()) && !p.params.UseSinglePeerConnection
|
||||
if subscriberAsPrimary {
|
||||
sth = PrimaryTransportHandler{sth, p}
|
||||
} else {
|
||||
@@ -1913,6 +1925,7 @@ func (p *ParticipantImpl) setupTransportManager() error {
|
||||
// primary connection does not change, canSubscribe can change if permission was updated
|
||||
// after the participant has joined
|
||||
SubscriberAsPrimary: subscriberAsPrimary,
|
||||
UseSinglePeerConnection: p.params.UseSinglePeerConnection,
|
||||
Config: p.params.Config,
|
||||
Twcc: p.twcc,
|
||||
ProtocolVersion: p.params.ProtocolVersion,
|
||||
@@ -2193,7 +2206,7 @@ func (p *ParticipantImpl) onMediaTrack(rtcTrack *webrtc.TrackRemote, rtpReceiver
|
||||
publishedTrack, isNewTrack, isReceiverAdded, sdpRids := p.mediaTrackReceived(track, rtpReceiver)
|
||||
if publishedTrack == nil {
|
||||
p.pubLogger.Debugw(
|
||||
"webrtc Track published but can't find MediaTrack in pendingTracks",
|
||||
"webrtc track published but can't find MediaTrack in pendingTracks",
|
||||
"kind", track.Kind().String(),
|
||||
"webrtcTrackID", track.ID(),
|
||||
"rid", track.RID(),
|
||||
@@ -2456,7 +2469,7 @@ func (p *ParticipantImpl) onPublisherInitialConnected() {
|
||||
p.supervisor.SetPublisherPeerConnectionConnected(true)
|
||||
}
|
||||
|
||||
if p.params.UseOneShotSignallingMode {
|
||||
if p.params.UseOneShotSignallingMode || p.params.UseSinglePeerConnection {
|
||||
go p.subscriberRTCPWorker()
|
||||
|
||||
p.setDownTracksConnected()
|
||||
@@ -2759,7 +2772,7 @@ func (p *ParticipantImpl) addPendingTrackLocked(req *livekit.AddTrackRequest) *l
|
||||
if !IsCodecEnabled(p.enabledPublishCodecs, webrtc.RTPCodecCapability{MimeType: mimeType}) {
|
||||
altCodec := selectAlternativeVideoCodec(p.enabledPublishCodecs)
|
||||
p.pubLogger.Infow(
|
||||
"falling back to alternative codec",
|
||||
"falling back to alternative video codec",
|
||||
"codec", mimeType,
|
||||
"altCodec", altCodec,
|
||||
"trackID", ti.Sid,
|
||||
@@ -2774,8 +2787,21 @@ func (p *ParticipantImpl) addPendingTrackLocked(req *livekit.AddTrackRequest) *l
|
||||
videoLayerMode = livekit.VideoLayer_ONE_SPATIAL_LAYER_PER_STREAM
|
||||
}
|
||||
}
|
||||
} else if req.Type == livekit.TrackType_AUDIO && !mime.IsMimeTypeStringAudio(mimeType) {
|
||||
mimeType = mime.MimeTypePrefixAudio + mimeType
|
||||
} else if req.Type == livekit.TrackType_AUDIO {
|
||||
if !mime.IsMimeTypeStringAudio(mimeType) {
|
||||
mimeType = mime.MimeTypePrefixAudio + mimeType
|
||||
}
|
||||
if !IsCodecEnabled(p.enabledPublishCodecs, webrtc.RTPCodecCapability{MimeType: mimeType}) {
|
||||
altCodec := selectAlternativeAudioCodec(p.enabledPublishCodecs)
|
||||
p.pubLogger.Infow(
|
||||
"falling back to alternative audio codec",
|
||||
"codec", mimeType,
|
||||
"altCodec", altCodec,
|
||||
"trackID", ti.Sid,
|
||||
)
|
||||
// select an alternative MIME type that's generally supported
|
||||
mimeType = altCodec
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := seenCodecs[mimeType]; ok || mimeType == "" {
|
||||
@@ -2821,18 +2847,23 @@ func (p *ParticipantImpl) addPendingTrackLocked(req *livekit.AddTrackRequest) *l
|
||||
}
|
||||
|
||||
p.params.Telemetry.TrackPublishRequested(context.Background(), p.ID(), p.Identity(), utils.CloneProto(ti))
|
||||
|
||||
if p.supervisor != nil {
|
||||
p.supervisor.AddPublication(livekit.TrackID(ti.Sid))
|
||||
p.supervisor.SetPublicationMute(livekit.TrackID(ti.Sid), ti.Muted)
|
||||
}
|
||||
|
||||
if p.getPublishedTrackBySignalCid(req.Cid) != nil || p.getPublishedTrackBySdpCid(req.Cid) != nil || p.pendingTracks[req.Cid] != nil {
|
||||
if p.pendingTracks[req.Cid] == nil {
|
||||
p.pendingTracks[req.Cid] = &pendingTrackInfo{
|
||||
pti := &pendingTrackInfo{
|
||||
trackInfos: []*livekit.TrackInfo{ti},
|
||||
sdpRids: buffer.DefaultVideoLayersRid, // could get updated from SDP
|
||||
createdAt: time.Now(),
|
||||
queued: true,
|
||||
}
|
||||
if ti.Type == livekit.TrackType_VIDEO {
|
||||
pti.sdpRids = buffer.DefaultVideoLayersRid // could get updated from SDP
|
||||
}
|
||||
p.pendingTracks[req.Cid] = pti
|
||||
} else {
|
||||
p.pendingTracks[req.Cid].trackInfos = append(p.pendingTracks[req.Cid].trackInfos, ti)
|
||||
}
|
||||
@@ -2845,11 +2876,14 @@ func (p *ParticipantImpl) addPendingTrackLocked(req *livekit.AddTrackRequest) *l
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pendingTracks[req.Cid] = &pendingTrackInfo{
|
||||
pti := &pendingTrackInfo{
|
||||
trackInfos: []*livekit.TrackInfo{ti},
|
||||
sdpRids: buffer.DefaultVideoLayersRid, // could get updated from SDP
|
||||
createdAt: time.Now(),
|
||||
}
|
||||
if ti.Type == livekit.TrackType_VIDEO {
|
||||
pti.sdpRids = buffer.DefaultVideoLayersRid // could get updated from SDP
|
||||
}
|
||||
p.pendingTracks[req.Cid] = pti
|
||||
p.pubLogger.Debugw(
|
||||
"pending track added",
|
||||
"trackID", ti.Sid,
|
||||
@@ -3295,7 +3329,7 @@ func (p *ParticipantImpl) getPendingTrack(clientId string, kind livekit.TrackTyp
|
||||
return signalCid, utils.CloneProto(pendingInfo.trackInfos[0]), pendingInfo.sdpRids, pendingInfo.migrated, pendingInfo.createdAt
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) getPendingTrackPrimaryBySdpCid(sdpCid string, skipQueued bool) *pendingTrackInfo {
|
||||
func (p *ParticipantImpl) getPendingTrackPrimaryBySdpCid(sdpCid string) *pendingTrackInfo {
|
||||
for _, pti := range p.pendingTracks {
|
||||
ti := pti.trackInfos[0]
|
||||
if len(ti.Codecs) == 0 {
|
||||
@@ -3674,7 +3708,12 @@ func (p *ParticipantImpl) setupEnabledCodecs(publishEnabledCodecs []*livekit.Cod
|
||||
subscribeCodecs = append(subscribeCodecs, c)
|
||||
}
|
||||
p.enabledSubscribeCodecs = subscribeCodecs
|
||||
p.params.Logger.Debugw("setup enabled codecs", "publish", p.enabledPublishCodecs, "subscribe", p.enabledSubscribeCodecs, "disabled", disabledCodecs)
|
||||
p.params.Logger.Debugw(
|
||||
"setup enabled codecs",
|
||||
"publish", logger.ProtoSlice(p.enabledPublishCodecs),
|
||||
"subscribe", logger.ProtoSlice(p.enabledSubscribeCodecs),
|
||||
"disabled", logger.Proto(disabledCodecs),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) replayJoiningReliableMessages() {
|
||||
@@ -3718,7 +3757,7 @@ func (p *ParticipantImpl) UpdateAudioTrack(update *livekit.UpdateLocalAudioTrack
|
||||
if ti.Sid == update.TrackSid {
|
||||
isPending = true
|
||||
|
||||
ti.AudioFeatures = update.Features
|
||||
ti.AudioFeatures = sutils.DedupeSlice(update.Features)
|
||||
ti.Stereo = false
|
||||
ti.DisableDtx = false
|
||||
for _, feature := range update.Features {
|
||||
@@ -3895,3 +3934,44 @@ func (p *ParticipantImpl) HandleLeaveRequest(reason types.ParticipantCloseReason
|
||||
func (p *ParticipantImpl) HandleSignalMessage(msg proto.Message) error {
|
||||
return p.signalHandler.HandleMessage(msg)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) IsUsingSinglePeerConnection() bool {
|
||||
return p.params.UseSinglePeerConnection
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) AddTrackLocal(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
|
||||
if p.params.UseSinglePeerConnection {
|
||||
return p.TransportManager.AddTrackLocal(
|
||||
trackLocal,
|
||||
params,
|
||||
p.enabledSubscribeCodecs,
|
||||
p.params.Config.Subscriber.RTCPFeedback,
|
||||
)
|
||||
} else {
|
||||
return p.TransportManager.AddTrackLocal(trackLocal, params, nil, RTCPFeedbackConfig{})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) AddTransceiverFromTrackLocal(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
|
||||
if p.params.UseSinglePeerConnection {
|
||||
return p.TransportManager.AddTransceiverFromTrackLocal(
|
||||
trackLocal,
|
||||
params,
|
||||
p.enabledSubscribeCodecs,
|
||||
p.params.Config.Subscriber.RTCPFeedback,
|
||||
)
|
||||
} else {
|
||||
return p.TransportManager.AddTransceiverFromTrackLocal(
|
||||
trackLocal,
|
||||
params,
|
||||
nil,
|
||||
RTCPFeedbackConfig{},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,10 +609,7 @@ func TestPreferAudioCodecForRed(t *testing.T) {
|
||||
|
||||
me := webrtc.MediaEngine{}
|
||||
me.RegisterDefaultCodecs()
|
||||
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: RedCodecCapability,
|
||||
PayloadType: 63,
|
||||
}, webrtc.RTPCodecTypeAudio))
|
||||
require.NoError(t, me.RegisterCodec(RedCodecParameters, webrtc.RTPCodecTypeAudio))
|
||||
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(&me))
|
||||
pc, err := api.NewPeerConnection(webrtc.Configuration{})
|
||||
|
||||
+29
-16
@@ -16,6 +16,7 @@ package rtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
lksdp "github.com/livekit/protocol/sdp"
|
||||
"github.com/livekit/protocol/utils"
|
||||
)
|
||||
@@ -91,6 +93,11 @@ func (p *ParticipantImpl) populateSdpCid(parsedOffer *sdp.SessionDescription) ([
|
||||
)
|
||||
}
|
||||
unmatchedTrack.(*MediaTrack).UpdateCodecSdpCid(unmatchedSdpMimeType, streamID)
|
||||
p.pubLogger.Debugw(
|
||||
"published track SDP cid updated",
|
||||
"trackID", unmatchedTrack.ID(),
|
||||
"track", logger.Proto(unmatchedTrack.ToProto()),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -176,7 +183,11 @@ func (p *ParticipantImpl) setCodecPreferencesForPublisher(
|
||||
livekit.TrackType_AUDIO,
|
||||
)
|
||||
parsedOffer = p.setCodecPreferencesOpusRedForPublisher(parsedOffer, unprocessedUnmatchAudios)
|
||||
parsedOffer, _ = p.setCodecPreferencesForPublisherMedia(parsedOffer, unmatchVideos, livekit.TrackType_VIDEO)
|
||||
parsedOffer, _ = p.setCodecPreferencesForPublisherMedia(
|
||||
parsedOffer,
|
||||
unmatchVideos,
|
||||
livekit.TrackType_VIDEO,
|
||||
)
|
||||
return parsedOffer
|
||||
}
|
||||
|
||||
@@ -191,10 +202,11 @@ func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(
|
||||
}
|
||||
|
||||
p.pendingTracksLock.RLock()
|
||||
_, info, _, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
|
||||
// if RED is disabled for this track, don't prefer RED codec in offer
|
||||
disableRed := info != nil && info.DisableRed
|
||||
_, ti, _, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
|
||||
p.pendingTracksLock.RUnlock()
|
||||
if ti == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
codecs, err := lksdp.CodecsFromMediaDescription(unmatchAudio)
|
||||
if err != nil {
|
||||
@@ -217,10 +229,11 @@ func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(
|
||||
continue
|
||||
}
|
||||
|
||||
// if RED is disabled for this track, don't prefer RED codec in offer
|
||||
var preferredCodecs, leftCodecs []string
|
||||
for _, codec := range codecs {
|
||||
// codec contain opus/red
|
||||
if !disableRed && mime.IsMimeTypeCodecStringRED(codec.Name) && strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
|
||||
if !ti.DisableRed && mime.IsMimeTypeCodecStringRED(codec.Name) && strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
|
||||
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
|
||||
} else {
|
||||
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
|
||||
@@ -262,38 +275,38 @@ func (p *ParticipantImpl) setCodecPreferencesForPublisherMedia(
|
||||
unprocessed := make([]*sdp.MediaDescription, 0, len(unmatches))
|
||||
// unmatched media is pending for publish, set codec preference
|
||||
for _, unmatch := range unmatches {
|
||||
var ti *livekit.TrackInfo
|
||||
var mimeType string
|
||||
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatch)
|
||||
if !ok {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
var info *livekit.TrackInfo
|
||||
p.pendingTracksLock.RLock()
|
||||
mt := p.getPublishedTrackBySdpCid(streamID)
|
||||
if mt != nil {
|
||||
info = mt.ToProto()
|
||||
ti = mt.ToProto()
|
||||
} else {
|
||||
_, info, _, _, _ = p.getPendingTrack(streamID, trackType, false)
|
||||
_, ti, _, _, _ = p.getPendingTrack(streamID, trackType, false)
|
||||
}
|
||||
p.pendingTracksLock.RUnlock()
|
||||
|
||||
if info == nil {
|
||||
p.pendingTracksLock.RUnlock()
|
||||
if ti == nil {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
var mimeType string
|
||||
for _, c := range info.Codecs {
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == streamID || c.SdpCid == streamID {
|
||||
mimeType = c.MimeType
|
||||
break
|
||||
}
|
||||
}
|
||||
if mimeType == "" && len(info.Codecs) > 0 {
|
||||
mimeType = info.Codecs[0].MimeType
|
||||
if mimeType == "" && len(ti.Codecs) > 0 {
|
||||
mimeType = ti.Codecs[0].MimeType
|
||||
}
|
||||
p.pendingTracksLock.RUnlock()
|
||||
|
||||
if mimeType == "" {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
@@ -422,7 +435,7 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
|
||||
if !ti.DisableDtx {
|
||||
attr.Value += ";usedtx=1"
|
||||
}
|
||||
if ti.Stereo {
|
||||
if slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO) {
|
||||
attr.Value += ";stereo=1;maxaveragebitrate=510000"
|
||||
}
|
||||
m.Attributes[i] = attr
|
||||
|
||||
@@ -330,3 +330,19 @@ func (p *ParticipantImpl) SendSubscriptionPermissionUpdate(publisherID livekit.P
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendMediaSectionsRequirement(numAudios uint32, numVideos uint32) error {
|
||||
p.pubLogger.Debugw(
|
||||
"sending media sections requirement",
|
||||
"numAudios", numAudios,
|
||||
"numVideos", numVideos,
|
||||
)
|
||||
err := p.signaller.WriteMessage(p.signalling.SignalMediaSectionsRequirement(&livekit.MediaSectionsRequirement{
|
||||
NumAudios: numAudios,
|
||||
NumVideos: numVideos,
|
||||
}))
|
||||
if err != nil {
|
||||
p.subLogger.Errorw("could not send media sections requirement", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+8
-5
@@ -610,6 +610,10 @@ func (r *Room) Join(
|
||||
} else {
|
||||
participant.Negotiate(true)
|
||||
}
|
||||
} else {
|
||||
if participant.IsUsingSinglePeerConnection() {
|
||||
go r.subscribeToExistingTracks(participant, false)
|
||||
}
|
||||
}
|
||||
|
||||
prometheus.ServiceOperationCounter.WithLabelValues("participant_join", "success", "").Add(1)
|
||||
@@ -1114,13 +1118,12 @@ func (r *Room) onTrackPublished(participant types.LocalParticipant, track types.
|
||||
continue
|
||||
}
|
||||
|
||||
r.logger.Debugw(
|
||||
existingParticipant.GetLogger().Debugw(
|
||||
"subscribing to new track",
|
||||
"participant", existingParticipant.Identity(),
|
||||
"pID", existingParticipant.ID(),
|
||||
"publisher", participant.Identity(),
|
||||
"publisherID", participant.ID(),
|
||||
"trackID", track.ID())
|
||||
"trackID", track.ID(),
|
||||
)
|
||||
existingParticipant.SubscribeToTrack(track.ID(), false)
|
||||
}
|
||||
onParticipantChanged := r.onParticipantChanged
|
||||
@@ -1393,7 +1396,7 @@ func (r *Room) subscribeToExistingTracks(p types.LocalParticipant, isSync bool)
|
||||
}
|
||||
}
|
||||
if len(trackIDs) > 0 {
|
||||
r.logger.Debugw("subscribed participant to existing tracks", "trackID", trackIDs)
|
||||
p.GetLogger().Debugw("subscribed participant to existing tracks", "trackID", trackIDs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,4 +57,5 @@ type ParticipantSignalling interface {
|
||||
SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message
|
||||
SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message
|
||||
SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message
|
||||
SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message
|
||||
}
|
||||
|
||||
@@ -220,3 +220,11 @@ func (s *signalling) SignalSubscriptionPermissionUpdate(subscriptionPermissionUp
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *signalling) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_MediaSectionsRequirement{
|
||||
MediaSectionsRequirement: mediaSectionsRequirement,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,3 +107,7 @@ func (u *signallingUnimplemented) SignalSubscriptionResponse(subscriptionRespons
|
||||
func (u *signallingUnimplemented) SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
+244
-45
@@ -246,6 +246,16 @@ type PCTransport struct {
|
||||
|
||||
eventsQueue *utils.TypedOpsQueue[event]
|
||||
|
||||
connectionDetails *types.ICEConnectionDetails
|
||||
selectedPair atomic.Pointer[webrtc.ICECandidatePair]
|
||||
mayFailedICEStats []iceCandidatePairStats
|
||||
mayFailedICEStatsTimer *time.Timer
|
||||
|
||||
numOutstandingAudios uint32
|
||||
numRequestSentAudios uint32
|
||||
numOutstandingVideos uint32
|
||||
numRequestSentVideos uint32
|
||||
|
||||
// the following should be accessed only in event processing go routine
|
||||
cacheLocalCandidates bool
|
||||
cachedLocalCandidates []*webrtc.ICECandidate
|
||||
@@ -257,11 +267,6 @@ type PCTransport struct {
|
||||
signalStateCheckTimer *time.Timer
|
||||
currentOfferIceCredential string // ice user:pwd, for publish side ice restart checking
|
||||
pendingRestartIceOffer *webrtc.SessionDescription
|
||||
|
||||
connectionDetails *types.ICEConnectionDetails
|
||||
selectedPair atomic.Pointer[webrtc.ICECandidatePair]
|
||||
mayFailedICEStats []iceCandidatePairStats
|
||||
mayFailedICEStatsTimer *time.Timer
|
||||
}
|
||||
|
||||
type TransportParams struct {
|
||||
@@ -466,6 +471,7 @@ func newPeerConnection(params TransportParams, onBandwidthEstimator func(estimat
|
||||
params.Logger.Debugw("rtx pair found from extension", "repair", repair, "base", base)
|
||||
params.Config.BufferFactory.SetRTXPair(repair, base)
|
||||
}, params.Logger))
|
||||
|
||||
api := webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(me),
|
||||
webrtc.WithSettingEngine(se),
|
||||
@@ -904,7 +910,12 @@ func (t *PCTransport) AddICECandidate(candidate webrtc.ICECandidateInit) {
|
||||
})
|
||||
}
|
||||
|
||||
func (t *PCTransport) AddTrack(trackLocal webrtc.TrackLocal, params types.AddTrackParams) (sender *webrtc.RTPSender, transceiver *webrtc.RTPTransceiver, err error) {
|
||||
func (t *PCTransport) AddTrack(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
) (sender *webrtc.RTPSender, transceiver *webrtc.RTPTransceiver, err error) {
|
||||
t.lock.Lock()
|
||||
canReuse := t.canReuseTransceiver
|
||||
td, ok := t.previousTrackDescription[trackLocal.ID()]
|
||||
@@ -924,7 +935,7 @@ func (t *PCTransport) AddTrack(trackLocal webrtc.TrackLocal, params types.AddTra
|
||||
|
||||
// if never negotiated with client, can't reuse transceiver for track not subscribed before migration
|
||||
if !canReuse {
|
||||
return t.AddTransceiverFromTrack(trackLocal, params)
|
||||
return t.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
}
|
||||
|
||||
sender, err = t.pc.AddTrack(trackLocal)
|
||||
@@ -932,7 +943,6 @@ func (t *PCTransport) AddTrack(trackLocal webrtc.TrackLocal, params types.AddTra
|
||||
return
|
||||
}
|
||||
|
||||
// as there is no way to get transceiver from sender, search
|
||||
for _, tr := range t.pc.GetTransceivers() {
|
||||
if tr.Sender() == sender {
|
||||
transceiver = tr
|
||||
@@ -948,10 +958,17 @@ func (t *PCTransport) AddTrack(trackLocal webrtc.TrackLocal, params types.AddTra
|
||||
if trackLocal.Kind() == webrtc.RTPCodecTypeAudio {
|
||||
configureAudioTransceiver(transceiver, params.Stereo, !params.Red || !t.params.ClientInfo.SupportsAudioRED())
|
||||
}
|
||||
configureTransceiverCodecs(transceiver, enabledCodecs, rtcpFeedbackConfig, !t.params.IsOfferer)
|
||||
t.adjustNumOutstandingMedia(transceiver)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *PCTransport) AddTransceiverFromTrack(trackLocal webrtc.TrackLocal, params types.AddTrackParams) (sender *webrtc.RTPSender, transceiver *webrtc.RTPTransceiver, err error) {
|
||||
func (t *PCTransport) AddTransceiverFromTrack(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
) (sender *webrtc.RTPSender, transceiver *webrtc.RTPTransceiver, err error) {
|
||||
transceiver, err = t.pc.AddTransceiverFromTrack(trackLocal)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -966,14 +983,42 @@ func (t *PCTransport) AddTransceiverFromTrack(trackLocal webrtc.TrackLocal, para
|
||||
if trackLocal.Kind() == webrtc.RTPCodecTypeAudio {
|
||||
configureAudioTransceiver(transceiver, params.Stereo, !params.Red || !t.params.ClientInfo.SupportsAudioRED())
|
||||
}
|
||||
|
||||
configureTransceiverCodecs(transceiver, enabledCodecs, rtcpFeedbackConfig, !t.params.IsOfferer)
|
||||
t.adjustNumOutstandingMedia(transceiver)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *PCTransport) AddTransceiverFromKind(
|
||||
kind webrtc.RTPCodecType,
|
||||
init webrtc.RTPTransceiverInit,
|
||||
) (*webrtc.RTPTransceiver, error) {
|
||||
return t.pc.AddTransceiverFromKind(kind, init)
|
||||
}
|
||||
|
||||
func (t *PCTransport) RemoveTrack(sender *webrtc.RTPSender) error {
|
||||
return t.pc.RemoveTrack(sender)
|
||||
}
|
||||
|
||||
func (t *PCTransport) CurrentLocalDescription() *webrtc.SessionDescription {
|
||||
cld := t.pc.CurrentLocalDescription()
|
||||
if cld == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ld := *cld
|
||||
return &ld
|
||||
}
|
||||
|
||||
func (t *PCTransport) CurrentRemoteDescription() *webrtc.SessionDescription {
|
||||
crd := t.pc.CurrentRemoteDescription()
|
||||
if crd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rd := *crd
|
||||
return &rd
|
||||
}
|
||||
|
||||
func (t *PCTransport) GetMid(rtpReceiver *webrtc.RTPReceiver) string {
|
||||
for _, tr := range t.pc.GetTransceivers() {
|
||||
if tr.Receiver() == rtpReceiver {
|
||||
@@ -994,6 +1039,30 @@ func (t *PCTransport) GetRTPReceiver(mid string) *webrtc.RTPReceiver {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *PCTransport) getNumUnmatchedTransceivers() (uint32, uint32) {
|
||||
if t.isClosed.Load() || t.pc.ConnectionState() == webrtc.PeerConnectionStateClosed {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
numAudios := uint32(0)
|
||||
numVideos := uint32(0)
|
||||
for _, tr := range t.pc.GetTransceivers() {
|
||||
if tr.Mid() != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch tr.Kind() {
|
||||
case webrtc.RTPCodecTypeAudio:
|
||||
numAudios++
|
||||
|
||||
case webrtc.RTPCodecTypeVideo:
|
||||
numVideos++
|
||||
}
|
||||
}
|
||||
|
||||
return numAudios, numVideos
|
||||
}
|
||||
|
||||
func (t *PCTransport) CreateDataChannel(label string, dci *webrtc.DataChannelInit) error {
|
||||
dc, err := t.pc.CreateDataChannel(label, dci)
|
||||
if err != nil {
|
||||
@@ -1003,6 +1072,7 @@ func (t *PCTransport) CreateDataChannel(label string, dci *webrtc.DataChannelIni
|
||||
dcPtr **datachannel.DataChannelWriter[*webrtc.DataChannel]
|
||||
dcReady *bool
|
||||
isUnlabeled bool
|
||||
kind livekit.DataPacket_Kind
|
||||
)
|
||||
switch dc.Label() {
|
||||
default:
|
||||
@@ -1011,9 +1081,11 @@ func (t *PCTransport) CreateDataChannel(label string, dci *webrtc.DataChannelIni
|
||||
case ReliableDataChannel:
|
||||
dcPtr = &t.reliableDC
|
||||
dcReady = &t.reliableDCOpened
|
||||
kind = livekit.DataPacket_RELIABLE
|
||||
case LossyDataChannel:
|
||||
dcPtr = &t.lossyDC
|
||||
dcReady = &t.lossyDCOpened
|
||||
kind = livekit.DataPacket_LOSSY
|
||||
}
|
||||
|
||||
dc.OnOpen(func() {
|
||||
@@ -1044,6 +1116,28 @@ func (t *PCTransport) CreateDataChannel(label string, dci *webrtc.DataChannelIni
|
||||
t.lock.Unlock()
|
||||
t.params.Logger.Debugw(dc.Label() + " data channel open")
|
||||
|
||||
go func() {
|
||||
defer rawDC.Close()
|
||||
buffer := make([]byte, dataChannelBufferSize)
|
||||
for {
|
||||
n, _, err := rawDC.ReadDataChannel(buffer)
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) && !strings.Contains(err.Error(), "state=Closed") {
|
||||
t.params.Logger.Warnw("error reading data channel", err, "label", dc.Label())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case isUnlabeled:
|
||||
t.params.Handler.OnDataMessageUnlabeled(buffer[:n])
|
||||
|
||||
default:
|
||||
t.params.Handler.OnDataMessage(kind, buffer[:n])
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
t.maybeNotifyFullyEstablished()
|
||||
})
|
||||
|
||||
@@ -1131,7 +1225,6 @@ func (t *PCTransport) GetRTT() (float64, bool) {
|
||||
return scps.CurrentRoundTripTime, true
|
||||
}
|
||||
|
||||
// IsEstablished returns true if the PeerConnection has been established
|
||||
func (t *PCTransport) IsEstablished() bool {
|
||||
return t.pc.ConnectionState() != webrtc.PeerConnectionStateNew
|
||||
}
|
||||
@@ -1801,11 +1894,11 @@ func (t *PCTransport) preparePC(previousAnswer webrtc.SessionDescription) error
|
||||
return err
|
||||
}
|
||||
|
||||
// replace client's fingerprint into dump pc's answer, for pion's dtls process, it will
|
||||
// replace client's fingerprint into dummy pc's answer, for pion's dtls process, it will
|
||||
// keep the fingerprint at first call of SetRemoteDescription, if dummy pc and client pc use
|
||||
// different fingerprint, that will cause pion denied dtls data after handshake with client
|
||||
// complete (can't pass fingerprint change).
|
||||
// in this step, we don't established connection with dump pc(no candidate swap), just use
|
||||
// in this step, we don't established connection with dummy pc(no candidate swap), just use
|
||||
// sdp negotiation to sticky data channel and keep client's fingerprint
|
||||
parsedAns, _ := ans.Unmarshal()
|
||||
fpLine := fpHahs + " " + fp
|
||||
@@ -1829,9 +1922,9 @@ func (t *PCTransport) preparePC(previousAnswer webrtc.SessionDescription) error
|
||||
return t.pc.SetRemoteDescription(ans)
|
||||
}
|
||||
|
||||
func (t *PCTransport) initPCWithPreviousAnswer(previousAnswer webrtc.SessionDescription) (map[string]*webrtc.RTPSender, error) {
|
||||
func (t *PCTransport) initPCWithPreviousRemoteDescription(previousRemoteDescription webrtc.SessionDescription) (map[string]*webrtc.RTPSender, error) {
|
||||
senders := make(map[string]*webrtc.RTPSender)
|
||||
parsed, err := previousAnswer.Unmarshal()
|
||||
parsed, err := previousRemoteDescription.Unmarshal()
|
||||
if err != nil {
|
||||
return senders, err
|
||||
}
|
||||
@@ -1843,19 +1936,33 @@ func (t *PCTransport) initPCWithPreviousAnswer(previousAnswer webrtc.SessionDesc
|
||||
case "audio":
|
||||
codecType = webrtc.RTPCodecTypeAudio
|
||||
case "application":
|
||||
// for pion generate unmatched sdp, it always appends data channel to last m-lines,
|
||||
// that not consistent with our previous answer that data channel might at middle-line
|
||||
// because sdp can negotiate multi times before migration.(it will sticky to the last m-line atfirst negotiate)
|
||||
// so use a dummy pc to negotiate sdp to fixed the datachannel's mid at same position with previous answer
|
||||
if err := t.preparePC(previousAnswer); err != nil {
|
||||
t.params.Logger.Warnw("prepare pc for migration failed", err)
|
||||
return senders, err
|
||||
if t.params.IsOfferer {
|
||||
// for pion generate unmatched sdp, it always appends data channel to last m-lines,
|
||||
// that not consistent with our previous answer that data channel might at middle-line
|
||||
// because sdp can negotiate multi times before migration.(it will sticky to the last m-line at first negotiate)
|
||||
// so use a dummy pc to negotiate sdp to fixed the datachannel's mid at same position with previous answer
|
||||
if err := t.preparePC(previousRemoteDescription); err != nil {
|
||||
t.params.Logger.Warnw("prepare pc for migration failed", err)
|
||||
return senders, err
|
||||
}
|
||||
}
|
||||
continue
|
||||
default:
|
||||
continue
|
||||
}
|
||||
tr, err := t.pc.AddTransceiverFromKind(codecType, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
|
||||
|
||||
_, ok1 := m.Attribute(webrtc.RTPTransceiverDirectionSendrecv.String())
|
||||
_, ok2 := m.Attribute(webrtc.RTPTransceiverDirectionRecvonly.String())
|
||||
if !ok1 && !ok2 {
|
||||
continue
|
||||
}
|
||||
|
||||
tr, err := t.pc.AddTransceiverFromKind(
|
||||
codecType,
|
||||
webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return senders, err
|
||||
}
|
||||
@@ -1870,43 +1977,51 @@ func (t *PCTransport) initPCWithPreviousAnswer(previousAnswer webrtc.SessionDesc
|
||||
senders[mid] = sender
|
||||
|
||||
// set transceiver to inactive
|
||||
tr.SetSender(tr.Sender(), nil)
|
||||
tr.SetSender(sender, nil)
|
||||
}
|
||||
return senders, nil
|
||||
}
|
||||
|
||||
func (t *PCTransport) SetPreviousSdp(offer, answer *webrtc.SessionDescription) {
|
||||
// when there is no previous answer, cannot migrate, force a full reconnect
|
||||
if answer == nil {
|
||||
t.onNegotiationFailed(true, "no previous answer for previous sdp")
|
||||
func (t *PCTransport) SetPreviousSdp(localDescription, remoteDescription *webrtc.SessionDescription) {
|
||||
// when there is no remote description, cannot migrate, force a full reconnect
|
||||
if remoteDescription == nil {
|
||||
t.onNegotiationFailed(true, "no previous remote description")
|
||||
return
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
if t.pc.RemoteDescription() == nil && t.previousAnswer == nil {
|
||||
t.previousAnswer = answer
|
||||
if senders, err := t.initPCWithPreviousAnswer(*t.previousAnswer); err != nil {
|
||||
if t.params.IsOfferer {
|
||||
t.previousAnswer = remoteDescription
|
||||
}
|
||||
if senders, err := t.initPCWithPreviousRemoteDescription(*remoteDescription); err != nil {
|
||||
t.lock.Unlock()
|
||||
|
||||
t.onNegotiationFailed(true, fmt.Sprintf("initPCWithPreviousAnswer failed, error: %s", err))
|
||||
t.onNegotiationFailed(true, fmt.Sprintf("initPCWithPreviousRemoteDescription failed, error: %s", err))
|
||||
return
|
||||
} else if offer != nil {
|
||||
} else if localDescription != nil {
|
||||
// in migration case, can't reuse transceiver before negotiated except track subscribed at previous node
|
||||
t.canReuseTransceiver = false
|
||||
if err := t.parseTrackMid(*offer, senders); err != nil {
|
||||
t.params.Logger.Warnw("parse previous offer failed", err, "offer", offer.SDP)
|
||||
if err := t.parseTrackMid(*localDescription, senders); err != nil {
|
||||
t.params.Logger.Warnw(
|
||||
"parse previous local description failed", err,
|
||||
"localDescription", localDescription.SDP,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// disable fast negotiation temporarily after migration to avoid sending offer
|
||||
// contains part of subscribed tracks before migration, let the subscribed track
|
||||
// resume at the same time.
|
||||
t.lastNegotiate = time.Now().Add(iceFailedTimeoutTotal)
|
||||
|
||||
if t.params.IsOfferer {
|
||||
// disable fast negotiation temporarily after migration to avoid sending offer
|
||||
// contains part of subscribed tracks before migration, let the subscribed track
|
||||
// resume at the same time.
|
||||
t.lastNegotiate = time.Now().Add(iceFailedTimeoutTotal)
|
||||
}
|
||||
t.lock.Unlock()
|
||||
}
|
||||
|
||||
func (t *PCTransport) parseTrackMid(offer webrtc.SessionDescription, senders map[string]*webrtc.RTPSender) error {
|
||||
parsed, err := offer.Unmarshal()
|
||||
func (t *PCTransport) parseTrackMid(sd webrtc.SessionDescription, senders map[string]*webrtc.RTPSender) error {
|
||||
parsed, err := sd.Unmarshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1924,9 +2039,8 @@ func (t *PCTransport) parseTrackMid(offer webrtc.SessionDescription, senders map
|
||||
if mid == "" {
|
||||
return ErrMidNotFound
|
||||
}
|
||||
t.previousTrackDescription[trackID] = &trackDescription{
|
||||
mid: mid,
|
||||
sender: senders[mid],
|
||||
if sender, ok := senders[mid]; ok {
|
||||
t.previousTrackDescription[trackID] = &trackDescription{mid, sender}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2164,6 +2278,40 @@ func (t *PCTransport) setupSignalStateCheckTimer() {
|
||||
})
|
||||
}
|
||||
|
||||
func (t *PCTransport) adjustNumOutstandingMedia(transceiver *webrtc.RTPTransceiver) {
|
||||
if transceiver.Mid() != "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
if transceiver.Kind() == webrtc.RTPCodecTypeAudio {
|
||||
t.numOutstandingAudios++
|
||||
} else {
|
||||
t.numOutstandingVideos++
|
||||
}
|
||||
t.lock.Unlock()
|
||||
}
|
||||
|
||||
func (t *PCTransport) sendUnmatchedMediaRequirement(force bool) error {
|
||||
// if there are unmatched media sections, notify remote peer to generate offer with
|
||||
// enough media section in subsequent offers
|
||||
t.lock.Lock()
|
||||
numAudios := t.numOutstandingAudios - t.numRequestSentAudios
|
||||
t.numRequestSentAudios += numAudios
|
||||
|
||||
numVideos := t.numOutstandingVideos - t.numRequestSentVideos
|
||||
t.numRequestSentVideos += numVideos
|
||||
t.lock.Unlock()
|
||||
|
||||
if force || (numAudios+numVideos) != 0 {
|
||||
if err := t.params.Handler.OnUnmatchedMedia(numAudios, numVideos); err != nil {
|
||||
return errors.Wrap(err, "could not send unmatched media requirements")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *PCTransport) createAndSendOffer(options *webrtc.OfferOptions) error {
|
||||
if t.pc.ConnectionState() == webrtc.PeerConnectionStateClosed {
|
||||
t.params.Logger.Warnw("trying to send offer on closed peer connection", nil)
|
||||
@@ -2262,12 +2410,16 @@ func (t *PCTransport) createAndSendOffer(options *webrtc.OfferOptions) error {
|
||||
prometheus.ServiceOperationCounter.WithLabelValues("offer", "error", "write_message").Add(1)
|
||||
return errors.Wrap(err, "could not send offer")
|
||||
}
|
||||
|
||||
prometheus.ServiceOperationCounter.WithLabelValues("offer", "success", "").Add(1)
|
||||
|
||||
return t.localDescriptionSent()
|
||||
}
|
||||
|
||||
func (t *PCTransport) handleSendOffer(_ event) error {
|
||||
if !t.params.IsOfferer {
|
||||
return t.sendUnmatchedMediaRequirement(true)
|
||||
}
|
||||
|
||||
return t.createAndSendOffer(nil)
|
||||
}
|
||||
|
||||
@@ -2343,6 +2495,12 @@ func (t *PCTransport) setRemoteDescription(sd webrtc.SessionDescription) error {
|
||||
}
|
||||
|
||||
func (t *PCTransport) createAndSendAnswer() error {
|
||||
numOutstandingAudios, numOutstandingVideos := t.getNumUnmatchedTransceivers()
|
||||
t.lock.Lock()
|
||||
t.numOutstandingAudios, t.numOutstandingVideos = numOutstandingAudios, numOutstandingVideos
|
||||
t.numRequestSentAudios, t.numRequestSentVideos = 0, 0
|
||||
t.lock.Unlock()
|
||||
|
||||
answer, err := t.pc.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, webrtc.ErrConnectionClosed) {
|
||||
@@ -2390,8 +2548,19 @@ func (t *PCTransport) createAndSendAnswer() error {
|
||||
return errors.Wrap(err, "could not send answer")
|
||||
}
|
||||
t.localAnswerId.Store(answerId)
|
||||
|
||||
prometheus.ServiceOperationCounter.WithLabelValues("answer", "success", "").Add(1)
|
||||
|
||||
if err := t.sendUnmatchedMediaRequirement(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
if !t.canReuseTransceiver {
|
||||
t.canReuseTransceiver = true
|
||||
t.previousTrackDescription = make(map[string]*trackDescription)
|
||||
}
|
||||
t.lock.Unlock()
|
||||
|
||||
return t.localDescriptionSent()
|
||||
}
|
||||
|
||||
@@ -2640,6 +2809,36 @@ func configureAudioTransceiver(tr *webrtc.RTPTransceiver, stereo bool, nack bool
|
||||
tr.SetCodecPreferences(configCodecs)
|
||||
}
|
||||
|
||||
// In single peer connection mode, set up enebled codecs,
|
||||
// the config provides config of direction, for publisher peer connection, it is publish enabled codecs
|
||||
// and for subscriber peer connection, it is subscribe enabled codecs.
|
||||
//
|
||||
// But, in single peer connection mode, if setting up a transceiver where the media is
|
||||
// flowing in the other direction, the other direction codec config needs to be set.
|
||||
func configureTransceiverCodecs(
|
||||
tr *webrtc.RTPTransceiver,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
filterOutH264HighProfile bool,
|
||||
) {
|
||||
if len(enabledCodecs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sender := tr.Sender()
|
||||
if sender == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filteredCodecs := filterCodecs(
|
||||
sender.GetParameters().Codecs,
|
||||
enabledCodecs,
|
||||
rtcpFeedbackConfig,
|
||||
filterOutH264HighProfile,
|
||||
)
|
||||
tr.SetCodecPreferences(filteredCodecs)
|
||||
}
|
||||
|
||||
func nonSimulcastRTXRepairsFromSDP(s *sdp.SessionDescription, logger logger.Logger) map[uint32]uint32 {
|
||||
rtxRepairFlows := map[uint32]uint32{}
|
||||
for _, media := range s.MediaDescriptions {
|
||||
|
||||
@@ -47,6 +47,7 @@ type Handler interface {
|
||||
OnNegotiationStateChanged(state NegotiationState)
|
||||
OnNegotiationFailed()
|
||||
OnStreamStateChange(update *streamallocator.StreamStateUpdate) error
|
||||
OnUnmatchedMedia(numAudios uint32, numVideos uint32) error
|
||||
}
|
||||
|
||||
type UnimplementedHandler struct{}
|
||||
@@ -72,3 +73,6 @@ func (h UnimplementedHandler) OnNegotiationFailed()
|
||||
func (h UnimplementedHandler) OnStreamStateChange(update *streamallocator.StreamStateUpdate) error {
|
||||
return nil
|
||||
}
|
||||
func (h UnimplementedHandler) OnUnmatchedMedia(numAudios uint32, numVideos uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -104,6 +104,18 @@ type FakeHandler struct {
|
||||
arg1 *webrtc.TrackRemote
|
||||
arg2 *webrtc.RTPReceiver
|
||||
}
|
||||
OnUnmatchedMediaStub func(uint32, uint32) error
|
||||
onUnmatchedMediaMutex sync.RWMutex
|
||||
onUnmatchedMediaArgsForCall []struct {
|
||||
arg1 uint32
|
||||
arg2 uint32
|
||||
}
|
||||
onUnmatchedMediaReturns struct {
|
||||
result1 error
|
||||
}
|
||||
onUnmatchedMediaReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
@@ -632,6 +644,68 @@ func (fake *FakeHandler) OnTrackArgsForCall(i int) (*webrtc.TrackRemote, *webrtc
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMedia(arg1 uint32, arg2 uint32) error {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
ret, specificReturn := fake.onUnmatchedMediaReturnsOnCall[len(fake.onUnmatchedMediaArgsForCall)]
|
||||
fake.onUnmatchedMediaArgsForCall = append(fake.onUnmatchedMediaArgsForCall, struct {
|
||||
arg1 uint32
|
||||
arg2 uint32
|
||||
}{arg1, arg2})
|
||||
stub := fake.OnUnmatchedMediaStub
|
||||
fakeReturns := fake.onUnmatchedMediaReturns
|
||||
fake.recordInvocation("OnUnmatchedMedia", []interface{}{arg1, arg2})
|
||||
fake.onUnmatchedMediaMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaCallCount() int {
|
||||
fake.onUnmatchedMediaMutex.RLock()
|
||||
defer fake.onUnmatchedMediaMutex.RUnlock()
|
||||
return len(fake.onUnmatchedMediaArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaCalls(stub func(uint32, uint32) error) {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
defer fake.onUnmatchedMediaMutex.Unlock()
|
||||
fake.OnUnmatchedMediaStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaArgsForCall(i int) (uint32, uint32) {
|
||||
fake.onUnmatchedMediaMutex.RLock()
|
||||
defer fake.onUnmatchedMediaMutex.RUnlock()
|
||||
argsForCall := fake.onUnmatchedMediaArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaReturns(result1 error) {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
defer fake.onUnmatchedMediaMutex.Unlock()
|
||||
fake.OnUnmatchedMediaStub = nil
|
||||
fake.onUnmatchedMediaReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaReturnsOnCall(i int, result1 error) {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
defer fake.onUnmatchedMediaMutex.Unlock()
|
||||
fake.OnUnmatchedMediaStub = nil
|
||||
if fake.onUnmatchedMediaReturnsOnCall == nil {
|
||||
fake.onUnmatchedMediaReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.onUnmatchedMediaReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
|
||||
+35
-12
@@ -112,9 +112,16 @@ func TestNegotiationTiming(t *testing.T) {
|
||||
require.False(t, transportB.IsEstablished())
|
||||
|
||||
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
|
||||
offer := atomic.Value{}
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, _offerId uint32) error {
|
||||
offer.Store(&sd)
|
||||
firstOffer := atomic.Value{}
|
||||
firstOfferId := atomic.Uint32{}
|
||||
secondOffer := atomic.Value{}
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32) error {
|
||||
if _, ok := firstOffer.Load().(*webrtc.SessionDescription); !ok {
|
||||
firstOffer.Store(&sd)
|
||||
firstOfferId.Store(offerId)
|
||||
} else {
|
||||
secondOffer.Store(&sd)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -157,15 +164,22 @@ func TestNegotiationTiming(t *testing.T) {
|
||||
return state == transport.NegotiationStateRetry
|
||||
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRetry")
|
||||
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
actualOffer, ok := offer.Load().(*webrtc.SessionDescription)
|
||||
require.True(t, ok)
|
||||
require.Eventually(t, func() bool {
|
||||
_, ok := firstOffer.Load().(*webrtc.SessionDescription)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if firstOfferId.Load() == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, 10*time.Second, 10*time.Millisecond, "first offer not received yet")
|
||||
|
||||
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32) error {
|
||||
transportA.HandleRemoteDescription(answer, answerId)
|
||||
return nil
|
||||
})
|
||||
transportB.HandleRemoteDescription(*actualOffer, 10)
|
||||
transportB.HandleRemoteDescription(*firstOffer.Load().(*webrtc.SessionDescription), firstOfferId.Load())
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return transportA.IsEstablished()
|
||||
@@ -174,11 +188,18 @@ func TestNegotiationTiming(t *testing.T) {
|
||||
return transportB.IsEstablished()
|
||||
}, 10*time.Second, time.Millisecond*10, "transportB is not established")
|
||||
|
||||
// it should still be negotiating again
|
||||
require.Equal(t, transport.NegotiationStateRemote, negotiationState.Load().(transport.NegotiationState))
|
||||
offer2, ok := offer.Load().(*webrtc.SessionDescription)
|
||||
// offerer should send another offer after processing the answer
|
||||
// as there were forced negotiations a couple of time above
|
||||
require.Eventually(t, func() bool {
|
||||
state, ok := negotiationState.Load().(transport.NegotiationState)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return state == transport.NegotiationStateRemote
|
||||
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRemote")
|
||||
_, ok := secondOffer.Load().(*webrtc.SessionDescription)
|
||||
require.True(t, ok)
|
||||
require.False(t, offer2 == actualOffer)
|
||||
|
||||
transportA.Close()
|
||||
transportB.Close()
|
||||
@@ -361,7 +382,9 @@ func TestNegotiationFailed(t *testing.T) {
|
||||
connectTransports(t, transportA, transportB, handlerA, handlerB, false, 1, 1)
|
||||
|
||||
// reset OnOffer to force a negotiation failure
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32) error { return nil })
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32) error {
|
||||
return nil
|
||||
})
|
||||
var failed atomic.Int32
|
||||
handlerA.OnNegotiationFailedCalls(func() {
|
||||
failed.Inc()
|
||||
|
||||
+168
-91
@@ -71,19 +71,9 @@ func (h TransportManagerTransportHandler) OnFailed(isShortLived bool, iceConnect
|
||||
|
||||
// -------------------------------
|
||||
|
||||
type TransportManagerPublisherTransportHandler struct {
|
||||
TransportManagerTransportHandler
|
||||
}
|
||||
|
||||
func (h TransportManagerPublisherTransportHandler) OnAnswer(sd webrtc.SessionDescription, answerId uint32) error {
|
||||
h.t.lastPublisherAnswer.Store(sd)
|
||||
return h.Handler.OnAnswer(sd, answerId)
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
|
||||
type TransportManagerParams struct {
|
||||
SubscriberAsPrimary bool
|
||||
UseSinglePeerConnection bool
|
||||
Config *WebRTCConfig
|
||||
Twcc *twcc.Responder
|
||||
ProtocolVersion types.ProtocolVersion
|
||||
@@ -124,8 +114,6 @@ type TransportManager struct {
|
||||
pendingOfferPublisher *webrtc.SessionDescription
|
||||
pendingOfferIdPublisher uint32
|
||||
pendingDataChannelsPublisher []*livekit.DataChannelInfo
|
||||
lastPublisherAnswer atomic.Value
|
||||
lastPublisherOffer atomic.Value
|
||||
iceConfig *livekit.ICEConfig
|
||||
|
||||
mediaLossProxy *MediaLossProxy
|
||||
@@ -159,8 +147,10 @@ func NewTransportManager(params TransportManagerParams) (*TransportManager, erro
|
||||
Logger: lgr,
|
||||
SimTracks: params.SimTracks,
|
||||
ClientInfo: params.ClientInfo,
|
||||
IsSendSide: params.UseOneShotSignallingMode || params.UseSinglePeerConnection,
|
||||
AllowPlayoutDelay: params.AllowPlayoutDelay,
|
||||
Transport: livekit.SignalTarget_PUBLISHER,
|
||||
Handler: TransportManagerPublisherTransportHandler{TransportManagerTransportHandler{params.PublisherHandler, t, lgr}},
|
||||
Handler: params.PublisherHandler,
|
||||
UseOneShotSignallingMode: params.UseOneShotSignallingMode,
|
||||
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
|
||||
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
|
||||
@@ -171,27 +161,31 @@ func NewTransportManager(params TransportManagerParams) (*TransportManager, erro
|
||||
}
|
||||
t.publisher = publisher
|
||||
|
||||
lgr = LoggerWithPCTarget(params.Logger, livekit.SignalTarget_SUBSCRIBER)
|
||||
subscriber, err := NewPCTransport(TransportParams{
|
||||
ProtocolVersion: params.ProtocolVersion,
|
||||
Config: params.Config,
|
||||
DirectionConfig: params.Config.Subscriber,
|
||||
CongestionControlConfig: params.CongestionControlConfig,
|
||||
EnabledCodecs: params.EnabledSubscribeCodecs,
|
||||
Logger: lgr,
|
||||
ClientInfo: params.ClientInfo,
|
||||
IsOfferer: true,
|
||||
IsSendSide: true,
|
||||
AllowPlayoutDelay: params.AllowPlayoutDelay,
|
||||
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
|
||||
Transport: livekit.SignalTarget_SUBSCRIBER,
|
||||
Handler: TransportManagerTransportHandler{params.SubscriberHandler, t, lgr},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !t.params.UseOneShotSignallingMode && !t.params.UseSinglePeerConnection {
|
||||
lgr := LoggerWithPCTarget(params.Logger, livekit.SignalTarget_SUBSCRIBER)
|
||||
subscriber, err := NewPCTransport(TransportParams{
|
||||
ProtocolVersion: params.ProtocolVersion,
|
||||
Config: params.Config,
|
||||
DirectionConfig: params.Config.Subscriber,
|
||||
CongestionControlConfig: params.CongestionControlConfig,
|
||||
EnabledCodecs: params.EnabledSubscribeCodecs,
|
||||
Logger: lgr,
|
||||
ClientInfo: params.ClientInfo,
|
||||
IsOfferer: true,
|
||||
IsSendSide: true,
|
||||
AllowPlayoutDelay: params.AllowPlayoutDelay,
|
||||
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
|
||||
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
|
||||
Transport: livekit.SignalTarget_SUBSCRIBER,
|
||||
Handler: TransportManagerTransportHandler{params.SubscriberHandler, t, lgr},
|
||||
FireOnTrackBySdp: params.FireOnTrackBySdp,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.subscriber = subscriber
|
||||
}
|
||||
t.subscriber = subscriber
|
||||
if !t.params.Migration {
|
||||
if !t.params.Migration && t.params.SubscriberAsPrimary {
|
||||
if err := t.createDataChannelsForSubscriber(nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -202,8 +196,12 @@ func NewTransportManager(params TransportManagerParams) (*TransportManager, erro
|
||||
}
|
||||
|
||||
func (t *TransportManager) Close() {
|
||||
t.publisher.Close()
|
||||
t.subscriber.Close()
|
||||
if t.publisher != nil {
|
||||
t.publisher.Close()
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) SubscriberClose() {
|
||||
@@ -235,37 +233,49 @@ func (t *TransportManager) WritePublisherRTCP(pkts []rtcp.Packet) error {
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetSubscriberRTT() (float64, bool) {
|
||||
return t.subscriber.GetRTT()
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.GetRTT()
|
||||
} else {
|
||||
return t.subscriber.GetRTT()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) HasSubscriberEverConnected() bool {
|
||||
return t.subscriber.HasEverConnected()
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.HasEverConnected()
|
||||
} else {
|
||||
return t.subscriber.HasEverConnected()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) AddTrackLocal(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
return t.publisher.AddTrack(trackLocal, params)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.AddTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
} else {
|
||||
return t.subscriber.AddTrack(trackLocal, params)
|
||||
return t.subscriber.AddTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) AddTransceiverFromTrackLocal(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
return t.publisher.AddTransceiverFromTrack(trackLocal, params)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
} else {
|
||||
return t.subscriber.AddTransceiverFromTrack(trackLocal, params)
|
||||
return t.subscriber.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) RemoveTrackLocal(sender *webrtc.RTPSender) error {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.RemoveTrack(sender)
|
||||
} else {
|
||||
return t.subscriber.RemoveTrack(sender)
|
||||
@@ -273,7 +283,7 @@ func (t *TransportManager) RemoveTrackLocal(sender *webrtc.RTPSender) error {
|
||||
}
|
||||
|
||||
func (t *TransportManager) WriteSubscriberRTCP(pkts []rtcp.Packet) error {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.WriteRTCP(pkts)
|
||||
} else {
|
||||
return t.subscriber.WriteRTCP(pkts)
|
||||
@@ -281,15 +291,27 @@ func (t *TransportManager) WriteSubscriberRTCP(pkts []rtcp.Packet) error {
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetSubscriberPacer() pacer.Pacer {
|
||||
return t.subscriber.GetPacer()
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.GetPacer()
|
||||
} else {
|
||||
return t.subscriber.GetPacer()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) AddSubscribedTrack(subTrack types.SubscribedTrack) {
|
||||
t.subscriber.AddTrackToStreamAllocator(subTrack)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.AddTrackToStreamAllocator(subTrack)
|
||||
} else {
|
||||
t.subscriber.AddTrackToStreamAllocator(subTrack)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) RemoveSubscribedTrack(subTrack types.SubscribedTrack) {
|
||||
t.subscriber.RemoveTrackFromStreamAllocator(subTrack)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.RemoveTrackFromStreamAllocator(subTrack)
|
||||
} else {
|
||||
t.subscriber.RemoveTrackFromStreamAllocator(subTrack)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) SendDataMessage(kind livekit.DataPacket_Kind, data []byte) error {
|
||||
@@ -393,12 +415,9 @@ func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels [
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetUnmatchMediaForOffer(parsedOffer *sdp.SessionDescription, mediaType string) (unmatched []*sdp.MediaDescription, err error) {
|
||||
// prefer codec from offer for clients that don't support setCodecPreferences
|
||||
var lastMatchedMid string
|
||||
lastAnswer := t.lastPublisherAnswer.Load()
|
||||
if lastAnswer != nil {
|
||||
answer := lastAnswer.(webrtc.SessionDescription)
|
||||
parsedAnswer, err1 := answer.Unmarshal()
|
||||
if lastAnswer := t.publisher.CurrentLocalDescription(); lastAnswer != nil {
|
||||
parsedAnswer, err1 := lastAnswer.Unmarshal()
|
||||
if err1 != nil {
|
||||
// should not happen
|
||||
t.params.Logger.Errorw("failed to parse last answer", err1)
|
||||
@@ -428,11 +447,8 @@ func (t *TransportManager) GetUnmatchMediaForOffer(parsedOffer *sdp.SessionDescr
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TransportManager) LastPublisherOffer() webrtc.SessionDescription {
|
||||
if sd := t.lastPublisherOffer.Load(); sd != nil {
|
||||
return sd.(webrtc.SessionDescription)
|
||||
}
|
||||
return webrtc.SessionDescription{}
|
||||
func (t *TransportManager) LastPublisherOffer() *webrtc.SessionDescription {
|
||||
return t.publisher.CurrentRemoteDescription()
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleOffer(offer webrtc.SessionDescription, offerId uint32, shouldPend bool) error {
|
||||
@@ -444,17 +460,12 @@ func (t *TransportManager) HandleOffer(offer webrtc.SessionDescription, offerId
|
||||
return nil
|
||||
}
|
||||
t.lock.Unlock()
|
||||
t.lastPublisherOffer.Store(offer)
|
||||
|
||||
return t.publisher.HandleRemoteDescription(offer, offerId)
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetAnswer() (webrtc.SessionDescription, uint32, error) {
|
||||
answer, answerId, err := t.publisher.GetAnswer()
|
||||
if err == nil {
|
||||
t.lastPublisherAnswer.Store(answer)
|
||||
}
|
||||
return answer, answerId, err
|
||||
return t.publisher.GetAnswer()
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetPublisherICESessionUfrag() (string, error) {
|
||||
@@ -501,7 +512,11 @@ func (t *TransportManager) AddICECandidate(candidate webrtc.ICECandidateInit, ta
|
||||
}
|
||||
|
||||
func (t *TransportManager) NegotiateSubscriber(force bool) {
|
||||
t.subscriber.Negotiate(force)
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.Negotiate(force)
|
||||
} else {
|
||||
t.publisher.Negotiate(force)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason) {
|
||||
@@ -512,12 +527,16 @@ func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason)
|
||||
)
|
||||
switch reason {
|
||||
case livekit.ReconnectReason_RR_PUBLISHER_FAILED:
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.publisher.IsShortConnection(time.Now())
|
||||
if t.publisher != nil {
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.publisher.IsShortConnection(time.Now())
|
||||
}
|
||||
|
||||
case livekit.ReconnectReason_RR_SUBSCRIBER_FAILED:
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.subscriber.IsShortConnection(time.Now())
|
||||
if t.subscriber != nil {
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.subscriber.IsShortConnection(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
if isShort {
|
||||
@@ -529,15 +548,23 @@ func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason)
|
||||
}
|
||||
|
||||
if resetShortConnection {
|
||||
t.publisher.ResetShortConnOnICERestart()
|
||||
t.subscriber.ResetShortConnOnICERestart()
|
||||
if t.publisher != nil {
|
||||
t.publisher.ResetShortConnOnICERestart()
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.ResetShortConnOnICERestart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) ICERestart(iceConfig *livekit.ICEConfig) error {
|
||||
t.SetICEConfig(iceConfig)
|
||||
|
||||
return t.subscriber.ICERestart()
|
||||
if t.subscriber != nil {
|
||||
return t.subscriber.ICERestart()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TransportManager) OnICEConfigChanged(f func(iceConfig *livekit.ICEConfig)) {
|
||||
@@ -589,8 +616,12 @@ func (t *TransportManager) configureICE(iceConfig *livekit.ICEConfig, reset bool
|
||||
t.mediaLossProxy.OnMediaLossUpdate(nil)
|
||||
}
|
||||
|
||||
t.publisher.SetPreferTCP(iceConfig.PreferencePublisher == livekit.ICECandidateType_ICT_TCP)
|
||||
t.subscriber.SetPreferTCP(iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TCP)
|
||||
if t.publisher != nil {
|
||||
t.publisher.SetPreferTCP(iceConfig.PreferencePublisher == livekit.ICECandidateType_ICT_TCP)
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.SetPreferTCP(iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TCP)
|
||||
}
|
||||
|
||||
if onICEConfigChanged != nil {
|
||||
onICEConfigChanged(iceConfig)
|
||||
@@ -604,6 +635,10 @@ func (t *TransportManager) SubscriberAsPrimary() bool {
|
||||
func (t *TransportManager) GetICEConnectionInfo() []*types.ICEConnectionInfo {
|
||||
infos := make([]*types.ICEConnectionInfo, 0, 2)
|
||||
for _, pc := range []*PCTransport{t.publisher, t.subscriber} {
|
||||
if pc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := pc.GetICEConnectionInfo()
|
||||
if info.HasCandidates() {
|
||||
infos = append(infos, info)
|
||||
@@ -613,20 +648,38 @@ func (t *TransportManager) GetICEConnectionInfo() []*types.ICEConnectionInfo {
|
||||
}
|
||||
|
||||
func (t *TransportManager) getTransport(isPrimary bool) *PCTransport {
|
||||
pcTransport := t.publisher
|
||||
if (isPrimary && t.params.SubscriberAsPrimary) || (!isPrimary && !t.params.SubscriberAsPrimary) {
|
||||
pcTransport = t.subscriber
|
||||
}
|
||||
switch {
|
||||
case t.publisher == nil:
|
||||
return t.subscriber
|
||||
|
||||
return pcTransport
|
||||
case t.subscriber == nil:
|
||||
return t.publisher
|
||||
|
||||
default:
|
||||
pcTransport := t.publisher
|
||||
if (isPrimary && t.params.SubscriberAsPrimary) || (!isPrimary && !t.params.SubscriberAsPrimary) {
|
||||
pcTransport = t.subscriber
|
||||
}
|
||||
|
||||
return pcTransport
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) getLowestPriorityConnectionType() types.ICEConnectionType {
|
||||
ctype := t.publisher.GetICEConnectionType()
|
||||
if stype := t.subscriber.GetICEConnectionType(); stype > ctype {
|
||||
ctype = stype
|
||||
switch {
|
||||
case t.publisher == nil:
|
||||
return t.subscriber.GetICEConnectionType()
|
||||
|
||||
case t.subscriber == nil:
|
||||
return t.publisher.GetICEConnectionType()
|
||||
|
||||
default:
|
||||
ctype := t.publisher.GetICEConnectionType()
|
||||
if stype := t.subscriber.GetICEConnectionType(); stype > ctype {
|
||||
ctype = stype
|
||||
}
|
||||
return ctype
|
||||
}
|
||||
return ctype
|
||||
}
|
||||
|
||||
func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
|
||||
@@ -739,7 +792,11 @@ func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
|
||||
}, false)
|
||||
}
|
||||
|
||||
func (t *TransportManager) SetMigrateInfo(previousOffer, previousAnswer *webrtc.SessionDescription, dataChannels []*livekit.DataChannelInfo) {
|
||||
func (t *TransportManager) SetMigrateInfo(
|
||||
previousOffer *webrtc.SessionDescription,
|
||||
previousAnswer *webrtc.SessionDescription,
|
||||
dataChannels []*livekit.DataChannelInfo,
|
||||
) {
|
||||
t.lock.Lock()
|
||||
t.pendingDataChannelsPublisher = make([]*livekit.DataChannelInfo, 0, len(dataChannels))
|
||||
pendingDataChannelsSubscriber := make([]*livekit.DataChannelInfo, 0, len(dataChannels))
|
||||
@@ -758,7 +815,11 @@ func (t *TransportManager) SetMigrateInfo(previousOffer, previousAnswer *webrtc.
|
||||
}
|
||||
}
|
||||
|
||||
t.subscriber.SetPreviousSdp(previousOffer, previousAnswer)
|
||||
if t.params.UseSinglePeerConnection {
|
||||
t.publisher.SetPreviousSdp(previousAnswer, previousOffer)
|
||||
} else {
|
||||
t.subscriber.SetPreviousSdp(previousOffer, previousAnswer)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) ProcessPendingPublisherDataChannels() {
|
||||
@@ -823,7 +884,11 @@ func (t *TransportManager) onMediaLossUpdate(loss uint8) {
|
||||
t.lock.Unlock()
|
||||
|
||||
t.params.Logger.Infow("udp connection unstable, switch to tcp", "signalingRTT", t.signalingRTT)
|
||||
t.params.SubscriberHandler.OnFailed(true, t.subscriber.GetICEConnectionInfo())
|
||||
if t.params.UseSinglePeerConnection {
|
||||
t.params.PublisherHandler.OnFailed(true, t.publisher.GetICEConnectionInfo())
|
||||
} else {
|
||||
t.params.SubscriberHandler.OnFailed(true, t.subscriber.GetICEConnectionInfo())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -835,8 +900,12 @@ func (t *TransportManager) UpdateSignalingRTT(rtt uint32) {
|
||||
t.lock.Lock()
|
||||
t.signalingRTT = rtt
|
||||
t.lock.Unlock()
|
||||
t.publisher.SetSignalingRTT(rtt)
|
||||
t.subscriber.SetSignalingRTT(rtt)
|
||||
if t.publisher != nil {
|
||||
t.publisher.SetSignalingRTT(rtt)
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.SetSignalingRTT(rtt)
|
||||
}
|
||||
|
||||
// TODO: considering using tcp rtt to calculate ice connection cost, if ice connection can't be established
|
||||
// within 5 * tcp rtt(at least 5s), means udp traffic might be block/dropped, switch to tcp.
|
||||
@@ -881,11 +950,19 @@ func (t *TransportManager) SetSignalSourceValid(valid bool) {
|
||||
}
|
||||
|
||||
func (t *TransportManager) SetSubscriberAllowPause(allowPause bool) {
|
||||
t.subscriber.SetAllowPauseOfStreamAllocator(allowPause)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.SetAllowPauseOfStreamAllocator(allowPause)
|
||||
} else {
|
||||
t.subscriber.SetAllowPauseOfStreamAllocator(allowPause)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) SetSubscriberChannelCapacity(channelCapacity int64) {
|
||||
t.subscriber.SetChannelCapacityOfStreamAllocator(channelCapacity)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.SetChannelCapacityOfStreamAllocator(channelCapacity)
|
||||
} else {
|
||||
t.subscriber.SetChannelCapacityOfStreamAllocator(channelCapacity)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) hasRecentSignalLocked() bool {
|
||||
|
||||
@@ -360,6 +360,7 @@ type LocalParticipant interface {
|
||||
ProtocolVersion() ProtocolVersion
|
||||
SupportsSyncStreamID() bool
|
||||
SupportsTransceiverReuse() bool
|
||||
IsUsingSinglePeerConnection() bool
|
||||
IsClosed() bool
|
||||
IsReady() bool
|
||||
IsDisconnected() bool
|
||||
@@ -490,7 +491,8 @@ type LocalParticipant interface {
|
||||
SetMigrateState(s MigrateState)
|
||||
MigrateState() MigrateState
|
||||
SetMigrateInfo(
|
||||
previousOffer, previousAnswer *webrtc.SessionDescription,
|
||||
previousOffer *webrtc.SessionDescription,
|
||||
previousAnswer *webrtc.SessionDescription,
|
||||
mediaTracks []*livekit.TrackPublishedResponse,
|
||||
dataChannels []*livekit.DataChannelInfo,
|
||||
dataChannelReceiveState []*livekit.DataChannelReceiveState,
|
||||
|
||||
@@ -799,6 +799,16 @@ type FakeLocalParticipant struct {
|
||||
isTrackNameSubscribedReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
IsUsingSinglePeerConnectionStub func() bool
|
||||
isUsingSinglePeerConnectionMutex sync.RWMutex
|
||||
isUsingSinglePeerConnectionArgsForCall []struct {
|
||||
}
|
||||
isUsingSinglePeerConnectionReturns struct {
|
||||
result1 bool
|
||||
}
|
||||
isUsingSinglePeerConnectionReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
IssueFullReconnectStub func(types.ParticipantCloseReason)
|
||||
issueFullReconnectMutex sync.RWMutex
|
||||
issueFullReconnectArgsForCall []struct {
|
||||
@@ -5521,6 +5531,59 @@ func (fake *FakeLocalParticipant) IsTrackNameSubscribedReturnsOnCall(i int, resu
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) IsUsingSinglePeerConnection() bool {
|
||||
fake.isUsingSinglePeerConnectionMutex.Lock()
|
||||
ret, specificReturn := fake.isUsingSinglePeerConnectionReturnsOnCall[len(fake.isUsingSinglePeerConnectionArgsForCall)]
|
||||
fake.isUsingSinglePeerConnectionArgsForCall = append(fake.isUsingSinglePeerConnectionArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.IsUsingSinglePeerConnectionStub
|
||||
fakeReturns := fake.isUsingSinglePeerConnectionReturns
|
||||
fake.recordInvocation("IsUsingSinglePeerConnection", []interface{}{})
|
||||
fake.isUsingSinglePeerConnectionMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) IsUsingSinglePeerConnectionCallCount() int {
|
||||
fake.isUsingSinglePeerConnectionMutex.RLock()
|
||||
defer fake.isUsingSinglePeerConnectionMutex.RUnlock()
|
||||
return len(fake.isUsingSinglePeerConnectionArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) IsUsingSinglePeerConnectionCalls(stub func() bool) {
|
||||
fake.isUsingSinglePeerConnectionMutex.Lock()
|
||||
defer fake.isUsingSinglePeerConnectionMutex.Unlock()
|
||||
fake.IsUsingSinglePeerConnectionStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) IsUsingSinglePeerConnectionReturns(result1 bool) {
|
||||
fake.isUsingSinglePeerConnectionMutex.Lock()
|
||||
defer fake.isUsingSinglePeerConnectionMutex.Unlock()
|
||||
fake.IsUsingSinglePeerConnectionStub = nil
|
||||
fake.isUsingSinglePeerConnectionReturns = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) IsUsingSinglePeerConnectionReturnsOnCall(i int, result1 bool) {
|
||||
fake.isUsingSinglePeerConnectionMutex.Lock()
|
||||
defer fake.isUsingSinglePeerConnectionMutex.Unlock()
|
||||
fake.IsUsingSinglePeerConnectionStub = nil
|
||||
if fake.isUsingSinglePeerConnectionReturnsOnCall == nil {
|
||||
fake.isUsingSinglePeerConnectionReturnsOnCall = make(map[int]struct {
|
||||
result1 bool
|
||||
})
|
||||
}
|
||||
fake.isUsingSinglePeerConnectionReturnsOnCall[i] = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) IssueFullReconnect(arg1 types.ParticipantCloseReason) {
|
||||
fake.issueFullReconnectMutex.Lock()
|
||||
fake.issueFullReconnectArgsForCall = append(fake.issueFullReconnectArgsForCall, struct {
|
||||
|
||||
@@ -63,16 +63,10 @@ func NewWrappedReceiver(params WrappedReceiverParams) *WrappedReceiver {
|
||||
normalizedMimeType := mime.NormalizeMimeType(codecs[0].MimeType)
|
||||
if normalizedMimeType == mime.MimeTypeRED {
|
||||
// if upstream is opus/red, then add opus to match clients that don't support red
|
||||
codecs = append(codecs, webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: OpusCodecCapability,
|
||||
PayloadType: 111,
|
||||
})
|
||||
codecs = append(codecs, OpusCodecParameters)
|
||||
} else if !params.DisableRed && normalizedMimeType == mime.MimeTypeOpus {
|
||||
// if upstream is opus only and red enabled, add red to match clients that support red
|
||||
codecs = append(codecs, webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: RedCodecCapability,
|
||||
PayloadType: 63,
|
||||
})
|
||||
codecs = append(codecs, RedCodecParameters)
|
||||
// prefer red codec
|
||||
codecs[0], codecs[1] = codecs[1], codecs[0]
|
||||
}
|
||||
|
||||
@@ -416,30 +416,37 @@ func (r *RoomManager) StartSession(
|
||||
if pi.DisableICELite {
|
||||
rtcConf.SettingEngine.SetLite(false)
|
||||
}
|
||||
rtcConf.UpdatePublisherConfig(pi.UseSinglePeerConnection)
|
||||
|
||||
// default allow forceTCP
|
||||
allowFallback := true
|
||||
if r.config.RTC.AllowTCPFallback != nil {
|
||||
allowFallback = *r.config.RTC.AllowTCPFallback
|
||||
}
|
||||
|
||||
// default do not force full reconnect on a publication error
|
||||
reconnectOnPublicationError := false
|
||||
if r.config.RTC.ReconnectOnPublicationError != nil {
|
||||
reconnectOnPublicationError = *r.config.RTC.ReconnectOnPublicationError
|
||||
}
|
||||
|
||||
// default do not force full reconnect on a subscription error
|
||||
reconnectOnSubscriptionError := false
|
||||
if r.config.RTC.ReconnectOnSubscriptionError != nil {
|
||||
reconnectOnSubscriptionError = *r.config.RTC.ReconnectOnSubscriptionError
|
||||
}
|
||||
|
||||
// default do not force full reconnect on a data channel error
|
||||
reconnectOnDataChannelError := false
|
||||
if r.config.RTC.ReconnectOnDataChannelError != nil {
|
||||
reconnectOnDataChannelError = *r.config.RTC.ReconnectOnDataChannelError
|
||||
}
|
||||
|
||||
subscriberAllowPause := r.config.RTC.CongestionControl.AllowPause
|
||||
if pi.SubscriberAllowPause != nil {
|
||||
subscriberAllowPause = *pi.SubscriberAllowPause
|
||||
}
|
||||
|
||||
participant, err = rtc.NewParticipant(rtc.ParticipantParams{
|
||||
Identity: pi.Identity,
|
||||
Name: pi.Name,
|
||||
@@ -486,6 +493,7 @@ func (r *RoomManager) StartSession(
|
||||
DataChannelMaxBufferedAmount: r.config.RTC.DataChannelMaxBufferedAmount,
|
||||
DatachannelSlowThreshold: r.config.RTC.DatachannelSlowThreshold,
|
||||
FireOnTrackBySdp: true,
|
||||
UseSinglePeerConnection: pi.UseSinglePeerConnection,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -118,6 +118,7 @@ func decodeAttributes(str string) (map[string]string, error) {
|
||||
|
||||
func (s *RTCService) validateInternal(lgr logger.Logger, r *http.Request, strict bool) (livekit.RoomName, routing.ParticipantInit, int, error) {
|
||||
var params ValidateConnectRequestParams
|
||||
useSinglePeerConnection := false
|
||||
joinRequest := &livekit.JoinRequest{}
|
||||
|
||||
wrappedJoinRequestBase64 := r.FormValue("join_request")
|
||||
@@ -137,6 +138,7 @@ func (s *RTCService) validateInternal(lgr logger.Logger, r *http.Request, strict
|
||||
params.attributes = attrs
|
||||
}
|
||||
} else {
|
||||
useSinglePeerConnection = true
|
||||
if wrappedProtoBytes, err := base64.URLEncoding.DecodeString(wrappedJoinRequestBase64); err != nil {
|
||||
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot base64 decode wrapped join request")
|
||||
} else {
|
||||
@@ -186,11 +188,12 @@ func (s *RTCService) validateInternal(lgr logger.Logger, r *http.Request, strict
|
||||
}
|
||||
|
||||
pi := routing.ParticipantInit{
|
||||
Identity: livekit.ParticipantIdentity(res.grants.Identity),
|
||||
Name: livekit.ParticipantName(res.grants.Name),
|
||||
Grants: res.grants,
|
||||
Region: res.region,
|
||||
CreateRoom: res.createRoomRequest,
|
||||
Identity: livekit.ParticipantIdentity(res.grants.Identity),
|
||||
Name: livekit.ParticipantName(res.grants.Name),
|
||||
Grants: res.grants,
|
||||
Region: res.region,
|
||||
CreateRoom: res.createRoomRequest,
|
||||
UseSinglePeerConnection: useSinglePeerConnection,
|
||||
}
|
||||
|
||||
if wrappedJoinRequestBase64 == "" {
|
||||
|
||||
+161
-151
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/testutils"
|
||||
testclient "github.com/livekit/livekit-server/test/client"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
@@ -33,194 +32,205 @@ var (
|
||||
)
|
||||
|
||||
func TestAgents(t *testing.T) {
|
||||
_, finish := setupSingleNodeTest("TestAgents")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, finish := setupSingleNodeTest("TestAgents")
|
||||
defer finish()
|
||||
|
||||
ac1, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac2, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac3, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac4, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac5, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac6, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
defer ac1.close()
|
||||
defer ac2.close()
|
||||
defer ac3.close()
|
||||
defer ac4.close()
|
||||
defer ac5.close()
|
||||
defer ac6.close()
|
||||
ac1.Run(livekit.JobType_JT_ROOM, "default")
|
||||
ac2.Run(livekit.JobType_JT_ROOM, "default")
|
||||
ac3.Run(livekit.JobType_JT_PUBLISHER, "default")
|
||||
ac4.Run(livekit.JobType_JT_PUBLISHER, "default")
|
||||
ac5.Run(livekit.JobType_JT_PARTICIPANT, "default")
|
||||
ac6.Run(livekit.JobType_JT_PARTICIPANT, "default")
|
||||
ac1, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac2, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac3, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac4, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac5, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac6, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
defer ac1.close()
|
||||
defer ac2.close()
|
||||
defer ac3.close()
|
||||
defer ac4.close()
|
||||
defer ac5.close()
|
||||
defer ac6.close()
|
||||
ac1.Run(livekit.JobType_JT_ROOM, "default")
|
||||
ac2.Run(livekit.JobType_JT_ROOM, "default")
|
||||
ac3.Run(livekit.JobType_JT_PUBLISHER, "default")
|
||||
ac4.Run(livekit.JobType_JT_PUBLISHER, "default")
|
||||
ac5.Run(livekit.JobType_JT_PARTICIPANT, "default")
|
||||
ac6.Run(livekit.JobType_JT_PARTICIPANT, "default")
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.registered.Load() != 1 || ac2.registered.Load() != 1 || ac3.registered.Load() != 1 || ac4.registered.Load() != 1 || ac5.registered.Load() != 1 || ac6.registered.Load() != 1 {
|
||||
return "worker not registered"
|
||||
}
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.registered.Load() != 1 || ac2.registered.Load() != 1 || ac3.registered.Load() != 1 || ac4.registered.Load() != 1 || ac5.registered.Load() != 1 || ac6.registered.Load() != 1 {
|
||||
return "worker not registered"
|
||||
}
|
||||
|
||||
return ""
|
||||
}, RegisterTimeout)
|
||||
return ""
|
||||
}, RegisterTimeout)
|
||||
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("c2", defaultServerPort, &testclient.Options{UseJoinRequestQueryParam: true})
|
||||
waitUntilConnected(t, c1, c2)
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("c2", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
|
||||
// publish 2 tracks
|
||||
t1, err := c1.AddStaticTrack("audio/opus", "audio", "micro")
|
||||
require.NoError(t, err)
|
||||
defer t1.Stop()
|
||||
t2, err := c1.AddStaticTrack("video/vp8", "video", "webcam")
|
||||
require.NoError(t, err)
|
||||
defer t2.Stop()
|
||||
// publish 2 tracks
|
||||
t1, err := c1.AddStaticTrack("audio/opus", "audio", "micro")
|
||||
require.NoError(t, err)
|
||||
defer t1.Stop()
|
||||
t2, err := c1.AddStaticTrack("video/vp8", "video", "webcam")
|
||||
require.NoError(t, err)
|
||||
defer t2.Stop()
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load()+ac2.roomJobs.Load() != 1 {
|
||||
return "room job not assigned"
|
||||
}
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load()+ac2.roomJobs.Load() != 1 {
|
||||
return "room job not assigned"
|
||||
}
|
||||
|
||||
if ac3.publisherJobs.Load()+ac4.publisherJobs.Load() != 1 {
|
||||
return fmt.Sprintf("publisher jobs not assigned, ac3: %d, ac4: %d", ac3.publisherJobs.Load(), ac4.publisherJobs.Load())
|
||||
}
|
||||
if ac3.publisherJobs.Load()+ac4.publisherJobs.Load() != 1 {
|
||||
return fmt.Sprintf("publisher jobs not assigned, ac3: %d, ac4: %d", ac3.publisherJobs.Load(), ac4.publisherJobs.Load())
|
||||
}
|
||||
|
||||
if ac5.participantJobs.Load()+ac6.participantJobs.Load() != 2 {
|
||||
return fmt.Sprintf("participant jobs not assigned, ac5: %d, ac6: %d", ac5.participantJobs.Load(), ac6.participantJobs.Load())
|
||||
}
|
||||
if ac5.participantJobs.Load()+ac6.participantJobs.Load() != 2 {
|
||||
return fmt.Sprintf("participant jobs not assigned, ac5: %d, ac6: %d", ac5.participantJobs.Load(), ac6.participantJobs.Load())
|
||||
}
|
||||
|
||||
return ""
|
||||
}, 6*time.Second)
|
||||
return ""
|
||||
}, 6*time.Second)
|
||||
|
||||
// publish 2 tracks
|
||||
t3, err := c2.AddStaticTrack("audio/opus", "audio", "micro")
|
||||
require.NoError(t, err)
|
||||
defer t3.Stop()
|
||||
t4, err := c2.AddStaticTrack("video/vp8", "video", "webcam")
|
||||
require.NoError(t, err)
|
||||
defer t4.Stop()
|
||||
// publish 2 tracks
|
||||
t3, err := c2.AddStaticTrack("audio/opus", "audio", "micro")
|
||||
require.NoError(t, err)
|
||||
defer t3.Stop()
|
||||
t4, err := c2.AddStaticTrack("video/vp8", "video", "webcam")
|
||||
require.NoError(t, err)
|
||||
defer t4.Stop()
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load()+ac2.roomJobs.Load() != 1 {
|
||||
return "room job must be assigned 1 time"
|
||||
}
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load()+ac2.roomJobs.Load() != 1 {
|
||||
return "room job must be assigned 1 time"
|
||||
}
|
||||
|
||||
if ac3.publisherJobs.Load()+ac4.publisherJobs.Load() != 2 {
|
||||
return "2 publisher jobs must assigned"
|
||||
}
|
||||
if ac3.publisherJobs.Load()+ac4.publisherJobs.Load() != 2 {
|
||||
return "2 publisher jobs must assigned"
|
||||
}
|
||||
|
||||
if ac5.participantJobs.Load()+ac6.participantJobs.Load() != 2 {
|
||||
return "2 participant jobs must assigned"
|
||||
}
|
||||
if ac5.participantJobs.Load()+ac6.participantJobs.Load() != 2 {
|
||||
return "2 participant jobs must assigned"
|
||||
}
|
||||
|
||||
return ""
|
||||
}, AssignJobTimeout)
|
||||
return ""
|
||||
}, AssignJobTimeout)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentNamespaces(t *testing.T) {
|
||||
_, finish := setupSingleNodeTest("TestAgentNamespaces")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, finish := setupSingleNodeTest("TestAgentNamespaces")
|
||||
defer finish()
|
||||
|
||||
ac1, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac2, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
defer ac1.close()
|
||||
defer ac2.close()
|
||||
ac1.Run(livekit.JobType_JT_ROOM, "namespace1")
|
||||
ac2.Run(livekit.JobType_JT_ROOM, "namespace2")
|
||||
ac1, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac2, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
defer ac1.close()
|
||||
defer ac2.close()
|
||||
ac1.Run(livekit.JobType_JT_ROOM, "namespace1")
|
||||
ac2.Run(livekit.JobType_JT_ROOM, "namespace2")
|
||||
|
||||
_, err = roomClient.CreateRoom(contextWithToken(createRoomToken()), &livekit.CreateRoomRequest{
|
||||
Name: testRoom,
|
||||
Agents: []*livekit.RoomAgentDispatch{
|
||||
{},
|
||||
{
|
||||
AgentName: "ag",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = roomClient.CreateRoom(contextWithToken(createRoomToken()), &livekit.CreateRoomRequest{
|
||||
Name: testRoom,
|
||||
Agents: []*livekit.RoomAgentDispatch{
|
||||
{},
|
||||
{
|
||||
AgentName: "ag",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.registered.Load() != 1 || ac2.registered.Load() != 1 {
|
||||
return "worker not registered"
|
||||
}
|
||||
return ""
|
||||
}, RegisterTimeout)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.registered.Load() != 1 || ac2.registered.Load() != 1 {
|
||||
return "worker not registered"
|
||||
}
|
||||
return ""
|
||||
}, RegisterTimeout)
|
||||
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load() != 1 || ac2.roomJobs.Load() != 1 {
|
||||
return "room job not assigned"
|
||||
}
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load() != 1 || ac2.roomJobs.Load() != 1 {
|
||||
return "room job not assigned"
|
||||
}
|
||||
|
||||
job1 := <-ac1.requestedJobs
|
||||
job2 := <-ac2.requestedJobs
|
||||
job1 := <-ac1.requestedJobs
|
||||
job2 := <-ac2.requestedJobs
|
||||
|
||||
if job1.Namespace != "namespace1" {
|
||||
return "namespace is not 'namespace'"
|
||||
}
|
||||
if job1.Namespace != "namespace1" {
|
||||
return "namespace is not 'namespace'"
|
||||
}
|
||||
|
||||
if job2.Namespace != "namespace2" {
|
||||
return "namespace is not 'namespace2'"
|
||||
}
|
||||
if job2.Namespace != "namespace2" {
|
||||
return "namespace is not 'namespace2'"
|
||||
}
|
||||
|
||||
if job1.Id == job2.Id {
|
||||
return "job ids are the same"
|
||||
}
|
||||
|
||||
return ""
|
||||
}, AssignJobTimeout)
|
||||
if job1.Id == job2.Id {
|
||||
return "job ids are the same"
|
||||
}
|
||||
|
||||
return ""
|
||||
}, AssignJobTimeout)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentMultiNode(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestAgentMultiNode")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestAgentMultiNode")
|
||||
defer finish()
|
||||
|
||||
ac1, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac2, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
defer ac1.close()
|
||||
defer ac2.close()
|
||||
ac1.Run(livekit.JobType_JT_ROOM, "default")
|
||||
ac2.Run(livekit.JobType_JT_PUBLISHER, "default")
|
||||
ac1, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
ac2, err := newAgentClient(agentToken(), defaultServerPort)
|
||||
require.NoError(t, err)
|
||||
defer ac1.close()
|
||||
defer ac2.close()
|
||||
ac1.Run(livekit.JobType_JT_ROOM, "default")
|
||||
ac2.Run(livekit.JobType_JT_PUBLISHER, "default")
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.registered.Load() != 1 || ac2.registered.Load() != 1 {
|
||||
return "worker not registered"
|
||||
}
|
||||
return ""
|
||||
}, RegisterTimeout)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.registered.Load() != 1 || ac2.registered.Load() != 1 {
|
||||
return "worker not registered"
|
||||
}
|
||||
return ""
|
||||
}, RegisterTimeout)
|
||||
|
||||
c1 := createRTCClient("c1", secondServerPort, nil) // Create a room on the second node
|
||||
waitUntilConnected(t, c1)
|
||||
c1 := createRTCClient("c1", secondServerPort, useSinglePeerConnection, nil) // Create a room on the second node
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
t1, err := c1.AddStaticTrack("audio/opus", "audio", "micro")
|
||||
require.NoError(t, err)
|
||||
defer t1.Stop()
|
||||
t1, err := c1.AddStaticTrack("audio/opus", "audio", "micro")
|
||||
require.NoError(t, err)
|
||||
defer t1.Stop()
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load() != 1 {
|
||||
return "room job not assigned"
|
||||
}
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ac1.roomJobs.Load() != 1 {
|
||||
return "room job not assigned"
|
||||
}
|
||||
|
||||
if ac2.publisherJobs.Load() != 1 {
|
||||
return "participant job not assigned"
|
||||
}
|
||||
if ac2.publisherJobs.Load() != 1 {
|
||||
return "participant job not assigned"
|
||||
}
|
||||
|
||||
return ""
|
||||
}, AssignJobTimeout)
|
||||
return ""
|
||||
}, AssignJobTimeout)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func agentToken() string {
|
||||
|
||||
+265
-119
@@ -55,10 +55,11 @@ type SignalResponseHandler func(msg *livekit.SignalResponse) error
|
||||
type SignalResponseInterceptor func(msg *livekit.SignalResponse, next SignalResponseHandler) error
|
||||
|
||||
type RTCClient struct {
|
||||
id livekit.ParticipantID
|
||||
conn *websocket.Conn
|
||||
publisher *rtc.PCTransport
|
||||
subscriber *rtc.PCTransport
|
||||
useSinglePeerConnection bool
|
||||
id livekit.ParticipantID
|
||||
conn *websocket.Conn
|
||||
publisher *rtc.PCTransport
|
||||
subscriber *rtc.PCTransport
|
||||
// sid => track
|
||||
localTracks map[string]webrtc.TrackLocal
|
||||
trackSenders map[string]*webrtc.RTPSender
|
||||
@@ -80,11 +81,13 @@ type RTCClient struct {
|
||||
publisherFullyEstablished atomic.Bool
|
||||
subscriberFullyEstablished atomic.Bool
|
||||
pongReceivedAt atomic.Int64
|
||||
lastAnswer atomic.Pointer[webrtc.SessionDescription]
|
||||
|
||||
// tracks waiting to be acked, cid => trackInfo
|
||||
pendingPublishedTracks map[string]*livekit.TrackInfo
|
||||
|
||||
// remote tracks waiting to be processed
|
||||
pendingRemoteTracks []*webrtc.TrackRemote
|
||||
|
||||
pendingTrackWriters []*TrackWriter
|
||||
OnConnected func()
|
||||
OnDataReceived func(data []byte, sid string)
|
||||
@@ -139,7 +142,7 @@ func NewWebSocketConn(host, token string, opts *Options) (*websocket.Conn, error
|
||||
clientInfo := &livekit.ClientInfo{
|
||||
Os: runtime.GOOS,
|
||||
Sdk: livekit.ClientInfo_GO,
|
||||
Protocol: types.CurrentProtocol,
|
||||
Protocol: int32(types.CurrentProtocol),
|
||||
}
|
||||
if opts.ClientInfo != nil {
|
||||
clientInfo = opts.ClientInfo
|
||||
@@ -202,19 +205,20 @@ func SetAuthorizationToken(header http.Header, token string) {
|
||||
header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
func NewRTCClient(conn *websocket.Conn, opts *Options) (*RTCClient, error) {
|
||||
func NewRTCClient(conn *websocket.Conn, useSinglePeerConnection bool, opts *Options) (*RTCClient, error) {
|
||||
var err error
|
||||
|
||||
c := &RTCClient{
|
||||
conn: conn,
|
||||
localTracks: make(map[string]webrtc.TrackLocal),
|
||||
trackSenders: make(map[string]*webrtc.RTPSender),
|
||||
pendingPublishedTracks: make(map[string]*livekit.TrackInfo),
|
||||
subscribedTracks: make(map[livekit.ParticipantID][]*webrtc.TrackRemote),
|
||||
remoteParticipants: make(map[livekit.ParticipantID]*livekit.ParticipantInfo),
|
||||
me: &webrtc.MediaEngine{},
|
||||
lastPackets: make(map[livekit.ParticipantID]*rtp.Packet),
|
||||
bytesReceived: make(map[livekit.ParticipantID]uint64),
|
||||
useSinglePeerConnection: useSinglePeerConnection,
|
||||
conn: conn,
|
||||
localTracks: make(map[string]webrtc.TrackLocal),
|
||||
trackSenders: make(map[string]*webrtc.RTPSender),
|
||||
pendingPublishedTracks: make(map[string]*livekit.TrackInfo),
|
||||
subscribedTracks: make(map[livekit.ParticipantID][]*webrtc.TrackRemote),
|
||||
remoteParticipants: make(map[livekit.ParticipantID]*livekit.ParticipantInfo),
|
||||
me: &webrtc.MediaEngine{},
|
||||
lastPackets: make(map[livekit.ParticipantID]*rtp.Packet),
|
||||
bytesReceived: make(map[livekit.ParticipantID]uint64),
|
||||
}
|
||||
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||
|
||||
@@ -261,24 +265,14 @@ func NewRTCClient(conn *websocket.Conn, opts *Options) (*RTCClient, error) {
|
||||
//
|
||||
publisherHandler := &transportfakes.FakeHandler{}
|
||||
c.publisher, err = rtc.NewPCTransport(rtc.TransportParams{
|
||||
Config: &conf,
|
||||
DirectionConfig: conf.Subscriber,
|
||||
EnabledCodecs: codecs,
|
||||
IsOfferer: true,
|
||||
IsSendSide: true,
|
||||
Handler: publisherHandler,
|
||||
DatachannelSlowThreshold: 1024 * 1024 * 1024,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subscriberHandler := &transportfakes.FakeHandler{}
|
||||
c.subscriber, err = rtc.NewPCTransport(rtc.TransportParams{
|
||||
Config: &conf,
|
||||
DirectionConfig: conf.Publisher,
|
||||
DirectionConfig: conf.Subscriber,
|
||||
EnabledCodecs: codecs,
|
||||
Handler: subscriberHandler,
|
||||
IsOfferer: true,
|
||||
IsSendSide: true,
|
||||
Handler: publisherHandler,
|
||||
DatachannelMaxReceiverBufferSize: 1500,
|
||||
DatachannelSlowThreshold: 1024 * 1024 * 1024,
|
||||
FireOnTrackBySdp: true,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -288,6 +282,28 @@ func NewRTCClient(conn *websocket.Conn, opts *Options) (*RTCClient, error) {
|
||||
publisherHandler.OnICECandidateCalls(func(ic *webrtc.ICECandidate, t livekit.SignalTarget) error {
|
||||
return c.SendIceCandidate(ic, livekit.SignalTarget_PUBLISHER)
|
||||
})
|
||||
publisherHandler.OnTrackCalls(func(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {
|
||||
go c.processRemoteTrack(track)
|
||||
})
|
||||
publisherHandler.OnDataMessageCalls(c.handleDataMessage)
|
||||
publisherHandler.OnDataMessageUnlabeledCalls(c.handleDataMessageUnlabeled)
|
||||
publisherHandler.OnInitialConnectedCalls(func() {
|
||||
logger.Debugw("publisher initial connected", "participant", c.localParticipant.Identity)
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, tw := range c.pendingTrackWriters {
|
||||
if err := tw.Start(); err != nil {
|
||||
logger.Errorw("track writer error", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.pendingTrackWriters = nil
|
||||
|
||||
if c.OnConnected != nil {
|
||||
go c.OnConnected()
|
||||
}
|
||||
})
|
||||
publisherHandler.OnOfferCalls(c.onOffer)
|
||||
publisherHandler.OnFullyEstablishedCalls(func() {
|
||||
logger.Debugw("publisher fully established", "participant", c.localParticipant.Identity, "pID", c.localParticipant.Sid)
|
||||
@@ -316,56 +332,74 @@ func NewRTCClient(conn *websocket.Conn, opts *Options) (*RTCClient, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.subscriber.CreateReadableDataChannel("subraw", &webrtc.DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscriberHandler.OnICECandidateCalls(func(ic *webrtc.ICECandidate, t livekit.SignalTarget) error {
|
||||
if ic == nil {
|
||||
return nil
|
||||
}
|
||||
return c.SendIceCandidate(ic, livekit.SignalTarget_SUBSCRIBER)
|
||||
})
|
||||
subscriberHandler.OnTrackCalls(func(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {
|
||||
go c.processTrack(track)
|
||||
})
|
||||
subscriberHandler.OnDataMessageCalls(c.handleDataMessage)
|
||||
subscriberHandler.OnDataMessageUnlabeledCalls(c.handleDataMessageUnlabeled)
|
||||
subscriberHandler.OnInitialConnectedCalls(func() {
|
||||
logger.Debugw("subscriber initial connected", "participant", c.localParticipant.Identity)
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, tw := range c.pendingTrackWriters {
|
||||
if err := tw.Start(); err != nil {
|
||||
logger.Errorw("track writer error", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.pendingTrackWriters = nil
|
||||
|
||||
if c.OnConnected != nil {
|
||||
go c.OnConnected()
|
||||
}
|
||||
})
|
||||
subscriberHandler.OnFullyEstablishedCalls(func() {
|
||||
logger.Debugw("subscriber fully established", "participant", c.localParticipant.Identity, "pID", c.localParticipant.Sid)
|
||||
c.subscriberFullyEstablished.Store(true)
|
||||
})
|
||||
subscriberHandler.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32) error {
|
||||
// send remote an answer
|
||||
logger.Infow("sending subscriber answer",
|
||||
"participant", c.localParticipant.Identity,
|
||||
// "sdp", answer,
|
||||
)
|
||||
return c.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_Answer{
|
||||
Answer: signalling.ToProtoSessionDescription(answer, answerId),
|
||||
},
|
||||
if !c.useSinglePeerConnection {
|
||||
subscriberHandler := &transportfakes.FakeHandler{}
|
||||
c.subscriber, err = rtc.NewPCTransport(rtc.TransportParams{
|
||||
Config: &conf,
|
||||
DirectionConfig: conf.Publisher,
|
||||
EnabledCodecs: codecs,
|
||||
Handler: subscriberHandler,
|
||||
DatachannelMaxReceiverBufferSize: 1500,
|
||||
DatachannelSlowThreshold: 1024 * 1024 * 1024,
|
||||
FireOnTrackBySdp: true,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ordered := false
|
||||
if err := c.subscriber.CreateReadableDataChannel("subraw", &webrtc.DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscriberHandler.OnICECandidateCalls(func(ic *webrtc.ICECandidate, t livekit.SignalTarget) error {
|
||||
if ic == nil {
|
||||
return nil
|
||||
}
|
||||
return c.SendIceCandidate(ic, livekit.SignalTarget_SUBSCRIBER)
|
||||
})
|
||||
subscriberHandler.OnTrackCalls(func(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {
|
||||
go c.processRemoteTrack(track)
|
||||
})
|
||||
subscriberHandler.OnDataMessageCalls(c.handleDataMessage)
|
||||
subscriberHandler.OnDataMessageUnlabeledCalls(c.handleDataMessageUnlabeled)
|
||||
subscriberHandler.OnInitialConnectedCalls(func() {
|
||||
logger.Debugw("subscriber initial connected", "participant", c.localParticipant.Identity)
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, tw := range c.pendingTrackWriters {
|
||||
if err := tw.Start(); err != nil {
|
||||
logger.Errorw("track writer error", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.pendingTrackWriters = nil
|
||||
|
||||
if c.OnConnected != nil {
|
||||
go c.OnConnected()
|
||||
}
|
||||
})
|
||||
subscriberHandler.OnFullyEstablishedCalls(func() {
|
||||
logger.Debugw("subscriber fully established", "participant", c.localParticipant.Identity, "pID", c.localParticipant.Sid)
|
||||
c.subscriberFullyEstablished.Store(true)
|
||||
})
|
||||
subscriberHandler.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32) error {
|
||||
// send remote an answer
|
||||
logger.Infow(
|
||||
"sending subscriber answer",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"sdp", answer,
|
||||
)
|
||||
return c.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_Answer{
|
||||
Answer: signalling.ToProtoSessionDescription(answer, answerId),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
c.signalRequestInterceptor = opts.SignalRequestInterceptor
|
||||
@@ -428,15 +462,20 @@ func (c *RTCClient) handleSignalResponse(res *livekit.SignalResponse) error {
|
||||
|
||||
logger.Infow("join accepted, awaiting offer", "participant", msg.Join.Participant.Identity)
|
||||
case *livekit.SignalResponse_Answer:
|
||||
// logger.Debugw("received server answer",
|
||||
// "participant", c.localParticipant.Identity,
|
||||
// "answer", msg.Answer.Sdp)
|
||||
logger.Infow(
|
||||
"received server answer",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"answer", msg.Answer.Sdp,
|
||||
)
|
||||
c.handleAnswer(signalling.FromProtoSessionDescription(msg.Answer))
|
||||
case *livekit.SignalResponse_Offer:
|
||||
logger.Infow("received server offer",
|
||||
"participant", c.localParticipant.Identity,
|
||||
)
|
||||
desc, offerId := signalling.FromProtoSessionDescription(msg.Offer)
|
||||
logger.Infow(
|
||||
"received server offer",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"sdp", desc,
|
||||
"offerId", offerId,
|
||||
)
|
||||
c.handleOffer(desc, offerId)
|
||||
case *livekit.SignalResponse_Trickle:
|
||||
candidateInit, err := signalling.FromProtoTrickle(msg.Trickle)
|
||||
@@ -474,8 +513,7 @@ func (c *RTCClient) handleSignalResponse(res *livekit.SignalResponse) error {
|
||||
case *livekit.SignalResponse_TrackUnpublished:
|
||||
sid := msg.TrackUnpublished.TrackSid
|
||||
c.lock.Lock()
|
||||
sender := c.trackSenders[sid]
|
||||
if sender != nil {
|
||||
if sender := c.trackSenders[sid]; sender != nil {
|
||||
if err := c.publisher.RemoveTrack(sender); err != nil {
|
||||
logger.Errorw("Could not unpublish track", err)
|
||||
}
|
||||
@@ -488,6 +526,14 @@ func (c *RTCClient) handleSignalResponse(res *livekit.SignalResponse) error {
|
||||
c.pongReceivedAt.Store(msg.Pong)
|
||||
case *livekit.SignalResponse_SubscriptionResponse:
|
||||
c.subscriptionResponse.Store(msg.SubscriptionResponse)
|
||||
case *livekit.SignalResponse_MediaSectionsRequirement:
|
||||
logger.Infow(
|
||||
"received media sections requirement",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"numAudios", msg.MediaSectionsRequirement.NumAudios,
|
||||
"numVideos", msg.MediaSectionsRequirement.NumVideos,
|
||||
)
|
||||
c.handleMediaSectionsRequirement(msg.MediaSectionsRequirement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -580,8 +626,12 @@ func (c *RTCClient) Stop() {
|
||||
c.publisherFullyEstablished.Store(false)
|
||||
c.subscriberFullyEstablished.Store(false)
|
||||
_ = c.conn.Close()
|
||||
c.publisher.Close()
|
||||
c.subscriber.Close()
|
||||
if c.publisher != nil {
|
||||
c.publisher.Close()
|
||||
}
|
||||
if c.subscriber != nil {
|
||||
c.subscriber.Close()
|
||||
}
|
||||
c.cancel()
|
||||
}
|
||||
|
||||
@@ -679,7 +729,18 @@ func (c *RTCClient) AddTrack(track *webrtc.TrackLocalStaticSample, path string,
|
||||
trackType = livekit.TrackType_VIDEO
|
||||
}
|
||||
|
||||
if err = c.SendAddTrack(track.ID(), track.StreamID(), trackType); err != nil {
|
||||
sender, _, err := c.publisher.AddTrack(track, types.AddTrackParams{}, nil, rtc.RTCPFeedbackConfig{})
|
||||
if err != nil {
|
||||
logger.Errorw(
|
||||
"add track failed", err,
|
||||
"participant", c.localParticipant.Identity,
|
||||
"pID", c.localParticipant.Sid,
|
||||
"trackID", track.ID(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.SendAddTrack(track.ID(), track.Codec().MimeType, track.StreamID(), trackType); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -693,10 +754,12 @@ func (c *RTCClient) AddTrack(track *webrtc.TrackLocalStaticSample, path string,
|
||||
default:
|
||||
c.lock.Lock()
|
||||
ti = c.pendingPublishedTracks[track.ID()]
|
||||
c.lock.Unlock()
|
||||
if ti != nil {
|
||||
delete(c.pendingPublishedTracks, track.ID())
|
||||
c.lock.Unlock()
|
||||
break
|
||||
}
|
||||
c.lock.Unlock()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
if ti != nil {
|
||||
@@ -707,11 +770,6 @@ func (c *RTCClient) AddTrack(track *webrtc.TrackLocalStaticSample, path string,
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
sender, _, err := c.publisher.AddTrack(track, types.AddTrackParams{})
|
||||
if err != nil {
|
||||
logger.Errorw("add track failed", err, "trackID", ti.Sid, "participant", c.localParticipant.Identity, "pID", c.localParticipant.Sid)
|
||||
return
|
||||
}
|
||||
c.localTracks[ti.Sid] = track
|
||||
c.trackSenders[ti.Sid] = sender
|
||||
c.publisher.Negotiate(false)
|
||||
@@ -750,9 +808,7 @@ func (c *RTCClient) AddFileTrack(path string, id string, label string) (writer *
|
||||
return nil, fmt.Errorf("%s has an unsupported extension", filepath.Base(path))
|
||||
}
|
||||
|
||||
logger.Debugw("adding file track",
|
||||
"mime", mime,
|
||||
)
|
||||
logger.Debugw("adding file track", "mime", mime)
|
||||
|
||||
track, err := webrtc.NewTrackLocalStaticSample(
|
||||
webrtc.RTPCodecCapability{MimeType: mime},
|
||||
@@ -767,13 +823,19 @@ func (c *RTCClient) AddFileTrack(path string, id string, label string) (writer *
|
||||
}
|
||||
|
||||
// send AddTrack command to server to initiate server-side negotiation
|
||||
func (c *RTCClient) SendAddTrack(cid string, name string, trackType livekit.TrackType) error {
|
||||
func (c *RTCClient) SendAddTrack(cid string, mimeType string, name string, trackType livekit.TrackType) error {
|
||||
return c.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_AddTrack{
|
||||
AddTrack: &livekit.AddTrackRequest{
|
||||
Cid: cid,
|
||||
Name: name,
|
||||
Type: trackType,
|
||||
SimulcastCodecs: []*livekit.SimulcastCodec{
|
||||
{
|
||||
Cid: cid,
|
||||
Codec: mimeType,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -816,7 +878,7 @@ func (c *RTCClient) GetPublishedTrackIDs() []string {
|
||||
|
||||
// LastAnswer return SDP of the last answer for the publisher connection
|
||||
func (c *RTCClient) LastAnswer() *webrtc.SessionDescription {
|
||||
return c.lastAnswer.Load()
|
||||
return c.publisher.CurrentRemoteDescription()
|
||||
}
|
||||
|
||||
func (c *RTCClient) ensurePublisherConnected() error {
|
||||
@@ -864,21 +926,58 @@ func (c *RTCClient) handleDataMessageUnlabeled(data []byte) {
|
||||
|
||||
// handles a server initiated offer, handle on subscriber PC
|
||||
func (c *RTCClient) handleOffer(desc webrtc.SessionDescription, offerId uint32) {
|
||||
logger.Infow("handling server offer", "participant", c.localParticipant.Identity)
|
||||
c.subscriber.HandleRemoteDescription(desc, offerId)
|
||||
c.processPendingRemoteTracks()
|
||||
}
|
||||
|
||||
// the client handles answer on the publisher PC
|
||||
func (c *RTCClient) handleAnswer(desc webrtc.SessionDescription, answerId uint32) {
|
||||
logger.Infow("handling server answer", "participant", c.localParticipant.Identity)
|
||||
|
||||
c.lastAnswer.Store(&desc)
|
||||
// remote answered the offer, establish connection
|
||||
c.publisher.HandleRemoteDescription(desc, answerId)
|
||||
c.processPendingRemoteTracks()
|
||||
}
|
||||
|
||||
// the client handles media sections requirement on the publisher PC
|
||||
func (c *RTCClient) handleMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) {
|
||||
addTransceivers := func(kind webrtc.RTPCodecType, count uint32) {
|
||||
for i := uint32(0); i < count; i++ {
|
||||
if _, err := c.publisher.AddTransceiverFromKind(
|
||||
kind,
|
||||
webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionRecvonly,
|
||||
},
|
||||
); err != nil {
|
||||
logger.Warnw(
|
||||
"could not add transceiver", err,
|
||||
"participant", c.localParticipant.Identity,
|
||||
"kind", kind,
|
||||
)
|
||||
} else {
|
||||
logger.Infow(
|
||||
"added transceiver of kind",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"kind", kind,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addTransceivers(webrtc.RTPCodecTypeAudio, mediaSectionsRequirement.NumAudios)
|
||||
addTransceivers(webrtc.RTPCodecTypeVideo, mediaSectionsRequirement.NumVideos)
|
||||
c.publisher.Negotiate(false)
|
||||
}
|
||||
|
||||
func (c *RTCClient) onOffer(offer webrtc.SessionDescription, offerId uint32) error {
|
||||
if c.localParticipant != nil {
|
||||
logger.Infow("starting negotiation", "participant", c.localParticipant.Identity)
|
||||
logger.Infow(
|
||||
"sending publisher offer",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"offer", offer,
|
||||
)
|
||||
}
|
||||
return c.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_Offer{
|
||||
@@ -887,25 +986,58 @@ func (c *RTCClient) onOffer(offer webrtc.SessionDescription, offerId uint32) err
|
||||
})
|
||||
}
|
||||
|
||||
func (c *RTCClient) processTrack(track *webrtc.TrackRemote) {
|
||||
lastUpdate := time.Time{}
|
||||
pId, trackId := rtc.UnpackStreamID(track.StreamID())
|
||||
if trackId == "" {
|
||||
trackId = livekit.TrackID(track.ID())
|
||||
}
|
||||
func (c *RTCClient) processPendingRemoteTracks() {
|
||||
c.lock.Lock()
|
||||
c.subscribedTracks[pId] = append(c.subscribedTracks[pId], track)
|
||||
pendingRemoteTracks := c.pendingRemoteTracks
|
||||
c.pendingRemoteTracks = nil
|
||||
c.lock.Unlock()
|
||||
|
||||
logger.Infow("client added track", "participant", c.localParticipant.Identity,
|
||||
"pID", pId,
|
||||
"trackID", trackId,
|
||||
for _, pendingRemoteTrack := range pendingRemoteTracks {
|
||||
go c.processRemoteTrack(pendingRemoteTrack)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RTCClient) processRemoteTrack(track *webrtc.TrackRemote) {
|
||||
lastUpdate := time.Time{}
|
||||
|
||||
// because of FireOnTrackBySdp, it is possible get an empty streamID
|
||||
// if media comes before SDP, cache and try later
|
||||
streamID := track.StreamID()
|
||||
if streamID == "" {
|
||||
logger.Infow(
|
||||
"client caching track",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"pID", c.ID(),
|
||||
"codec", track.Codec(),
|
||||
"ssrc", track.SSRC(),
|
||||
)
|
||||
c.lock.Lock()
|
||||
c.pendingRemoteTracks = append(c.pendingRemoteTracks, track)
|
||||
c.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
publisherID, trackID := rtc.UnpackStreamID(streamID)
|
||||
if trackID == "" {
|
||||
trackID = livekit.TrackID(track.ID())
|
||||
}
|
||||
c.lock.Lock()
|
||||
c.subscribedTracks[publisherID] = append(c.subscribedTracks[publisherID], track)
|
||||
c.lock.Unlock()
|
||||
|
||||
logger.Infow(
|
||||
"client added track",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"pID", c.ID(),
|
||||
"publisherID", publisherID,
|
||||
"trackID", trackID,
|
||||
"codec", track.Codec(),
|
||||
"ssrc", track.SSRC(),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
c.lock.Lock()
|
||||
c.subscribedTracks[pId] = funk.Without(c.subscribedTracks[pId], track).([]*webrtc.TrackRemote)
|
||||
c.subscribedTracks[publisherID] = funk.Without(c.subscribedTracks[publisherID], track).([]*webrtc.TrackRemote)
|
||||
c.lock.Unlock()
|
||||
}()
|
||||
|
||||
@@ -916,6 +1048,15 @@ func (c *RTCClient) processTrack(track *webrtc.TrackRemote) {
|
||||
break
|
||||
}
|
||||
if rtc.IsEOF(err) {
|
||||
logger.Infow(
|
||||
"client track removed",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"pID", c.ID(),
|
||||
"publisherID", publisherID,
|
||||
"trackID", trackID,
|
||||
"codec", track.Codec(),
|
||||
"ssrc", track.SSRC(),
|
||||
)
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
@@ -923,14 +1064,19 @@ func (c *RTCClient) processTrack(track *webrtc.TrackRemote) {
|
||||
continue
|
||||
}
|
||||
c.lock.Lock()
|
||||
c.lastPackets[pId] = pkt
|
||||
c.bytesReceived[pId] += uint64(pkt.MarshalSize())
|
||||
c.lastPackets[publisherID] = pkt
|
||||
c.bytesReceived[publisherID] += uint64(pkt.MarshalSize())
|
||||
c.lock.Unlock()
|
||||
numBytes += pkt.MarshalSize()
|
||||
if time.Since(lastUpdate) > 30*time.Second {
|
||||
logger.Infow("consumed from participant",
|
||||
"trackID", trackId, "pID", pId,
|
||||
"size", numBytes)
|
||||
logger.Infow(
|
||||
"consumed from participant",
|
||||
"participant", c.localParticipant.Identity,
|
||||
"pID", c.ID(),
|
||||
"publisherID", publisherID,
|
||||
"trackID", trackID,
|
||||
"size", numBytes,
|
||||
)
|
||||
lastUpdate = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
+12
-15
@@ -202,35 +202,32 @@ func createMultiNodeServer(nodeID string, port uint32) *service.LivekitServer {
|
||||
}
|
||||
|
||||
// creates a client and runs against server
|
||||
func createRTCClient(name string, port int, opts *testclient.Options) *testclient.RTCClient {
|
||||
func createRTCClient(name string, port int, useSinglePeerConnection bool, opts *testclient.Options) *testclient.RTCClient {
|
||||
var customizer func(token *auth.AccessToken, grants *auth.VideoGrant)
|
||||
if opts != nil {
|
||||
customizer = opts.TokenCustomizer
|
||||
}
|
||||
token := joinToken(testRoom, name, customizer)
|
||||
ws, err := testclient.NewWebSocketConn(fmt.Sprintf("ws://localhost:%d", port), token, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c, err := testclient.NewRTCClient(ws, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go c.Run()
|
||||
|
||||
return c
|
||||
return createRTCClientWithToken(token, port, useSinglePeerConnection, opts)
|
||||
}
|
||||
|
||||
// creates a client and runs against server
|
||||
func createRTCClientWithToken(token string, port int, opts *testclient.Options) *testclient.RTCClient {
|
||||
func createRTCClientWithToken(token string, port int, useSinglePeerConnection bool, opts *testclient.Options) *testclient.RTCClient {
|
||||
if opts == nil {
|
||||
opts = &testclient.Options{
|
||||
AutoSubscribe: true,
|
||||
}
|
||||
}
|
||||
if useSinglePeerConnection {
|
||||
opts.UseJoinRequestQueryParam = true
|
||||
}
|
||||
ws, err := testclient.NewWebSocketConn(fmt.Sprintf("ws://localhost:%d", port), token, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c, err := testclient.NewRTCClient(ws, opts)
|
||||
c, err := testclient.NewRTCClient(ws, useSinglePeerConnection, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -61,24 +61,28 @@ func TestMultiNodeUpdateRoomMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("when room has a participant", func(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeUpdateRoomMetadata_with_participant")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeUpdateRoomMetadata_with_participant")
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
defer c1.Stop()
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
defer c1.Stop()
|
||||
|
||||
_, err := roomClient.CreateRoom(contextWithToken(createRoomToken()), &livekit.CreateRoomRequest{
|
||||
Name: "emptyRoom",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err := roomClient.CreateRoom(contextWithToken(createRoomToken()), &livekit.CreateRoomRequest{
|
||||
Name: "emptyRoom",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rm, err := roomClient.UpdateRoomMetadata(contextWithToken(adminRoomToken("emptyRoom")), &livekit.UpdateRoomMetadataRequest{
|
||||
Room: "emptyRoom",
|
||||
Metadata: "updated metadata",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated metadata", rm.Metadata)
|
||||
rm, err := roomClient.UpdateRoomMetadata(contextWithToken(adminRoomToken("emptyRoom")), &livekit.UpdateRoomMetadataRequest{
|
||||
Room: "emptyRoom",
|
||||
Metadata: "updated metadata",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated metadata", rm.Metadata)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,85 +93,97 @@ func TestMultiNodeRemoveParticipant(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeRemoveParticipant")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeRemoveParticipant")
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("mn_remove_participant", defaultServerPort, nil)
|
||||
defer c1.Stop()
|
||||
waitUntilConnected(t, c1)
|
||||
c1 := createRTCClient("mn_remove_participant", defaultServerPort, useSinglePeerConnection, nil)
|
||||
defer c1.Stop()
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
_, err := roomClient.RemoveParticipant(ctx, &livekit.RoomParticipantIdentity{
|
||||
Room: testRoom,
|
||||
Identity: "mn_remove_participant",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
_, err := roomClient.RemoveParticipant(ctx, &livekit.RoomParticipantIdentity{
|
||||
Room: testRoom,
|
||||
Identity: "mn_remove_participant",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// participant list doesn't show the participant
|
||||
listRes, err := roomClient.ListParticipants(ctx, &livekit.ListParticipantsRequest{
|
||||
Room: testRoom,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listRes.Participants, 0)
|
||||
// participant list doesn't show the participant
|
||||
listRes, err := roomClient.ListParticipants(ctx, &livekit.ListParticipantsRequest{
|
||||
Room: testRoom,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listRes.Participants, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// update participant metadata
|
||||
func TestMultiNodeUpdateParticipantMetadata(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeUpdateParticipantMetadata")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeUpdateParticipantMetadata")
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("update_participant_metadata", defaultServerPort, nil)
|
||||
defer c1.Stop()
|
||||
waitUntilConnected(t, c1)
|
||||
c1 := createRTCClient("update_participant_metadata", defaultServerPort, useSinglePeerConnection, nil)
|
||||
defer c1.Stop()
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
res, err := roomClient.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "update_participant_metadata",
|
||||
Metadata: "the new metadata",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "the new metadata", res.Metadata)
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
res, err := roomClient.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "update_participant_metadata",
|
||||
Metadata: "the new metadata",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "the new metadata", res.Metadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// admin mute published track
|
||||
func TestMultiNodeMutePublishedTrack(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeMutePublishedTrack")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeMutePublishedTrack")
|
||||
defer finish()
|
||||
|
||||
identity := "mute_published_track"
|
||||
c1 := createRTCClient(identity, defaultServerPort, nil)
|
||||
defer c1.Stop()
|
||||
waitUntilConnected(t, c1)
|
||||
identity := "mute_published_track"
|
||||
c1 := createRTCClient(identity, defaultServerPort, useSinglePeerConnection, nil)
|
||||
defer c1.Stop()
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
|
||||
trackIDs := c1.GetPublishedTrackIDs()
|
||||
require.NotEmpty(t, trackIDs)
|
||||
trackIDs := c1.GetPublishedTrackIDs()
|
||||
require.NotEmpty(t, trackIDs)
|
||||
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
// wait for it to be published before
|
||||
testutils.WithTimeout(t, func() string {
|
||||
res, err := roomClient.GetParticipant(ctx, &livekit.RoomParticipantIdentity{
|
||||
Room: testRoom,
|
||||
Identity: identity,
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
// wait for it to be published before
|
||||
testutils.WithTimeout(t, func() string {
|
||||
res, err := roomClient.GetParticipant(ctx, &livekit.RoomParticipantIdentity{
|
||||
Room: testRoom,
|
||||
Identity: identity,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if len(res.Tracks) == 2 {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprintf("expected 2 tracks to be published, actual: %d", len(res.Tracks))
|
||||
}
|
||||
})
|
||||
|
||||
res, err := roomClient.MutePublishedTrack(ctx, &livekit.MuteRoomTrackRequest{
|
||||
Room: testRoom,
|
||||
Identity: identity,
|
||||
TrackSid: trackIDs[0],
|
||||
Muted: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, trackIDs[0], res.Track.Sid)
|
||||
require.True(t, res.Track.Muted)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if len(res.Tracks) == 2 {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprintf("expected 2 tracks to be published, actual: %d", len(res.Tracks))
|
||||
}
|
||||
})
|
||||
|
||||
res, err := roomClient.MutePublishedTrack(ctx, &livekit.MuteRoomTrackRequest{
|
||||
Room: testRoom,
|
||||
Identity: identity,
|
||||
TrackSid: trackIDs[0],
|
||||
Muted: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, trackIDs[0], res.Track.Sid)
|
||||
require.True(t, res.Track.Muted)
|
||||
}
|
||||
}
|
||||
|
||||
+244
-214
@@ -15,6 +15,7 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -33,6 +34,7 @@ func TestMultiNodeRouting(t *testing.T) {
|
||||
t.SkipNow()
|
||||
return
|
||||
}
|
||||
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeRouting")
|
||||
defer finish()
|
||||
|
||||
@@ -42,35 +44,39 @@ func TestMultiNodeRouting(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// one node connecting to node 1, and another connecting to node 2
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("c2", secondServerPort, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
// one node connecting to node 1, and another connecting to node 2
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("c2", secondServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
|
||||
// c1 publishing, and c2 receiving
|
||||
t1, err := c1.AddStaticTrack("audio/opus", "audio", "webcam")
|
||||
require.NoError(t, err)
|
||||
if t1 != nil {
|
||||
defer t1.Stop()
|
||||
// c1 publishing, and c2 receiving
|
||||
t1, err := c1.AddStaticTrack("audio/opus", "audio", "webcam")
|
||||
require.NoError(t, err)
|
||||
if t1 != nil {
|
||||
defer t1.Stop()
|
||||
}
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c2.SubscribedTracks()) == 0 {
|
||||
return "c2 received no tracks"
|
||||
}
|
||||
if len(c2.SubscribedTracks()[c1.ID()]) != 1 {
|
||||
return "c2 didn't receive track published by c1"
|
||||
}
|
||||
tr1 := c2.SubscribedTracks()[c1.ID()][0]
|
||||
streamID, _ := rtc.UnpackStreamID(tr1.StreamID())
|
||||
require.Equal(t, c1.ID(), streamID)
|
||||
return ""
|
||||
})
|
||||
|
||||
remoteC1 := c2.GetRemoteParticipant(c1.ID())
|
||||
require.Equal(t, "c1", remoteC1.Name)
|
||||
require.Equal(t, "metadatac1", remoteC1.Metadata)
|
||||
})
|
||||
}
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c2.SubscribedTracks()) == 0 {
|
||||
return "c2 received no tracks"
|
||||
}
|
||||
if len(c2.SubscribedTracks()[c1.ID()]) != 1 {
|
||||
return "c2 didn't receive track published by c1"
|
||||
}
|
||||
tr1 := c2.SubscribedTracks()[c1.ID()][0]
|
||||
streamID, _ := rtc.UnpackStreamID(tr1.StreamID())
|
||||
require.Equal(t, c1.ID(), streamID)
|
||||
return ""
|
||||
})
|
||||
|
||||
remoteC1 := c2.GetRemoteParticipant(c1.ID())
|
||||
require.Equal(t, "c1", remoteC1.Name)
|
||||
require.Equal(t, "metadatac1", remoteC1.Metadata)
|
||||
}
|
||||
|
||||
func TestConnectWithoutCreation(t *testing.T) {
|
||||
@@ -82,10 +88,14 @@ func TestConnectWithoutCreation(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestConnectWithoutCreation")
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
c1.Stop()
|
||||
c1.Stop()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testing multiple scenarios rooms
|
||||
@@ -118,30 +128,34 @@ func TestMultinodeReconnectAfterNodeShutdown(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
_, s2, finish := setupMultiNodeTest("TestMultinodeReconnectAfterNodeShutdown")
|
||||
defer finish()
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
_, s2, finish := setupMultiNodeTest("TestMultinodeReconnectAfterNodeShutdown")
|
||||
defer finish()
|
||||
|
||||
// creating room on node 1
|
||||
_, err := roomClient.CreateRoom(contextWithToken(createRoomToken()), &livekit.CreateRoomRequest{
|
||||
Name: testRoom,
|
||||
NodeId: s2.Node().Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// creating room on node 1
|
||||
_, err := roomClient.CreateRoom(contextWithToken(createRoomToken()), &livekit.CreateRoomRequest{
|
||||
Name: testRoom,
|
||||
NodeId: s2.Node().Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// one node connecting to node 1, and another connecting to node 2
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("c2", secondServerPort, nil)
|
||||
// one node connecting to node 1, and another connecting to node 2
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("c2", secondServerPort, useSinglePeerConnection, nil)
|
||||
|
||||
waitUntilConnected(t, c1, c2)
|
||||
stopClients(c1, c2)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
stopClients(c1, c2)
|
||||
|
||||
// stop s2, and connect to room again
|
||||
s2.Stop(true)
|
||||
// stop s2, and connect to room again
|
||||
s2.Stop(true)
|
||||
|
||||
time.Sleep(syncDelay)
|
||||
time.Sleep(syncDelay)
|
||||
|
||||
c3 := createRTCClient("c3", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c3)
|
||||
c3 := createRTCClient("c3", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c3)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultinodeDataPublishing(t *testing.T) {
|
||||
@@ -186,48 +200,52 @@ func TestMultiNodeRefreshToken(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeJoinAfterClose")
|
||||
defer finish()
|
||||
|
||||
// a participant joining with full permissions
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
// a participant joining with full permissions
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
// update permissions and metadata
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
_, err := roomClient.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "c1",
|
||||
Permission: &livekit.ParticipantPermission{
|
||||
CanPublish: false,
|
||||
CanSubscribe: true,
|
||||
},
|
||||
Metadata: "metadata",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// update permissions and metadata
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
_, err := roomClient.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "c1",
|
||||
Permission: &livekit.ParticipantPermission{
|
||||
CanPublish: false,
|
||||
CanSubscribe: true,
|
||||
},
|
||||
Metadata: "metadata",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if c1.RefreshToken() == "" {
|
||||
return "did not receive refresh token"
|
||||
}
|
||||
// parse token to ensure it's correct
|
||||
verifier, err := auth.ParseAPIToken(c1.RefreshToken())
|
||||
require.NoError(t, err)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if c1.RefreshToken() == "" {
|
||||
return "did not receive refresh token"
|
||||
}
|
||||
// parse token to ensure it's correct
|
||||
verifier, err := auth.ParseAPIToken(c1.RefreshToken())
|
||||
require.NoError(t, err)
|
||||
|
||||
grants, err := verifier.Verify(testApiSecret)
|
||||
require.NoError(t, err)
|
||||
grants, err := verifier.Verify(testApiSecret)
|
||||
require.NoError(t, err)
|
||||
|
||||
if grants.Metadata != "metadata" {
|
||||
return "metadata did not match"
|
||||
}
|
||||
if *grants.Video.CanPublish {
|
||||
return "canPublish should be false"
|
||||
}
|
||||
if *grants.Video.CanPublishData {
|
||||
return "canPublishData should be false"
|
||||
}
|
||||
if !*grants.Video.CanSubscribe {
|
||||
return "canSubscribe should be true"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
if grants.Metadata != "metadata" {
|
||||
return "metadata did not match"
|
||||
}
|
||||
if *grants.Video.CanPublish {
|
||||
return "canPublish should be false"
|
||||
}
|
||||
if *grants.Video.CanPublishData {
|
||||
return "canPublishData should be false"
|
||||
}
|
||||
if !*grants.Video.CanSubscribe {
|
||||
return "canSubscribe should be true"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that token accurately reflects out of band updates
|
||||
@@ -240,158 +258,170 @@ func TestMultiNodeUpdateAttributes(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeUpdateAttributes")
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("au1", defaultServerPort, &client.Options{
|
||||
TokenCustomizer: func(token *auth.AccessToken, grants *auth.VideoGrant) {
|
||||
token.SetAttributes(map[string]string{
|
||||
"mykey": "au1",
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("au1", defaultServerPort, useSinglePeerConnection, &client.Options{
|
||||
TokenCustomizer: func(token *auth.AccessToken, grants *auth.VideoGrant) {
|
||||
token.SetAttributes(map[string]string{
|
||||
"mykey": "au1",
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
c2 := createRTCClient("au2", secondServerPort, &client.Options{
|
||||
TokenCustomizer: func(token *auth.AccessToken, grants *auth.VideoGrant) {
|
||||
token.SetAttributes(map[string]string{
|
||||
"mykey": "au2",
|
||||
c2 := createRTCClient("au2", secondServerPort, useSinglePeerConnection, &client.Options{
|
||||
TokenCustomizer: func(token *auth.AccessToken, grants *auth.VideoGrant) {
|
||||
token.SetAttributes(map[string]string{
|
||||
"mykey": "au2",
|
||||
})
|
||||
grants.SetCanUpdateOwnMetadata(true)
|
||||
},
|
||||
})
|
||||
grants.SetCanUpdateOwnMetadata(true)
|
||||
},
|
||||
})
|
||||
waitUntilConnected(t, c1, c2)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rc2 := c1.GetRemoteParticipant(c2.ID())
|
||||
rc1 := c2.GetRemoteParticipant(c1.ID())
|
||||
if rc2 == nil || rc1 == nil {
|
||||
return "participants could not see each other"
|
||||
}
|
||||
if rc1.Attributes == nil || rc1.Attributes["mykey"] != "au1" {
|
||||
return "rc1's initial attributes are incorrect"
|
||||
}
|
||||
if rc2.Attributes == nil || rc2.Attributes["mykey"] != "au2" {
|
||||
return "rc2's initial attributes are incorrect"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rc2 := c1.GetRemoteParticipant(c2.ID())
|
||||
rc1 := c2.GetRemoteParticipant(c1.ID())
|
||||
if rc2 == nil || rc1 == nil {
|
||||
return "participants could not see each other"
|
||||
}
|
||||
if rc1.Attributes == nil || rc1.Attributes["mykey"] != "au1" {
|
||||
return "rc1's initial attributes are incorrect"
|
||||
}
|
||||
if rc2.Attributes == nil || rc2.Attributes["mykey"] != "au2" {
|
||||
return "rc2's initial attributes are incorrect"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// this one should not go through
|
||||
_ = c1.SetAttributes(map[string]string{"mykey": "shouldnotchange"})
|
||||
_ = c2.SetAttributes(map[string]string{"secondkey": "au2"})
|
||||
// this one should not go through
|
||||
_ = c1.SetAttributes(map[string]string{"mykey": "shouldnotchange"})
|
||||
_ = c2.SetAttributes(map[string]string{"secondkey": "au2"})
|
||||
|
||||
// updates using room API should succeed
|
||||
_, err := roomClient.UpdateParticipant(contextWithToken(adminRoomToken(testRoom)), &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "au1",
|
||||
Attributes: map[string]string{
|
||||
"secondkey": "au1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// updates using room API should succeed
|
||||
_, err := roomClient.UpdateParticipant(contextWithToken(adminRoomToken(testRoom)), &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "au1",
|
||||
Attributes: map[string]string{
|
||||
"secondkey": "au1",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rc1 := c2.GetRemoteParticipant(c1.ID())
|
||||
rc2 := c1.GetRemoteParticipant(c2.ID())
|
||||
if rc1.Attributes["secondkey"] != "au1" {
|
||||
return "au1's attribute update failed"
|
||||
}
|
||||
if rc2.Attributes["secondkey"] != "au2" {
|
||||
return "au2's attribute update failed"
|
||||
}
|
||||
if rc1.Attributes["mykey"] != "au1" {
|
||||
return "au1's mykey should not change"
|
||||
}
|
||||
if rc2.Attributes["mykey"] != "au2" {
|
||||
return "au2's mykey should not change"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rc1 := c2.GetRemoteParticipant(c1.ID())
|
||||
rc2 := c1.GetRemoteParticipant(c2.ID())
|
||||
if rc1.Attributes["secondkey"] != "au1" {
|
||||
return "au1's attribute update failed"
|
||||
}
|
||||
if rc2.Attributes["secondkey"] != "au2" {
|
||||
return "au2's attribute update failed"
|
||||
}
|
||||
if rc1.Attributes["mykey"] != "au1" {
|
||||
return "au1's mykey should not change"
|
||||
}
|
||||
if rc2.Attributes["mykey"] != "au2" {
|
||||
return "au2's mykey should not change"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiNodeRevokePublishPermission(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestMultiNodeRevokePublishPermission")
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("c2", secondServerPort, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("c2", secondServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
|
||||
// c1 publishes a track for c2
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
// c1 publishes a track for c2
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c2.SubscribedTracks()[c1.ID()]) != 2 {
|
||||
return "c2 did not receive c1's tracks"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c2.SubscribedTracks()[c1.ID()]) != 2 {
|
||||
return "c2 did not receive c1's tracks"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// revoke permission
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
_, err := roomClient.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "c1",
|
||||
Permission: &livekit.ParticipantPermission{
|
||||
CanPublish: false,
|
||||
CanPublishData: true,
|
||||
CanSubscribe: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// revoke permission
|
||||
ctx := contextWithToken(adminRoomToken(testRoom))
|
||||
_, err := roomClient.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
||||
Room: testRoom,
|
||||
Identity: "c1",
|
||||
Permission: &livekit.ParticipantPermission{
|
||||
CanPublish: false,
|
||||
CanPublishData: true,
|
||||
CanSubscribe: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure c1 no longer has track published, c2 no longer see track under C1
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.GetPublishedTrackIDs()) != 0 {
|
||||
return "c1 did not unpublish tracks"
|
||||
}
|
||||
remoteC1 := c2.GetRemoteParticipant(c1.ID())
|
||||
if remoteC1 == nil {
|
||||
return "c2 doesn't know about c1"
|
||||
}
|
||||
if len(remoteC1.Tracks) != 0 {
|
||||
return "c2 still has c1's tracks"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
// ensure c1 no longer has track published, c2 no longer see track under C1
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.GetPublishedTrackIDs()) != 0 {
|
||||
return "c1 did not unpublish tracks"
|
||||
}
|
||||
remoteC1 := c2.GetRemoteParticipant(c1.ID())
|
||||
if remoteC1 == nil {
|
||||
return "c2 doesn't know about c1"
|
||||
}
|
||||
if len(remoteC1.Tracks) != 0 {
|
||||
return "c2 still has c1's tracks"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseDisconnectedParticipantOnSignalClose(t *testing.T) {
|
||||
_, _, finish := setupMultiNodeTest("TestCloseDisconnectedParticipantOnSignalClose")
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("c1", secondServerPort, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("c1", secondServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
c2 := createRTCClient("c2", defaultServerPort, &client.Options{
|
||||
SignalRequestInterceptor: func(msg *livekit.SignalRequest, next client.SignalRequestHandler) error {
|
||||
switch msg.Message.(type) {
|
||||
case *livekit.SignalRequest_Offer, *livekit.SignalRequest_Answer, *livekit.SignalRequest_Leave:
|
||||
return nil
|
||||
default:
|
||||
return next(msg)
|
||||
}
|
||||
},
|
||||
SignalResponseInterceptor: func(msg *livekit.SignalResponse, next client.SignalResponseHandler) error {
|
||||
switch msg.Message.(type) {
|
||||
case *livekit.SignalResponse_Offer, *livekit.SignalResponse_Answer:
|
||||
return nil
|
||||
default:
|
||||
return next(msg)
|
||||
}
|
||||
},
|
||||
})
|
||||
c2 := createRTCClient("c2", defaultServerPort, useSinglePeerConnection, &client.Options{
|
||||
SignalRequestInterceptor: func(msg *livekit.SignalRequest, next client.SignalRequestHandler) error {
|
||||
switch msg.Message.(type) {
|
||||
case *livekit.SignalRequest_Offer, *livekit.SignalRequest_Answer, *livekit.SignalRequest_Leave:
|
||||
return nil
|
||||
default:
|
||||
return next(msg)
|
||||
}
|
||||
},
|
||||
SignalResponseInterceptor: func(msg *livekit.SignalResponse, next client.SignalResponseHandler) error {
|
||||
switch msg.Message.(type) {
|
||||
case *livekit.SignalResponse_Offer, *livekit.SignalResponse_Answer:
|
||||
return nil
|
||||
default:
|
||||
return next(msg)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.RemoteParticipants()) != 1 {
|
||||
return "c1 did not see c2 join"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.RemoteParticipants()) != 1 {
|
||||
return "c1 did not see c2 join"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
c2.Stop()
|
||||
c2.Stop()
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.RemoteParticipants()) != 0 {
|
||||
return "c1 did not see c2 removed"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.RemoteParticipants()) != 0 {
|
||||
return "c1 did not see c2 removed"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+158
-138
@@ -31,179 +31,199 @@ import (
|
||||
|
||||
// a scenario with lots of clients connecting, publishing, and leaving at random periods
|
||||
func scenarioPublishingUponJoining(t *testing.T) {
|
||||
c1 := createRTCClient("puj_1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("puj_2", secondServerPort, &testclient.Options{AutoSubscribe: true})
|
||||
c3 := createRTCClient("puj_3", defaultServerPort, &testclient.Options{AutoSubscribe: true})
|
||||
defer stopClients(c1, c2, c3)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("puj_1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("puj_2", secondServerPort, useSinglePeerConnection, &testclient.Options{AutoSubscribe: true})
|
||||
c3 := createRTCClient("puj_3", defaultServerPort, useSinglePeerConnection, &testclient.Options{AutoSubscribe: true})
|
||||
defer stopClients(c1, c2, c3)
|
||||
|
||||
waitUntilConnected(t, c1, c2, c3)
|
||||
waitUntilConnected(t, c1, c2, c3)
|
||||
|
||||
// c1 and c2 publishing, c3 just receiving
|
||||
writers := publishTracksForClients(t, c1, c2)
|
||||
defer stopWriters(writers...)
|
||||
// c1 and c2 publishing, c3 just receiving
|
||||
writers := publishTracksForClients(t, c1, c2)
|
||||
defer stopWriters(writers...)
|
||||
|
||||
logger.Infow("waiting to receive tracks from c1 and c2")
|
||||
testutils.WithTimeout(t, func() string {
|
||||
tracks := c3.SubscribedTracks()
|
||||
if len(tracks[c1.ID()]) != 2 {
|
||||
return "did not receive tracks from c1"
|
||||
}
|
||||
if len(tracks[c2.ID()]) != 2 {
|
||||
return "did not receive tracks from c2"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
logger.Infow("waiting to receive tracks from c1 and c2")
|
||||
testutils.WithTimeout(t, func() string {
|
||||
tracks := c3.SubscribedTracks()
|
||||
if len(tracks[c1.ID()]) != 2 {
|
||||
return "did not receive tracks from c1"
|
||||
}
|
||||
if len(tracks[c2.ID()]) != 2 {
|
||||
return "did not receive tracks from c2"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// after a delay, c2 reconnects, then publishing
|
||||
time.Sleep(syncDelay)
|
||||
c2.Stop()
|
||||
// after a delay, c2 reconnects, then publishing
|
||||
time.Sleep(syncDelay)
|
||||
c2.Stop()
|
||||
|
||||
logger.Infow("waiting for c2 tracks to be gone")
|
||||
testutils.WithTimeout(t, func() string {
|
||||
tracks := c3.SubscribedTracks()
|
||||
logger.Infow("waiting for c2 tracks to be gone")
|
||||
testutils.WithTimeout(t, func() string {
|
||||
tracks := c3.SubscribedTracks()
|
||||
|
||||
if len(tracks[c1.ID()]) != 2 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 2 tracks from c1, actual: %d", len(tracks[c1.ID()]))
|
||||
}
|
||||
if len(tracks[c2.ID()]) != 0 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 0 tracks from c2, actual: %d", len(tracks[c2.ID()]))
|
||||
}
|
||||
if len(c1.SubscribedTracks()[c2.ID()]) != 0 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 0 tracks from c2, actual: %d", len(c1.SubscribedTracks()[c2.ID()]))
|
||||
}
|
||||
return ""
|
||||
})
|
||||
if len(tracks[c1.ID()]) != 2 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 2 tracks from c1, actual: %d", len(tracks[c1.ID()]))
|
||||
}
|
||||
if len(tracks[c2.ID()]) != 0 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 0 tracks from c2, actual: %d", len(tracks[c2.ID()]))
|
||||
}
|
||||
if len(c1.SubscribedTracks()[c2.ID()]) != 0 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 0 tracks from c2, actual: %d", len(c1.SubscribedTracks()[c2.ID()]))
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
logger.Infow("c2 reconnecting")
|
||||
// connect to a diff port
|
||||
c2 = createRTCClient("puj_2", defaultServerPort, nil)
|
||||
defer c2.Stop()
|
||||
waitUntilConnected(t, c2)
|
||||
writers = publishTracksForClients(t, c2)
|
||||
defer stopWriters(writers...)
|
||||
logger.Infow("c2 reconnecting")
|
||||
// connect to a diff port
|
||||
c2 = createRTCClient("puj_2", defaultServerPort, useSinglePeerConnection, nil)
|
||||
defer c2.Stop()
|
||||
waitUntilConnected(t, c2)
|
||||
writers = publishTracksForClients(t, c2)
|
||||
defer stopWriters(writers...)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
tracks := c3.SubscribedTracks()
|
||||
// "new c2 tracks should be published again",
|
||||
if len(tracks[c2.ID()]) != 2 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 2 tracks from c2, actual: %d", len(tracks[c2.ID()]))
|
||||
}
|
||||
if len(c1.SubscribedTracks()[c2.ID()]) != 2 {
|
||||
return fmt.Sprintf("c1 should be subscribed to 2 tracks from c2, actual: %d", len(c1.SubscribedTracks()[c2.ID()]))
|
||||
}
|
||||
return ""
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
tracks := c3.SubscribedTracks()
|
||||
// "new c2 tracks should be published again",
|
||||
if len(tracks[c2.ID()]) != 2 {
|
||||
return fmt.Sprintf("c3 should be subscribed to 2 tracks from c2, actual: %d", len(tracks[c2.ID()]))
|
||||
}
|
||||
if len(c1.SubscribedTracks()[c2.ID()]) != 2 {
|
||||
return fmt.Sprintf("c1 should be subscribed to 2 tracks from c2, actual: %d", len(c1.SubscribedTracks()[c2.ID()]))
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func scenarioReceiveBeforePublish(t *testing.T) {
|
||||
c1 := createRTCClient("rbp_1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("rbp_2", defaultServerPort, nil)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("rbp_1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("rbp_2", defaultServerPort, useSinglePeerConnection, nil)
|
||||
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
|
||||
// c1 publishes
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
// c1 publishes
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
|
||||
// c2 should see some bytes flowing through
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if c2.BytesReceived() > 20 {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprintf("c2 only received %d bytes", c2.BytesReceived())
|
||||
}
|
||||
})
|
||||
// c2 should see some bytes flowing through
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if c2.BytesReceived() > 20 {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprintf("c2 only received %d bytes", c2.BytesReceived())
|
||||
}
|
||||
})
|
||||
|
||||
// now publish on C2
|
||||
writers = publishTracksForClients(t, c2)
|
||||
defer stopWriters(writers...)
|
||||
// now publish on C2
|
||||
writers = publishTracksForClients(t, c2)
|
||||
defer stopWriters(writers...)
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.SubscribedTracks()[c2.ID()]) == 2 {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprintf("expected c1 to receive 2 tracks from c2, actual: %d", len(c1.SubscribedTracks()[c2.ID()]))
|
||||
}
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.SubscribedTracks()[c2.ID()]) == 2 {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprintf("expected c1 to receive 2 tracks from c2, actual: %d", len(c1.SubscribedTracks()[c2.ID()]))
|
||||
}
|
||||
})
|
||||
|
||||
// now leave, and ensure that it's immediate
|
||||
c2.Stop()
|
||||
// now leave, and ensure that it's immediate
|
||||
c2.Stop()
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.RemoteParticipants()) > 0 {
|
||||
return fmt.Sprintf("expected no remote participants, actual: %v", c1.RemoteParticipants())
|
||||
}
|
||||
return ""
|
||||
})
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if len(c1.RemoteParticipants()) > 0 {
|
||||
return fmt.Sprintf("expected no remote participants, actual: %v", c1.RemoteParticipants())
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func scenarioDataPublish(t *testing.T) {
|
||||
c1 := createRTCClient("dp1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("dp2", secondServerPort, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("dp1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("dp2", secondServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
|
||||
payload := "test bytes"
|
||||
payload := "test bytes"
|
||||
|
||||
received := atomic.NewBool(false)
|
||||
c2.OnDataReceived = func(data []byte, sid string) {
|
||||
if string(data) == payload && livekit.ParticipantID(sid) == c1.ID() {
|
||||
received.Store(true)
|
||||
}
|
||||
received := atomic.NewBool(false)
|
||||
c2.OnDataReceived = func(data []byte, sid string) {
|
||||
if string(data) == payload && livekit.ParticipantID(sid) == c1.ID() {
|
||||
received.Store(true)
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, c1.PublishData([]byte(payload), livekit.DataPacket_RELIABLE))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if received.Load() {
|
||||
return ""
|
||||
} else {
|
||||
return "c2 did not receive published data"
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(t, c1.PublishData([]byte(payload), livekit.DataPacket_RELIABLE))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if received.Load() {
|
||||
return ""
|
||||
} else {
|
||||
return "c2 did not receive published data"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func scenarioDataUnlabeledPublish(t *testing.T) {
|
||||
c1 := createRTCClient("dp1", defaultServerPort, nil)
|
||||
c2 := createRTCClient("dp2", secondServerPort, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("dp1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
c2 := createRTCClient("dp2", secondServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1, c2)
|
||||
defer stopClients(c1, c2)
|
||||
|
||||
payload := "test unlabeled bytes"
|
||||
payload := "test unlabeled bytes"
|
||||
|
||||
received := atomic.NewBool(false)
|
||||
c2.OnDataReceived = func(data []byte, _sid string) {
|
||||
if string(data) == payload {
|
||||
received.Store(true)
|
||||
}
|
||||
received := atomic.NewBool(false)
|
||||
c2.OnDataReceived = func(data []byte, _sid string) {
|
||||
if string(data) == payload {
|
||||
received.Store(true)
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, c1.PublishDataUnlabeled([]byte(payload)))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if received.Load() {
|
||||
return ""
|
||||
} else {
|
||||
return "c2 did not receive published data unlabeled"
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(t, c1.PublishDataUnlabeled([]byte(payload)))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if received.Load() {
|
||||
return ""
|
||||
} else {
|
||||
return "c2 did not receive published data unlabeled"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func scenarioJoinClosedRoom(t *testing.T) {
|
||||
c1 := createRTCClient("jcr1", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("jcr1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
|
||||
// close room with room client
|
||||
_, err := roomClient.DeleteRoom(contextWithToken(createRoomToken()), &livekit.DeleteRoomRequest{
|
||||
Room: testRoom,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// close room with room client
|
||||
_, err := roomClient.DeleteRoom(contextWithToken(createRoomToken()), &livekit.DeleteRoomRequest{
|
||||
Room: testRoom,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// now join again
|
||||
c2 := createRTCClient("jcr2", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c2)
|
||||
stopClients(c2)
|
||||
// now join again
|
||||
c2 := createRTCClient("jcr2", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c2)
|
||||
stopClients(c2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// close a room that has been created, but no participant has joined
|
||||
|
||||
+618
-554
File diff suppressed because it is too large
Load Diff
+71
-67
@@ -45,78 +45,82 @@ func TestWebhooks(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer finish()
|
||||
|
||||
c1 := createRTCClient("c1", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventRoomStarted) == nil {
|
||||
return "did not receive RoomStarted"
|
||||
}
|
||||
if ts.GetEvent(webhook.EventParticipantJoined) == nil {
|
||||
return "did not receive ParticipantJoined"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
for _, useSinglePeerConnection := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("singlePeerConnection=%+v", useSinglePeerConnection), func(t *testing.T) {
|
||||
c1 := createRTCClient("c1", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c1)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventRoomStarted) == nil {
|
||||
return "did not receive RoomStarted"
|
||||
}
|
||||
if ts.GetEvent(webhook.EventParticipantJoined) == nil {
|
||||
return "did not receive ParticipantJoined"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// first participant join should have started the room
|
||||
started := ts.GetEvent(webhook.EventRoomStarted)
|
||||
require.Equal(t, testRoom, started.Room.Name)
|
||||
require.NotEmpty(t, started.Id)
|
||||
require.Greater(t, started.CreatedAt, time.Now().Unix()-100)
|
||||
require.GreaterOrEqual(t, time.Now().Unix(), started.CreatedAt)
|
||||
joined := ts.GetEvent(webhook.EventParticipantJoined)
|
||||
require.Equal(t, "c1", joined.Participant.Identity)
|
||||
ts.ClearEvents()
|
||||
// first participant join should have started the room
|
||||
started := ts.GetEvent(webhook.EventRoomStarted)
|
||||
require.Equal(t, testRoom, started.Room.Name)
|
||||
require.NotEmpty(t, started.Id)
|
||||
require.Greater(t, started.CreatedAt, time.Now().Unix()-100)
|
||||
require.GreaterOrEqual(t, time.Now().Unix(), started.CreatedAt)
|
||||
joined := ts.GetEvent(webhook.EventParticipantJoined)
|
||||
require.Equal(t, "c1", joined.Participant.Identity)
|
||||
ts.ClearEvents()
|
||||
|
||||
// another participant joins
|
||||
c2 := createRTCClient("c2", defaultServerPort, nil)
|
||||
waitUntilConnected(t, c2)
|
||||
defer c2.Stop()
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventParticipantJoined) == nil {
|
||||
return "did not receive ParticipantJoined"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
joined = ts.GetEvent(webhook.EventParticipantJoined)
|
||||
require.Equal(t, "c2", joined.Participant.Identity)
|
||||
ts.ClearEvents()
|
||||
// another participant joins
|
||||
c2 := createRTCClient("c2", defaultServerPort, useSinglePeerConnection, nil)
|
||||
waitUntilConnected(t, c2)
|
||||
defer c2.Stop()
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventParticipantJoined) == nil {
|
||||
return "did not receive ParticipantJoined"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
joined = ts.GetEvent(webhook.EventParticipantJoined)
|
||||
require.Equal(t, "c2", joined.Participant.Identity)
|
||||
ts.ClearEvents()
|
||||
|
||||
// track published
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
ev := ts.GetEvent(webhook.EventTrackPublished)
|
||||
if ev == nil {
|
||||
return "did not receive TrackPublished"
|
||||
}
|
||||
require.NotNil(t, ev.Track, "TrackPublished did not include trackInfo")
|
||||
require.Equal(t, string(c1.ID()), ev.Participant.Sid)
|
||||
return ""
|
||||
})
|
||||
ts.ClearEvents()
|
||||
// track published
|
||||
writers := publishTracksForClients(t, c1)
|
||||
defer stopWriters(writers...)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
ev := ts.GetEvent(webhook.EventTrackPublished)
|
||||
if ev == nil {
|
||||
return "did not receive TrackPublished"
|
||||
}
|
||||
require.NotNil(t, ev.Track, "TrackPublished did not include trackInfo")
|
||||
require.Equal(t, string(c1.ID()), ev.Participant.Sid)
|
||||
return ""
|
||||
})
|
||||
ts.ClearEvents()
|
||||
|
||||
// first participant leaves
|
||||
c1.Stop()
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventParticipantLeft) == nil {
|
||||
return "did not receive ParticipantLeft"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
left := ts.GetEvent(webhook.EventParticipantLeft)
|
||||
require.Equal(t, "c1", left.Participant.Identity)
|
||||
ts.ClearEvents()
|
||||
// first participant leaves
|
||||
c1.Stop()
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventParticipantLeft) == nil {
|
||||
return "did not receive ParticipantLeft"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
left := ts.GetEvent(webhook.EventParticipantLeft)
|
||||
require.Equal(t, "c1", left.Participant.Identity)
|
||||
ts.ClearEvents()
|
||||
|
||||
// room closed
|
||||
rm := server.RoomManager().GetRoom(context.Background(), testRoom)
|
||||
rm.Close(types.ParticipantCloseReasonNone)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventRoomFinished) == nil {
|
||||
return "did not receive RoomFinished"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
require.Equal(t, testRoom, ts.GetEvent(webhook.EventRoomFinished).Room.Name)
|
||||
// room closed
|
||||
rm := server.RoomManager().GetRoom(context.Background(), testRoom)
|
||||
rm.Close(types.ParticipantCloseReasonNone)
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if ts.GetEvent(webhook.EventRoomFinished) == nil {
|
||||
return "did not receive RoomFinished"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
require.Equal(t, testRoom, ts.GetEvent(webhook.EventRoomFinished).Room.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupServerWithWebhook() (server *service.LivekitServer, testServer *webhookTestServer, finishFunc func(), err error) {
|
||||
|
||||
Reference in New Issue
Block a user