├── .github ├── banner_dark.png ├── banner_light.png ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── release.yaml │ ├── docker.yaml │ └── buildtest.yaml ├── deploy └── README.md ├── pkg ├── utils │ ├── iceconfigcache_test.go │ ├── logging.go │ ├── protocol.go │ ├── iceconfigcache.go │ ├── math.go │ ├── context.go │ ├── incrementaldispatcher.go │ ├── incrementaldispatcher_test.go │ ├── changenotifier.go │ └── opsqueue.go ├── sfu │ ├── errors.go │ ├── buffer │ │ ├── streamstats.go │ │ ├── rtcpreader.go │ │ ├── videolayer.go │ │ ├── datastats_test.go │ │ ├── datastats.go │ │ ├── frameintegrity_test.go │ │ └── helpers_test.go │ ├── NOTICE │ ├── videolayerselector │ │ ├── null.go │ │ ├── temporallayerselector │ │ │ ├── temporallayerselector.go │ │ │ ├── null.go │ │ │ └── vp8.go │ │ ├── videolayerselector.go │ │ ├── framenumberwrapper.go │ │ ├── decodetarget.go │ │ └── vp9.go │ ├── sfu.go │ ├── pacer │ │ ├── packet_time.go │ │ ├── pass_through.go │ │ ├── pacer.go │ │ ├── no_queue.go │ │ └── base.go │ ├── codecmunger │ │ ├── codecmunger.go │ │ └── null.go │ ├── rtpextension │ │ ├── playoutdelay │ │ │ ├── playoutdelay_test.go │ │ │ └── playoutdelay.go │ │ └── dependencydescriptor │ │ │ └── dependencydescriptorextension_test.go │ ├── streamtracker │ │ ├── interfaces.go │ │ ├── streamtracker_packet.go │ │ └── streamtracker_dd_test.go │ ├── streamallocator │ │ └── streamstateupdate.go │ ├── playoutdelay_test.go │ ├── forwardstats.go │ ├── utils │ │ └── helpers.go │ ├── testutils │ │ └── data.go │ └── downtrackspreader.go ├── service │ ├── basic_auth.go │ ├── utils.go │ ├── docker_test.go │ ├── utils_test.go │ ├── auth_test.go │ ├── agent_dispatch_service.go │ ├── clients.go │ ├── errors.go │ ├── roomallocator_test.go │ └── ioservice_ingress.go ├── telemetry │ ├── prometheus │ │ ├── node_nonlinux.go │ │ ├── node_windows.go │ │ ├── node_linux.go │ │ ├── node_nonwindows.go │ │ └── quality.go │ ├── analyticsservice.go │ └── stats.go ├── clientconfiguration │ ├── types.go │ ├── conf.go │ ├── staticconfiguration.go │ └── match.go ├── routing │ ├── selector │ │ ├── any.go │ │ ├── errors.go │ │ ├── utils_test.go │ │ ├── cpuload_test.go │ │ ├── cpuload.go │ │ ├── sysload.go │ │ ├── sortby_test.go │ │ ├── interfaces.go │ │ └── sysload_test.go │ ├── node.go │ ├── messagechannel_test.go │ ├── errors.go │ ├── roommanager.go │ └── messagechannel.go ├── rtc │ ├── transport │ │ ├── negotiationstate.go │ │ └── handler.go │ ├── utils_test.go │ ├── clientinfo_test.go │ ├── mediaengine_test.go │ ├── errors.go │ ├── types │ │ └── protocol_version.go │ ├── medialossproxy.go │ └── testutils.go ├── testutils │ └── timeout.go └── config │ ├── configtest │ └── checkyamltag.go │ └── config_test.go ├── .gitignore ├── NOTICE ├── version └── version.go ├── magefile_windows.go ├── tools └── tools.go ├── renovate.json ├── bootstrap.sh ├── magefile_unix.go ├── Dockerfile ├── .goreleaser.yaml ├── cmd └── server │ └── main_test.go └── install-livekit.sh /.github/banner_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afgarcia86/livekit/master/.github/banner_dark.png -------------------------------------------------------------------------------- /.github/banner_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afgarcia86/livekit/master/.github/banner_light.png -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # LiveKit Server Deployment 2 | 3 | Deployment Guides: 4 | 5 | - [Deploy to a VM](https://docs.livekit.io/deploy/vm) 6 | - [Deploy to Kubernetes](https://docs.livekit.io/deploy/kubernetes) 7 | 8 | Also included are Grafana charts for metrics gathered in Prometheus. 9 | -------------------------------------------------------------------------------- /pkg/utils/iceconfigcache_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/livekit/protocol/livekit" 10 | ) 11 | 12 | func TestIceConfigCache(t *testing.T) { 13 | cache := NewIceConfigCache[string](10 * time.Second) 14 | t.Cleanup(cache.Stop) 15 | 16 | cache.Put("test", &livekit.ICEConfig{}) 17 | require.NotNil(t, cache) 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # checksums of file tree 12 | .checksumgo 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | livekit-server 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | bin/ 21 | proto/ 22 | 23 | # Mac 24 | .DS_Store 25 | 26 | # IDE 27 | .idea/ 28 | 29 | dist/ 30 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2023 LiveKit, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /pkg/sfu/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sfu 16 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | const Version = "1.7.2" 18 | -------------------------------------------------------------------------------- /magefile_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build mage 16 | // +build mage 17 | 18 | package main 19 | 20 | func setULimit() error { 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /pkg/service/basic_auth.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func GenBasicAuthMiddleware(username string, password string) (func(http.ResponseWriter, *http.Request, http.HandlerFunc) ) { 8 | return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 9 | given_username, given_password, ok := r.BasicAuth() 10 | unauthorized := func() { 11 | rw.Header().Set("WWW-Authenticate", "Basic realm=\"Protected Area\"") 12 | rw.WriteHeader(http.StatusUnauthorized) 13 | } 14 | if !ok { 15 | unauthorized() 16 | return 17 | } 18 | 19 | if given_username != username { 20 | unauthorized() 21 | return 22 | } 23 | 24 | if given_password != password { 25 | unauthorized() 26 | return 27 | } 28 | 29 | next(rw, r) 30 | } 31 | } -------------------------------------------------------------------------------- /pkg/sfu/buffer/streamstats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buffer 16 | 17 | type StreamStatsWithLayers struct { 18 | RTPStats *RTPDeltaInfo 19 | Layers map[int32]*RTPDeltaInfo 20 | } 21 | -------------------------------------------------------------------------------- /pkg/sfu/NOTICE: -------------------------------------------------------------------------------- 1 | Portions of this package originated from ion-sfu: https://github.com/pion/ion-sfu. 2 | 3 | MIT License 4 | 5 | Copyright (c) 2019 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | ---------------------------------------------------------------------------------------- 17 | -------------------------------------------------------------------------------- /pkg/telemetry/prometheus/node_nonlinux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !linux 16 | 17 | package prometheus 18 | 19 | func getTCStats() (packets, drops uint32, err error) { 20 | // linux only 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build tools 16 | // +build tools 17 | 18 | package tools 19 | 20 | import ( 21 | _ "github.com/google/wire/cmd/wire" 22 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 23 | ) 24 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "commitBody": "Generated by renovateBot", 7 | "packageRules": [ 8 | { 9 | "matchManagers": ["github-actions"], 10 | "groupName": "github workflows" 11 | }, 12 | { 13 | "matchManagers": ["dockerfile"], 14 | "groupName": "docker deps" 15 | }, 16 | { 17 | "matchManagers": ["gomod"], 18 | "groupName": "go deps" 19 | }, 20 | { 21 | "matchManagers": ["gomod"], 22 | "matchPackagePrefixes": ["github.com/pion"], 23 | "groupName": "pion deps" 24 | }, 25 | { 26 | "matchManagers": ["gomod"], 27 | "matchPackagePrefixes": ["github.com/livekit"], 28 | "groupName": "livekit deps" 29 | } 30 | ], 31 | "postUpdateOptions": [ 32 | "gomodTidy" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /pkg/clientconfiguration/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package clientconfiguration 16 | 17 | import ( 18 | "github.com/livekit/protocol/livekit" 19 | ) 20 | 21 | type ClientConfigurationManager interface { 22 | GetConfiguration(clientInfo *livekit.ClientInfo) *livekit.ClientConfiguration 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: File a bug report and help us improve LiveKit 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Server** 14 | - Version: [0.x.x] 15 | - Environment: [e.g. local dev, EKS] 16 | - any other information about your deployment setup 17 | 18 | **Client** 19 | - SDK: [e.g. js, ios] 20 | - Version: [0.x.x] 21 | 22 | **To Reproduce** 23 | Steps to reproduce the behavior: 24 | 1. two clients are connected to room 25 | 2. 3rd client joins 26 | 3. 3rd client does '...' 27 | 4. See error 28 | 29 | **Expected behavior** 30 | A clear and concise description of what you expected to happen. 31 | 32 | **Screenshots** 33 | If applicable, add screenshots to help explain your problem. 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/null.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package videolayerselector 16 | 17 | import ( 18 | "github.com/livekit/protocol/logger" 19 | ) 20 | 21 | type Null struct { 22 | *Base 23 | } 24 | 25 | func NewNull(logger logger.Logger) *Null { 26 | return &Null{ 27 | Base: NewBase(logger), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/sfu/sfu.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sfu 16 | 17 | import ( 18 | "sync" 19 | ) 20 | 21 | var ( 22 | PacketFactory *sync.Pool 23 | ) 24 | 25 | func init() { 26 | // Init packet factory 27 | PacketFactory = &sync.Pool{ 28 | New: func() interface{} { 29 | b := make([]byte, 1460) 30 | return &b 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/temporallayerselector/temporallayerselector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package temporallayerselector 16 | 17 | import "github.com/livekit/livekit-server/pkg/sfu/buffer" 18 | 19 | type TemporalLayerSelector interface { 20 | Select(extPkt *buffer.ExtPacket, current int32, target int32) (this int32, next int32) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/utils/logging.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 LiveKit, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | const ( 20 | ComponentPub = "pub" 21 | ComponentSub = "sub" 22 | ComponentRoom = "room" 23 | ComponentAPI = "api" 24 | ComponentTransport = "transport" 25 | ComponentSFU = "sfu" 26 | // transport subcomponents 27 | ComponentCongestionControl = "cc" 28 | ) 29 | -------------------------------------------------------------------------------- /pkg/telemetry/prometheus/node_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | /* 4 | * Copyright 2023 LiveKit, Inc 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package prometheus 20 | 21 | import "github.com/mackerelio/go-osstat/loadavg" 22 | 23 | func getLoadAvg() (*loadavg.Stats, error) { 24 | return &loadavg.Stats{}, nil 25 | } 26 | 27 | func getCPUStats() (cpuLoad float32, numCPUs uint32, err error) { 28 | return 1, 1, nil 29 | } 30 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2023 LiveKit, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | if ! command -v mage &> /dev/null 18 | then 19 | pushd /tmp 20 | git clone https://github.com/magefile/mage 21 | cd mage 22 | go run bootstrap.go 23 | rm -rf /tmp/mage 24 | popd 25 | fi 26 | 27 | if ! command -v mage &> /dev/null 28 | then 29 | echo "Ensure `go env GOPATH`/bin is in your \$PATH" 30 | exit 1 31 | fi 32 | 33 | go mod download 34 | -------------------------------------------------------------------------------- /pkg/utils/protocol.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 LiveKit, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "google.golang.org/protobuf/proto" 21 | 22 | "github.com/livekit/protocol/livekit" 23 | ) 24 | 25 | func ClientInfoWithoutAddress(c *livekit.ClientInfo) *livekit.ClientInfo { 26 | if c == nil { 27 | return nil 28 | } 29 | clone := proto.Clone(c).(*livekit.ClientInfo) 30 | clone.Address = "" 31 | return clone 32 | } 33 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/temporallayerselector/null.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package temporallayerselector 16 | 17 | import ( 18 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 19 | ) 20 | 21 | type Null struct{} 22 | 23 | func NewNull() *Null { 24 | return &Null{} 25 | } 26 | 27 | func Select(_extPkt *buffer.ExtPacket, current int32, _target int32) (this int32, next int32) { 28 | this = current 29 | next = current 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /pkg/utils/iceconfigcache.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jellydator/ttlcache/v3" 7 | 8 | "github.com/livekit/protocol/livekit" 9 | ) 10 | 11 | const ( 12 | iceConfigTTLMin = 5 * time.Minute 13 | ) 14 | 15 | type IceConfigCache[T comparable] struct { 16 | c *ttlcache.Cache[T, *livekit.ICEConfig] 17 | } 18 | 19 | func NewIceConfigCache[T comparable](ttl time.Duration) *IceConfigCache[T] { 20 | cache := ttlcache.New( 21 | ttlcache.WithTTL[T, *livekit.ICEConfig](max(ttl, iceConfigTTLMin)), 22 | ttlcache.WithDisableTouchOnHit[T, *livekit.ICEConfig](), 23 | ) 24 | go cache.Start() 25 | 26 | return &IceConfigCache[T]{cache} 27 | } 28 | 29 | func (icc *IceConfigCache[T]) Stop() { 30 | icc.c.Stop() 31 | } 32 | 33 | func (icc *IceConfigCache[T]) Put(key T, iceConfig *livekit.ICEConfig) { 34 | icc.c.Set(key, iceConfig, ttlcache.DefaultTTL) 35 | } 36 | 37 | func (icc *IceConfigCache[T]) Get(key T) *livekit.ICEConfig { 38 | if it := icc.c.Get(key); it != nil { 39 | return it.Value() 40 | } 41 | return &livekit.ICEConfig{} 42 | } 43 | -------------------------------------------------------------------------------- /magefile_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build mage && !windows 16 | // +build mage,!windows 17 | 18 | package main 19 | 20 | import ( 21 | "syscall" 22 | ) 23 | 24 | func setULimit() error { 25 | // raise ulimit on unix 26 | var rLimit syscall.Rlimit 27 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) 28 | if err != nil { 29 | return err 30 | } 31 | rLimit.Max = 10000 32 | rLimit.Cur = 10000 33 | return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/sfu/pacer/packet_time.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pacer 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | type PacketTime struct { 22 | baseTime time.Time 23 | } 24 | 25 | func NewPacketTime() *PacketTime { 26 | return &PacketTime{ 27 | baseTime: time.Now(), 28 | } 29 | } 30 | 31 | func (p *PacketTime) Get() time.Time { 32 | // construct current time based on monotonic clock 33 | return p.baseTime.Add(time.Since(p.baseTime)) 34 | } 35 | 36 | // ------------------------------------------------ 37 | -------------------------------------------------------------------------------- /pkg/routing/selector/any.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector 16 | 17 | import ( 18 | "github.com/livekit/protocol/livekit" 19 | ) 20 | 21 | // AnySelector selects any available node with no limitations 22 | type AnySelector struct { 23 | SortBy string 24 | } 25 | 26 | func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) { 27 | nodes = GetAvailableNodes(nodes) 28 | if len(nodes) == 0 { 29 | return nil, ErrNoAvailableNodes 30 | } 31 | 32 | return SelectSortedNode(nodes, s.SortBy) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/sfu/pacer/pass_through.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pacer 16 | 17 | import ( 18 | "github.com/livekit/protocol/logger" 19 | ) 20 | 21 | type PassThrough struct { 22 | *Base 23 | } 24 | 25 | func NewPassThrough(logger logger.Logger) *PassThrough { 26 | return &PassThrough{ 27 | Base: NewBase(logger), 28 | } 29 | } 30 | 31 | func (p *PassThrough) Stop() { 32 | } 33 | 34 | func (p *PassThrough) Enqueue(pkt Packet) { 35 | p.Base.SendPacket(&pkt) 36 | } 37 | 38 | // ------------------------------------------------ 39 | -------------------------------------------------------------------------------- /pkg/routing/selector/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector 16 | 17 | import "errors" 18 | 19 | var ( 20 | ErrNoAvailableNodes = errors.New("could not find any available nodes") 21 | ErrCurrentRegionNotSet = errors.New("current region cannot be blank") 22 | ErrCurrentRegionUnknownLatLon = errors.New("unknown lat and lon for the current region") 23 | ErrSortByNotSet = errors.New("sort by option cannot be blank") 24 | ErrSortByUnknown = errors.New("unknown sort by option") 25 | ) 26 | -------------------------------------------------------------------------------- /pkg/utils/math.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import "sort" 18 | 19 | // MedianFloat32 gets median value for an array of float32 20 | func MedianFloat32(input []float32) float32 { 21 | num := len(input) 22 | if num == 0 { 23 | return 0 24 | } else if num == 1 { 25 | return input[0] 26 | } 27 | sort.Slice(input, func(i, j int) bool { 28 | return input[i] < input[j] 29 | }) 30 | if num%2 != 0 { 31 | return input[num/2] 32 | } 33 | left := input[num/2-1] 34 | right := input[num/2] 35 | return (left + right) / 2 36 | } 37 | -------------------------------------------------------------------------------- /pkg/rtc/transport/negotiationstate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package transport 16 | 17 | import "fmt" 18 | 19 | type NegotiationState int 20 | 21 | const ( 22 | NegotiationStateNone NegotiationState = iota 23 | // waiting for remote description 24 | NegotiationStateRemote 25 | // need to Negotiate again 26 | NegotiationStateRetry 27 | ) 28 | 29 | func (n NegotiationState) String() string { 30 | switch n { 31 | case NegotiationStateNone: 32 | return "NONE" 33 | case NegotiationStateRemote: 34 | return "WAITING_FOR_REMOTE" 35 | case NegotiationStateRetry: 36 | return "RETRY" 37 | default: 38 | return fmt.Sprintf("%d", int(n)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/telemetry/prometheus/node_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build linux 16 | // +build linux 17 | 18 | package prometheus 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/florianl/go-tc" 24 | ) 25 | 26 | func getTCStats() (packets, drops uint32, err error) { 27 | rtnl, err := tc.Open(&tc.Config{}) 28 | if err != nil { 29 | err = fmt.Errorf("could not open rtnetlink socket: %v", err) 30 | return 31 | } 32 | defer rtnl.Close() 33 | 34 | qdiscs, err := rtnl.Qdisc().Get() 35 | if err != nil { 36 | err = fmt.Errorf("could not get qdiscs: %v", err) 37 | return 38 | } 39 | 40 | for _, qdisc := range qdiscs { 41 | packets = packets + qdisc.Stats.Packets 42 | drops = drops + qdisc.Stats.Drops 43 | } 44 | 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /pkg/sfu/pacer/pacer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pacer 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | 21 | "github.com/pion/rtp" 22 | "github.com/pion/webrtc/v3" 23 | ) 24 | 25 | type ExtensionData struct { 26 | ID uint8 27 | Payload []byte 28 | } 29 | 30 | type Packet struct { 31 | Header *rtp.Header 32 | Extensions []ExtensionData 33 | Payload []byte 34 | AbsSendTimeExtID uint8 35 | TransportWideExtID uint8 36 | WriteStream webrtc.TrackLocalWriter 37 | Pool *sync.Pool 38 | PoolEntity *[]byte 39 | } 40 | 41 | type Pacer interface { 42 | Enqueue(p Packet) 43 | Stop() 44 | 45 | SetInterval(interval time.Duration) 46 | SetBitrate(bitrate int) 47 | } 48 | 49 | // ------------------------------------------------ 50 | -------------------------------------------------------------------------------- /pkg/testutils/timeout.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testutils 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | var ( 24 | ConnectTimeout = 30 * time.Second 25 | ) 26 | 27 | func WithTimeout(t *testing.T, f func() string, timeouts ...time.Duration) { 28 | timeout := ConnectTimeout 29 | if len(timeouts) > 0 { 30 | timeout = timeouts[0] 31 | } 32 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 33 | defer cancel() 34 | lastErr := "" 35 | for { 36 | select { 37 | case <-ctx.Done(): 38 | if lastErr != "" { 39 | t.Fatalf("did not reach expected state after %v: %s", timeout, lastErr) 40 | } 41 | case <-time.After(10 * time.Millisecond): 42 | lastErr = f() 43 | if lastErr == "" { 44 | return 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/routing/selector/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector_test 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/livekit/protocol/livekit" 24 | 25 | "github.com/livekit/livekit-server/pkg/routing/selector" 26 | ) 27 | 28 | func TestIsAvailable(t *testing.T) { 29 | t.Run("still available", func(t *testing.T) { 30 | n := &livekit.Node{ 31 | Stats: &livekit.NodeStats{ 32 | UpdatedAt: time.Now().Unix() - 3, 33 | }, 34 | } 35 | require.True(t, selector.IsAvailable(n)) 36 | }) 37 | 38 | t.Run("expired", func(t *testing.T) { 39 | n := &livekit.Node{ 40 | Stats: &livekit.NodeStats{ 41 | UpdatedAt: time.Now().Unix() - 20, 42 | }, 43 | } 44 | require.False(t, selector.IsAvailable(n)) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/sfu/codecmunger/codecmunger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codecmunger 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 21 | ) 22 | 23 | var ( 24 | ErrNotVP8 = errors.New("not VP8") 25 | ErrOutOfOrderVP8PictureIdCacheMiss = errors.New("out-of-order VP8 picture id not found in cache") 26 | ErrFilteredVP8TemporalLayer = errors.New("filtered VP8 temporal layer") 27 | ) 28 | 29 | type CodecMunger interface { 30 | GetState() interface{} 31 | SeedState(state interface{}) 32 | 33 | SetLast(extPkt *buffer.ExtPacket) 34 | UpdateOffsets(extPkt *buffer.ExtPacket) 35 | 36 | UpdateAndGet(extPkt *buffer.ExtPacket, snOutOfOrder bool, snHasGap bool, maxTemporal int32) (int, []byte, error) 37 | 38 | UpdateAndGetPadding(newPicture bool) ([]byte, error) 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 LiveKit, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release 16 | 17 | on: 18 | workflow_dispatch: 19 | push: 20 | # only publish on version tags 21 | tags: 22 | - 'v*.*.*' 23 | 24 | permissions: 25 | contents: write 26 | 27 | jobs: 28 | release: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Fetch all tags 33 | run: git fetch --force --tags 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v5 37 | with: 38 | go-version-file: "go.mod" 39 | 40 | - name: Run GoReleaser 41 | uses: goreleaser/goreleaser-action@v6 42 | with: 43 | distribution: goreleaser 44 | version: '~> v2' 45 | args: release --clean 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 LiveKit, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM golang:1.22-alpine as builder 16 | 17 | ARG TARGETPLATFORM 18 | ARG TARGETARCH 19 | RUN echo building for "$TARGETPLATFORM" 20 | 21 | WORKDIR /workspace 22 | 23 | # Copy the Go Modules manifests 24 | COPY go.mod go.mod 25 | COPY go.sum go.sum 26 | # cache deps before building and copying source so that we don't need to re-download as much 27 | # and so that source changes don't invalidate our downloaded layer 28 | RUN go mod download 29 | 30 | # Copy the go source 31 | COPY cmd/ cmd/ 32 | COPY pkg/ pkg/ 33 | COPY test/ test/ 34 | COPY tools/ tools/ 35 | COPY version/ version/ 36 | 37 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH GO111MODULE=on go build -a -o livekit-server ./cmd/server 38 | 39 | FROM alpine 40 | 41 | COPY --from=builder /workspace/livekit-server /livekit-server 42 | 43 | # Run the binary. 44 | ENTRYPOINT ["/livekit-server"] 45 | -------------------------------------------------------------------------------- /pkg/utils/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 LiveKit, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/livekit/protocol/logger" 23 | ) 24 | 25 | type attemptKey struct{} 26 | type loggerKey = struct{} 27 | 28 | func ContextWithAttempt(ctx context.Context, attempt int) context.Context { 29 | return context.WithValue(ctx, attemptKey{}, attempt) 30 | } 31 | 32 | func GetAttempt(ctx context.Context) int { 33 | if attempt, ok := ctx.Value(attemptKey{}).(int); ok { 34 | return attempt 35 | } 36 | return 0 37 | } 38 | 39 | func ContextWithLogger(ctx context.Context, logger logger.Logger) context.Context { 40 | return context.WithValue(ctx, loggerKey{}, logger) 41 | } 42 | 43 | func GetLogger(ctx context.Context) logger.Logger { 44 | if l, ok := ctx.Value(loggerKey{}).(logger.Logger); ok { 45 | return l 46 | } 47 | return logger.GetLogger() 48 | } 49 | -------------------------------------------------------------------------------- /pkg/routing/node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package routing 16 | 17 | import ( 18 | "runtime" 19 | "time" 20 | 21 | "github.com/livekit/protocol/livekit" 22 | "github.com/livekit/protocol/utils" 23 | "github.com/livekit/protocol/utils/guid" 24 | 25 | "github.com/livekit/livekit-server/pkg/config" 26 | ) 27 | 28 | type LocalNode *livekit.Node 29 | 30 | func NewLocalNode(conf *config.Config) (LocalNode, error) { 31 | nodeID := guid.New(utils.NodePrefix) 32 | if conf.RTC.NodeIP == "" { 33 | return nil, ErrIPNotSet 34 | } 35 | node := &livekit.Node{ 36 | Id: nodeID, 37 | Ip: conf.RTC.NodeIP, 38 | NumCpus: uint32(runtime.NumCPU()), 39 | Region: conf.Region, 40 | State: livekit.NodeState_SERVING, 41 | Stats: &livekit.NodeStats{ 42 | StartedAt: time.Now().Unix(), 43 | UpdatedAt: time.Now().Unix(), 44 | }, 45 | } 46 | 47 | return node, nil 48 | } 49 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 LiveKit, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | version: 2 16 | 17 | before: 18 | hooks: 19 | - go mod tidy 20 | - go generate ./... 21 | builds: 22 | - id: livekit 23 | env: 24 | - CGO_ENABLED=0 25 | main: ./cmd/server 26 | binary: livekit-server 27 | goarm: 28 | - "7" 29 | goarch: 30 | - amd64 31 | - arm64 32 | - arm 33 | goos: 34 | - linux 35 | - windows 36 | 37 | archives: 38 | - format_overrides: 39 | - goos: windows 40 | format: zip 41 | files: 42 | - LICENSE 43 | release: 44 | github: 45 | owner: livekit 46 | name: livekit 47 | draft: true 48 | prerelease: auto 49 | changelog: 50 | sort: asc 51 | filters: 52 | exclude: 53 | - '^docs:' 54 | - '^test:' 55 | gomod: 56 | proxy: true 57 | mod: mod 58 | checksum: 59 | name_template: 'checksums.txt' 60 | snapshot: 61 | name_template: "{{ incpatch .Version }}-next" 62 | -------------------------------------------------------------------------------- /pkg/routing/messagechannel_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package routing_test 16 | 17 | import ( 18 | "sync" 19 | "testing" 20 | 21 | "github.com/livekit/protocol/livekit" 22 | 23 | "github.com/livekit/livekit-server/pkg/routing" 24 | ) 25 | 26 | func TestMessageChannel_WriteMessageClosed(t *testing.T) { 27 | // ensure it doesn't panic when written to after closing 28 | m := routing.NewMessageChannel(livekit.ConnectionID("test"), routing.DefaultMessageChannelSize) 29 | go func() { 30 | for msg := range m.ReadChan() { 31 | if msg == nil { 32 | return 33 | } 34 | } 35 | }() 36 | 37 | wg := sync.WaitGroup{} 38 | wg.Add(1) 39 | go func() { 40 | defer wg.Done() 41 | for i := 0; i < 100; i++ { 42 | _ = m.WriteMessage(&livekit.SignalRequest{}) 43 | } 44 | }() 45 | _ = m.WriteMessage(&livekit.SignalRequest{}) 46 | m.Close() 47 | _ = m.WriteMessage(&livekit.SignalRequest{}) 48 | 49 | wg.Wait() 50 | } 51 | -------------------------------------------------------------------------------- /pkg/rtc/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rtc 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | 22 | "github.com/livekit/protocol/livekit" 23 | ) 24 | 25 | func TestPackStreamId(t *testing.T) { 26 | packed := "PA_123abc|uuid-id" 27 | pID, trackID := UnpackStreamID(packed) 28 | require.Equal(t, livekit.ParticipantID("PA_123abc"), pID) 29 | require.Equal(t, livekit.TrackID("uuid-id"), trackID) 30 | 31 | require.Equal(t, packed, PackStreamID(pID, trackID)) 32 | } 33 | 34 | func TestPackDataTrackLabel(t *testing.T) { 35 | pID := livekit.ParticipantID("PA_123abc") 36 | trackID := livekit.TrackID("TR_b3da25") 37 | label := "trackLabel" 38 | packed := "PA_123abc|TR_b3da25|trackLabel" 39 | require.Equal(t, packed, PackDataTrackLabel(pID, trackID, label)) 40 | 41 | p, tr, l := UnpackDataTrackLabel(packed) 42 | require.Equal(t, pID, p) 43 | require.Equal(t, trackID, tr) 44 | require.Equal(t, label, l) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/temporallayerselector/vp8.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package temporallayerselector 16 | 17 | import ( 18 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 19 | "github.com/livekit/protocol/logger" 20 | ) 21 | 22 | type VP8 struct { 23 | logger logger.Logger 24 | } 25 | 26 | func NewVP8(logger logger.Logger) *VP8 { 27 | return &VP8{ 28 | logger: logger, 29 | } 30 | } 31 | 32 | func (v *VP8) Select(extPkt *buffer.ExtPacket, current int32, target int32) (this int32, next int32) { 33 | this = current 34 | next = current 35 | if current == target { 36 | return 37 | } 38 | 39 | vp8, ok := extPkt.Payload.(buffer.VP8) 40 | if !ok || !vp8.T { 41 | return 42 | } 43 | 44 | tid := int32(vp8.TID) 45 | if current < target { 46 | if tid > current && tid <= target && vp8.S && vp8.Y { 47 | this = tid 48 | next = tid 49 | } 50 | } else { 51 | if extPkt.Packet.Marker { 52 | next = target 53 | } 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /pkg/telemetry/prometheus/node_nonwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | * Copyright 2023 LiveKit, Inc 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package prometheus 20 | 21 | import ( 22 | "runtime" 23 | "sync" 24 | 25 | "github.com/mackerelio/go-osstat/cpu" 26 | "github.com/mackerelio/go-osstat/loadavg" 27 | ) 28 | 29 | var ( 30 | cpuStatsLock sync.RWMutex 31 | lastCPUTotal, lastCPUIdle uint64 32 | ) 33 | 34 | func getLoadAvg() (*loadavg.Stats, error) { 35 | return loadavg.Get() 36 | } 37 | 38 | func getCPUStats() (cpuLoad float32, numCPUs uint32, err error) { 39 | cpuInfo, err := cpu.Get() 40 | if err != nil { 41 | return 42 | } 43 | 44 | cpuStatsLock.Lock() 45 | if lastCPUTotal > 0 && lastCPUTotal < cpuInfo.Total { 46 | cpuLoad = 1 - float32(cpuInfo.Idle-lastCPUIdle)/float32(cpuInfo.Total-lastCPUTotal) 47 | } 48 | 49 | lastCPUTotal = cpuInfo.Total 50 | lastCPUIdle = cpuInfo.Idle 51 | cpuStatsLock.Unlock() 52 | 53 | numCPUs = uint32(runtime.NumCPU()) 54 | 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /pkg/sfu/codecmunger/null.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codecmunger 16 | 17 | import ( 18 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 19 | "github.com/livekit/protocol/logger" 20 | ) 21 | 22 | type Null struct { 23 | seededState interface{} 24 | } 25 | 26 | func NewNull(_logger logger.Logger) *Null { 27 | return &Null{} 28 | } 29 | 30 | func (n *Null) GetState() interface{} { 31 | return nil 32 | } 33 | 34 | func (n *Null) SeedState(state interface{}) { 35 | n.seededState = state 36 | } 37 | 38 | func (n *Null) GetSeededState() interface{} { 39 | return n.seededState 40 | } 41 | 42 | func (n *Null) SetLast(_extPkt *buffer.ExtPacket) { 43 | } 44 | 45 | func (n *Null) UpdateOffsets(_extPkt *buffer.ExtPacket) { 46 | } 47 | 48 | func (n *Null) UpdateAndGet(_extPkt *buffer.ExtPacket, snOutOfOrder bool, snHasGap bool, maxTemporal int32) (int, []byte, error) { 49 | return 0, nil, nil 50 | } 51 | 52 | func (n *Null) UpdateAndGetPadding(newPicture bool) ([]byte, error) { 53 | return nil, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/sfu/buffer/rtcpreader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buffer 16 | 17 | import ( 18 | "io" 19 | 20 | "go.uber.org/atomic" 21 | ) 22 | 23 | type RTCPReader struct { 24 | ssrc uint32 25 | closed atomic.Bool 26 | onPacket atomic.Value // func([]byte) 27 | onClose func() 28 | } 29 | 30 | func NewRTCPReader(ssrc uint32) *RTCPReader { 31 | return &RTCPReader{ssrc: ssrc} 32 | } 33 | 34 | func (r *RTCPReader) Write(p []byte) (n int, err error) { 35 | if r.closed.Load() { 36 | err = io.EOF 37 | return 38 | } 39 | if f, ok := r.onPacket.Load().(func([]byte)); ok && f != nil { 40 | f(p) 41 | } 42 | return 43 | } 44 | 45 | func (r *RTCPReader) OnClose(fn func()) { 46 | r.onClose = fn 47 | } 48 | 49 | func (r *RTCPReader) Close() error { 50 | if r.closed.Swap(true) { 51 | return nil 52 | } 53 | 54 | r.onClose() 55 | return nil 56 | } 57 | 58 | func (r *RTCPReader) OnPacket(f func([]byte)) { 59 | r.onPacket.Store(f) 60 | } 61 | 62 | func (r *RTCPReader) Read(_ []byte) (n int, err error) { return } 63 | -------------------------------------------------------------------------------- /pkg/routing/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package routing 16 | 17 | import ( 18 | "errors" 19 | ) 20 | 21 | var ( 22 | ErrNotFound = errors.New("could not find object") 23 | ErrIPNotSet = errors.New("ip address is required and not set") 24 | ErrHandlerNotDefined = errors.New("handler not defined") 25 | ErrIncorrectRTCNode = errors.New("current node isn't the RTC node for the room") 26 | ErrNodeNotFound = errors.New("could not locate the node") 27 | ErrNodeLimitReached = errors.New("reached configured limit for node") 28 | ErrInvalidRouterMessage = errors.New("invalid router message") 29 | ErrChannelClosed = errors.New("channel closed") 30 | ErrChannelFull = errors.New("channel is full") 31 | 32 | // errors when starting signal connection 33 | ErrRequestChannelClosed = errors.New("request channel closed") 34 | ErrCouldNotMigrateParticipant = errors.New("could not migrate participant") 35 | ErrClientInfoNotSet = errors.New("client info not set") 36 | ) 37 | -------------------------------------------------------------------------------- /pkg/routing/selector/cpuload_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector_test 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | 22 | "github.com/livekit/protocol/livekit" 23 | 24 | "github.com/livekit/livekit-server/pkg/routing/selector" 25 | ) 26 | 27 | func TestCPULoadSelector_SelectNode(t *testing.T) { 28 | sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random"} 29 | 30 | var nodes []*livekit.Node 31 | _, err := sel.SelectNode(nodes) 32 | require.Error(t, err, "should error no available nodes") 33 | 34 | // Select a node with high load when no nodes with low load are available 35 | nodes = []*livekit.Node{nodeLoadHigh} 36 | if _, err := sel.SelectNode(nodes); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | // Select a node with low load when available 41 | nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh} 42 | for i := 0; i < 5; i++ { 43 | node, err := sel.SelectNode(nodes) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | if node != nodeLoadLow { 48 | t.Error("selected the wrong node") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/config/configtest/checkyamltag.go: -------------------------------------------------------------------------------- 1 | package configtest 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "slices" 7 | "strings" 8 | 9 | "go.uber.org/multierr" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | var protoMessageType = reflect.TypeOf((*proto.Message)(nil)).Elem() 14 | 15 | func checkYAMLTags(t reflect.Type, seen map[reflect.Type]struct{}) error { 16 | if _, ok := seen[t]; ok { 17 | return nil 18 | } 19 | seen[t] = struct{}{} 20 | 21 | switch t.Kind() { 22 | case reflect.Array, reflect.Map, reflect.Slice, reflect.Pointer: 23 | return checkYAMLTags(t.Elem(), seen) 24 | case reflect.Struct: 25 | if reflect.PointerTo(t).Implements(protoMessageType) { 26 | // ignore protobuf messages 27 | return nil 28 | } 29 | 30 | var errs error 31 | for i := 0; i < t.NumField(); i++ { 32 | field := t.Field(i) 33 | 34 | if field.Type.Kind() == reflect.Bool { 35 | // ignore boolean fields 36 | continue 37 | } 38 | 39 | if field.Tag.Get("config") == "allowempty" { 40 | // ignore configured exceptions 41 | continue 42 | } 43 | 44 | parts := strings.Split(field.Tag.Get("yaml"), ",") 45 | if parts[0] == "-" { 46 | // ignore unparsed fields 47 | continue 48 | } 49 | 50 | if !slices.Contains(parts, "omitempty") && !slices.Contains(parts, "inline") { 51 | errs = multierr.Append(errs, fmt.Errorf("%s/%s.%s missing omitempty tag", t.PkgPath(), t.Name(), field.Name)) 52 | } 53 | 54 | errs = multierr.Append(errs, checkYAMLTags(field.Type, seen)) 55 | } 56 | return errs 57 | default: 58 | return nil 59 | } 60 | } 61 | 62 | func CheckYAMLTags(config any) error { 63 | return checkYAMLTags(reflect.TypeOf(config), map[reflect.Type]struct{}{}) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/routing/selector/cpuload.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector 16 | 17 | import ( 18 | "github.com/livekit/protocol/livekit" 19 | ) 20 | 21 | // CPULoadSelector eliminates nodes that have CPU usage higher than CPULoadLimit 22 | // then selects a node from nodes that are not overloaded 23 | type CPULoadSelector struct { 24 | CPULoadLimit float32 25 | SortBy string 26 | } 27 | 28 | func (s *CPULoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) { 29 | nodes = GetAvailableNodes(nodes) 30 | if len(nodes) == 0 { 31 | return nil, ErrNoAvailableNodes 32 | } 33 | 34 | nodesLowLoad := make([]*livekit.Node, 0) 35 | for _, node := range nodes { 36 | stats := node.Stats 37 | if stats.CpuLoad < s.CPULoadLimit { 38 | nodesLowLoad = append(nodesLowLoad, node) 39 | } 40 | } 41 | if len(nodesLowLoad) > 0 { 42 | nodes = nodesLowLoad 43 | } 44 | return nodes, nil 45 | } 46 | 47 | func (s *CPULoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) { 48 | nodes, err := s.filterNodes(nodes) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return SelectSortedNode(nodes, s.SortBy) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/routing/selector/sysload.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector 16 | 17 | import ( 18 | "github.com/livekit/protocol/livekit" 19 | ) 20 | 21 | // SystemLoadSelector eliminates nodes that surpass has a per-cpu node higher than SysloadLimit 22 | // then selects a node from nodes that are not overloaded 23 | type SystemLoadSelector struct { 24 | SysloadLimit float32 25 | SortBy string 26 | } 27 | 28 | func (s *SystemLoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) { 29 | nodes = GetAvailableNodes(nodes) 30 | if len(nodes) == 0 { 31 | return nil, ErrNoAvailableNodes 32 | } 33 | 34 | nodesLowLoad := make([]*livekit.Node, 0) 35 | for _, node := range nodes { 36 | if GetNodeSysload(node) < s.SysloadLimit { 37 | nodesLowLoad = append(nodesLowLoad, node) 38 | } 39 | } 40 | if len(nodesLowLoad) > 0 { 41 | nodes = nodesLowLoad 42 | } 43 | return nodes, nil 44 | } 45 | 46 | func (s *SystemLoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) { 47 | nodes, err := s.filterNodes(nodes) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return SelectSortedNode(nodes, s.SortBy) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/sfu/buffer/videolayer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buffer 16 | 17 | import "fmt" 18 | 19 | const ( 20 | InvalidLayerSpatial = int32(-1) 21 | InvalidLayerTemporal = int32(-1) 22 | 23 | DefaultMaxLayerSpatial = int32(2) 24 | DefaultMaxLayerTemporal = int32(3) 25 | ) 26 | 27 | var ( 28 | InvalidLayer = VideoLayer{ 29 | Spatial: InvalidLayerSpatial, 30 | Temporal: InvalidLayerTemporal, 31 | } 32 | 33 | DefaultMaxLayer = VideoLayer{ 34 | Spatial: DefaultMaxLayerSpatial, 35 | Temporal: DefaultMaxLayerTemporal, 36 | } 37 | ) 38 | 39 | type VideoLayer struct { 40 | Spatial int32 41 | Temporal int32 42 | } 43 | 44 | func (v VideoLayer) String() string { 45 | return fmt.Sprintf("VideoLayer{s: %d, t: %d}", v.Spatial, v.Temporal) 46 | } 47 | 48 | func (v VideoLayer) GreaterThan(v2 VideoLayer) bool { 49 | return v.Spatial > v2.Spatial || (v.Spatial == v2.Spatial && v.Temporal > v2.Temporal) 50 | } 51 | 52 | func (v VideoLayer) SpatialGreaterThanOrEqual(v2 VideoLayer) bool { 53 | return v.Spatial >= v2.Spatial 54 | } 55 | 56 | func (v VideoLayer) IsValid() bool { 57 | return v.Spatial != InvalidLayerSpatial && v.Temporal != InvalidLayerTemporal 58 | } 59 | -------------------------------------------------------------------------------- /pkg/sfu/buffer/datastats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buffer 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/require" 22 | "google.golang.org/protobuf/proto" 23 | 24 | "github.com/livekit/protocol/livekit" 25 | ) 26 | 27 | func TestDataStats(t *testing.T) { 28 | stats := NewDataStats(DataStatsParam{WindowDuration: time.Second}) 29 | 30 | time.Sleep(time.Millisecond) 31 | r := stats.ToProtoAggregateOnly() 32 | require.Equal(t, r.StartTime.AsTime().UnixNano(), stats.startTime.UnixNano()) 33 | require.NotZero(t, r.EndTime) 34 | require.NotZero(t, r.Duration) 35 | r.StartTime = nil 36 | r.EndTime = nil 37 | r.Duration = 0 38 | require.True(t, proto.Equal(r, &livekit.RTPStats{})) 39 | 40 | stats.Update(100, time.Now().UnixNano()) 41 | r = stats.ToProtoActive() 42 | require.EqualValues(t, 100, r.Bytes) 43 | require.NotZero(t, r.Bitrate) 44 | 45 | // wait for window duration 46 | time.Sleep(time.Second) 47 | r = stats.ToProtoActive() 48 | require.True(t, proto.Equal(r, &livekit.RTPStats{})) 49 | stats.Stop() 50 | r = stats.ToProtoAggregateOnly() 51 | require.EqualValues(t, 100, r.Bytes) 52 | require.NotZero(t, r.Bitrate) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/rtc/clientinfo_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 LiveKit, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package rtc 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/livekit/protocol/livekit" 25 | ) 26 | 27 | func TestClientInfo_CompareVersion(t *testing.T) { 28 | c := ClientInfo{ 29 | ClientInfo: &livekit.ClientInfo{ 30 | Version: "1", 31 | }, 32 | } 33 | require.Equal(t, 1, c.compareVersion("0.1.0")) 34 | require.Equal(t, 0, c.compareVersion("1.0.0")) 35 | require.Equal(t, -1, c.compareVersion("1.0.5")) 36 | } 37 | 38 | func TestClientInfo_SupportsICETCP(t *testing.T) { 39 | t.Run("GO SDK cannot support TCP", func(t *testing.T) { 40 | c := ClientInfo{ 41 | ClientInfo: &livekit.ClientInfo{ 42 | Sdk: livekit.ClientInfo_GO, 43 | }, 44 | } 45 | require.False(t, c.SupportsICETCP()) 46 | }) 47 | 48 | t.Run("Swift SDK cannot support TCP before 1.0.5", func(t *testing.T) { 49 | c := ClientInfo{ 50 | ClientInfo: &livekit.ClientInfo{ 51 | Sdk: livekit.ClientInfo_SWIFT, 52 | Version: "1.0.4", 53 | }, 54 | } 55 | require.False(t, c.SupportsICETCP()) 56 | c.Version = "1.0.5" 57 | require.True(t, c.SupportsICETCP()) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/clientconfiguration/conf.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package clientconfiguration 16 | 17 | import ( 18 | "github.com/livekit/protocol/livekit" 19 | ) 20 | 21 | // StaticConfigurations list specific device-side limitations that should be disabled at a global level 22 | var StaticConfigurations = []ConfigurationItem{ 23 | // { 24 | // Match: &ScriptMatch{Expr: `c.protocol <= 5 || c.browser == "firefox"`}, 25 | // Configuration: &livekit.ClientConfiguration{ResumeConnection: livekit.ClientConfigSetting_DISABLED}, 26 | // Merge: false, 27 | // }, 28 | // { 29 | // Match: &ScriptMatch{Expr: `c.browser == "safari" && c.os == "ios"`}, 30 | // Configuration: &livekit.ClientConfiguration{DisabledCodecs: &livekit.DisabledCodecs{Codecs: []*livekit.Codec{ 31 | // {Mime: "video/vp9"}, 32 | // }}}, 33 | // Merge: false, 34 | // }, 35 | { 36 | Match: &ScriptMatch{Expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android") || 37 | ((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`}, 38 | Configuration: &livekit.ClientConfiguration{ 39 | DisabledCodecs: &livekit.DisabledCodecs{ 40 | Publish: []*livekit.Codec{{Mime: "video/h264"}}, 41 | }, 42 | }, 43 | Merge: false, 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /pkg/rtc/mediaengine_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rtc 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/pion/webrtc/v3" 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/livekit/protocol/livekit" 24 | ) 25 | 26 | func TestIsCodecEnabled(t *testing.T) { 27 | t.Run("empty fmtp requirement should match all", func(t *testing.T) { 28 | enabledCodecs := []*livekit.Codec{{Mime: "video/h264"}} 29 | require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, SDPFmtpLine: "special"})) 30 | require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264})) 31 | require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8})) 32 | }) 33 | 34 | t.Run("when fmtp is provided, require match", func(t *testing.T) { 35 | enabledCodecs := []*livekit.Codec{{Mime: "video/h264", FmtpLine: "special"}} 36 | require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, SDPFmtpLine: "special"})) 37 | require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264})) 38 | require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8})) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/sfu/rtpextension/playoutdelay/playoutdelay_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package playoutdelay 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestPlayoutDelay(t *testing.T) { 24 | p1 := PlayOutDelay{Min: 100, Max: 200} 25 | b, err := p1.Marshal() 26 | require.NoError(t, err) 27 | require.Len(t, b, playoutDelayExtensionSize) 28 | var p2 PlayOutDelay 29 | err = p2.Unmarshal(b) 30 | require.NoError(t, err) 31 | require.Equal(t, p1, p2) 32 | 33 | // overflow 34 | p3 := PlayOutDelay{Min: 100, Max: (1 << 12) * 10} 35 | _, err = p3.Marshal() 36 | require.ErrorIs(t, err, errPlayoutDelayOverflow) 37 | 38 | // too small 39 | p4 := PlayOutDelay{} 40 | err = p4.Unmarshal([]byte{0x00, 0x00}) 41 | require.ErrorIs(t, err, errTooSmall) 42 | 43 | // from value 44 | p5 := PlayoutDelayFromValue(1<<12*10, 1<<12*10+10) 45 | _, err = p5.Marshal() 46 | require.NoError(t, err) 47 | require.Equal(t, uint16((1<<12)-1)*10, p5.Min) 48 | require.Equal(t, uint16((1<<12)-1)*10, p5.Max) 49 | 50 | p6 := PlayOutDelay{Min: 100, Max: PlayoutDelayMaxValue} 51 | bytes, err := p6.Marshal() 52 | require.NoError(t, err) 53 | p6Unmarshal := PlayOutDelay{} 54 | err = p6Unmarshal.Unmarshal(bytes) 55 | require.NoError(t, err) 56 | require.Equal(t, p6, p6Unmarshal) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/server/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | type testStruct struct { 25 | configFileName string 26 | configBody string 27 | 28 | expectedError error 29 | expectedConfigBody string 30 | } 31 | 32 | func TestGetConfigString(t *testing.T) { 33 | tests := []testStruct{ 34 | {"", "", nil, ""}, 35 | {"", "configBody", nil, "configBody"}, 36 | {"file", "configBody", nil, "configBody"}, 37 | {"file", "", nil, "fileContent"}, 38 | } 39 | for _, test := range tests { 40 | func() { 41 | writeConfigFile(test, t) 42 | defer os.Remove(test.configFileName) 43 | 44 | configBody, err := getConfigString(test.configFileName, test.configBody) 45 | require.Equal(t, test.expectedError, err) 46 | require.Equal(t, test.expectedConfigBody, configBody) 47 | }() 48 | } 49 | } 50 | 51 | func TestShouldReturnErrorIfConfigFileDoesNotExist(t *testing.T) { 52 | configBody, err := getConfigString("notExistingFile", "") 53 | require.Error(t, err) 54 | require.Empty(t, configBody) 55 | } 56 | 57 | func writeConfigFile(test testStruct, t *testing.T) { 58 | if test.configFileName != "" { 59 | d1 := []byte(test.expectedConfigBody) 60 | err := os.WriteFile(test.configFileName, d1, 0o644) 61 | require.NoError(t, err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/service/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "net" 21 | "net/http" 22 | "regexp" 23 | 24 | "github.com/livekit/protocol/logger" 25 | ) 26 | 27 | func handleError(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...interface{}) { 28 | keysAndValues = append(keysAndValues, "status", status) 29 | if r != nil && r.URL != nil { 30 | keysAndValues = append(keysAndValues, "method", r.Method, "path", r.URL.Path) 31 | } 32 | if !errors.Is(err, context.Canceled) { 33 | logger.GetLogger().WithCallDepth(1).Warnw("error handling request", err, keysAndValues...) 34 | } 35 | w.WriteHeader(status) 36 | _, _ = w.Write([]byte(err.Error())) 37 | } 38 | 39 | func boolValue(s string) bool { 40 | return s == "1" || s == "true" 41 | } 42 | 43 | func IsValidDomain(domain string) bool { 44 | domainRegexp := regexp.MustCompile(`^(?i)[a-z0-9-]+(\.[a-z0-9-]+)+\.?$`) 45 | return domainRegexp.MatchString(domain) 46 | } 47 | 48 | func GetClientIP(r *http.Request) string { 49 | // CF proxy typically is first thing the user reaches 50 | if ip := r.Header.Get("CF-Connecting-IP"); ip != "" { 51 | return ip 52 | } 53 | if ip := r.Header.Get("X-Forwarded-For"); ip != "" { 54 | return ip 55 | } 56 | if ip := r.Header.Get("X-Real-IP"); ip != "" { 57 | return ip 58 | } 59 | ip, _, _ := net.SplitHostPort(r.RemoteAddr) 60 | return ip 61 | } 62 | -------------------------------------------------------------------------------- /pkg/routing/selector/sortby_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector_test 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/livekit/protocol/livekit" 21 | 22 | "github.com/livekit/livekit-server/pkg/routing/selector" 23 | ) 24 | 25 | func SortByTest(t *testing.T, sortBy string) { 26 | sel := selector.SystemLoadSelector{SortBy: sortBy} 27 | nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh} 28 | 29 | for i := 0; i < 5; i++ { 30 | node, err := sel.SelectNode(nodes) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | if node != nodeLoadLow { 35 | t.Error("selected the wrong node for SortBy:", sortBy) 36 | } 37 | } 38 | } 39 | 40 | func TestSortByErrors(t *testing.T) { 41 | sel := selector.SystemLoadSelector{} 42 | nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh} 43 | 44 | // Test unset sort by option error 45 | _, err := sel.SelectNode(nodes) 46 | if err != selector.ErrSortByNotSet { 47 | t.Error("shouldn't allow empty sortBy") 48 | } 49 | 50 | // Test unknown sort by option error 51 | sel.SortBy = "testFail" 52 | _, err = sel.SelectNode(nodes) 53 | if err != selector.ErrSortByUnknown { 54 | t.Error("shouldn't allow unknown sortBy") 55 | } 56 | } 57 | 58 | func TestSortBy(t *testing.T) { 59 | sortByTests := []string{"sysload", "cpuload", "rooms", "clients", "tracks", "bytespersec"} 60 | 61 | for _, sortBy := range sortByTests { 62 | SortByTest(t, sortBy) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/sfu/streamtracker/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package streamtracker 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 22 | ) 23 | 24 | // ------------------------------------------------------------ 25 | 26 | type StreamStatusChange int32 27 | 28 | func (s StreamStatusChange) String() string { 29 | switch s { 30 | case StreamStatusChangeNone: 31 | return "none" 32 | case StreamStatusChangeStopped: 33 | return "stopped" 34 | case StreamStatusChangeActive: 35 | return "active" 36 | default: 37 | return fmt.Sprintf("unknown: %d", int(s)) 38 | } 39 | } 40 | 41 | const ( 42 | StreamStatusChangeNone StreamStatusChange = iota 43 | StreamStatusChangeStopped 44 | StreamStatusChangeActive 45 | ) 46 | 47 | // ------------------------------------------------------------ 48 | 49 | type StreamTrackerImpl interface { 50 | Start() 51 | Stop() 52 | Reset() 53 | 54 | GetCheckInterval() time.Duration 55 | 56 | Observe(hasMarker bool, ts uint32) StreamStatusChange 57 | CheckStatus() StreamStatusChange 58 | } 59 | 60 | type StreamTrackerWorker interface { 61 | Start() 62 | Stop() 63 | Reset() 64 | OnStatusChanged(f func(status StreamStatus)) 65 | OnBitrateAvailable(f func()) 66 | Status() StreamStatus 67 | BitrateTemporalCumulative() []int64 68 | SetPaused(paused bool) 69 | Observe(temporalLayer int32, pktSize int, payloadSize int, hasMarker bool, ts uint32, dd *buffer.ExtDependencyDescriptor) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/service/docker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service_test 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "net" 21 | "os" 22 | "sync/atomic" 23 | "testing" 24 | 25 | "github.com/ory/dockertest/v3" 26 | ) 27 | 28 | var Docker *dockertest.Pool 29 | 30 | func TestMain(m *testing.M) { 31 | pool, err := dockertest.NewPool("") 32 | if err != nil { 33 | log.Fatalf("Could not construct pool: %s", err) 34 | } 35 | 36 | // uses pool to try to connect to Docker 37 | err = pool.Client.Ping() 38 | if err != nil { 39 | log.Fatalf("Could not connect to Docker: %s", err) 40 | } 41 | Docker = pool 42 | 43 | code := m.Run() 44 | os.Exit(code) 45 | } 46 | 47 | func waitTCPPort(t testing.TB, addr string) { 48 | if err := Docker.Retry(func() error { 49 | conn, err := net.Dial("tcp", addr) 50 | if err != nil { 51 | t.Log(err) 52 | return err 53 | } 54 | _ = conn.Close() 55 | return nil 56 | }); err != nil { 57 | t.Fatal(err) 58 | } 59 | } 60 | 61 | var redisLast uint32 62 | 63 | func runRedis(t testing.TB) string { 64 | c, err := Docker.RunWithOptions(&dockertest.RunOptions{ 65 | Name: fmt.Sprintf("lktest-redis-%d", atomic.AddUint32(&redisLast, 1)), 66 | Repository: "redis", Tag: "latest", 67 | }) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | t.Cleanup(func() { 72 | _ = Docker.Purge(c) 73 | }) 74 | addr := c.GetHostPort("6379/tcp") 75 | waitTCPPort(t, addr) 76 | 77 | t.Log("Redis running on", addr) 78 | return addr 79 | } 80 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/videolayerselector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package videolayerselector 16 | 17 | import ( 18 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 19 | "github.com/livekit/livekit-server/pkg/sfu/videolayerselector/temporallayerselector" 20 | ) 21 | 22 | type VideoLayerSelectorResult struct { 23 | IsSelected bool 24 | IsRelevant bool 25 | IsSwitching bool 26 | IsResuming bool 27 | RTPMarker bool 28 | DependencyDescriptorExtension []byte 29 | } 30 | 31 | type VideoLayerSelector interface { 32 | IsOvershootOkay() bool 33 | 34 | SetTemporalLayerSelector(tls temporallayerselector.TemporalLayerSelector) 35 | 36 | SetMax(maxLayer buffer.VideoLayer) 37 | SetMaxSpatial(layer int32) 38 | SetMaxTemporal(layer int32) 39 | GetMax() buffer.VideoLayer 40 | 41 | SetTarget(targetLayer buffer.VideoLayer) 42 | GetTarget() buffer.VideoLayer 43 | 44 | SetRequestSpatial(layer int32) 45 | GetRequestSpatial() int32 46 | 47 | CheckSync() (locked bool, layer int32) 48 | 49 | SetMaxSeen(maxSeenLayer buffer.VideoLayer) 50 | SetMaxSeenSpatial(layer int32) 51 | SetMaxSeenTemporal(layer int32) 52 | GetMaxSeen() buffer.VideoLayer 53 | 54 | SetCurrent(currentLayer buffer.VideoLayer) 55 | GetCurrent() buffer.VideoLayer 56 | 57 | Select(extPkt *buffer.ExtPacket, layer int32) VideoLayerSelectorResult 58 | SelectTemporal(extPkt *buffer.ExtPacket) int32 59 | Rollback() 60 | } 61 | -------------------------------------------------------------------------------- /pkg/service/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service_test 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | "github.com/redis/go-redis/v9" 23 | "github.com/stretchr/testify/require" 24 | 25 | "github.com/livekit/livekit-server/pkg/service" 26 | ) 27 | 28 | func redisClient(t testing.TB) *redis.Client { 29 | cli := redis.NewClient(&redis.Options{ 30 | Addr: "localhost:6379", 31 | }) 32 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 33 | defer cancel() 34 | err := cli.Ping(ctx).Err() 35 | if err == nil { 36 | t.Cleanup(func() { 37 | _ = cli.Close() 38 | }) 39 | return cli 40 | } 41 | _ = cli.Close() 42 | t.Logf("local redis not available: %v", err) 43 | 44 | t.Logf("starting redis in docker") 45 | addr := runRedis(t) 46 | cli = redis.NewClient(&redis.Options{ 47 | Addr: addr, 48 | }) 49 | ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) 50 | defer cancel() 51 | 52 | if err = cli.Ping(ctx).Err(); err != nil { 53 | _ = cli.Close() 54 | t.Fatal(err) 55 | } 56 | t.Cleanup(func() { 57 | _ = cli.Close() 58 | }) 59 | return cli 60 | } 61 | 62 | func TestIsValidDomain(t *testing.T) { 63 | list := map[string]bool{ 64 | "turn.myhost.com": true, 65 | "turn.google.com": true, 66 | "https://host.com": false, 67 | "turn://host.com": false, 68 | } 69 | for key, result := range list { 70 | service.IsValidDomain(key) 71 | require.Equal(t, service.IsValidDomain(key), result) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/sfu/rtpextension/dependencydescriptor/dependencydescriptorextension_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package dependencydescriptor 16 | 17 | import ( 18 | "encoding/hex" 19 | "testing" 20 | ) 21 | 22 | func TestDependencyDescriptorUnmarshal(t *testing.T) { 23 | 24 | // hex bytes from traffic capture 25 | hexes := []string{ 26 | "c1017280081485214eafffaaaa863cf0430c10c302afc0aaa0063c00430010c002a000a80006000040001d954926e082b04a0941b820ac1282503157f974000ca864330e222222eca8655304224230eca877530077004200ef008601df010d", 27 | "86017340fc", 28 | "46017340fc", 29 | "c3017540fc", 30 | "88017640fc", 31 | "48017640fc", 32 | "c2017840fc", 33 | // 34 | "c1017280081485214eafffaaaa863cf0430c10c302afc0aaa0063c00430010c002a000a80006000040001d954926e082b04a0941b820ac1282503157f974000ca864330e222222eca8655304224230eca877530077004200ef008601df010d", 35 | "860173", 36 | "460173", 37 | "8b0174", 38 | "0b0174", 39 | "0b0174", 40 | "c30175", 41 | } 42 | 43 | var structure *FrameDependencyStructure 44 | 45 | for _, h := range hexes { 46 | buf, err := hex.DecodeString(h) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | var ddVal DependencyDescriptor 52 | var d = DependencyDescriptorExtension{ 53 | Structure: structure, 54 | Descriptor: &ddVal, 55 | } 56 | if _, err := d.Unmarshal(buf); err != nil { 57 | t.Fatal(err) 58 | } 59 | if ddVal.AttachedStructure != nil { 60 | structure = ddVal.AttachedStructure 61 | } 62 | 63 | t.Log(ddVal.String()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/sfu/pacer/no_queue.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pacer 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/gammazero/deque" 21 | "github.com/livekit/protocol/logger" 22 | ) 23 | 24 | type NoQueue struct { 25 | *Base 26 | 27 | logger logger.Logger 28 | 29 | lock sync.RWMutex 30 | packets deque.Deque[Packet] 31 | wake chan struct{} 32 | isStopped bool 33 | } 34 | 35 | func NewNoQueue(logger logger.Logger) *NoQueue { 36 | n := &NoQueue{ 37 | Base: NewBase(logger), 38 | logger: logger, 39 | wake: make(chan struct{}, 1), 40 | } 41 | n.packets.SetMinCapacity(9) 42 | 43 | go n.sendWorker() 44 | return n 45 | } 46 | 47 | func (n *NoQueue) Stop() { 48 | n.lock.Lock() 49 | if n.isStopped { 50 | n.lock.Unlock() 51 | return 52 | } 53 | 54 | close(n.wake) 55 | n.isStopped = true 56 | n.lock.Unlock() 57 | } 58 | 59 | func (n *NoQueue) Enqueue(p Packet) { 60 | n.lock.Lock() 61 | defer n.lock.Unlock() 62 | 63 | n.packets.PushBack(p) 64 | if n.packets.Len() == 1 && !n.isStopped { 65 | select { 66 | case n.wake <- struct{}{}: 67 | default: 68 | } 69 | } 70 | } 71 | 72 | func (n *NoQueue) sendWorker() { 73 | for { 74 | <-n.wake 75 | for { 76 | n.lock.Lock() 77 | if n.isStopped { 78 | n.lock.Unlock() 79 | return 80 | } 81 | 82 | if n.packets.Len() == 0 { 83 | n.lock.Unlock() 84 | break 85 | } 86 | p := n.packets.PopFront() 87 | n.lock.Unlock() 88 | 89 | n.Base.SendPacket(&p) 90 | } 91 | } 92 | } 93 | 94 | // ------------------------------------------------ 95 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/framenumberwrapper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package videolayerselector 16 | 17 | import "github.com/livekit/protocol/logger" 18 | 19 | type FrameNumberWrapper struct { 20 | offset uint64 21 | last uint64 22 | inited bool 23 | logger logger.Logger 24 | } 25 | 26 | // UpdateAndGet returns the wrapped frame number from the given frame number, and updates the offset to 27 | // make sure the returned frame number is always inorder. Should only updateOffset if the new frame is a keyframe 28 | // because frame dependencies uses on the frame number diff so frames inside a GOP should have the same offset. 29 | func (f *FrameNumberWrapper) UpdateAndGet(new uint64, updateOffset bool) uint64 { 30 | if !f.inited { 31 | f.last = new 32 | f.inited = true 33 | return new 34 | } 35 | 36 | if new <= f.last { 37 | return new + f.offset 38 | } 39 | 40 | if updateOffset { 41 | new16 := uint16(new + f.offset) 42 | last16 := uint16(f.last + f.offset) 43 | // if new frame number wraps around and is considered as earlier by client, increase offset to make it later 44 | if diff := new16 - last16; diff > 0x8000 || (diff == 0x8000 && new16 < last16) { 45 | // increase offset by 6000, nearly 10 seconds for 30fps video with 3 spatial layers 46 | prevOffset := f.offset 47 | f.offset += uint64(65535 - diff + 6000) 48 | 49 | f.logger.Debugw("wrap around frame number seen, update offset", "new", new, "last", f.last, "offset", f.offset, "prevOffset", prevOffset, "lastWrapFn", last16, "newWrapFn", new16) 50 | } 51 | } 52 | f.last = new 53 | return new + f.offset 54 | } 55 | -------------------------------------------------------------------------------- /pkg/routing/roommanager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package routing 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/livekit/livekit-server/pkg/config" 21 | "github.com/livekit/protocol/livekit" 22 | "github.com/livekit/protocol/rpc" 23 | "github.com/livekit/psrpc" 24 | "github.com/livekit/psrpc/pkg/middleware" 25 | ) 26 | 27 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 28 | 29 | //counterfeiter:generate . RoomManagerClient 30 | type RoomManagerClient interface { 31 | rpc.TypedRoomManagerClient 32 | } 33 | 34 | type roomManagerClient struct { 35 | config config.RoomConfig 36 | client rpc.TypedRoomManagerClient 37 | } 38 | 39 | func NewRoomManagerClient(clientParams rpc.ClientParams, config config.RoomConfig) (RoomManagerClient, error) { 40 | c, err := rpc.NewTypedRoomManagerClient( 41 | clientParams.Bus, 42 | psrpc.WithClientChannelSize(clientParams.BufferSize), 43 | middleware.WithClientMetrics(clientParams.Observer), 44 | rpc.WithClientLogger(clientParams.Logger), 45 | ) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &roomManagerClient{ 51 | config: config, 52 | client: c, 53 | }, nil 54 | } 55 | 56 | func (c *roomManagerClient) CreateRoom(ctx context.Context, nodeID livekit.NodeID, req *livekit.CreateRoomRequest, opts ...psrpc.RequestOption) (*livekit.Room, error) { 57 | return c.client.CreateRoom(ctx, nodeID, req, append(opts, psrpc.WithRequestInterceptors(middleware.NewRPCRetryInterceptor(middleware.RetryOptions{ 58 | MaxAttempts: c.config.CreateRoomAttempts, 59 | Timeout: c.config.CreateRoomTimeout, 60 | })))...) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/routing/selector/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/livekit/protocol/livekit" 21 | "github.com/livekit/protocol/logger" 22 | 23 | "github.com/livekit/livekit-server/pkg/config" 24 | ) 25 | 26 | var ErrUnsupportedSelector = errors.New("unsupported node selector") 27 | 28 | // NodeSelector selects an appropriate node to run the current session 29 | type NodeSelector interface { 30 | SelectNode(nodes []*livekit.Node) (*livekit.Node, error) 31 | } 32 | 33 | func CreateNodeSelector(conf *config.Config) (NodeSelector, error) { 34 | kind := conf.NodeSelector.Kind 35 | if kind == "" { 36 | kind = "any" 37 | } 38 | switch kind { 39 | case "any": 40 | return &AnySelector{conf.NodeSelector.SortBy}, nil 41 | case "cpuload": 42 | return &CPULoadSelector{ 43 | CPULoadLimit: conf.NodeSelector.CPULoadLimit, 44 | SortBy: conf.NodeSelector.SortBy, 45 | }, nil 46 | case "sysload": 47 | return &SystemLoadSelector{ 48 | SysloadLimit: conf.NodeSelector.SysloadLimit, 49 | SortBy: conf.NodeSelector.SortBy, 50 | }, nil 51 | case "regionaware": 52 | s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy) 53 | if err != nil { 54 | return nil, err 55 | } 56 | s.SysloadLimit = conf.NodeSelector.SysloadLimit 57 | return s, nil 58 | case "random": 59 | logger.Warnw("random node selector is deprecated, please switch to \"any\" or another selector", nil) 60 | return &AnySelector{conf.NodeSelector.SortBy}, nil 61 | default: 62 | return nil, ErrUnsupportedSelector 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/utils/incrementaldispatcher.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "sync" 21 | 22 | "github.com/frostbyte73/core" 23 | ) 24 | 25 | // IncrementalDispatcher is a dispatcher that allows multiple consumers to consume items as they become 26 | // available, while producers can add items at anytime. 27 | type IncrementalDispatcher[T any] struct { 28 | done core.Fuse 29 | lock sync.RWMutex 30 | cond *sync.Cond 31 | items []T 32 | } 33 | 34 | func NewIncrementalDispatcher[T any]() *IncrementalDispatcher[T] { 35 | p := &IncrementalDispatcher[T]{} 36 | p.cond = sync.NewCond(&p.lock) 37 | return p 38 | } 39 | 40 | func (d *IncrementalDispatcher[T]) Add(item T) { 41 | if d.done.IsBroken() { 42 | return 43 | } 44 | d.lock.Lock() 45 | d.items = append(d.items, item) 46 | d.lock.Unlock() 47 | d.cond.Broadcast() 48 | } 49 | 50 | func (d *IncrementalDispatcher[T]) Done() { 51 | d.lock.Lock() 52 | d.done.Break() 53 | d.cond.Broadcast() 54 | d.lock.Unlock() 55 | } 56 | 57 | func (d *IncrementalDispatcher[T]) ForEach(fn func(T)) { 58 | idx := 0 59 | dispatchFromIdx := func() { 60 | var itemsToDispatch []T 61 | d.lock.RLock() 62 | for idx < len(d.items) { 63 | itemsToDispatch = append(itemsToDispatch, d.items[idx]) 64 | idx++ 65 | } 66 | d.lock.RUnlock() 67 | for _, item := range itemsToDispatch { 68 | fn(item) 69 | } 70 | } 71 | for !d.done.IsBroken() { 72 | dispatchFromIdx() 73 | d.lock.Lock() 74 | // need to check again because Done may have been called while dispatching 75 | if d.done.IsBroken() { 76 | d.lock.Unlock() 77 | break 78 | } 79 | if idx == len(d.items) { 80 | d.cond.Wait() 81 | } 82 | d.lock.Unlock() 83 | } 84 | 85 | dispatchFromIdx() 86 | } 87 | -------------------------------------------------------------------------------- /pkg/telemetry/prometheus/quality.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package prometheus 16 | 17 | import ( 18 | "github.com/prometheus/client_golang/prometheus" 19 | 20 | "github.com/livekit/protocol/livekit" 21 | ) 22 | 23 | var ( 24 | qualityRating prometheus.Histogram 25 | qualityScore prometheus.Histogram 26 | qualityDrop *prometheus.CounterVec 27 | ) 28 | 29 | func initQualityStats(nodeID string, nodeType livekit.NodeType) { 30 | qualityRating = prometheus.NewHistogram(prometheus.HistogramOpts{ 31 | Namespace: livekitNamespace, 32 | Subsystem: "quality", 33 | Name: "rating", 34 | ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()}, 35 | Buckets: []float64{0, 1, 2}, 36 | }) 37 | qualityScore = prometheus.NewHistogram(prometheus.HistogramOpts{ 38 | Namespace: livekitNamespace, 39 | Subsystem: "quality", 40 | Name: "score", 41 | ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()}, 42 | Buckets: []float64{1.0, 2.0, 2.5, 3.0, 3.25, 3.5, 3.75, 4.0, 4.25, 4.5}, 43 | }) 44 | qualityDrop = prometheus.NewCounterVec(prometheus.CounterOpts{ 45 | Namespace: livekitNamespace, 46 | Subsystem: "quality", 47 | Name: "drop", 48 | ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()}, 49 | }, []string{"direction"}) 50 | 51 | prometheus.MustRegister(qualityRating) 52 | prometheus.MustRegister(qualityScore) 53 | prometheus.MustRegister(qualityDrop) 54 | } 55 | 56 | func RecordQuality(rating livekit.ConnectionQuality, score float32, numUpDrops int, numDownDrops int) { 57 | qualityRating.Observe(float64(rating)) 58 | qualityScore.Observe(float64(score)) 59 | qualityDrop.WithLabelValues("up").Add(float64(numUpDrops)) 60 | qualityDrop.WithLabelValues("down").Add(float64(numDownDrops)) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/rtc/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rtc 16 | 17 | import ( 18 | "errors" 19 | ) 20 | 21 | var ( 22 | ErrRoomClosed = errors.New("room has already closed") 23 | ErrPermissionDenied = errors.New("no permissions to access the room") 24 | ErrMaxParticipantsExceeded = errors.New("room has exceeded its max participants") 25 | ErrLimitExceeded = errors.New("node has exceeded its configured limit") 26 | ErrAlreadyJoined = errors.New("a participant with the same identity is already in the room") 27 | ErrDataChannelUnavailable = errors.New("data channel is not available") 28 | ErrDataChannelBufferFull = errors.New("data channel buffer is full") 29 | ErrTransportFailure = errors.New("transport failure") 30 | ErrEmptyIdentity = errors.New("participant identity cannot be empty") 31 | ErrEmptyParticipantID = errors.New("participant ID cannot be empty") 32 | ErrMissingGrants = errors.New("VideoGrant is missing") 33 | ErrInternalError = errors.New("internal error") 34 | ErrNameExceedsLimits = errors.New("name length exceeds limits") 35 | ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits") 36 | ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits") 37 | 38 | // Track subscription related 39 | ErrNoTrackPermission = errors.New("participant is not allowed to subscribe to this track") 40 | ErrNoSubscribePermission = errors.New("participant is not given permission to subscribe to tracks") 41 | ErrTrackNotFound = errors.New("track cannot be found") 42 | ErrTrackNotAttached = errors.New("track is not yet attached") 43 | ErrTrackNotBound = errors.New("track not bound") 44 | ErrSubscriptionLimitExceeded = errors.New("participant has exceeded its subscription limit") 45 | ) 46 | -------------------------------------------------------------------------------- /pkg/clientconfiguration/staticconfiguration.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package clientconfiguration 16 | 17 | import ( 18 | "google.golang.org/protobuf/proto" 19 | 20 | "github.com/livekit/livekit-server/pkg/utils" 21 | "github.com/livekit/protocol/livekit" 22 | "github.com/livekit/protocol/logger" 23 | ) 24 | 25 | type ConfigurationItem struct { 26 | Match 27 | Configuration *livekit.ClientConfiguration 28 | Merge bool 29 | } 30 | 31 | type StaticClientConfigurationManager struct { 32 | confs []ConfigurationItem 33 | } 34 | 35 | func NewStaticClientConfigurationManager(confs []ConfigurationItem) *StaticClientConfigurationManager { 36 | return &StaticClientConfigurationManager{confs: confs} 37 | } 38 | 39 | func (s *StaticClientConfigurationManager) GetConfiguration(clientInfo *livekit.ClientInfo) *livekit.ClientConfiguration { 40 | var matchedConf []*livekit.ClientConfiguration 41 | for _, c := range s.confs { 42 | matched, err := c.Match.Match(clientInfo) 43 | if err != nil { 44 | logger.Errorw("matchrule failed", err, 45 | "clientInfo", logger.Proto(utils.ClientInfoWithoutAddress(clientInfo)), 46 | ) 47 | continue 48 | } 49 | if !matched { 50 | continue 51 | } 52 | if !c.Merge { 53 | return c.Configuration 54 | } 55 | matchedConf = append(matchedConf, c.Configuration) 56 | } 57 | 58 | var conf *livekit.ClientConfiguration 59 | for k, v := range matchedConf { 60 | if k == 0 { 61 | conf = proto.Clone(matchedConf[0]).(*livekit.ClientConfiguration) 62 | } else { 63 | // TODO : there is a problem use protobuf merge, we don't have flag to indicate 'no value', 64 | // don't override default behavior or other configuration's field. So a bool value = false or 65 | // a int value = 0 will override same field in other configuration 66 | proto.Merge(conf, v) 67 | } 68 | } 69 | return conf 70 | } 71 | -------------------------------------------------------------------------------- /pkg/service/auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service_test 16 | 17 | import ( 18 | "net/http" 19 | "net/http/httptest" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/livekit/protocol/auth" 25 | "github.com/livekit/protocol/auth/authfakes" 26 | 27 | "github.com/livekit/livekit-server/pkg/service" 28 | ) 29 | 30 | func TestAuthMiddleware(t *testing.T) { 31 | api := "APIabcdefg" 32 | secret := "somesecretencodedinbase62extendto32bytes" 33 | provider := &authfakes.FakeKeyProvider{} 34 | provider.GetSecretReturns(secret) 35 | 36 | m := service.NewAPIKeyAuthMiddleware(provider) 37 | var grants *auth.ClaimGrants 38 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | grants = service.GetGrants(r.Context()) 40 | w.WriteHeader(http.StatusOK) 41 | }) 42 | 43 | orig := &auth.VideoGrant{Room: "abcdefg", RoomJoin: true} 44 | // ensure that the original claim could be retrieved 45 | at := auth.NewAccessToken(api, secret). 46 | AddGrant(orig) 47 | token, err := at.ToJWT() 48 | require.NoError(t, err) 49 | 50 | r := &http.Request{Header: http.Header{}} 51 | w := httptest.NewRecorder() 52 | service.SetAuthorizationToken(r, token) 53 | m.ServeHTTP(w, r, handler) 54 | 55 | require.NotNil(t, grants) 56 | require.EqualValues(t, orig, grants.Video) 57 | 58 | // no authorization == no claims 59 | grants = nil 60 | w = httptest.NewRecorder() 61 | r = &http.Request{Header: http.Header{}} 62 | m.ServeHTTP(w, r, handler) 63 | require.Nil(t, grants) 64 | require.Equal(t, http.StatusOK, w.Code) 65 | 66 | // incorrect authorization: error 67 | grants = nil 68 | w = httptest.NewRecorder() 69 | r = &http.Request{Header: http.Header{}} 70 | service.SetAuthorizationToken(r, "invalid token") 71 | m.ServeHTTP(w, r, handler) 72 | require.Nil(t, grants) 73 | require.Equal(t, http.StatusUnauthorized, w.Code) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/service/agent_dispatch_service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/livekit/protocol/livekit" 21 | "github.com/livekit/protocol/rpc" 22 | ) 23 | 24 | type AgentDispatchService struct { 25 | agentDispatchClient rpc.TypedAgentDispatchInternalClient 26 | topicFormatter rpc.TopicFormatter 27 | } 28 | 29 | func NewAgentDispatchService(agentDispatchClient rpc.TypedAgentDispatchInternalClient, topicFormatter rpc.TopicFormatter) *AgentDispatchService { 30 | return &AgentDispatchService{ 31 | agentDispatchClient: agentDispatchClient, 32 | topicFormatter: topicFormatter, 33 | } 34 | } 35 | 36 | func (ag *AgentDispatchService) CreateDispatch(ctx context.Context, req *livekit.CreateAgentDispatchRequest) (*livekit.AgentDispatch, error) { 37 | err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)) 38 | if err != nil { 39 | return nil, twirpAuthError(err) 40 | } 41 | 42 | return ag.agentDispatchClient.CreateDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req) 43 | } 44 | 45 | func (ag *AgentDispatchService) DeleteDispatch(ctx context.Context, req *livekit.DeleteAgentDispatchRequest) (*livekit.AgentDispatch, error) { 46 | err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)) 47 | if err != nil { 48 | return nil, twirpAuthError(err) 49 | } 50 | 51 | return ag.agentDispatchClient.DeleteDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req) 52 | } 53 | 54 | func (ag *AgentDispatchService) ListDispatch(ctx context.Context, req *livekit.ListAgentDispatchRequest) (*livekit.ListAgentDispatchResponse, error) { 55 | err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)) 56 | if err != nil { 57 | return nil, twirpAuthError(err) 58 | } 59 | 60 | return ag.agentDispatchClient.ListDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/routing/messagechannel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package routing 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/livekit/protocol/livekit" 21 | "google.golang.org/protobuf/proto" 22 | ) 23 | 24 | const DefaultMessageChannelSize = 200 25 | 26 | type MessageChannel struct { 27 | connectionID livekit.ConnectionID 28 | msgChan chan proto.Message 29 | onClose func() 30 | isClosed bool 31 | lock sync.RWMutex 32 | } 33 | 34 | func NewDefaultMessageChannel(connectionID livekit.ConnectionID) *MessageChannel { 35 | return NewMessageChannel(connectionID, DefaultMessageChannelSize) 36 | } 37 | 38 | func NewMessageChannel(connectionID livekit.ConnectionID, size int) *MessageChannel { 39 | return &MessageChannel{ 40 | connectionID: connectionID, 41 | // allow some buffer to avoid blocked writes 42 | msgChan: make(chan proto.Message, size), 43 | } 44 | } 45 | 46 | func (m *MessageChannel) OnClose(f func()) { 47 | m.onClose = f 48 | } 49 | 50 | func (m *MessageChannel) IsClosed() bool { 51 | m.lock.RLock() 52 | defer m.lock.RUnlock() 53 | return m.isClosed 54 | } 55 | 56 | func (m *MessageChannel) WriteMessage(msg proto.Message) error { 57 | m.lock.RLock() 58 | defer m.lock.RUnlock() 59 | if m.isClosed { 60 | return ErrChannelClosed 61 | } 62 | 63 | select { 64 | case m.msgChan <- msg: 65 | // published 66 | return nil 67 | default: 68 | // channel is full 69 | return ErrChannelFull 70 | } 71 | } 72 | 73 | func (m *MessageChannel) ReadChan() <-chan proto.Message { 74 | return m.msgChan 75 | } 76 | 77 | func (m *MessageChannel) Close() { 78 | m.lock.Lock() 79 | if m.isClosed { 80 | m.lock.Unlock() 81 | return 82 | } 83 | m.isClosed = true 84 | close(m.msgChan) 85 | m.lock.Unlock() 86 | 87 | if m.onClose != nil { 88 | m.onClose() 89 | } 90 | } 91 | 92 | func (m *MessageChannel) ConnectionID() livekit.ConnectionID { 93 | return m.connectionID 94 | } 95 | -------------------------------------------------------------------------------- /pkg/sfu/rtpextension/playoutdelay/playoutdelay.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package playoutdelay 16 | 17 | import ( 18 | "encoding/binary" 19 | "errors" 20 | ) 21 | 22 | const ( 23 | PlayoutDelayURI = "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" 24 | MaxPlayoutDelayDefault = 10000 // 10s, equal to chrome's default max playout delay 25 | PlayoutDelayMaxValue = 10 * (1<<12 - 1) // max value for playout delay can be represented 26 | 27 | playoutDelayExtensionSize = 3 28 | ) 29 | 30 | var ( 31 | errPlayoutDelayOverflow = errors.New("playout delay overflow") 32 | errTooSmall = errors.New("buffer too small") 33 | ) 34 | 35 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 36 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 37 | // | ID | len=2 | MIN delay | MAX delay | 38 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 39 | // The wired MIN/MAX delay is in 10ms unit 40 | 41 | type PlayOutDelay struct { 42 | Min, Max uint16 // delay in ms 43 | } 44 | 45 | func PlayoutDelayFromValue(min, max uint16) PlayOutDelay { 46 | if min > PlayoutDelayMaxValue { 47 | min = PlayoutDelayMaxValue 48 | } 49 | if max > PlayoutDelayMaxValue { 50 | max = PlayoutDelayMaxValue 51 | } 52 | return PlayOutDelay{Min: min, Max: max} 53 | } 54 | 55 | func (p PlayOutDelay) Marshal() ([]byte, error) { 56 | min, max := p.Min/10, p.Max/10 57 | if min >= 1<<12 || max >= 1<<12 { 58 | return nil, errPlayoutDelayOverflow 59 | } 60 | 61 | return []byte{byte(min >> 4), byte(min<<4) | byte(max>>8), byte(max)}, nil 62 | } 63 | 64 | func (p *PlayOutDelay) Unmarshal(rawData []byte) error { 65 | if len(rawData) < playoutDelayExtensionSize { 66 | return errTooSmall 67 | } 68 | 69 | p.Min = (binary.BigEndian.Uint16(rawData) >> 4) * 10 70 | p.Max = (binary.BigEndian.Uint16(rawData[1:]) & 0x0FFF) * 10 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/sfu/streamallocator/streamstateupdate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package streamallocator 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/livekit/protocol/livekit" 21 | ) 22 | 23 | // ------------------------------------------------ 24 | 25 | type StreamState int 26 | 27 | const ( 28 | StreamStateInactive StreamState = iota 29 | StreamStateActive 30 | StreamStatePaused 31 | ) 32 | 33 | func (s StreamState) String() string { 34 | switch s { 35 | case StreamStateInactive: 36 | return "INACTIVE" 37 | case StreamStateActive: 38 | return "ACTIVE" 39 | case StreamStatePaused: 40 | return "PAUSED" 41 | default: 42 | return fmt.Sprintf("UNKNOWN: %d", int(s)) 43 | } 44 | } 45 | 46 | // ------------------------------------------------ 47 | 48 | type StreamStateInfo struct { 49 | ParticipantID livekit.ParticipantID 50 | TrackID livekit.TrackID 51 | State StreamState 52 | } 53 | 54 | type StreamStateUpdate struct { 55 | StreamStates []*StreamStateInfo 56 | } 57 | 58 | func NewStreamStateUpdate() *StreamStateUpdate { 59 | return &StreamStateUpdate{} 60 | } 61 | 62 | func (s *StreamStateUpdate) HandleStreamingChange(track *Track, streamState StreamState) { 63 | switch streamState { 64 | case StreamStateInactive: 65 | // inactive is not a notification, could get into this state because of mute 66 | case StreamStateActive: 67 | s.StreamStates = append(s.StreamStates, &StreamStateInfo{ 68 | ParticipantID: track.PublisherID(), 69 | TrackID: track.ID(), 70 | State: StreamStateActive, 71 | }) 72 | case StreamStatePaused: 73 | s.StreamStates = append(s.StreamStates, &StreamStateInfo{ 74 | ParticipantID: track.PublisherID(), 75 | TrackID: track.ID(), 76 | State: StreamStatePaused, 77 | }) 78 | } 79 | } 80 | 81 | func (s *StreamStateUpdate) Empty() bool { 82 | return len(s.StreamStates) == 0 83 | } 84 | 85 | // ------------------------------------------------ 86 | -------------------------------------------------------------------------------- /pkg/utils/incrementaldispatcher_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 LiveKit, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package utils_test 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | "sync/atomic" 23 | "testing" 24 | "time" 25 | 26 | "github.com/stretchr/testify/require" 27 | 28 | "github.com/livekit/livekit-server/pkg/testutils" 29 | "github.com/livekit/livekit-server/pkg/utils" 30 | ) 31 | 32 | func TestForEach(t *testing.T) { 33 | producer := utils.NewIncrementalDispatcher[int]() 34 | go func() { 35 | defer producer.Done() 36 | producer.Add(1) 37 | producer.Add(2) 38 | producer.Add(3) 39 | }() 40 | 41 | sum := 0 42 | producer.ForEach(func(item int) { 43 | sum += item 44 | }) 45 | 46 | require.Equal(t, 6, sum) 47 | } 48 | 49 | func TestConcurrentConsumption(t *testing.T) { 50 | producer := utils.NewIncrementalDispatcher[int]() 51 | numConsumers := 100 52 | sums := make([]atomic.Int32, numConsumers) 53 | var wg sync.WaitGroup 54 | 55 | for i := 0; i < numConsumers; i++ { 56 | wg.Add(1) 57 | i := i 58 | go func() { 59 | defer wg.Done() 60 | producer.ForEach(func(item int) { 61 | sums[i].Add(int32(item)) 62 | }) 63 | }() 64 | } 65 | 66 | // Add items 67 | expectedSum := 0 68 | for i := 0; i < 20; i++ { 69 | expectedSum += i 70 | producer.Add(i) 71 | } 72 | 73 | for i := 0; i < numConsumers; i++ { 74 | testutils.WithTimeout(t, func() string { 75 | if sums[i].Load() != int32(expectedSum) { 76 | return fmt.Sprintf("consumer %d did not consume all the items. expected %d, actual: %d", 77 | i, expectedSum, sums[i].Load()) 78 | } 79 | return "" 80 | }, time.Second) 81 | } 82 | 83 | // keep adding and ensure it's consumed 84 | for i := 20; i < 30; i++ { 85 | expectedSum += i 86 | producer.Add(i) 87 | } 88 | 89 | // wait for all consumers to finish 90 | producer.Done() 91 | wg.Wait() 92 | 93 | for i := 0; i < numConsumers; i++ { 94 | require.Equal(t, int32(expectedSum), sums[i].Load(), "consumer %d did not match", i) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/service/clients.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "context" 19 | 20 | "google.golang.org/protobuf/types/known/emptypb" 21 | 22 | "github.com/livekit/livekit-server/pkg/rtc" 23 | "github.com/livekit/protocol/livekit" 24 | "github.com/livekit/protocol/logger" 25 | "github.com/livekit/protocol/rpc" 26 | "github.com/livekit/protocol/utils" 27 | "github.com/livekit/protocol/utils/guid" 28 | ) 29 | 30 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 31 | 32 | //counterfeiter:generate . IOClient 33 | type IOClient interface { 34 | CreateEgress(ctx context.Context, info *livekit.EgressInfo) (*emptypb.Empty, error) 35 | GetEgress(ctx context.Context, req *rpc.GetEgressRequest) (*livekit.EgressInfo, error) 36 | ListEgress(ctx context.Context, req *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error) 37 | CreateIngress(ctx context.Context, req *livekit.IngressInfo) (*emptypb.Empty, error) 38 | UpdateIngressState(ctx context.Context, req *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error) 39 | } 40 | 41 | type egressLauncher struct { 42 | client rpc.EgressClient 43 | io IOClient 44 | } 45 | 46 | func NewEgressLauncher(client rpc.EgressClient, io IOClient) rtc.EgressLauncher { 47 | if client == nil { 48 | return nil 49 | } 50 | return &egressLauncher{ 51 | client: client, 52 | io: io, 53 | } 54 | } 55 | 56 | func (s *egressLauncher) StartEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) { 57 | if s.client == nil { 58 | return nil, ErrEgressNotConnected 59 | } 60 | 61 | // Ensure we have an Egress ID 62 | if req.EgressId == "" { 63 | req.EgressId = guid.New(utils.EgressPrefix) 64 | } 65 | 66 | info, err := s.client.StartEgress(ctx, "", req) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | _, err = s.io.CreateEgress(ctx, info) 72 | if err != nil { 73 | logger.Errorw("failed to create egress", err) 74 | } 75 | 76 | return info, nil 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 LiveKit, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release to Docker 16 | 17 | # Controls when the action will run. 18 | on: 19 | workflow_dispatch: 20 | push: 21 | branches: 22 | - master # publish to 'master' tag 23 | tags: 24 | - 'v*.*.*' # publish on version tags, updates 'latest' tag 25 | jobs: 26 | docker: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Docker meta 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | # list of Docker images to use as base name for tags 35 | images: | 36 | livekit/livekit-server 37 | # generate Docker tags based on the following events/attributes 38 | tags: | 39 | type=ref,event=branch 40 | type=semver,pattern=v{{version}} 41 | type=semver,pattern=v{{major}}.{{minor}} 42 | 43 | - name: Set up Go 44 | uses: actions/setup-go@v5 45 | with: 46 | go-version-file: "go.mod" 47 | 48 | - name: Download Go modules 49 | run: go mod download 50 | 51 | - name: Generate code 52 | uses: magefile/mage-action@v3 53 | with: 54 | version: latest 55 | args: generate 56 | 57 | - name: Set up Docker Buildx 58 | uses: docker/setup-buildx-action@v3 59 | 60 | - name: Login to DockerHub 61 | if: github.event_name != 'pull_request' 62 | uses: docker/login-action@v3 63 | with: 64 | username: ${{ secrets.DOCKERHUB_USERNAME }} 65 | password: ${{ secrets.DOCKERHUB_TOKEN }} 66 | 67 | - name: Build and push 68 | id: docker_build 69 | uses: docker/build-push-action@v6 70 | with: 71 | context: . 72 | push: ${{ github.event_name != 'pull_request' }} 73 | platforms: linux/amd64,linux/arm64 74 | tags: ${{ steps.meta.outputs.tags }} 75 | labels: ${{ steps.meta.outputs.labels }} 76 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/decodetarget.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package videolayerselector 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 21 | dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor" 22 | ) 23 | 24 | type DecodeTarget struct { 25 | buffer.DependencyDescriptorDecodeTarget 26 | chain *FrameChain 27 | active bool 28 | } 29 | 30 | type FrameDetectionResult struct { 31 | TargetValid bool 32 | DTI dd.DecodeTargetIndication 33 | } 34 | 35 | func NewDecodeTarget(target buffer.DependencyDescriptorDecodeTarget, chain *FrameChain) *DecodeTarget { 36 | return &DecodeTarget{ 37 | DependencyDescriptorDecodeTarget: target, 38 | chain: chain, 39 | } 40 | } 41 | 42 | func (dt *DecodeTarget) Valid() bool { 43 | return dt.chain == nil || !dt.chain.Broken() 44 | } 45 | 46 | func (dt *DecodeTarget) Active() bool { 47 | return dt.active 48 | } 49 | 50 | func (dt *DecodeTarget) UpdateActive(activeBitmask uint32) { 51 | active := (activeBitmask & (1 << dt.Target)) != 0 52 | dt.active = active 53 | if dt.chain != nil { 54 | dt.chain.UpdateActive(active) 55 | } 56 | } 57 | 58 | func (dt *DecodeTarget) OnFrame(extFrameNum uint64, fd *dd.FrameDependencyTemplate) (FrameDetectionResult, error) { 59 | result := FrameDetectionResult{} 60 | if len(fd.DecodeTargetIndications) <= dt.Target { 61 | return result, fmt.Errorf("mismatch target %d and len(DecodeTargetIndications) %d", dt.Target, len(fd.DecodeTargetIndications)) 62 | } 63 | 64 | result.DTI = fd.DecodeTargetIndications[dt.Target] 65 | // The encoder can choose not to use frame chain in theory, and we need to trace every required frame is decodable in this case. 66 | // But we don't observe this in browser and it makes no sense to not use the chain with svc, so only use chain to detect decode target broken now, 67 | // and always return decodable if it is not protect by chain. 68 | result.TargetValid = dt.Valid() 69 | return result, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/sfu/playoutdelay_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sfu 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 24 | pd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/playoutdelay" 25 | "github.com/livekit/protocol/logger" 26 | ) 27 | 28 | func TestPlayoutDelay(t *testing.T) { 29 | stats := buffer.NewRTPStatsSender(buffer.RTPStatsParams{ClockRate: 900000, Logger: logger.GetLogger()}) 30 | c, err := NewPlayoutDelayController(100, 120, logger.GetLogger(), stats) 31 | require.NoError(t, err) 32 | 33 | ext := c.GetDelayExtension(100) 34 | playoutDelayEqual(t, ext, 100, 120) 35 | 36 | ext = c.GetDelayExtension(105) 37 | playoutDelayEqual(t, ext, 100, 120) 38 | 39 | // seq acked before delay changed 40 | c.OnSeqAcked(65534) 41 | ext = c.GetDelayExtension(105) 42 | playoutDelayEqual(t, ext, 100, 120) 43 | 44 | c.OnSeqAcked(90) 45 | ext = c.GetDelayExtension(105) 46 | playoutDelayEqual(t, ext, 100, 120) 47 | 48 | // seq acked, no extension sent for new packet 49 | c.OnSeqAcked(103) 50 | ext = c.GetDelayExtension(106) 51 | require.Nil(t, ext) 52 | 53 | // delay on change(can't go below min), no extension sent 54 | c.SetJitter(0) 55 | ext = c.GetDelayExtension(107) 56 | require.Nil(t, ext) 57 | 58 | // delay changed, generate new extension to send 59 | time.Sleep(200 * time.Millisecond) 60 | c.SetJitter(50) 61 | t.Log(c.currentDelay, c.state.Load()) 62 | ext = c.GetDelayExtension(108) 63 | var delay pd.PlayOutDelay 64 | require.NoError(t, delay.Unmarshal(ext)) 65 | require.Greater(t, delay.Min, uint16(100)) 66 | 67 | // can't go above max 68 | time.Sleep(200 * time.Millisecond) 69 | c.SetJitter(10000) 70 | ext = c.GetDelayExtension(109) 71 | playoutDelayEqual(t, ext, 120, 120) 72 | } 73 | 74 | func playoutDelayEqual(t *testing.T, data []byte, min, max uint16) { 75 | var delay pd.PlayOutDelay 76 | require.NoError(t, delay.Unmarshal(data)) 77 | require.Equal(t, min, delay.Min) 78 | require.Equal(t, max, delay.Max) 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/buildtest.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 LiveKit, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Test 16 | 17 | on: 18 | workflow_dispatch: 19 | push: 20 | branches: [master] 21 | pull_request: 22 | branches: [master] 23 | 24 | jobs: 25 | test: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: shogo82148/actions-setup-redis@v1 30 | with: 31 | redis-version: "6.x" 32 | auto-start: true 33 | - run: redis-cli ping 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v5 37 | with: 38 | go-version-file: "go.mod" 39 | 40 | - name: Set up gotestfmt 41 | run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.4.1 42 | 43 | - name: Replace mutexes 44 | run: | 45 | go get github.com/sasha-s/go-deadlock 46 | grep -rl sync.Mutex ./pkg | xargs sed -i 's/sync\.Mutex/deadlock\.Mutex/g' 47 | grep -rl sync.RWMutex ./pkg | xargs sed -i 's/sync\.RWMutex/deadlock\.RWMutex/g' 48 | go install golang.org/x/tools/cmd/goimports 49 | grep -rl deadlock.Mutex ./pkg | xargs goimports -w 50 | grep -rl deadlock.RWMutex ./pkg | xargs goimports -w 51 | go mod tidy 52 | 53 | - name: Mage Build 54 | uses: magefile/mage-action@v3 55 | with: 56 | version: latest 57 | args: build 58 | 59 | - name: Static Check 60 | uses: amarpal/staticcheck-action@master 61 | with: 62 | checks: '["all", "-ST1000", "-ST1003", "-ST1020", "-ST1021", "-ST1022", "-SA1019"]' 63 | install-go: false 64 | 65 | - name: Test 66 | run: | 67 | set -euo pipefail 68 | MallocNanoZone=0 go test -race -json -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt 69 | 70 | # Upload the original go test log as an artifact for later review. 71 | - name: Upload test log 72 | uses: actions/upload-artifact@v4 73 | if: always() 74 | with: 75 | name: test-log 76 | path: /tmp/gotest.log 77 | if-no-files-found: error 78 | -------------------------------------------------------------------------------- /pkg/sfu/forwardstats.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/livekit/livekit-server/pkg/telemetry/prometheus" 10 | "github.com/livekit/protocol/logger" 11 | "github.com/livekit/protocol/utils" 12 | ) 13 | 14 | type ForwardStats struct { 15 | lock sync.Mutex 16 | lastLeftNano atomic.Int64 17 | latency *utils.LatencyAggregate 18 | closeCh chan struct{} 19 | } 20 | 21 | func NewForwardStats(latencyUpdateInterval, reportInterval, latencyWindowLength time.Duration) *ForwardStats { 22 | s := &ForwardStats{ 23 | latency: utils.NewLatencyAggregate(latencyUpdateInterval, latencyWindowLength), 24 | closeCh: make(chan struct{}), 25 | } 26 | 27 | go s.report(reportInterval) 28 | return s 29 | } 30 | 31 | func (s *ForwardStats) Update(arrival, left int64) { 32 | transit := left - arrival 33 | 34 | // ignore if transit is too large or negative, this could happen if system time is adjusted 35 | if transit < 0 || time.Duration(transit) > 5*time.Second { 36 | return 37 | } 38 | lastLeftNano := s.lastLeftNano.Load() 39 | if left < lastLeftNano || !s.lastLeftNano.CompareAndSwap(lastLeftNano, left) { 40 | return 41 | } 42 | 43 | s.lock.Lock() 44 | defer s.lock.Unlock() 45 | s.latency.Update(time.Duration(arrival), float64(transit)) 46 | } 47 | 48 | func (s *ForwardStats) GetStats() (latency, jitter time.Duration) { 49 | s.lock.Lock() 50 | w := s.latency.Summarize() 51 | s.lock.Unlock() 52 | latency, jitter = time.Duration(w.Mean()), time.Duration(w.StdDev()) 53 | // TODO: remove this check after debugging unexpected jitter issue 54 | if jitter > 10*time.Second { 55 | logger.Infow("unexpected forward jitter", 56 | "jitter", jitter, 57 | "stats", fmt.Sprintf("count %.2f, mean %.2f, stdDev %.2f", w.Count(), w.Mean(), w.StdDev()), 58 | ) 59 | } 60 | return 61 | } 62 | 63 | func (s *ForwardStats) GetLastStats(duration time.Duration) (latency, jitter time.Duration) { 64 | s.lock.Lock() 65 | defer s.lock.Unlock() 66 | w := s.latency.SummarizeLast(duration) 67 | return time.Duration(w.Mean()), time.Duration(w.StdDev()) 68 | } 69 | 70 | func (s *ForwardStats) Stop() { 71 | close(s.closeCh) 72 | } 73 | 74 | func (s *ForwardStats) report(reportInterval time.Duration) { 75 | ticker := time.NewTicker(reportInterval) 76 | defer ticker.Stop() 77 | for { 78 | select { 79 | case <-s.closeCh: 80 | return 81 | case <-ticker.C: 82 | latency, jitter := s.GetLastStats(reportInterval) 83 | latencySlow, jitterSlow := s.GetStats() 84 | prometheus.RecordForwardJitter(uint32(jitter/time.Millisecond), uint32(jitterSlow/time.Millisecond)) 85 | prometheus.RecordForwardLatency(uint32(latency/time.Millisecond), uint32(latencySlow/time.Millisecond)) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/sfu/streamtracker/streamtracker_packet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package streamtracker 16 | 17 | import ( 18 | "time" 19 | 20 | "github.com/livekit/livekit-server/pkg/config" 21 | "github.com/livekit/protocol/logger" 22 | ) 23 | 24 | type StreamTrackerPacketParams struct { 25 | Config config.StreamTrackerPacketConfig 26 | Logger logger.Logger 27 | } 28 | 29 | type StreamTrackerPacket struct { 30 | params StreamTrackerPacketParams 31 | 32 | countSinceLast uint32 // number of packets received since last check 33 | 34 | initialized bool 35 | 36 | cycleCount uint32 37 | } 38 | 39 | func NewStreamTrackerPacket(params StreamTrackerPacketParams) StreamTrackerImpl { 40 | return &StreamTrackerPacket{ 41 | params: params, 42 | } 43 | } 44 | 45 | func (s *StreamTrackerPacket) Start() { 46 | } 47 | 48 | func (s *StreamTrackerPacket) Stop() { 49 | } 50 | 51 | func (s *StreamTrackerPacket) Reset() { 52 | s.countSinceLast = 0 53 | s.cycleCount = 0 54 | 55 | s.initialized = false 56 | } 57 | 58 | func (s *StreamTrackerPacket) GetCheckInterval() time.Duration { 59 | return s.params.Config.CycleDuration 60 | } 61 | 62 | func (s *StreamTrackerPacket) Observe(_hasMarker bool, _ts uint32) StreamStatusChange { 63 | if !s.initialized { 64 | // first packet 65 | s.initialized = true 66 | s.countSinceLast = 1 67 | return StreamStatusChangeActive 68 | } 69 | 70 | s.countSinceLast++ 71 | return StreamStatusChangeNone 72 | } 73 | 74 | func (s *StreamTrackerPacket) CheckStatus() StreamStatusChange { 75 | if !s.initialized { 76 | // should not be getting called when not initialized, but be safe 77 | return StreamStatusChangeNone 78 | } 79 | 80 | if s.countSinceLast >= s.params.Config.SamplesRequired { 81 | s.cycleCount++ 82 | } else { 83 | s.cycleCount = 0 84 | } 85 | 86 | statusChange := StreamStatusChangeNone 87 | if s.cycleCount == 0 { 88 | // no packets seen for a period, flip to stopped 89 | statusChange = StreamStatusChangeStopped 90 | } else if s.cycleCount >= s.params.Config.CyclesRequired { 91 | // packets seen for some time after resume, flip to active 92 | statusChange = StreamStatusChangeActive 93 | } 94 | 95 | s.countSinceLast = 0 96 | return statusChange 97 | } 98 | -------------------------------------------------------------------------------- /pkg/sfu/pacer/base.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pacer 16 | 17 | import ( 18 | "errors" 19 | "io" 20 | "time" 21 | 22 | "github.com/livekit/protocol/logger" 23 | "github.com/pion/rtp" 24 | ) 25 | 26 | type Base struct { 27 | logger logger.Logger 28 | 29 | packetTime *PacketTime 30 | } 31 | 32 | func NewBase(logger logger.Logger) *Base { 33 | return &Base{ 34 | logger: logger, 35 | packetTime: NewPacketTime(), 36 | } 37 | } 38 | 39 | func (b *Base) SetInterval(_interval time.Duration) { 40 | } 41 | 42 | func (b *Base) SetBitrate(_bitrate int) { 43 | } 44 | 45 | func (b *Base) SendPacket(p *Packet) (int, error) { 46 | defer func() { 47 | if p.Pool != nil && p.PoolEntity != nil { 48 | p.Pool.Put(p.PoolEntity) 49 | } 50 | }() 51 | 52 | _, err := b.writeRTPHeaderExtensions(p) 53 | if err != nil { 54 | b.logger.Errorw("writing rtp header extensions err", err) 55 | return 0, err 56 | } 57 | 58 | var written int 59 | written, err = p.WriteStream.WriteRTP(p.Header, p.Payload) 60 | if err != nil { 61 | if !errors.Is(err, io.ErrClosedPipe) { 62 | b.logger.Errorw("write rtp packet failed", err) 63 | } 64 | return 0, err 65 | } 66 | 67 | return written, nil 68 | } 69 | 70 | // writes RTP header extensions of track 71 | func (b *Base) writeRTPHeaderExtensions(p *Packet) (time.Time, error) { 72 | // clear out extensions that may have been in the forwarded header 73 | p.Header.Extension = false 74 | p.Header.ExtensionProfile = 0 75 | p.Header.Extensions = []rtp.Extension{} 76 | 77 | for _, ext := range p.Extensions { 78 | if ext.ID == 0 || len(ext.Payload) == 0 { 79 | continue 80 | } 81 | 82 | p.Header.SetExtension(ext.ID, ext.Payload) 83 | } 84 | 85 | sendingAt := b.packetTime.Get() 86 | if p.AbsSendTimeExtID != 0 { 87 | sendTime := rtp.NewAbsSendTimeExtension(sendingAt) 88 | b, err := sendTime.Marshal() 89 | if err != nil { 90 | return time.Time{}, err 91 | } 92 | 93 | err = p.Header.SetExtension(p.AbsSendTimeExtID, b) 94 | if err != nil { 95 | return time.Time{}, err 96 | } 97 | } 98 | 99 | return sendingAt, nil 100 | } 101 | 102 | // ------------------------------------------------ 103 | -------------------------------------------------------------------------------- /pkg/rtc/types/protocol_version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package types 16 | 17 | type ProtocolVersion int 18 | 19 | const CurrentProtocol = 15 20 | 21 | func (v ProtocolVersion) SupportsPackedStreamId() bool { 22 | return v > 0 23 | } 24 | 25 | func (v ProtocolVersion) SupportsProtobuf() bool { 26 | return v > 0 27 | } 28 | 29 | func (v ProtocolVersion) HandlesDataPackets() bool { 30 | return v > 1 31 | } 32 | 33 | // SubscriberAsPrimary indicates clients initiate subscriber connection as primary 34 | func (v ProtocolVersion) SubscriberAsPrimary() bool { 35 | return v > 2 36 | } 37 | 38 | // SupportsSpeakerChanged - if client handles speaker info deltas, instead of a comprehensive list 39 | func (v ProtocolVersion) SupportsSpeakerChanged() bool { 40 | return v > 2 41 | } 42 | 43 | // SupportsTransceiverReuse - if transceiver reuse is supported, optimizes SDP size 44 | func (v ProtocolVersion) SupportsTransceiverReuse() bool { 45 | return v > 3 46 | } 47 | 48 | // SupportsConnectionQuality - avoid sending frequent ConnectionQuality updates for lower protocol versions 49 | func (v ProtocolVersion) SupportsConnectionQuality() bool { 50 | return v > 4 51 | } 52 | 53 | func (v ProtocolVersion) SupportsSessionMigrate() bool { 54 | return v > 5 55 | } 56 | 57 | func (v ProtocolVersion) SupportsICELite() bool { 58 | return v > 5 59 | } 60 | 61 | func (v ProtocolVersion) SupportsUnpublish() bool { 62 | return v > 6 63 | } 64 | 65 | // SupportFastStart - if client supports fast start, server side will send media streams 66 | // in the first offer 67 | func (v ProtocolVersion) SupportFastStart() bool { 68 | return v > 7 69 | } 70 | 71 | func (v ProtocolVersion) SupportHandlesDisconnectedUpdate() bool { 72 | return v > 8 73 | } 74 | 75 | func (v ProtocolVersion) SupportSyncStreamID() bool { 76 | return v > 9 77 | } 78 | 79 | func (v ProtocolVersion) SupportsConnectionQualityLost() bool { 80 | return v > 10 81 | } 82 | 83 | func (v ProtocolVersion) SupportsAsyncRoomID() bool { 84 | return v > 11 85 | } 86 | 87 | func (v ProtocolVersion) SupportsIdentityBasedReconnection() bool { 88 | return v > 11 89 | } 90 | 91 | func (v ProtocolVersion) SupportsRegionsInLeaveRequest() bool { 92 | return v > 12 93 | } 94 | 95 | func (v ProtocolVersion) SupportsNonErrorSignalResponse() bool { 96 | return v > 14 97 | } 98 | -------------------------------------------------------------------------------- /pkg/clientconfiguration/match.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package clientconfiguration 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "strings" 21 | 22 | "github.com/d5/tengo/v2" 23 | 24 | "github.com/livekit/protocol/livekit" 25 | ) 26 | 27 | type Match interface { 28 | Match(clientInfo *livekit.ClientInfo) (bool, error) 29 | } 30 | 31 | type ScriptMatch struct { 32 | Expr string 33 | } 34 | 35 | // use result of eval script expression for match. 36 | // expression examples: 37 | // protocol bigger than 5 : c.protocol > 5 38 | // browser if firefox: c.browser == "firefox" 39 | // combined rule : c.protocol > 5 && c.browser == "firefox" 40 | func (m *ScriptMatch) Match(clientInfo *livekit.ClientInfo) (bool, error) { 41 | res, err := tengo.Eval(context.TODO(), m.Expr, map[string]interface{}{"c": &clientObject{info: clientInfo}}) 42 | if err != nil { 43 | return false, err 44 | } 45 | 46 | if val, ok := res.(bool); ok { 47 | return val, nil 48 | } 49 | return false, errors.New("invalid match expression result") 50 | } 51 | 52 | type clientObject struct { 53 | tengo.ObjectImpl 54 | info *livekit.ClientInfo 55 | } 56 | 57 | func (c *clientObject) TypeName() string { 58 | return "clientObject" 59 | } 60 | 61 | func (c *clientObject) String() string { 62 | return c.info.String() 63 | } 64 | 65 | func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error) { 66 | field, ok := index.(*tengo.String) 67 | if !ok { 68 | return nil, tengo.ErrInvalidIndexType 69 | } 70 | 71 | switch field.Value { 72 | case "sdk": 73 | return &tengo.String{Value: strings.ToLower(c.info.Sdk.String())}, nil 74 | case "version": 75 | return &tengo.String{Value: c.info.Version}, nil 76 | case "protocol": 77 | return &tengo.Int{Value: int64(c.info.Protocol)}, nil 78 | case "os": 79 | return &tengo.String{Value: strings.ToLower(c.info.Os)}, nil 80 | case "os_version": 81 | return &tengo.String{Value: c.info.OsVersion}, nil 82 | case "device_model": 83 | return &tengo.String{Value: strings.ToLower(c.info.DeviceModel)}, nil 84 | case "browser": 85 | return &tengo.String{Value: strings.ToLower(c.info.Browser)}, nil 86 | case "browser_version": 87 | return &tengo.String{Value: c.info.BrowserVersion}, nil 88 | case "address": 89 | return &tengo.String{Value: c.info.Address}, nil 90 | } 91 | return &tengo.Undefined{}, nil 92 | } 93 | -------------------------------------------------------------------------------- /install-livekit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2023 LiveKit, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # LiveKit install script for Linux 17 | 18 | set -u 19 | set -o errtrace 20 | set -o errexit 21 | set -o pipefail 22 | 23 | REPO="livekit" 24 | INSTALL_PATH="/usr/local/bin" 25 | 26 | log() { printf "%b\n" "$*"; } 27 | abort() { 28 | printf "%s\n" "$@" >&2 29 | exit 1 30 | } 31 | 32 | # returns the latest version according to GH 33 | # i.e. 1.0.0 34 | get_latest_version() 35 | { 36 | latest_version=$(curl -s https://api.github.com/repos/livekit/$REPO/releases/latest | grep -oP '"tarball_url": ".*/tarball/v\K([^/]*)(?=")') 37 | printf "%s" "$latest_version" 38 | } 39 | 40 | # Ensure bash is used 41 | if [ -z "${BASH_VERSION:-}" ] 42 | then 43 | abort "This script requires bash" 44 | fi 45 | 46 | # Check if $INSTALL_PATH exists 47 | if [ ! -d ${INSTALL_PATH} ] 48 | then 49 | abort "Could not install, ${INSTALL_PATH} doesn't exist" 50 | fi 51 | 52 | # Needs SUDO if no permissions to write 53 | SUDO_PREFIX="" 54 | if [ ! -w ${INSTALL_PATH} ] 55 | then 56 | SUDO_PREFIX="sudo" 57 | log "sudo is required to install to ${INSTALL_PATH}" 58 | fi 59 | 60 | # Check cURL is installed 61 | if ! command -v curl >/dev/null 62 | then 63 | abort "cURL is required and is not found" 64 | fi 65 | 66 | # OS check 67 | OS="$(uname)" 68 | if [[ "${OS}" == "Darwin" ]] 69 | then 70 | abort "Installer not supported on MacOS, please install using Homebrew." 71 | elif [[ "${OS}" != "Linux" ]] 72 | then 73 | abort "Installer is only supported on Linux." 74 | fi 75 | 76 | ARCH="$(uname -m)" 77 | 78 | # fix arch on linux 79 | if [[ "${ARCH}" == "aarch64" ]] 80 | then 81 | ARCH="arm64" 82 | elif [[ "${ARCH}" == "x86_64" ]] 83 | then 84 | ARCH="amd64" 85 | fi 86 | 87 | VERSION=$(get_latest_version) 88 | ARCHIVE_URL="https://github.com/livekit/$REPO/releases/download/v${VERSION}/${REPO}_${VERSION}_linux_${ARCH}.tar.gz" 89 | 90 | # Ensure version follows SemVer 91 | if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] 92 | then 93 | abort "Invalid version: ${VERSION}" 94 | fi 95 | 96 | log "Installing ${REPO} ${VERSION}" 97 | log "Downloading from ${ARCHIVE_URL}..." 98 | 99 | curl -s -L "${ARCHIVE_URL}" | ${SUDO_PREFIX} tar xzf - -C "${INSTALL_PATH}" --wildcards --no-anchored "$REPO*" 100 | 101 | log "\nlivekit-server is installed to $INSTALL_PATH\n" 102 | -------------------------------------------------------------------------------- /pkg/sfu/utils/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/pion/interceptor" 23 | "github.com/pion/rtp" 24 | "github.com/pion/webrtc/v3" 25 | ) 26 | 27 | // Do a fuzzy find for a codec in the list of codecs 28 | // Used for lookup up a codec in an existing list to find a match 29 | func CodecParametersFuzzySearch(needle webrtc.RTPCodecParameters, haystack []webrtc.RTPCodecParameters) (webrtc.RTPCodecParameters, error) { 30 | // First attempt to match on MimeType + SDPFmtpLine 31 | for _, c := range haystack { 32 | if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) && 33 | c.RTPCodecCapability.SDPFmtpLine == needle.RTPCodecCapability.SDPFmtpLine { 34 | return c, nil 35 | } 36 | } 37 | 38 | // Fallback to just MimeType 39 | for _, c := range haystack { 40 | if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) { 41 | return c, nil 42 | } 43 | } 44 | 45 | return webrtc.RTPCodecParameters{}, webrtc.ErrCodecNotFound 46 | } 47 | 48 | // GetHeaderExtensionID returns the ID of a header extension, or 0 if not found 49 | func GetHeaderExtensionID(extensions []interceptor.RTPHeaderExtension, extension webrtc.RTPHeaderExtensionCapability) int { 50 | for _, h := range extensions { 51 | if extension.URI == h.URI { 52 | return h.ID 53 | } 54 | } 55 | return 0 56 | } 57 | 58 | var ( 59 | ErrInvalidRTPVersion = errors.New("invalid RTP version") 60 | ErrRTPPayloadTypeMismatch = errors.New("RTP payload type mismatch") 61 | ErrRTPSSRCMismatch = errors.New("RTP SSRC mismatch") 62 | ) 63 | 64 | // ValidateRTPPacket checks for a valid RTP packet and returns an error if fields are incorrect 65 | func ValidateRTPPacket(pkt *rtp.Packet, expectedPayloadType uint8, expectedSSRC uint32) error { 66 | if pkt.Version != 2 { 67 | return fmt.Errorf("%w, expected: 2, actual: %d", ErrInvalidRTPVersion, pkt.Version) 68 | } 69 | 70 | if expectedPayloadType != 0 && pkt.PayloadType != expectedPayloadType { 71 | return fmt.Errorf("%w, expected: %d, actual: %d", ErrRTPPayloadTypeMismatch, expectedPayloadType, pkt.PayloadType) 72 | } 73 | 74 | if expectedSSRC != 0 && pkt.SSRC != expectedSSRC { 75 | return fmt.Errorf("%w, expected: %d, actual: %d", ErrRTPSSRCMismatch, expectedSSRC, pkt.SSRC) 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/rtc/medialossproxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rtc 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | 21 | "github.com/pion/rtcp" 22 | 23 | "github.com/livekit/protocol/livekit" 24 | "github.com/livekit/protocol/logger" 25 | 26 | "github.com/livekit/livekit-server/pkg/sfu" 27 | ) 28 | 29 | const ( 30 | downLostUpdateDelta = time.Second 31 | ) 32 | 33 | type MediaLossProxyParams struct { 34 | Logger logger.Logger 35 | } 36 | 37 | type MediaLossProxy struct { 38 | params MediaLossProxyParams 39 | 40 | lock sync.Mutex 41 | maxDownFracLost uint8 42 | maxDownFracLostTs time.Time 43 | maxDownFracLostValid bool 44 | 45 | onMediaLossUpdate func(fractionalLoss uint8) 46 | } 47 | 48 | func NewMediaLossProxy(params MediaLossProxyParams) *MediaLossProxy { 49 | return &MediaLossProxy{params: params} 50 | } 51 | 52 | func (m *MediaLossProxy) OnMediaLossUpdate(f func(fractionalLoss uint8)) { 53 | m.lock.Lock() 54 | m.onMediaLossUpdate = f 55 | m.lock.Unlock() 56 | } 57 | 58 | func (m *MediaLossProxy) HandleMaxLossFeedback(_ *sfu.DownTrack, report *rtcp.ReceiverReport) { 59 | m.lock.Lock() 60 | for _, rr := range report.Reports { 61 | m.maxDownFracLostValid = true 62 | if m.maxDownFracLost < rr.FractionLost { 63 | m.maxDownFracLost = rr.FractionLost 64 | } 65 | } 66 | m.lock.Unlock() 67 | 68 | m.maybeUpdateLoss() 69 | } 70 | 71 | func (m *MediaLossProxy) NotifySubscriberNodeMediaLoss(_nodeID livekit.NodeID, fractionalLoss uint8) { 72 | m.lock.Lock() 73 | m.maxDownFracLostValid = true 74 | if m.maxDownFracLost < fractionalLoss { 75 | m.maxDownFracLost = fractionalLoss 76 | } 77 | m.lock.Unlock() 78 | 79 | m.maybeUpdateLoss() 80 | } 81 | 82 | func (m *MediaLossProxy) maybeUpdateLoss() { 83 | var ( 84 | shouldUpdate bool 85 | maxLost uint8 86 | ) 87 | 88 | m.lock.Lock() 89 | now := time.Now() 90 | if now.Sub(m.maxDownFracLostTs) > downLostUpdateDelta && m.maxDownFracLostValid { 91 | shouldUpdate = true 92 | maxLost = m.maxDownFracLost 93 | m.maxDownFracLost = 0 94 | m.maxDownFracLostTs = now 95 | m.maxDownFracLostValid = false 96 | } 97 | onMediaLossUpdate := m.onMediaLossUpdate 98 | m.lock.Unlock() 99 | 100 | if shouldUpdate { 101 | if onMediaLossUpdate != nil { 102 | onMediaLossUpdate(maxLost) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/utils/changenotifier.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 LiveKit, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import "sync" 20 | 21 | type ChangeNotifier struct { 22 | lock sync.Mutex 23 | observers map[string]func() 24 | } 25 | 26 | func NewChangeNotifier() *ChangeNotifier { 27 | return &ChangeNotifier{ 28 | observers: make(map[string]func()), 29 | } 30 | } 31 | 32 | func (n *ChangeNotifier) AddObserver(key string, onChanged func()) { 33 | n.lock.Lock() 34 | defer n.lock.Unlock() 35 | 36 | n.observers[key] = onChanged 37 | } 38 | 39 | func (n *ChangeNotifier) RemoveObserver(key string) { 40 | n.lock.Lock() 41 | defer n.lock.Unlock() 42 | 43 | delete(n.observers, key) 44 | } 45 | 46 | func (n *ChangeNotifier) HasObservers() bool { 47 | n.lock.Lock() 48 | defer n.lock.Unlock() 49 | 50 | return len(n.observers) > 0 51 | } 52 | 53 | func (n *ChangeNotifier) NotifyChanged() { 54 | n.lock.Lock() 55 | if len(n.observers) == 0 { 56 | n.lock.Unlock() 57 | return 58 | } 59 | observers := make([]func(), 0, len(n.observers)) 60 | for _, f := range n.observers { 61 | observers = append(observers, f) 62 | } 63 | n.lock.Unlock() 64 | 65 | go func() { 66 | for _, f := range observers { 67 | f() 68 | } 69 | }() 70 | } 71 | 72 | type ChangeNotifierManager struct { 73 | lock sync.Mutex 74 | notifiers map[string]*ChangeNotifier 75 | } 76 | 77 | func NewChangeNotifierManager() *ChangeNotifierManager { 78 | return &ChangeNotifierManager{ 79 | notifiers: make(map[string]*ChangeNotifier), 80 | } 81 | } 82 | 83 | func (m *ChangeNotifierManager) GetNotifier(key string) *ChangeNotifier { 84 | m.lock.Lock() 85 | defer m.lock.Unlock() 86 | 87 | return m.notifiers[key] 88 | } 89 | 90 | func (m *ChangeNotifierManager) GetOrCreateNotifier(key string) *ChangeNotifier { 91 | m.lock.Lock() 92 | defer m.lock.Unlock() 93 | 94 | if notifier, ok := m.notifiers[key]; ok { 95 | return notifier 96 | } 97 | 98 | notifier := NewChangeNotifier() 99 | m.notifiers[key] = notifier 100 | return notifier 101 | } 102 | 103 | func (m *ChangeNotifierManager) RemoveNotifier(key string, force bool) { 104 | m.lock.Lock() 105 | defer m.lock.Unlock() 106 | 107 | if notifier, ok := m.notifiers[key]; ok { 108 | if force || !notifier.HasObservers() { 109 | delete(m.notifiers, key) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /pkg/rtc/transport/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package transport 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/pion/webrtc/v3" 21 | 22 | "github.com/livekit/livekit-server/pkg/sfu/streamallocator" 23 | "github.com/livekit/protocol/livekit" 24 | ) 25 | 26 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 27 | 28 | var ( 29 | ErrNoICECandidateHandler = errors.New("no ICE candidate handler") 30 | ErrNoOfferHandler = errors.New("no offer handler") 31 | ErrNoAnswerHandler = errors.New("no answer handler") 32 | ) 33 | 34 | //counterfeiter:generate . Handler 35 | type Handler interface { 36 | OnICECandidate(c *webrtc.ICECandidate, target livekit.SignalTarget) error 37 | OnInitialConnected() 38 | OnFullyEstablished() 39 | OnFailed(isShortLived bool) 40 | OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) 41 | OnDataPacket(kind livekit.DataPacket_Kind, data []byte) 42 | OnOffer(sd webrtc.SessionDescription) error 43 | OnAnswer(sd webrtc.SessionDescription) error 44 | OnNegotiationStateChanged(state NegotiationState) 45 | OnNegotiationFailed() 46 | OnStreamStateChange(update *streamallocator.StreamStateUpdate) error 47 | } 48 | 49 | type UnimplementedHandler struct{} 50 | 51 | func (h UnimplementedHandler) OnICECandidate(c *webrtc.ICECandidate, target livekit.SignalTarget) error { 52 | return ErrNoICECandidateHandler 53 | } 54 | func (h UnimplementedHandler) OnInitialConnected() {} 55 | func (h UnimplementedHandler) OnFullyEstablished() {} 56 | func (h UnimplementedHandler) OnFailed(isShortLived bool) {} 57 | func (h UnimplementedHandler) OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {} 58 | func (h UnimplementedHandler) OnDataPacket(kind livekit.DataPacket_Kind, data []byte) {} 59 | func (h UnimplementedHandler) OnOffer(sd webrtc.SessionDescription) error { 60 | return ErrNoOfferHandler 61 | } 62 | func (h UnimplementedHandler) OnAnswer(sd webrtc.SessionDescription) error { 63 | return ErrNoAnswerHandler 64 | } 65 | func (h UnimplementedHandler) OnNegotiationStateChanged(state NegotiationState) {} 66 | func (h UnimplementedHandler) OnNegotiationFailed() {} 67 | func (h UnimplementedHandler) OnStreamStateChange(update *streamallocator.StreamStateUpdate) error { 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/routing/selector/sysload_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package selector_test 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/livekit/protocol/livekit" 24 | 25 | "github.com/livekit/livekit-server/pkg/routing/selector" 26 | ) 27 | 28 | var ( 29 | nodeLoadLow = &livekit.Node{ 30 | State: livekit.NodeState_SERVING, 31 | Stats: &livekit.NodeStats{ 32 | UpdatedAt: time.Now().Unix(), 33 | NumCpus: 1, 34 | CpuLoad: 0.1, 35 | LoadAvgLast1Min: 0.0, 36 | NumRooms: 1, 37 | NumClients: 2, 38 | NumTracksIn: 4, 39 | NumTracksOut: 8, 40 | BytesInPerSec: 1000, 41 | BytesOutPerSec: 2000, 42 | }, 43 | } 44 | 45 | nodeLoadMedium = &livekit.Node{ 46 | State: livekit.NodeState_SERVING, 47 | Stats: &livekit.NodeStats{ 48 | UpdatedAt: time.Now().Unix(), 49 | NumCpus: 1, 50 | CpuLoad: 0.5, 51 | LoadAvgLast1Min: 0.5, 52 | NumRooms: 5, 53 | NumClients: 10, 54 | NumTracksIn: 20, 55 | NumTracksOut: 200, 56 | BytesInPerSec: 5000, 57 | BytesOutPerSec: 10000, 58 | }, 59 | } 60 | 61 | nodeLoadHigh = &livekit.Node{ 62 | State: livekit.NodeState_SERVING, 63 | Stats: &livekit.NodeStats{ 64 | UpdatedAt: time.Now().Unix(), 65 | NumCpus: 1, 66 | CpuLoad: 0.99, 67 | LoadAvgLast1Min: 2.0, 68 | NumRooms: 10, 69 | NumClients: 20, 70 | NumTracksIn: 40, 71 | NumTracksOut: 800, 72 | BytesInPerSec: 10000, 73 | BytesOutPerSec: 40000, 74 | }, 75 | } 76 | ) 77 | 78 | func TestSystemLoadSelector_SelectNode(t *testing.T) { 79 | sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random"} 80 | 81 | var nodes []*livekit.Node 82 | _, err := sel.SelectNode(nodes) 83 | require.Error(t, err, "should error no available nodes") 84 | 85 | // Select a node with high load when no nodes with low load are available 86 | nodes = []*livekit.Node{nodeLoadHigh} 87 | if _, err := sel.SelectNode(nodes); err != nil { 88 | t.Error(err) 89 | } 90 | 91 | // Select a node with low load when available 92 | nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh} 93 | for i := 0; i < 5; i++ { 94 | node, err := sel.SelectNode(nodes) 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | if node != nodeLoadLow { 99 | t.Error("selected the wrong node") 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/sfu/buffer/datastats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buffer 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | 21 | "google.golang.org/protobuf/types/known/timestamppb" 22 | 23 | "github.com/livekit/protocol/livekit" 24 | ) 25 | 26 | type DataStatsParam struct { 27 | WindowDuration time.Duration 28 | } 29 | 30 | type DataStats struct { 31 | params DataStatsParam 32 | lock sync.RWMutex 33 | totalBytes int64 34 | startTime time.Time 35 | endTime time.Time 36 | windowStart int64 37 | windowBytes int64 38 | } 39 | 40 | func NewDataStats(params DataStatsParam) *DataStats { 41 | return &DataStats{ 42 | params: params, 43 | startTime: time.Now(), 44 | windowStart: time.Now().UnixNano(), 45 | } 46 | } 47 | 48 | func (s *DataStats) Update(bytes int, time int64) { 49 | s.lock.Lock() 50 | defer s.lock.Unlock() 51 | s.totalBytes += int64(bytes) 52 | 53 | if s.params.WindowDuration > 0 && time-s.windowStart > s.params.WindowDuration.Nanoseconds() { 54 | s.windowBytes = 0 55 | s.windowStart = time 56 | } 57 | s.windowBytes += int64(bytes) 58 | } 59 | 60 | func (s *DataStats) ToProtoActive() *livekit.RTPStats { 61 | if s.params.WindowDuration == 0 { 62 | return &livekit.RTPStats{} 63 | } 64 | s.lock.RLock() 65 | defer s.lock.RUnlock() 66 | now := time.Now().UnixNano() 67 | duration := now - s.windowStart 68 | if duration > s.params.WindowDuration.Nanoseconds() { 69 | return &livekit.RTPStats{} 70 | } 71 | 72 | return &livekit.RTPStats{ 73 | StartTime: timestamppb.New(time.Unix(s.windowStart/1e9, s.windowStart%1e9)), 74 | EndTime: timestamppb.New(time.Unix(0, now)), 75 | Duration: float64(duration / 1e9), 76 | Bytes: uint64(s.windowBytes), 77 | Bitrate: float64(s.windowBytes) * 8 / float64(duration) / 1e9, 78 | } 79 | } 80 | 81 | func (s *DataStats) Stop() { 82 | s.lock.Lock() 83 | s.endTime = time.Now() 84 | s.lock.Unlock() 85 | } 86 | 87 | func (s *DataStats) ToProtoAggregateOnly() *livekit.RTPStats { 88 | s.lock.RLock() 89 | defer s.lock.RUnlock() 90 | 91 | end := s.endTime 92 | if end.IsZero() { 93 | end = time.Now() 94 | } 95 | return &livekit.RTPStats{ 96 | StartTime: timestamppb.New(s.startTime), 97 | EndTime: timestamppb.New(end), 98 | Duration: end.Sub(s.startTime).Seconds(), 99 | Bytes: uint64(s.totalBytes), 100 | Bitrate: float64(s.totalBytes) * 8 / end.Sub(s.startTime).Seconds(), 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "flag" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | "github.com/urfave/cli/v2" 23 | 24 | "github.com/livekit/livekit-server/pkg/config/configtest" 25 | ) 26 | 27 | func TestConfig_UnmarshalKeys(t *testing.T) { 28 | conf, err := NewConfig("", true, nil, nil) 29 | require.NoError(t, err) 30 | 31 | require.NoError(t, conf.unmarshalKeys("key1: secret1")) 32 | require.Equal(t, "secret1", conf.Keys["key1"]) 33 | } 34 | 35 | func TestConfig_DefaultsKept(t *testing.T) { 36 | const content = `room: 37 | empty_timeout: 10` 38 | conf, err := NewConfig(content, true, nil, nil) 39 | require.NoError(t, err) 40 | require.Equal(t, true, conf.Room.AutoCreate) 41 | require.Equal(t, uint32(10), conf.Room.EmptyTimeout) 42 | } 43 | 44 | func TestConfig_UnknownKeys(t *testing.T) { 45 | const content = `unknown: 10 46 | room: 47 | empty_timeout: 10` 48 | _, err := NewConfig(content, true, nil, nil) 49 | require.Error(t, err) 50 | } 51 | 52 | func TestGeneratedFlags(t *testing.T) { 53 | generatedFlags, err := GenerateCLIFlags(nil, false) 54 | require.NoError(t, err) 55 | 56 | app := cli.NewApp() 57 | app.Name = "test" 58 | app.Flags = append(app.Flags, generatedFlags...) 59 | 60 | set := flag.NewFlagSet("test", 0) 61 | set.Bool("rtc.use_ice_lite", true, "") // bool 62 | set.String("redis.address", "localhost:6379", "") // string 63 | set.Uint("prometheus.port", 9999, "") // uint32 64 | set.Bool("rtc.allow_tcp_fallback", true, "") // pointer 65 | set.Bool("rtc.reconnect_on_publication_error", true, "") // pointer 66 | set.Bool("rtc.reconnect_on_subscription_error", false, "") // pointer 67 | 68 | c := cli.NewContext(app, set, nil) 69 | conf, err := NewConfig("", true, c, nil) 70 | require.NoError(t, err) 71 | 72 | require.True(t, conf.RTC.UseICELite) 73 | require.Equal(t, "localhost:6379", conf.Redis.Address) 74 | require.Equal(t, uint32(9999), conf.Prometheus.Port) 75 | 76 | require.NotNil(t, conf.RTC.AllowTCPFallback) 77 | require.True(t, *conf.RTC.AllowTCPFallback) 78 | 79 | require.NotNil(t, conf.RTC.ReconnectOnPublicationError) 80 | require.True(t, *conf.RTC.ReconnectOnPublicationError) 81 | 82 | require.NotNil(t, conf.RTC.ReconnectOnSubscriptionError) 83 | require.False(t, *conf.RTC.ReconnectOnSubscriptionError) 84 | } 85 | 86 | func TestYAMLTag(t *testing.T) { 87 | require.NoError(t, configtest.CheckYAMLTags(Config{})) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/sfu/testutils/data.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testutils 16 | 17 | import ( 18 | "time" 19 | 20 | "github.com/pion/rtp" 21 | "github.com/pion/webrtc/v3" 22 | 23 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 24 | ) 25 | 26 | // ----------------------------------------------------------- 27 | 28 | type TestExtPacketParams struct { 29 | SetMarker bool 30 | IsKeyFrame bool 31 | PayloadType uint8 32 | SequenceNumber uint16 33 | SNCycles int 34 | Timestamp uint32 35 | TSCycles int 36 | SSRC uint32 37 | PayloadSize int 38 | PaddingSize byte 39 | ArrivalTime time.Time 40 | VideoLayer buffer.VideoLayer 41 | } 42 | 43 | // ----------------------------------------------------------- 44 | 45 | func GetTestExtPacket(params *TestExtPacketParams) (*buffer.ExtPacket, error) { 46 | packet := rtp.Packet{ 47 | Header: rtp.Header{ 48 | Version: 2, 49 | Padding: params.PaddingSize != 0, 50 | Marker: params.SetMarker, 51 | PayloadType: params.PayloadType, 52 | SequenceNumber: params.SequenceNumber, 53 | Timestamp: params.Timestamp, 54 | SSRC: params.SSRC, 55 | }, 56 | Payload: make([]byte, params.PayloadSize), 57 | PaddingSize: params.PaddingSize, 58 | } 59 | 60 | raw, err := packet.Marshal() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | ep := &buffer.ExtPacket{ 66 | VideoLayer: params.VideoLayer, 67 | ExtSequenceNumber: uint64(params.SNCycles<<16) + uint64(params.SequenceNumber), 68 | ExtTimestamp: uint64(params.TSCycles<<32) + uint64(params.Timestamp), 69 | Arrival: params.ArrivalTime.UnixNano(), 70 | Packet: &packet, 71 | KeyFrame: params.IsKeyFrame, 72 | RawPacket: raw, 73 | } 74 | 75 | return ep, nil 76 | } 77 | 78 | // -------------------------------------- 79 | 80 | func GetTestExtPacketVP8(params *TestExtPacketParams, vp8 *buffer.VP8) (*buffer.ExtPacket, error) { 81 | ep, err := GetTestExtPacket(params) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | ep.KeyFrame = vp8.IsKeyFrame 87 | ep.Payload = *vp8 88 | return ep, nil 89 | } 90 | 91 | // -------------------------------------- 92 | 93 | var TestVP8Codec = webrtc.RTPCodecCapability{ 94 | MimeType: "video/vp8", 95 | ClockRate: 90000, 96 | } 97 | 98 | var TestOpusCodec = webrtc.RTPCodecCapability{ 99 | MimeType: "audio/opus", 100 | ClockRate: 48000, 101 | } 102 | 103 | // -------------------------------------- 104 | -------------------------------------------------------------------------------- /pkg/rtc/testutils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rtc 16 | 17 | import ( 18 | "github.com/livekit/protocol/livekit" 19 | "github.com/livekit/protocol/logger" 20 | "github.com/livekit/protocol/utils" 21 | "github.com/livekit/protocol/utils/guid" 22 | 23 | "github.com/livekit/livekit-server/pkg/rtc/types" 24 | "github.com/livekit/livekit-server/pkg/rtc/types/typesfakes" 25 | ) 26 | 27 | func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.ProtocolVersion, hidden bool, publisher bool) *typesfakes.FakeLocalParticipant { 28 | p := &typesfakes.FakeLocalParticipant{} 29 | sid := guid.New(utils.ParticipantPrefix) 30 | p.IDReturns(livekit.ParticipantID(sid)) 31 | p.IdentityReturns(identity) 32 | p.StateReturns(livekit.ParticipantInfo_JOINED) 33 | p.ProtocolVersionReturns(protocol) 34 | p.CanSubscribeReturns(true) 35 | p.CanPublishSourceReturns(!hidden) 36 | p.CanPublishDataReturns(!hidden) 37 | p.HiddenReturns(hidden) 38 | p.ToProtoReturns(&livekit.ParticipantInfo{ 39 | Sid: sid, 40 | Identity: string(identity), 41 | State: livekit.ParticipantInfo_JOINED, 42 | IsPublisher: publisher, 43 | }) 44 | p.ToProtoWithVersionReturns(&livekit.ParticipantInfo{ 45 | Sid: sid, 46 | Identity: string(identity), 47 | State: livekit.ParticipantInfo_JOINED, 48 | IsPublisher: publisher, 49 | }, utils.TimedVersion(0)) 50 | 51 | p.SetMetadataCalls(func(m string) { 52 | var f func(participant types.LocalParticipant) 53 | if p.OnParticipantUpdateCallCount() > 0 { 54 | f = p.OnParticipantUpdateArgsForCall(p.OnParticipantUpdateCallCount() - 1) 55 | } 56 | if f != nil { 57 | f(p) 58 | } 59 | }) 60 | updateTrack := func() { 61 | var f func(participant types.LocalParticipant, track types.MediaTrack) 62 | if p.OnTrackUpdatedCallCount() > 0 { 63 | f = p.OnTrackUpdatedArgsForCall(p.OnTrackUpdatedCallCount() - 1) 64 | } 65 | if f != nil { 66 | f(p, NewMockTrack(livekit.TrackType_VIDEO, "testcam")) 67 | } 68 | } 69 | 70 | p.SetTrackMutedCalls(func(sid livekit.TrackID, muted bool, fromServer bool) *livekit.TrackInfo { 71 | updateTrack() 72 | return nil 73 | }) 74 | p.AddTrackCalls(func(req *livekit.AddTrackRequest) { 75 | updateTrack() 76 | }) 77 | p.GetLoggerReturns(logger.GetLogger()) 78 | 79 | return p 80 | } 81 | 82 | func NewMockTrack(kind livekit.TrackType, name string) *typesfakes.FakeMediaTrack { 83 | t := &typesfakes.FakeMediaTrack{} 84 | t.IDReturns(livekit.TrackID(guid.New(utils.TrackPrefix))) 85 | t.KindReturns(kind) 86 | t.NameReturns(name) 87 | t.ToProtoReturns(&livekit.TrackInfo{ 88 | Type: kind, 89 | Name: name, 90 | }) 91 | return t 92 | } 93 | -------------------------------------------------------------------------------- /pkg/service/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "github.com/livekit/psrpc" 19 | ) 20 | 21 | var ( 22 | ErrEgressNotFound = psrpc.NewErrorf(psrpc.NotFound, "egress does not exist") 23 | ErrEgressNotConnected = psrpc.NewErrorf(psrpc.Internal, "egress not connected (redis required)") 24 | ErrIdentityEmpty = psrpc.NewErrorf(psrpc.InvalidArgument, "identity cannot be empty") 25 | ErrIngressNotConnected = psrpc.NewErrorf(psrpc.Internal, "ingress not connected (redis required)") 26 | ErrIngressNotFound = psrpc.NewErrorf(psrpc.NotFound, "ingress does not exist") 27 | ErrIngressNonReusable = psrpc.NewErrorf(psrpc.InvalidArgument, "ingress is not reusable and cannot be modified") 28 | ErrNameExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "name length exceeds limits") 29 | ErrMetadataExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "metadata size exceeds limits") 30 | ErrAttributeExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "attribute size exceeds limits") 31 | ErrRoomNameExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "room name length exceeds limits") 32 | ErrParticipantIdentityExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "participant identity length exceeds limits") 33 | ErrOperationFailed = psrpc.NewErrorf(psrpc.Internal, "operation cannot be completed") 34 | ErrParticipantNotFound = psrpc.NewErrorf(psrpc.NotFound, "participant does not exist") 35 | ErrRoomNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested room does not exist") 36 | ErrRoomLockFailed = psrpc.NewErrorf(psrpc.Internal, "could not lock room") 37 | ErrRoomUnlockFailed = psrpc.NewErrorf(psrpc.Internal, "could not unlock room, lock token does not match") 38 | ErrRemoteUnmuteNoteEnabled = psrpc.NewErrorf(psrpc.FailedPrecondition, "remote unmute not enabled") 39 | ErrTrackNotFound = psrpc.NewErrorf(psrpc.NotFound, "track is not found") 40 | ErrWebHookMissingAPIKey = psrpc.NewErrorf(psrpc.InvalidArgument, "api_key is required to use webhooks") 41 | ErrSIPNotConnected = psrpc.NewErrorf(psrpc.Internal, "sip not connected (redis required)") 42 | ErrSIPTrunkNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip trunk does not exist") 43 | ErrSIPDispatchRuleNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip dispatch rule does not exist") 44 | ErrSIPParticipantNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip participant does not exist") 45 | ) 46 | -------------------------------------------------------------------------------- /pkg/telemetry/analyticsservice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import ( 18 | "context" 19 | 20 | "go.uber.org/atomic" 21 | "google.golang.org/protobuf/types/known/timestamppb" 22 | 23 | "github.com/livekit/protocol/livekit" 24 | "github.com/livekit/protocol/logger" 25 | "github.com/livekit/protocol/rpc" 26 | "github.com/livekit/protocol/utils/guid" 27 | 28 | "github.com/livekit/livekit-server/pkg/config" 29 | "github.com/livekit/livekit-server/pkg/routing" 30 | ) 31 | 32 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . AnalyticsService 33 | type AnalyticsService interface { 34 | SendStats(ctx context.Context, stats []*livekit.AnalyticsStat) 35 | SendEvent(ctx context.Context, events *livekit.AnalyticsEvent) 36 | SendNodeRoomStates(ctx context.Context, nodeRooms *livekit.AnalyticsNodeRooms) 37 | } 38 | 39 | type analyticsService struct { 40 | analyticsKey string 41 | nodeID string 42 | sequenceNumber atomic.Uint64 43 | 44 | events rpc.AnalyticsRecorderService_IngestEventsClient 45 | stats rpc.AnalyticsRecorderService_IngestStatsClient 46 | nodeRooms rpc.AnalyticsRecorderService_IngestNodeRoomStatesClient 47 | } 48 | 49 | func NewAnalyticsService(_ *config.Config, currentNode routing.LocalNode) AnalyticsService { 50 | return &analyticsService{ 51 | analyticsKey: "", // TODO: conf.AnalyticsKey 52 | nodeID: currentNode.Id, 53 | } 54 | } 55 | 56 | func (a *analyticsService) SendStats(_ context.Context, stats []*livekit.AnalyticsStat) { 57 | if a.stats == nil { 58 | return 59 | } 60 | 61 | for _, stat := range stats { 62 | stat.Id = guid.New("AS_") 63 | stat.AnalyticsKey = a.analyticsKey 64 | stat.Node = a.nodeID 65 | } 66 | if err := a.stats.Send(&livekit.AnalyticsStats{Stats: stats}); err != nil { 67 | logger.Errorw("failed to send stats", err) 68 | } 69 | } 70 | 71 | func (a *analyticsService) SendEvent(_ context.Context, event *livekit.AnalyticsEvent) { 72 | if a.events == nil { 73 | return 74 | } 75 | 76 | event.Id = guid.New("AE_") 77 | event.NodeId = a.nodeID 78 | event.AnalyticsKey = a.analyticsKey 79 | if err := a.events.Send(&livekit.AnalyticsEvents{ 80 | Events: []*livekit.AnalyticsEvent{event}, 81 | }); err != nil { 82 | logger.Errorw("failed to send event", err, "eventType", event.Type.String()) 83 | } 84 | } 85 | 86 | func (a *analyticsService) SendNodeRoomStates(_ context.Context, nodeRooms *livekit.AnalyticsNodeRooms) { 87 | if a.nodeRooms == nil { 88 | return 89 | } 90 | 91 | nodeRooms.NodeId = a.nodeID 92 | nodeRooms.SequenceNumber = a.sequenceNumber.Add(1) 93 | nodeRooms.Timestamp = timestamppb.Now() 94 | if err := a.nodeRooms.Send(nodeRooms); err != nil { 95 | logger.Errorw("failed to send node room states", err) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pkg/service/roomallocator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service_test 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/livekit/protocol/livekit" 24 | 25 | "github.com/livekit/livekit-server/pkg/config" 26 | "github.com/livekit/livekit-server/pkg/routing" 27 | "github.com/livekit/livekit-server/pkg/routing/routingfakes" 28 | "github.com/livekit/livekit-server/pkg/service" 29 | "github.com/livekit/livekit-server/pkg/service/servicefakes" 30 | ) 31 | 32 | func TestCreateRoom(t *testing.T) { 33 | t.Run("ensure default room settings are applied", func(t *testing.T) { 34 | conf, err := config.NewConfig("", true, nil, nil) 35 | require.NoError(t, err) 36 | 37 | node, err := routing.NewLocalNode(conf) 38 | require.NoError(t, err) 39 | 40 | ra, conf := newTestRoomAllocator(t, conf, node) 41 | 42 | room, _, _, err := ra.CreateRoom(context.Background(), &livekit.CreateRoomRequest{Name: "myroom"}, true) 43 | require.NoError(t, err) 44 | require.Equal(t, conf.Room.EmptyTimeout, room.EmptyTimeout) 45 | require.Equal(t, conf.Room.DepartureTimeout, room.DepartureTimeout) 46 | require.NotEmpty(t, room.EnabledCodecs) 47 | }) 48 | } 49 | 50 | func SelectRoomNode(t *testing.T) { 51 | t.Run("reject new participants when track limit has been reached", func(t *testing.T) { 52 | conf, err := config.NewConfig("", true, nil, nil) 53 | require.NoError(t, err) 54 | conf.Limit.NumTracks = 10 55 | 56 | node, err := routing.NewLocalNode(conf) 57 | require.NoError(t, err) 58 | node.Stats.NumTracksIn = 100 59 | node.Stats.NumTracksOut = 100 60 | 61 | ra, _ := newTestRoomAllocator(t, conf, node) 62 | 63 | err = ra.SelectRoomNode(context.Background(), "low-limit-room", "") 64 | require.ErrorIs(t, err, routing.ErrNodeLimitReached) 65 | }) 66 | 67 | t.Run("reject new participants when bandwidth limit has been reached", func(t *testing.T) { 68 | conf, err := config.NewConfig("", true, nil, nil) 69 | require.NoError(t, err) 70 | conf.Limit.BytesPerSec = 100 71 | 72 | node, err := routing.NewLocalNode(conf) 73 | require.NoError(t, err) 74 | node.Stats.BytesInPerSec = 1000 75 | node.Stats.BytesOutPerSec = 1000 76 | 77 | ra, _ := newTestRoomAllocator(t, conf, node) 78 | 79 | err = ra.SelectRoomNode(context.Background(), "low-limit-room", "") 80 | require.ErrorIs(t, err, routing.ErrNodeLimitReached) 81 | }) 82 | } 83 | 84 | func newTestRoomAllocator(t *testing.T, conf *config.Config, node *livekit.Node) (service.RoomAllocator, *config.Config) { 85 | store := &servicefakes.FakeObjectStore{} 86 | store.LoadRoomReturns(nil, nil, service.ErrRoomNotFound) 87 | router := &routingfakes.FakeRouter{} 88 | 89 | router.GetNodeForRoomReturns(node, nil) 90 | 91 | ra, err := service.NewRoomAllocator(conf, router, store) 92 | require.NoError(t, err) 93 | return ra, conf 94 | } 95 | -------------------------------------------------------------------------------- /pkg/sfu/buffer/frameintegrity_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buffer 16 | 17 | import ( 18 | "math/rand" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor" 24 | ) 25 | 26 | func TestFrameIntegrityChecker(t *testing.T) { 27 | fc := NewFrameIntegrityChecker(100, 1000) 28 | 29 | // first frame out of order 30 | fc.AddPacket(10, 10, &dd.DependencyDescriptor{}) 31 | require.False(t, fc.FrameIntegrity(10)) 32 | fc.AddPacket(9, 10, &dd.DependencyDescriptor{FirstPacketInFrame: true}) 33 | require.False(t, fc.FrameIntegrity(10)) 34 | fc.AddPacket(11, 10, &dd.DependencyDescriptor{LastPacketInFrame: true}) 35 | require.True(t, fc.FrameIntegrity(10)) 36 | 37 | // single packet frame 38 | fc.AddPacket(100, 100, &dd.DependencyDescriptor{FirstPacketInFrame: true, LastPacketInFrame: true}) 39 | require.True(t, fc.FrameIntegrity(100)) 40 | require.False(t, fc.FrameIntegrity(101)) 41 | require.False(t, fc.FrameIntegrity(99)) 42 | 43 | // frame too old than first frame 44 | fc.AddPacket(99, 99, &dd.DependencyDescriptor{FirstPacketInFrame: true, LastPacketInFrame: true}) 45 | 46 | // multiple packet frame, out of order 47 | fc.AddPacket(2001, 2001, &dd.DependencyDescriptor{}) 48 | require.False(t, fc.FrameIntegrity(2001)) 49 | require.False(t, fc.FrameIntegrity(1999)) 50 | // out of frame count(100) 51 | require.False(t, fc.FrameIntegrity(100)) 52 | require.False(t, fc.FrameIntegrity(1900)) 53 | 54 | fc.AddPacket(2000, 2001, &dd.DependencyDescriptor{FirstPacketInFrame: true}) 55 | require.False(t, fc.FrameIntegrity(2001)) 56 | fc.AddPacket(2002, 2001, &dd.DependencyDescriptor{LastPacketInFrame: true}) 57 | require.True(t, fc.FrameIntegrity(2001)) 58 | // duplicate packet 59 | fc.AddPacket(2001, 2001, &dd.DependencyDescriptor{}) 60 | require.True(t, fc.FrameIntegrity(2001)) 61 | 62 | // frame too old 63 | fc.AddPacket(900, 1900, &dd.DependencyDescriptor{FirstPacketInFrame: true, LastPacketInFrame: true}) 64 | require.False(t, fc.FrameIntegrity(1900)) 65 | 66 | for frame := uint64(2002); frame < 2102; frame++ { 67 | // large frame (1000 packets) out of order / retransmitted 68 | firstFrame := uint64(3000 + (frame-2002)*1000) 69 | lastFrame := uint64(3999 + (frame-2002)*1000) 70 | frames := make([]uint64, 0, lastFrame-firstFrame+1) 71 | for i := firstFrame; i <= lastFrame; i++ { 72 | frames = append(frames, i) 73 | } 74 | require.False(t, fc.FrameIntegrity(frame)) 75 | rand.Seed(int64(frame)) 76 | rand.Shuffle(len(frames), func(i, j int) { frames[i], frames[j] = frames[j], frames[i] }) 77 | for i, f := range frames { 78 | fc.AddPacket(f, frame, &dd.DependencyDescriptor{ 79 | FirstPacketInFrame: f == firstFrame, 80 | LastPacketInFrame: f == lastFrame, 81 | }) 82 | require.Equal(t, i == len(frames)-1, fc.FrameIntegrity(frame), i) 83 | } 84 | require.True(t, fc.FrameIntegrity(frame)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkg/sfu/videolayerselector/vp9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package videolayerselector 16 | 17 | import ( 18 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 19 | "github.com/livekit/protocol/logger" 20 | "github.com/pion/rtp/codecs" 21 | ) 22 | 23 | type VP9 struct { 24 | *Base 25 | } 26 | 27 | func NewVP9(logger logger.Logger) *VP9 { 28 | return &VP9{ 29 | Base: NewBase(logger), 30 | } 31 | } 32 | 33 | func NewVP9FromNull(vls VideoLayerSelector) *VP9 { 34 | return &VP9{ 35 | Base: vls.(*Null).Base, 36 | } 37 | } 38 | 39 | func (v *VP9) IsOvershootOkay() bool { 40 | return false 41 | } 42 | 43 | func (v *VP9) Select(extPkt *buffer.ExtPacket, _layer int32) (result VideoLayerSelectorResult) { 44 | vp9, ok := extPkt.Payload.(codecs.VP9Packet) 45 | if !ok { 46 | return 47 | } 48 | 49 | currentLayer := v.currentLayer 50 | if v.currentLayer != v.targetLayer { 51 | updatedLayer := v.currentLayer 52 | 53 | if !v.currentLayer.IsValid() { 54 | if !extPkt.KeyFrame { 55 | return 56 | } 57 | 58 | updatedLayer = extPkt.VideoLayer 59 | } else { 60 | if v.currentLayer.Temporal != v.targetLayer.Temporal { 61 | if v.currentLayer.Temporal < v.targetLayer.Temporal { 62 | // temporal scale up 63 | if extPkt.VideoLayer.Temporal > v.currentLayer.Temporal && extPkt.VideoLayer.Temporal <= v.targetLayer.Temporal && vp9.U && vp9.B { 64 | currentLayer.Temporal = extPkt.VideoLayer.Temporal 65 | updatedLayer.Temporal = extPkt.VideoLayer.Temporal 66 | } 67 | } else { 68 | // temporal scale down 69 | if vp9.E { 70 | updatedLayer.Temporal = v.targetLayer.Temporal 71 | } 72 | } 73 | } 74 | 75 | if v.currentLayer.Spatial != v.targetLayer.Spatial { 76 | if v.currentLayer.Spatial < v.targetLayer.Spatial { 77 | // spatial scale up 78 | if extPkt.VideoLayer.Spatial > v.currentLayer.Spatial && extPkt.VideoLayer.Spatial <= v.targetLayer.Spatial && !vp9.P && vp9.B { 79 | currentLayer.Spatial = extPkt.VideoLayer.Spatial 80 | updatedLayer.Spatial = extPkt.VideoLayer.Spatial 81 | } 82 | } else { 83 | // spatial scale down 84 | if vp9.E { 85 | updatedLayer.Spatial = v.targetLayer.Spatial 86 | } 87 | } 88 | } 89 | } 90 | 91 | if updatedLayer != v.currentLayer { 92 | result.IsSwitching = true 93 | if !v.currentLayer.IsValid() && updatedLayer.IsValid() { 94 | result.IsResuming = true 95 | } 96 | 97 | v.previousLayer = v.currentLayer 98 | v.currentLayer = updatedLayer 99 | } 100 | } 101 | 102 | result.RTPMarker = extPkt.Packet.Marker 103 | if vp9.E && extPkt.VideoLayer.Spatial == currentLayer.Spatial && (vp9.P || v.targetLayer.Spatial <= v.currentLayer.Spatial) { 104 | result.RTPMarker = true 105 | } 106 | result.IsSelected = !extPkt.VideoLayer.GreaterThan(currentLayer) 107 | result.IsRelevant = true 108 | return 109 | } 110 | -------------------------------------------------------------------------------- /pkg/sfu/downtrackspreader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sfu 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/livekit/protocol/livekit" 21 | "github.com/livekit/protocol/logger" 22 | "github.com/livekit/protocol/utils" 23 | ) 24 | 25 | type DownTrackSpreaderParams struct { 26 | Threshold int 27 | Logger logger.Logger 28 | } 29 | 30 | type DownTrackSpreader struct { 31 | params DownTrackSpreaderParams 32 | 33 | downTrackMu sync.RWMutex 34 | downTracks map[livekit.ParticipantID]TrackSender 35 | downTracksShadow []TrackSender 36 | } 37 | 38 | func NewDownTrackSpreader(params DownTrackSpreaderParams) *DownTrackSpreader { 39 | d := &DownTrackSpreader{ 40 | params: params, 41 | downTracks: make(map[livekit.ParticipantID]TrackSender), 42 | } 43 | 44 | return d 45 | } 46 | 47 | func (d *DownTrackSpreader) GetDownTracks() []TrackSender { 48 | d.downTrackMu.RLock() 49 | defer d.downTrackMu.RUnlock() 50 | return d.downTracksShadow 51 | } 52 | 53 | func (d *DownTrackSpreader) ResetAndGetDownTracks() []TrackSender { 54 | d.downTrackMu.Lock() 55 | defer d.downTrackMu.Unlock() 56 | 57 | downTracks := d.downTracksShadow 58 | 59 | d.downTracks = make(map[livekit.ParticipantID]TrackSender) 60 | d.downTracksShadow = nil 61 | 62 | return downTracks 63 | } 64 | 65 | func (d *DownTrackSpreader) Store(ts TrackSender) { 66 | d.downTrackMu.Lock() 67 | defer d.downTrackMu.Unlock() 68 | 69 | d.downTracks[ts.SubscriberID()] = ts 70 | d.shadowDownTracks() 71 | } 72 | 73 | func (d *DownTrackSpreader) Free(subscriberID livekit.ParticipantID) { 74 | d.downTrackMu.Lock() 75 | defer d.downTrackMu.Unlock() 76 | 77 | delete(d.downTracks, subscriberID) 78 | d.shadowDownTracks() 79 | } 80 | 81 | func (d *DownTrackSpreader) HasDownTrack(subscriberID livekit.ParticipantID) bool { 82 | d.downTrackMu.RLock() 83 | defer d.downTrackMu.RUnlock() 84 | 85 | _, ok := d.downTracks[subscriberID] 86 | return ok 87 | } 88 | 89 | func (d *DownTrackSpreader) Broadcast(writer func(TrackSender)) int { 90 | downTracks := d.GetDownTracks() 91 | if len(downTracks) == 0 { 92 | return 0 93 | } 94 | 95 | threshold := uint64(d.params.Threshold) 96 | if threshold == 0 { 97 | threshold = 1000000 98 | } 99 | 100 | // 100µs is enough to amortize the overhead and provide sufficient load balancing. 101 | // WriteRTP takes about 50µs on average, so we write to 2 down tracks per loop. 102 | step := uint64(2) 103 | utils.ParallelExec(downTracks, threshold, step, writer) 104 | return len(downTracks) 105 | } 106 | 107 | func (d *DownTrackSpreader) DownTrackCount() int { 108 | d.downTrackMu.RLock() 109 | defer d.downTrackMu.RUnlock() 110 | return len(d.downTracksShadow) 111 | } 112 | 113 | func (d *DownTrackSpreader) shadowDownTracks() { 114 | d.downTracksShadow = make([]TrackSender, 0, len(d.downTracks)) 115 | for _, dt := range d.downTracks { 116 | d.downTracksShadow = append(d.downTracksShadow, dt) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pkg/service/ioservice_ingress.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | 21 | "github.com/livekit/protocol/livekit" 22 | "github.com/livekit/protocol/logger" 23 | "github.com/livekit/protocol/rpc" 24 | "google.golang.org/protobuf/types/known/emptypb" 25 | ) 26 | 27 | func (s *IOInfoService) CreateIngress(ctx context.Context, info *livekit.IngressInfo) (*emptypb.Empty, error) { 28 | err := s.is.StoreIngress(ctx, info) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | s.telemetry.IngressCreated(ctx, info) 34 | 35 | return &emptypb.Empty{}, nil 36 | } 37 | 38 | func (s *IOInfoService) GetIngressInfo(ctx context.Context, req *rpc.GetIngressInfoRequest) (*rpc.GetIngressInfoResponse, error) { 39 | info, err := s.loadIngressFromInfoRequest(req) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return &rpc.GetIngressInfoResponse{Info: info}, nil 45 | } 46 | 47 | func (s *IOInfoService) loadIngressFromInfoRequest(req *rpc.GetIngressInfoRequest) (info *livekit.IngressInfo, err error) { 48 | if req.IngressId != "" { 49 | info, err = s.is.LoadIngress(context.Background(), req.IngressId) 50 | } else if req.StreamKey != "" { 51 | info, err = s.is.LoadIngressFromStreamKey(context.Background(), req.StreamKey) 52 | } else { 53 | err = errors.New("request needs to specify either IngressId or StreamKey") 54 | } 55 | return info, err 56 | } 57 | 58 | func (s *IOInfoService) UpdateIngressState(ctx context.Context, req *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error) { 59 | info, err := s.is.LoadIngress(ctx, req.IngressId) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | if err = s.is.UpdateIngressState(ctx, req.IngressId, req.State); err != nil { 65 | logger.Errorw("could not update ingress", err) 66 | return nil, err 67 | } 68 | 69 | if info.State.Status != req.State.Status { 70 | info.State = req.State 71 | 72 | switch req.State.Status { 73 | case livekit.IngressState_ENDPOINT_ERROR, 74 | livekit.IngressState_ENDPOINT_INACTIVE, 75 | livekit.IngressState_ENDPOINT_COMPLETE: 76 | s.telemetry.IngressEnded(ctx, info) 77 | 78 | if req.State.Error != "" { 79 | logger.Infow("ingress failed", "error", req.State.Error, "ingressID", req.IngressId) 80 | } else { 81 | logger.Infow("ingress ended", "ingressID", req.IngressId) 82 | } 83 | 84 | case livekit.IngressState_ENDPOINT_PUBLISHING: 85 | s.telemetry.IngressStarted(ctx, info) 86 | 87 | logger.Infow("ingress started", "ingressID", req.IngressId) 88 | 89 | case livekit.IngressState_ENDPOINT_BUFFERING: 90 | s.telemetry.IngressUpdated(ctx, info) 91 | 92 | logger.Infow("ingress buffering", "ingressID", req.IngressId) 93 | } 94 | } else { 95 | // Status didn't change, send Updated event 96 | info.State = req.State 97 | 98 | s.telemetry.IngressUpdated(ctx, info) 99 | 100 | logger.Infow("ingress state updated", "ingressID", req.IngressId, "status", info.State.Status) 101 | } 102 | 103 | return &emptypb.Empty{}, nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/telemetry/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import ( 18 | "github.com/livekit/livekit-server/pkg/telemetry/prometheus" 19 | "github.com/livekit/protocol/livekit" 20 | ) 21 | 22 | type StatsKey struct { 23 | streamType livekit.StreamType 24 | participantID livekit.ParticipantID 25 | trackID livekit.TrackID 26 | trackSource livekit.TrackSource 27 | trackType livekit.TrackType 28 | track bool 29 | } 30 | 31 | func StatsKeyForTrack(streamType livekit.StreamType, participantID livekit.ParticipantID, trackID livekit.TrackID, trackSource livekit.TrackSource, trackType livekit.TrackType) StatsKey { 32 | return StatsKey{ 33 | streamType: streamType, 34 | participantID: participantID, 35 | trackID: trackID, 36 | trackSource: trackSource, 37 | trackType: trackType, 38 | track: true, 39 | } 40 | } 41 | 42 | func StatsKeyForData(streamType livekit.StreamType, participantID livekit.ParticipantID, trackID livekit.TrackID) StatsKey { 43 | return StatsKey{ 44 | streamType: streamType, 45 | participantID: participantID, 46 | trackID: trackID, 47 | } 48 | } 49 | 50 | func (t *telemetryService) TrackStats(key StatsKey, stat *livekit.AnalyticsStat) { 51 | t.enqueue(func() { 52 | direction := prometheus.Incoming 53 | if key.streamType == livekit.StreamType_DOWNSTREAM { 54 | direction = prometheus.Outgoing 55 | } 56 | 57 | nacks := uint32(0) 58 | plis := uint32(0) 59 | firs := uint32(0) 60 | packets := uint32(0) 61 | bytes := uint64(0) 62 | retransmitBytes := uint64(0) 63 | retransmitPackets := uint32(0) 64 | for _, stream := range stat.Streams { 65 | nacks += stream.Nacks 66 | plis += stream.Plis 67 | firs += stream.Firs 68 | packets += stream.PrimaryPackets + stream.PaddingPackets 69 | bytes += stream.PrimaryBytes + stream.PaddingBytes 70 | if key.streamType == livekit.StreamType_DOWNSTREAM { 71 | retransmitPackets += stream.RetransmitPackets 72 | retransmitBytes += stream.RetransmitBytes 73 | } else { 74 | // for upstream, we don't account for these separately for now 75 | packets += stream.RetransmitPackets 76 | bytes += stream.RetransmitBytes 77 | } 78 | if key.track { 79 | prometheus.RecordPacketLoss(direction, key.trackSource, key.trackType, stream.PacketsLost, stream.PrimaryPackets+stream.PaddingPackets) 80 | prometheus.RecordRTT(direction, key.trackSource, key.trackType, stream.Rtt) 81 | prometheus.RecordJitter(direction, key.trackSource, key.trackType, stream.Jitter) 82 | } 83 | } 84 | prometheus.IncrementRTCP(direction, nacks, plis, firs) 85 | prometheus.IncrementPackets(direction, uint64(packets), false) 86 | prometheus.IncrementBytes(direction, bytes, false) 87 | if retransmitPackets != 0 { 88 | prometheus.IncrementPackets(direction, uint64(retransmitPackets), true) 89 | } 90 | if retransmitBytes != 0 { 91 | prometheus.IncrementBytes(direction, retransmitBytes, true) 92 | } 93 | 94 | if worker, ok := t.getWorker(key.participantID); ok { 95 | worker.OnTrackStat(key.trackID, key.streamType, stat) 96 | } 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /pkg/sfu/streamtracker/streamtracker_dd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package streamtracker 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/livekit/livekit-server/pkg/sfu/buffer" 24 | dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor" 25 | "github.com/livekit/protocol/logger" 26 | ) 27 | 28 | func createDescriptorDependencyForTargets(maxSpatial, maxTemporal int) *buffer.ExtDependencyDescriptor { 29 | var targets []buffer.DependencyDescriptorDecodeTarget 30 | var mask uint32 31 | for i := 0; i <= maxSpatial; i++ { 32 | for j := 0; j <= maxTemporal; j++ { 33 | targets = append(targets, buffer.DependencyDescriptorDecodeTarget{Target: len(targets), Layer: buffer.VideoLayer{Spatial: int32(i), Temporal: int32(j)}}) 34 | mask |= 1 << uint32(len(targets)-1) 35 | } 36 | } 37 | 38 | dtis := make([]dd.DecodeTargetIndication, len(targets)) 39 | for _, t := range targets { 40 | dtis[t.Target] = dd.DecodeTargetRequired 41 | } 42 | 43 | return &buffer.ExtDependencyDescriptor{ 44 | Descriptor: &dd.DependencyDescriptor{ 45 | ActiveDecodeTargetsBitmask: &mask, 46 | FrameDependencies: &dd.FrameDependencyTemplate{ 47 | DecodeTargetIndications: dtis, 48 | }, 49 | }, 50 | DecodeTargets: targets, 51 | ActiveDecodeTargetsUpdated: true, 52 | } 53 | } 54 | 55 | func checkStatues(t *testing.T, statuses []StreamStatus, expected StreamStatus, maxSpatial int) { 56 | for i := 0; i <= maxSpatial; i++ { 57 | require.Equal(t, expected, statuses[i]) 58 | } 59 | 60 | for i := maxSpatial + 1; i < len(statuses); i++ { 61 | require.NotEqual(t, expected, statuses[i]) 62 | } 63 | } 64 | 65 | func TestStreamTrackerDD(t *testing.T) { 66 | ddTracker := NewStreamTrackerDependencyDescriptor(StreamTrackerParams{ 67 | BitrateReportInterval: 1 * time.Second, 68 | Logger: logger.GetLogger(), 69 | }) 70 | layeredTrackers := make([]StreamTrackerWorker, buffer.DefaultMaxLayerSpatial+1) 71 | statuses := make([]StreamStatus, buffer.DefaultMaxLayerSpatial+1) 72 | for i := 0; i <= int(buffer.DefaultMaxLayerSpatial); i++ { 73 | layeredTrack := ddTracker.LayeredTracker(int32(i)) 74 | layer := i 75 | layeredTrack.OnStatusChanged(func(status StreamStatus) { 76 | statuses[layer] = status 77 | }) 78 | layeredTrack.Start() 79 | layeredTrackers[i] = layeredTrack 80 | } 81 | defer ddTracker.Stop() 82 | 83 | // no active layers 84 | ddTracker.Observe(0, 1000, 1000, false, 0, nil) 85 | checkStatues(t, statuses, StreamStatusActive, int(buffer.InvalidLayerSpatial)) 86 | 87 | // layer seen [0,1] 88 | ddTracker.Observe(0, 1000, 1000, false, 0, createDescriptorDependencyForTargets(1, 1)) 89 | checkStatues(t, statuses, StreamStatusActive, 1) 90 | 91 | // layer seen [0,1,2] 92 | ddTracker.Observe(0, 1000, 1000, false, 0, createDescriptorDependencyForTargets(2, 1)) 93 | checkStatues(t, statuses, StreamStatusActive, 2) 94 | 95 | // layer 2 gone, layer seen [0,1] 96 | ddTracker.Observe(0, 1000, 1000, false, 0, createDescriptorDependencyForTargets(1, 1)) 97 | checkStatues(t, statuses, StreamStatusActive, 1) 98 | } 99 | -------------------------------------------------------------------------------- /pkg/sfu/buffer/helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buffer 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestVP8Helper_Unmarshal(t *testing.T) { 24 | type args struct { 25 | payload []byte 26 | } 27 | tests := []struct { 28 | name string 29 | args args 30 | wantErr bool 31 | checkTemporal bool 32 | temporalSupport bool 33 | checkKeyFrame bool 34 | keyFrame bool 35 | checkPictureID bool 36 | pictureID uint16 37 | checkTlzIdx bool 38 | tlzIdx uint8 39 | checkTempID bool 40 | temporalID uint8 41 | }{ 42 | { 43 | name: "Empty or nil payload must return error", 44 | args: args{payload: []byte{}}, 45 | wantErr: true, 46 | }, 47 | { 48 | name: "Temporal must be supported by setting T bit to 1", 49 | args: args{payload: []byte{0xff, 0x20, 0x1, 0x2, 0x3, 0x4}}, 50 | checkTemporal: true, 51 | temporalSupport: true, 52 | }, 53 | { 54 | name: "Picture must be ID 7 bits by setting M bit to 0 and present by I bit set to 1", 55 | args: args{payload: []byte{0xff, 0xff, 0x11, 0x2, 0x3, 0x4}}, 56 | checkPictureID: true, 57 | pictureID: 17, 58 | }, 59 | { 60 | name: "Picture ID must be 15 bits by setting M bit to 1 and present by I bit set to 1", 61 | args: args{payload: []byte{0xff, 0xff, 0x92, 0x67, 0x3, 0x4, 0x5}}, 62 | checkPictureID: true, 63 | pictureID: 4711, 64 | }, 65 | { 66 | name: "Temporal level zero index must be present if L set to 1", 67 | args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x4, 0x5}}, 68 | checkTlzIdx: true, 69 | tlzIdx: 180, 70 | }, 71 | { 72 | name: "Temporal index must be present and used if T bit set to 1", 73 | args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x5, 0x6}}, 74 | checkTempID: true, 75 | temporalID: 2, 76 | }, 77 | { 78 | name: "Check if packet is a keyframe by looking at P bit set to 0", 79 | args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}}, 80 | checkKeyFrame: true, 81 | keyFrame: true, 82 | }, 83 | } 84 | for _, tt := range tests { 85 | tt := tt 86 | t.Run(tt.name, func(t *testing.T) { 87 | p := &VP8{} 88 | if err := p.Unmarshal(tt.args.payload); (err != nil) != tt.wantErr { 89 | t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr) 90 | } 91 | if tt.checkTemporal { 92 | require.Equal(t, tt.temporalSupport, p.T) 93 | } 94 | if tt.checkKeyFrame { 95 | require.Equal(t, tt.keyFrame, p.IsKeyFrame) 96 | } 97 | if tt.checkPictureID { 98 | require.Equal(t, tt.pictureID, p.PictureID) 99 | } 100 | if tt.checkTlzIdx { 101 | require.Equal(t, tt.tlzIdx, p.TL0PICIDX) 102 | } 103 | if tt.checkTempID { 104 | require.Equal(t, tt.temporalID, p.TID) 105 | } 106 | }) 107 | } 108 | } 109 | 110 | // ------------------------------------------ 111 | -------------------------------------------------------------------------------- /pkg/utils/opsqueue.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "math/bits" 19 | "sync" 20 | 21 | "github.com/gammazero/deque" 22 | 23 | "github.com/livekit/protocol/logger" 24 | ) 25 | 26 | type OpsQueueParams struct { 27 | Name string 28 | MinSize uint 29 | FlushOnStop bool 30 | Logger logger.Logger 31 | } 32 | 33 | type UntypedQueueOp func() 34 | 35 | func (op UntypedQueueOp) run() { 36 | op() 37 | } 38 | 39 | type OpsQueue struct { 40 | opsQueueBase[UntypedQueueOp] 41 | } 42 | 43 | func NewOpsQueue(params OpsQueueParams) *OpsQueue { 44 | return &OpsQueue{*newOpsQueueBase[UntypedQueueOp](params)} 45 | } 46 | 47 | type typedQueueOp[T any] struct { 48 | fn func(T) 49 | arg T 50 | } 51 | 52 | func (op typedQueueOp[T]) run() { 53 | op.fn(op.arg) 54 | } 55 | 56 | type TypedOpsQueue[T any] struct { 57 | opsQueueBase[typedQueueOp[T]] 58 | } 59 | 60 | func NewTypedOpsQueue[T any](params OpsQueueParams) *TypedOpsQueue[T] { 61 | return &TypedOpsQueue[T]{*newOpsQueueBase[typedQueueOp[T]](params)} 62 | } 63 | 64 | func (oq *TypedOpsQueue[T]) Enqueue(fn func(T), arg T) { 65 | oq.opsQueueBase.Enqueue(typedQueueOp[T]{fn, arg}) 66 | } 67 | 68 | type opsQueueItem interface { 69 | run() 70 | } 71 | 72 | type opsQueueBase[T opsQueueItem] struct { 73 | params OpsQueueParams 74 | 75 | lock sync.Mutex 76 | ops deque.Deque[T] 77 | wake chan struct{} 78 | isStarted bool 79 | doneChan chan struct{} 80 | isStopped bool 81 | } 82 | 83 | func newOpsQueueBase[T opsQueueItem](params OpsQueueParams) *opsQueueBase[T] { 84 | return &opsQueueBase[T]{ 85 | params: params, 86 | ops: *deque.New[T](min(bits.Len64(uint64(params.MinSize-1)), 7)), 87 | wake: make(chan struct{}, 1), 88 | doneChan: make(chan struct{}), 89 | } 90 | } 91 | 92 | func (oq *opsQueueBase[T]) Start() { 93 | oq.lock.Lock() 94 | if oq.isStarted { 95 | oq.lock.Unlock() 96 | return 97 | } 98 | 99 | oq.isStarted = true 100 | oq.lock.Unlock() 101 | 102 | go oq.process() 103 | } 104 | 105 | func (oq *opsQueueBase[T]) Stop() <-chan struct{} { 106 | oq.lock.Lock() 107 | if oq.isStopped { 108 | oq.lock.Unlock() 109 | return oq.doneChan 110 | } 111 | 112 | oq.isStopped = true 113 | close(oq.wake) 114 | oq.lock.Unlock() 115 | return oq.doneChan 116 | } 117 | 118 | func (oq *opsQueueBase[T]) Enqueue(op T) { 119 | oq.lock.Lock() 120 | defer oq.lock.Unlock() 121 | 122 | if oq.isStopped { 123 | return 124 | } 125 | 126 | oq.ops.PushBack(op) 127 | if oq.ops.Len() == 1 { 128 | select { 129 | case oq.wake <- struct{}{}: 130 | default: 131 | } 132 | } 133 | } 134 | 135 | func (oq *opsQueueBase[T]) process() { 136 | defer close(oq.doneChan) 137 | 138 | for { 139 | <-oq.wake 140 | for { 141 | oq.lock.Lock() 142 | if oq.isStopped && (!oq.params.FlushOnStop || oq.ops.Len() == 0) { 143 | oq.lock.Unlock() 144 | return 145 | } 146 | 147 | if oq.ops.Len() == 0 { 148 | oq.lock.Unlock() 149 | break 150 | } 151 | op := oq.ops.PopFront() 152 | oq.lock.Unlock() 153 | 154 | op.run() 155 | } 156 | } 157 | } 158 | --------------------------------------------------------------------------------