diff --git a/config/config.go b/config/config.go index 134743b8..aa7211c6 100644 --- a/config/config.go +++ b/config/config.go @@ -54,6 +54,8 @@ type Config struct { TurnExternalSecret string `split_words:"true"` TrustProxyHeaders bool `split_words:"true"` + SFUMode bool `default:"false" split_words:"true"` + TestClient bool `default:"false" split_words:"true"` AuthMode string `default:"turn" split_words:"true"` CorsAllowedOrigins []string `split_words:"true"` UsersFile string `split_words:"true"` diff --git a/go.mod b/go.mod index 51785474..8b818981 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,11 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 + github.com/pion/interceptor v0.1.45 github.com/pion/randutil v0.1.0 - github.com/pion/turn/v4 v4.1.4 + github.com/pion/rtcp v1.2.16 + github.com/pion/turn/v5 v5.0.9 + github.com/pion/webrtc/v4 v4.2.15 github.com/prometheus/client_golang v1.23.2 github.com/rs/xid v1.6.0 github.com/rs/zerolog v1.35.1 @@ -27,16 +30,22 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pion/dtls/v3 v3.0.7 // indirect + github.com/pion/datachannel v1.6.0 // indirect + github.com/pion/dtls/v3 v3.1.4 // indirect + github.com/pion/ice/v4 v4.2.7 // indirect github.com/pion/logging v0.2.4 // indirect - github.com/pion/stun/v3 v3.0.1 // indirect - github.com/pion/transport/v3 v3.1.1 // indirect - github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pion/mdns/v2 v2.1.0 // indirect + github.com/pion/rtp v1.10.2 // indirect + github.com/pion/sctp v1.10.0 // indirect + github.com/pion/sdp/v3 v3.0.18 // indirect + github.com/pion/srtp/v3 v3.0.11 // indirect + github.com/pion/stun/v3 v3.1.5 // indirect + github.com/pion/transport/v4 v4.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect @@ -44,7 +53,9 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/sys v0.43.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b4a089be..4b44a377 100644 --- a/go.sum +++ b/go.sum @@ -5,7 +5,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -15,6 +14,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -43,20 +44,40 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= -github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= +github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= +github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= +github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= +github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= +github.com/pion/ice/v4 v4.2.7 h1:zDEbC6MiEdhQpF8TxBOTws+NU6ZgGpveHrQq4Lc1kao= +github.com/pion/ice/v4 v4.2.7/go.mod h1:9SNPaq0c7El/ki8leJzyCkK10zsskprR3zTNbO3monY= +github.com/pion/interceptor v0.1.45 h1:6PUo/5829bIfRFIPPJQzuDn8EjxRTSB/CSD7QVCOaqo= +github.com/pion/interceptor v0.1.45/go.mod h1:gNDYM/uFKcLe/B3gS2/7+aw6z+RDiMy2qKTnF1LO31w= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= +github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA= -github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo= +github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk= +github.com/pion/sctp v1.10.0 h1:qeoD6swF/2M5bYRcAGayqSbTKX3m4AW29CiQxG1+Pfg= +github.com/pion/sctp v1.10.0/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= +github.com/pion/srtp/v3 v3.0.11 h1:GiESUr54/K4UuPigfq/CvWUed80JenQAHXn0C2MQQIQ= +github.com/pion/srtp/v3 v3.0.11/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns= +github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= +github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= -github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= -github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= -github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ= -github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ= +github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= +github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= +github.com/pion/turn/v5 v5.0.9 h1:zNeBfRyzGn7MPyUTvmvxeltLEjlFdSLPT1tlakoaOXM= +github.com/pion/turn/v5 v5.0.9/go.mod h1:u3XjBqy2Z4+NhCUpDoOSsNuQDrPLvKStlCGWk6sTQ1E= +github.com/pion/webrtc/v4 v4.2.15 h1:Ir/MauNFCfg+kgyBYPQLiGdVWFlzEcLxqtuzAkYkky0= +github.com/pion/webrtc/v4 v4.2.15/go.mod h1:CPTcyLfIzC4scOkQ4UY4pj6WvbUGhcNLIpK28cP5h6M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= @@ -104,8 +125,8 @@ golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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= diff --git a/router/router.go b/router/router.go index 99eaabbd..a0e28eb6 100644 --- a/router/router.go +++ b/router/router.go @@ -71,7 +71,7 @@ func Router(conf config.Config, rooms *ws.Rooms, users *auth.Users, version stri router.Methods("GET").Path("/metrics").Handler(basicAuth(promhttp.Handler(), users)) } - ui.Register(router) + ui.Register(router, conf.TestClient) return router } diff --git a/turn/none.go b/turn/none.go index 24a2ef9b..44bbdba2 100644 --- a/turn/none.go +++ b/turn/none.go @@ -2,8 +2,10 @@ package turn import ( "errors" + "fmt" "net" - "strconv" + + "github.com/pion/turn/v5" ) type RelayAddressGeneratorNone struct{} @@ -12,8 +14,8 @@ func (r *RelayAddressGeneratorNone) Validate() error { return nil } -func (r *RelayAddressGeneratorNone) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) { - conn, err := net.ListenPacket("udp", ":"+strconv.Itoa(requestedPort)) +func (r *RelayAddressGeneratorNone) AllocatePacketConn(conf turn.AllocateListenerConfig) (net.PacketConn, net.Addr, error) { + conn, err := net.ListenPacket("udp", fmt.Sprintf(":%d", conf.RequestedPort)) if err != nil { return nil, nil, err } @@ -21,6 +23,10 @@ func (r *RelayAddressGeneratorNone) AllocatePacketConn(network string, requested return conn, conn.LocalAddr(), nil } -func (r *RelayAddressGeneratorNone) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) { - return nil, nil, errors.New("todo") +func (r *RelayAddressGeneratorNone) AllocateListener(_ turn.AllocateListenerConfig) (net.Listener, net.Addr, error) { + return nil, nil, errors.New("TCP relay not supported") +} + +func (r *RelayAddressGeneratorNone) AllocateConn(_ turn.AllocateConnConfig) (net.Conn, error) { + return nil, errors.New("TCP relay not supported") } diff --git a/turn/portrange.go b/turn/portrange.go index bd7ff79a..903c0caf 100644 --- a/turn/portrange.go +++ b/turn/portrange.go @@ -6,6 +6,7 @@ import ( "net" "github.com/pion/randutil" + "github.com/pion/turn/v5" ) type RelayAddressGeneratorPortRange struct { @@ -22,9 +23,9 @@ func (r *RelayAddressGeneratorPortRange) Validate() error { return nil } -func (r *RelayAddressGeneratorPortRange) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) { - if requestedPort != 0 { - conn, err := net.ListenPacket("udp", fmt.Sprintf(":%d", requestedPort)) +func (r *RelayAddressGeneratorPortRange) AllocatePacketConn(conf turn.AllocateListenerConfig) (net.PacketConn, net.Addr, error) { + if conf.RequestedPort != 0 { + conn, err := net.ListenPacket("udp", fmt.Sprintf(":%d", conf.RequestedPort)) if err != nil { return nil, nil, err } @@ -46,6 +47,10 @@ func (r *RelayAddressGeneratorPortRange) AllocatePacketConn(network string, requ return nil, nil, errors.New("could not find free port: max retries exceeded") } -func (r *RelayAddressGeneratorPortRange) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) { - return nil, nil, errors.New("todo") +func (r *RelayAddressGeneratorPortRange) AllocateListener(_ turn.AllocateListenerConfig) (net.Listener, net.Addr, error) { + return nil, nil, errors.New("TCP relay not supported") +} + +func (r *RelayAddressGeneratorPortRange) AllocateConn(_ turn.AllocateConnConfig) (net.Conn, error) { + return nil, errors.New("TCP relay not supported") } diff --git a/turn/server.go b/turn/server.go index 9d78e2a9..97967a34 100644 --- a/turn/server.go +++ b/turn/server.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/pion/turn/v4" + "github.com/pion/turn/v5" "github.com/rs/zerolog/log" "github.com/screego/server/config" "github.com/screego/server/config/ipdns" @@ -43,8 +43,8 @@ type Generator struct { IPProvider ipdns.Provider } -func (r *Generator) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) { - conn, addr, err := r.RelayAddressGenerator.AllocatePacketConn(network, requestedPort) +func (r *Generator) AllocatePacketConn(conf turn.AllocateListenerConfig) (net.PacketConn, net.Addr, error) { + conn, addr, err := r.RelayAddressGenerator.AllocatePacketConn(conf) if err != nil { return conn, addr, err } @@ -155,19 +155,19 @@ func (a *ExternalServer) Disallow(username string) { // not supported, will expire on TTL } -func (a *InternalServer) authenticate(username, realm string, addr net.Addr) ([]byte, bool) { +func (a *InternalServer) authenticate(ra *turn.RequestAttributes) (string, []byte, bool) { a.lock.RLock() defer a.lock.RUnlock() - entry, ok := a.lookup[username] + entry, ok := a.lookup[ra.Username] if !ok { - log.Debug().Interface("addr", addr).Str("username", username).Msg("TURN username not found") - return nil, false + log.Debug().Interface("addr", ra.SrcAddr).Str("username", ra.Username).Msg("TURN username not found") + return "", nil, false } - log.Debug().Interface("addr", addr.String()).Str("realm", realm).Msg("TURN authenticated") - return entry.password, true + log.Debug().Str("addr", ra.SrcAddr.String()).Str("realm", ra.Realm).Msg("TURN authenticated") + return ra.Username, entry.password, true } func (a *InternalServer) Credentials(id string, addr net.IP) (string, string) { diff --git a/ui/public/test-client.html b/ui/public/test-client.html new file mode 100644 index 00000000..8428c17a --- /dev/null +++ b/ui/public/test-client.html @@ -0,0 +1,205 @@ + + + + +screego SFU test client + + + +

screego SFU test client

+ +
+ + + + +
+ +
Disconnected
+ +
+ + + + diff --git a/ui/serve.go b/ui/serve.go index f3c839e3..594eb87b 100644 --- a/ui/serve.go +++ b/ui/serve.go @@ -15,11 +15,14 @@ var buildFiles embed.FS var files, _ = fs.Sub(buildFiles, "build") // Register registers the ui on the root path. -func Register(r *mux.Router) { +func Register(r *mux.Router, testClient bool) { r.Handle("/", serveFile("index.html", "text/html")) r.Handle("/index.html", serveFile("index.html", "text/html")) r.Handle("/assets/{resource}", http.FileServer(http.FS(files))) + if testClient { + r.Handle("/test-client.html", serveFile("test-client.html", "text/html")) + } r.Handle("/favicon.ico", serveFile("favicon.ico", "image/x-icon")) r.Handle("/logo.svg", serveFile("logo.svg", "image/svg+xml")) r.Handle("/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")) diff --git a/ui/src/Room.tsx b/ui/src/Room.tsx index de04cf8f..72e8e559 100644 --- a/ui/src/Room.tsx +++ b/ui/src/Room.tsx @@ -7,6 +7,9 @@ import PeopleIcon from '@mui/icons-material/People'; import VolumeMuteIcon from '@mui/icons-material/VolumeOff'; import VolumeIcon from '@mui/icons-material/VolumeUp'; import SettingsIcon from '@mui/icons-material/Settings'; +import CloseIcon from '@mui/icons-material/Close'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import {useHotkeys} from 'react-hotkeys-hook'; import {Video} from './Video'; import {makeStyles} from 'tss-react/mui'; @@ -58,11 +61,15 @@ export const Room = ({ share, stopShare, setName, + subscribeStream, + unsubscribeStream, }: { state: ConnectedRoom; share: () => void; stopShare: () => void; setName: (name: string) => void; + subscribeStream: (userID: string) => void; + unsubscribeStream: (sessionID: string) => void; }) => { const {classes} = useStyles(); const [open, setOpen] = React.useState(false); @@ -70,8 +77,10 @@ export const Room = ({ const [settings, setSettings] = useSettings(); const [showControl, setShowControl] = React.useState(true); const [hoverControl, setHoverControl] = React.useState(false); + const [showStrip, setShowStrip] = React.useState(true); const [selectedStream, setSelectedStream] = React.useState(); const [videoElement, setVideoElement] = React.useState(null); + const explicitDeselect = React.useRef(false); useShowOnMouseMovement(setShowControl); @@ -79,18 +88,30 @@ export const Room = ({ React.useEffect(() => { if (selectedStream === HostStream && state.hostStream) { + explicitDeselect.current = false; return; } if (state.clientStreams.some(({id}) => id === selectedStream)) { + explicitDeselect.current = false; return; } if (state.clientStreams.length === 0 && selectedStream) { setSelectedStream(undefined); return; } - setSelectedStream(state.clientStreams[0]?.id); + if (!explicitDeselect.current) { + setSelectedStream(state.clientStreams[0]?.id); + } }, [state.clientStreams, selectedStream, state.hostStream]); + React.useEffect(() => { + if (!showStrip) { + state.clientStreams + .filter((cs) => cs.id !== selectedStream) + .forEach((cs) => unsubscribeStream(cs.id)); + } + }, [state.clientStreams, showStrip, selectedStream, unsubscribeStream]); + const stream = selectedStream === HostStream ? state.hostStream @@ -214,11 +235,30 @@ export const Room = ({ )} {stream ? ( -