Merge commit '0da97ebd2149ec2a0412a273b17270f540431d81' as 'livekit-server'

This commit is contained in:
2026-06-25 14:35:28 +09:00
339 changed files with 114111 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
---
name: Bug report
about: File a bug report and help us improve LiveKit
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Server**
- Version: [0.x.x]
- Environment: [e.g. local dev, EKS]
- any other information about your deployment setup
**Client**
- SDK: [e.g. js, ios]
- Version: [0.x.x]
**To Reproduce**
Steps to reproduce the behavior:
1. two clients are connected to room
2. 3rd client joins
3. 3rd client does '...'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+77
View File
@@ -0,0 +1,77 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Test
on:
workflow_dispatch:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shogo82148/actions-setup-redis@v1
with:
redis-version: "6.x"
auto-start: true
- run: redis-cli ping
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Set up gotestfmt
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.4.1
- name: Replace mutexes
run: |
go get github.com/sasha-s/go-deadlock
grep -rl sync.Mutex ./pkg | xargs sed -i 's/sync\.Mutex/deadlock\.Mutex/g'
grep -rl sync.RWMutex ./pkg | xargs sed -i 's/sync\.RWMutex/deadlock\.RWMutex/g'
go install golang.org/x/tools/cmd/goimports
grep -rl deadlock.Mutex ./pkg | xargs goimports -w
grep -rl deadlock.RWMutex ./pkg | xargs goimports -w
go mod tidy
- name: Mage Build
uses: magefile/mage-action@v3
with:
version: latest
args: build
- name: Static Check
uses: amarpal/staticcheck-action@master
with:
checks: '["all", "-ST1000", "-ST1003", "-ST1020", "-ST1021", "-ST1022", "-SA1019"]'
install-go: false
- name: Test
run: |
set -euo pipefail
MallocNanoZone=0 go test -race -json -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt
# Upload the original go test log as an artifact for later review.
- name: Upload test log
uses: actions/upload-artifact@v4
if: always()
with:
name: test-log
path: /tmp/gotest.log
if-no-files-found: error
+75
View File
@@ -0,0 +1,75 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Release to Docker
# Controls when the action will run.
on:
workflow_dispatch:
push:
branches:
- master # publish to 'master' tag
tags:
- 'v*.*.*' # publish on version tags, updates 'latest' tag
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
livekit/livekit-server
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Download Go modules
run: go mod download
- name: Generate code
uses: magefile/mage-action@v3
with:
version: latest
args: generate
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+47
View File
@@ -0,0 +1,47 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Release
on:
workflow_dispatch:
push:
# only publish on version tags
tags:
- 'v*.*.*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fetch all tags
run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+29
View File
@@ -0,0 +1,29 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# checksums of file tree
.checksumgo
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
livekit-server
# Dependency directories (remove the comment below to include it)
# vendor/
bin/
proto/
# Mac
.DS_Store
# IDE
.idea/
dist/
+61
View File
@@ -0,0 +1,61 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
version: 2
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- id: livekit
env:
- CGO_ENABLED=0
main: ./cmd/server
binary: livekit-server
goarm:
- "7"
goarch:
- amd64
- arm64
- arm
goos:
- linux
- windows
archives:
- format_overrides:
- goos: windows
format: zip
files:
- LICENSE
release:
github:
owner: livekit
name: livekit
draft: true
prerelease: auto
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
gomod:
proxy: true
mod: mod
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.24-alpine AS builder
ARG TARGETPLATFORM
ARG TARGETARCH
RUN echo building for "$TARGETPLATFORM"
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY cmd/ cmd/
COPY pkg/ pkg/
COPY test/ test/
COPY tools/ tools/
COPY version/ version/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH GO111MODULE=on go build -a -o livekit-server ./cmd/server
FROM alpine
COPY --from=builder /workspace/livekit-server /livekit-server
# Run the binary.
ENTRYPOINT ["/livekit-server"]
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+13
View File
@@ -0,0 +1,13 @@
Copyright 2023 LiveKit, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+318
View File
@@ -0,0 +1,318 @@
<!--BEGIN_BANNER_IMAGE-->
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/.github/banner_dark.png">
<source media="(prefers-color-scheme: light)" srcset="/.github/banner_light.png">
<img style="width:100%;" alt="The LiveKit icon, the name of the repository and some sample code in the background." src="https://raw.githubusercontent.com/livekit/livekit/main/.github/banner_light.png">
</picture>
<!--END_BANNER_IMAGE-->
# LiveKit: Real-time video, audio and data for developers
[LiveKit](https://livekit.io) is an open source project that provides scalable, multi-user conferencing based on WebRTC.
It's designed to provide everything you need to build real-time video audio data capabilities in your applications.
LiveKit's server is written in Go, using the awesome [Pion WebRTC](https://github.com/pion/webrtc) implementation.
[![GitHub stars](https://img.shields.io/github/stars/livekit/livekit?style=social&label=Star&maxAge=2592000)](https://github.com/livekit/livekit/stargazers/)
[![Slack community](https://img.shields.io/endpoint?url=https%3A%2F%2Flivekit.io%2Fbadges%2Fslack)](https://livekit.io/join-slack)
[![Twitter Follow](https://img.shields.io/twitter/follow/livekit)](https://twitter.com/livekit)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/livekit/livekit)](https://github.com/livekit/livekit/releases/latest)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/livekit/livekit/buildtest.yaml?branch=master)](https://github.com/livekit/livekit/actions/workflows/buildtest.yaml)
[![License](https://img.shields.io/github/license/livekit/livekit)](https://github.com/livekit/livekit/blob/master/LICENSE)
## Features
- Scalable, distributed WebRTC SFU (Selective Forwarding Unit)
- Modern, full-featured client SDKs
- Built for production, supports JWT authentication
- Robust networking and connectivity, UDP/TCP/TURN
- Easy to deploy: single binary, Docker or Kubernetes
- Advanced features including:
- [speaker detection](https://docs.livekit.io/home/client/tracks/subscribe/#speaker-detection)
- [simulcast](https://docs.livekit.io/home/client/tracks/publish/#video-simulcast)
- [end-to-end optimizations](https://blog.livekit.io/livekit-one-dot-zero/)
- [selective subscription](https://docs.livekit.io/home/client/tracks/subscribe/#selective-subscription)
- [moderation APIs](https://docs.livekit.io/home/server/managing-participants/)
- end-to-end encryption
- SVC codecs (VP9, AV1)
- [webhooks](https://docs.livekit.io/home/server/webhooks/)
- [distributed and multi-region](https://docs.livekit.io/home/self-hosting/distributed/)
## Documentation & Guides
https://docs.livekit.io
## Live Demos
- [LiveKit Meet](https://meet.livekit.io) ([source](https://github.com/livekit-examples/meet))
- [Spatial Audio](https://spatial-audio-demo.livekit.io/) ([source](https://github.com/livekit-examples/spatial-audio))
- Livestreaming from OBS Studio ([source](https://github.com/livekit-examples/livestream))
- [AI voice assistant using ChatGPT](https://livekit.io/kitt) ([source](https://github.com/livekit-examples/kitt))
## Ecosystem
- [Agents](https://github.com/livekit/agents): build real-time multimodal AI applications with programmable backend participants
- [Egress](https://github.com/livekit/egress): record or multi-stream rooms and export individual tracks
- [Ingress](https://github.com/livekit/ingress): ingest streams from external sources like RTMP, WHIP, HLS, or OBS Studio
## SDKs & Tools
### Client SDKs
Client SDKs enable your frontend to include interactive, multi-user experiences.
<table>
<tr>
<th>Language</th>
<th>Repo</th>
<th>
<a href="https://docs.livekit.io/home/client/events/#declarative-ui" target="_blank" rel="noopener noreferrer">Declarative UI</a>
</th>
<th>Links</th>
</tr>
<!-- BEGIN Template
<tr>
<td>Language</td>
<td>
<a href="" target="_blank" rel="noopener noreferrer"></a>
</td>
<td></td>
<td></td>
</tr>
END -->
<!-- JavaScript -->
<tr>
<td>JavaScript (TypeScript)</td>
<td>
<a href="https://github.com/livekit/client-sdk-js" target="_blank" rel="noopener noreferrer">client-sdk-js</a>
</td>
<td>
<a href="https://github.com/livekit/livekit-react" target="_blank" rel="noopener noreferrer">React</a>
</td>
<td>
<a href="https://docs.livekit.io/client-sdk-js/" target="_blank" rel="noopener noreferrer">docs</a>
|
<a href="https://github.com/livekit/client-sdk-js/tree/main/example" target="_blank" rel="noopener noreferrer">JS example</a>
|
<a href="https://github.com/livekit/client-sdk-js/tree/main/example" target="_blank" rel="noopener noreferrer">React example</a>
</td>
</tr>
<!-- Swift -->
<tr>
<td>Swift (iOS / MacOS)</td>
<td>
<a href="https://github.com/livekit/client-sdk-swift" target="_blank" rel="noopener noreferrer">client-sdk-swift</a>
</td>
<td>Swift UI</td>
<td>
<a href="https://docs.livekit.io/client-sdk-swift/" target="_blank" rel="noopener noreferrer">docs</a>
|
<a href="https://github.com/livekit/client-example-swift" target="_blank" rel="noopener noreferrer">example</a>
</td>
</tr>
<!-- Kotlin -->
<tr>
<td>Kotlin (Android)</td>
<td>
<a href="https://github.com/livekit/client-sdk-android" target="_blank" rel="noopener noreferrer">client-sdk-android</a>
</td>
<td>Compose</td>
<td>
<a href="https://docs.livekit.io/client-sdk-android/index.html" target="_blank" rel="noopener noreferrer">docs</a>
|
<a href="https://github.com/livekit/client-sdk-android/tree/main/sample-app/src/main/java/io/livekit/android/sample" target="_blank" rel="noopener noreferrer">example</a>
|
<a href="https://github.com/livekit/client-sdk-android/tree/main/sample-app-compose/src/main/java/io/livekit/android/composesample" target="_blank" rel="noopener noreferrer">Compose example</a>
</td>
</tr>
<!-- Flutter -->
<tr>
<td>Flutter (all platforms)</td>
<td>
<a href="https://github.com/livekit/client-sdk-flutter" target="_blank" rel="noopener noreferrer">client-sdk-flutter</a>
</td>
<td>native</td>
<td>
<a href="https://docs.livekit.io/client-sdk-flutter/" target="_blank" rel="noopener noreferrer">docs</a>
|
<a href="https://github.com/livekit/client-sdk-flutter/tree/main/example" target="_blank" rel="noopener noreferrer">example</a>
</td>
</tr>
<!-- Unity -->
<tr>
<td>Unity WebGL</td>
<td>
<a href="https://github.com/livekit/client-sdk-unity-web" target="_blank" rel="noopener noreferrer">client-sdk-unity-web</a>
</td>
<td></td>
<td>
<a href="https://livekit.github.io/client-sdk-unity-web/" target="_blank" rel="noopener noreferrer">docs</a>
</td>
</tr>
<!-- React Native -->
<tr>
<td>React Native (beta)</td>
<td>
<a href="https://github.com/livekit/client-sdk-react-native" target="_blank" rel="noopener noreferrer">client-sdk-react-native</a>
</td>
<td>native</td>
<td></td>
</tr>
<!-- Rust -->
<tr>
<td>Rust</td>
<td>
<a href="https://github.com/livekit/client-sdk-rust" target="_blank" rel="noopener noreferrer">client-sdk-rust</a>
</td>
<td></td>
<td></td>
</tr>
</table>
### Server SDKs
Server SDKs enable your backend to generate [access tokens](https://docs.livekit.io/home/get-started/authentication/),
call [server APIs](https://docs.livekit.io/reference/server/server-apis/), and
receive [webhooks](https://docs.livekit.io/home/server/webhooks/). In addition, the Go SDK includes client capabilities,
enabling you to build automations that behave like end-users.
| Language | Repo | Docs |
| :---------------------- | :-------------------------------------------------------------------------------------- | :---------------------------------------------------------- |
| Go | [server-sdk-go](https://github.com/livekit/server-sdk-go) | [docs](https://pkg.go.dev/github.com/livekit/server-sdk-go) |
| JavaScript (TypeScript) | [server-sdk-js](https://github.com/livekit/server-sdk-js) | [docs](https://docs.livekit.io/server-sdk-js/) |
| Ruby | [server-sdk-ruby](https://github.com/livekit/server-sdk-ruby) | |
| Java (Kotlin) | [server-sdk-kotlin](https://github.com/livekit/server-sdk-kotlin) | |
| Python (community) | [python-sdks](https://github.com/livekit/python-sdks) | |
| PHP (community) | [agence104/livekit-server-sdk-php](https://github.com/agence104/livekit-server-sdk-php) | |
### Tools
- [CLI](https://github.com/livekit/livekit-cli) - command line interface & load tester
- [Docker image](https://hub.docker.com/r/livekit/livekit-server)
- [Helm charts](https://github.com/livekit/livekit-helm)
## Install
> [!TIP]
> We recommend installing [LiveKit CLI](https://github.com/livekit/livekit-cli) along with the server. It lets you access
> server APIs, create tokens, and generate test traffic.
The following will install LiveKit's media server:
### MacOS
```shell
brew install livekit
```
### Linux
```shell
curl -sSL https://get.livekit.io | bash
```
### Windows
Download the [latest release here](https://github.com/livekit/livekit/releases/latest)
## Getting Started
### Starting LiveKit
Start LiveKit in development mode by running `livekit-server --dev`. It'll use a placeholder API key/secret pair.
```
API Key: devkey
API Secret: secret
```
To customize your setup for production, refer to our [deployment docs](https://docs.livekit.io/deploy/)
### Creating access token
A user connecting to a LiveKit room requires an [access token](https://docs.livekit.io/home/get-started/authentication/#creating-a-token). Access
tokens (JWT) encode the user's identity and the room permissions they've been granted. You can generate a token with our
CLI:
```shell
lk token create \
--api-key devkey --api-secret secret \
--join --room my-first-room --identity user1 \
--valid-for 24h
```
### Test with example app
Head over to our [example app](https://example.livekit.io) and enter a generated token to connect to your LiveKit
server. This app is built with our [React SDK](https://github.com/livekit/livekit-react).
Once connected, your video and audio are now being published to your new LiveKit instance!
### Simulating a test publisher
```shell
lk room join \
--url ws://localhost:7880 \
--api-key devkey --api-secret secret \
--identity bot-user1 \
--publish-demo \
my-first-room
```
This command publishes a looped demo video to a room. Due to how the video clip was encoded (keyframes every 3s),
there's a slight delay before the browser has sufficient data to begin rendering frames. This is an artifact of the
simulation.
## Deployment
### Use LiveKit Cloud
LiveKit Cloud is the fastest and most reliable way to run LiveKit. Every project gets free monthly bandwidth and
transcoding credits.
Sign up for [LiveKit Cloud](https://cloud.livekit.io/).
### Self-host
Read our [deployment docs](https://docs.livekit.io/deploy/) for more information.
## Building from source
Pre-requisites:
- Go 1.23+ is installed
- GOPATH/bin is in your PATH
Then run
```shell
git clone https://github.com/livekit/livekit
cd livekit
./bootstrap.sh
mage
```
## Contributing
We welcome your contributions toward improving LiveKit! Please join us
[on Slack](http://livekit.io/join-slack) to discuss your ideas and/or PRs.
## License
LiveKit server is licensed under Apache License v2.0.
<!--BEGIN_REPO_NAV-->
<br/><table>
<thead><tr><th colspan="2">LiveKit Ecosystem</th></tr></thead>
<tbody>
<tr><td>LiveKit SDKs</td><td><a href="https://github.com/livekit/client-sdk-js">Browser</a> · <a href="https://github.com/livekit/client-sdk-swift">iOS/macOS/visionOS</a> · <a href="https://github.com/livekit/client-sdk-android">Android</a> · <a href="https://github.com/livekit/client-sdk-flutter">Flutter</a> · <a href="https://github.com/livekit/client-sdk-react-native">React Native</a> · <a href="https://github.com/livekit/rust-sdks">Rust</a> · <a href="https://github.com/livekit/node-sdks">Node.js</a> · <a href="https://github.com/livekit/python-sdks">Python</a> · <a href="https://github.com/livekit/client-sdk-unity">Unity</a> · <a href="https://github.com/livekit/client-sdk-unity-web">Unity (WebGL)</a></td></tr><tr></tr>
<tr><td>Server APIs</td><td><a href="https://github.com/livekit/node-sdks">Node.js</a> · <a href="https://github.com/livekit/server-sdk-go">Golang</a> · <a href="https://github.com/livekit/server-sdk-ruby">Ruby</a> · <a href="https://github.com/livekit/server-sdk-kotlin">Java/Kotlin</a> · <a href="https://github.com/livekit/python-sdks">Python</a> · <a href="https://github.com/livekit/rust-sdks">Rust</a> · <a href="https://github.com/agence104/livekit-server-sdk-php">PHP (community)</a> · <a href="https://github.com/pabloFuente/livekit-server-sdk-dotnet">.NET (community)</a></td></tr><tr></tr>
<tr><td>UI Components</td><td><a href="https://github.com/livekit/components-js">React</a> · <a href="https://github.com/livekit/components-android">Android Compose</a> · <a href="https://github.com/livekit/components-swift">SwiftUI</a></td></tr><tr></tr>
<tr><td>Agents Frameworks</td><td><a href="https://github.com/livekit/agents">Python</a> · <a href="https://github.com/livekit/agents-js">Node.js</a> · <a href="https://github.com/livekit/agent-playground">Playground</a></td></tr><tr></tr>
<tr><td>Services</td><td><b>LiveKit server</b> · <a href="https://github.com/livekit/egress">Egress</a> · <a href="https://github.com/livekit/ingress">Ingress</a> · <a href="https://github.com/livekit/sip">SIP</a></td></tr><tr></tr>
<tr><td>Resources</td><td><a href="https://docs.livekit.io">Docs</a> · <a href="https://github.com/livekit-examples">Example apps</a> · <a href="https://livekit.io/cloud">Cloud</a> · <a href="https://docs.livekit.io/home/self-hosting/deployment">Self-hosting</a> · <a href="https://github.com/livekit/livekit-cli">CLI</a></td></tr>
</tbody>
</table>
<!--END_REPO_NAV-->
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
if ! command -v mage &> /dev/null
then
pushd /tmp
git clone https://github.com/magefile/mage
cd mage
go run bootstrap.go
rm -rf /tmp/mage
popd
fi
if ! command -v mage &> /dev/null
then
echo "Ensure `go env GOPATH`/bin is in your \$PATH"
exit 1
fi
go mod download
+248
View File
@@ -0,0 +1,248 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/service"
)
func generateKeys(_ *cli.Context) error {
apiKey := guid.New(utils.APIKeyPrefix)
secret := utils.RandomSecret()
fmt.Println("API Key: ", apiKey)
fmt.Println("API Secret: ", secret)
return nil
}
func printPorts(c *cli.Context) error {
conf, err := getConfig(c)
if err != nil {
return err
}
udpPorts := make([]string, 0)
tcpPorts := make([]string, 0)
tcpPorts = append(tcpPorts, fmt.Sprintf("%d - HTTP service", conf.Port))
if conf.RTC.TCPPort != 0 {
tcpPorts = append(tcpPorts, fmt.Sprintf("%d - ICE/TCP", conf.RTC.TCPPort))
}
if conf.RTC.UDPPort.Valid() {
portStr, _ := conf.RTC.UDPPort.MarshalYAML()
udpPorts = append(udpPorts, fmt.Sprintf("%s - ICE/UDP", portStr))
} else {
udpPorts = append(udpPorts, fmt.Sprintf("%d-%d - ICE/UDP range", conf.RTC.ICEPortRangeStart, conf.RTC.ICEPortRangeEnd))
}
if conf.TURN.Enabled {
if conf.TURN.TLSPort > 0 {
tcpPorts = append(tcpPorts, fmt.Sprintf("%d - TURN/TLS", conf.TURN.TLSPort))
}
if conf.TURN.UDPPort > 0 {
udpPorts = append(udpPorts, fmt.Sprintf("%d - TURN/UDP", conf.TURN.UDPPort))
}
}
fmt.Println("TCP Ports")
for _, p := range tcpPorts {
fmt.Println(p)
}
fmt.Println("UDP Ports")
for _, p := range udpPorts {
fmt.Println(p)
}
return nil
}
func helpVerbose(c *cli.Context) error {
generatedFlags, err := config.GenerateCLIFlags(baseFlags, false)
if err != nil {
return err
}
c.App.Flags = append(baseFlags, generatedFlags...)
return cli.ShowAppHelp(c)
}
func createToken(c *cli.Context) error {
room := c.String("room")
identity := c.String("identity")
conf, err := getConfig(c)
if err != nil {
return err
}
// use the first API key from config
if len(conf.Keys) == 0 {
// try to load from file
if _, err := os.Stat(conf.KeyFile); err != nil {
return err
}
f, err := os.Open(conf.KeyFile)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
decoder := yaml.NewDecoder(f)
if err = decoder.Decode(conf.Keys); err != nil {
return err
}
if len(conf.Keys) == 0 {
return fmt.Errorf("keys are not configured")
}
}
var apiKey string
var apiSecret string
for k, v := range conf.Keys {
apiKey = k
apiSecret = v
break
}
grant := &auth.VideoGrant{
RoomJoin: true,
Room: room,
}
if c.Bool("recorder") {
grant.Hidden = true
grant.Recorder = true
grant.SetCanPublish(false)
grant.SetCanPublishData(false)
}
at := auth.NewAccessToken(apiKey, apiSecret).
AddGrant(grant).
SetIdentity(identity).
SetValidFor(30 * 24 * time.Hour)
token, err := at.ToJWT()
if err != nil {
return err
}
fmt.Println("Token:", token)
return nil
}
func listNodes(c *cli.Context) error {
conf, err := getConfig(c)
if err != nil {
return err
}
currentNode, err := routing.NewLocalNode(conf)
if err != nil {
return err
}
router, err := service.InitializeRouter(conf, currentNode)
if err != nil {
return err
}
nodes, err := router.ListNodes()
if err != nil {
return err
}
table := tablewriter.NewWriter(os.Stdout)
table.SetRowLine(true)
table.SetAutoWrapText(false)
table.SetHeader([]string{
"ID", "IP Address", "Region",
"CPUs", "CPU Usage\nLoad Avg",
"Memory Used/Total",
"Rooms", "Clients\nTracks In/Out",
"Bytes/s In/Out\nBytes Total", "Packets/s In/Out\nPackets Total", "System Dropped Pkts/s\nPkts/s Out/Dropped",
"Nack/s\nNack Total", "Retrans/s\nRetrans Total",
"Started At\nUpdated At",
})
table.SetColumnAlignment([]int{
tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER,
tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT,
tablewriter.ALIGN_CENTER,
})
for _, node := range nodes {
stats := node.Stats
// Id and state
idAndState := fmt.Sprintf("%s\n(%s)", node.Id, node.State.Enum().String())
// System stats
cpus := strconv.Itoa(int(stats.NumCpus))
cpuUsageAndLoadAvg := fmt.Sprintf("%.2f %%\n%.2f %.2f %.2f", stats.CpuLoad*100,
stats.LoadAvgLast1Min, stats.LoadAvgLast5Min, stats.LoadAvgLast15Min)
memUsage := fmt.Sprintf("%s / %s", humanize.Bytes(stats.MemoryUsed), humanize.Bytes(stats.MemoryTotal))
// Room stats
rooms := strconv.Itoa(int(stats.NumRooms))
clientsAndTracks := fmt.Sprintf("%d\n%d / %d", stats.NumClients, stats.NumTracksIn, stats.NumTracksOut)
// Packet stats
bytes := fmt.Sprintf("%sps / %sps\n%s / %s", humanize.Bytes(uint64(stats.BytesInPerSec)), humanize.Bytes(uint64(stats.BytesOutPerSec)),
humanize.Bytes(stats.BytesIn), humanize.Bytes(stats.BytesOut))
packets := fmt.Sprintf("%s / %s\n%s / %s", humanize.Comma(int64(stats.PacketsInPerSec)), humanize.Comma(int64(stats.PacketsOutPerSec)),
strings.TrimSpace(humanize.SIWithDigits(float64(stats.PacketsIn), 2, "")), strings.TrimSpace(humanize.SIWithDigits(float64(stats.PacketsOut), 2, "")))
sysPackets := fmt.Sprintf("%.2f %%\n%v / %v", stats.SysPacketsDroppedPctPerSec*100, float64(stats.SysPacketsOutPerSec), float64(stats.SysPacketsDroppedPerSec))
nacks := fmt.Sprintf("%.2f\n%s", stats.NackPerSec, strings.TrimSpace(humanize.SIWithDigits(float64(stats.NackTotal), 2, "")))
retransmit := fmt.Sprintf("%.2f\n%s", stats.RetransmitPacketsOutPerSec, strings.TrimSpace(humanize.SIWithDigits(float64(stats.RetransmitPacketsOut), 2, "")))
// Date
startedAndUpdated := fmt.Sprintf("%s\n%s", time.Unix(stats.StartedAt, 0).UTC().UTC().Format("2006-01-02 15:04:05"),
time.Unix(stats.UpdatedAt, 0).UTC().Format("2006-01-02 15:04:05"))
table.Append([]string{
idAndState, node.Ip, node.Region,
cpus, cpuUsageAndLoadAvg,
memUsage,
rooms, clientsAndTracks,
bytes, packets, sysPackets,
nacks, retransmit,
startedAndUpdated,
})
}
table.Render()
return nil
}
+310
View File
@@ -0,0 +1,310 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"math/rand"
"net"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"syscall"
"time"
"github.com/urfave/cli/v2"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/service"
"github.com/livekit/livekit-server/version"
)
var baseFlags = []cli.Flag{
&cli.StringSliceFlag{
Name: "bind",
Usage: "IP address to listen on, use flag multiple times to specify multiple addresses",
},
&cli.StringFlag{
Name: "config",
Usage: "path to LiveKit config file",
},
&cli.StringFlag{
Name: "config-body",
Usage: "LiveKit config in YAML, typically passed in as an environment var in a container",
EnvVars: []string{"LIVEKIT_CONFIG"},
},
&cli.StringFlag{
Name: "key-file",
Usage: "path to file that contains API keys/secrets",
},
&cli.StringFlag{
Name: "keys",
Usage: "api keys (key: secret\\n)",
EnvVars: []string{"LIVEKIT_KEYS"},
},
&cli.StringFlag{
Name: "region",
Usage: "region of the current node. Used by regionaware node selector",
EnvVars: []string{"LIVEKIT_REGION"},
},
&cli.StringFlag{
Name: "node-ip",
Usage: "IP address of the current node, used to advertise to clients. Automatically determined by default",
EnvVars: []string{"NODE_IP"},
},
&cli.StringFlag{
Name: "udp-port",
Usage: "UDP port(s) to use for WebRTC traffic",
EnvVars: []string{"UDP_PORT"},
},
&cli.StringFlag{
Name: "redis-host",
Usage: "host (incl. port) to redis server",
EnvVars: []string{"REDIS_HOST"},
},
&cli.StringFlag{
Name: "redis-password",
Usage: "password to redis",
EnvVars: []string{"REDIS_PASSWORD"},
},
&cli.StringFlag{
Name: "turn-cert",
Usage: "tls cert file for TURN server",
EnvVars: []string{"LIVEKIT_TURN_CERT"},
},
&cli.StringFlag{
Name: "turn-key",
Usage: "tls key file for TURN server",
EnvVars: []string{"LIVEKIT_TURN_KEY"},
},
// debugging flags
&cli.StringFlag{
Name: "memprofile",
Usage: "write memory profile to `file`",
},
&cli.BoolFlag{
Name: "dev",
Usage: "sets log-level to debug, console formatter, and /debug/pprof. insecure for production",
},
&cli.BoolFlag{
Name: "disable-strict-config",
Usage: "disables strict config parsing",
Hidden: true,
},
}
func init() {
rand.Seed(time.Now().Unix())
}
func main() {
defer func() {
if rtc.Recover(logger.GetLogger()) != nil {
os.Exit(1)
}
}()
generatedFlags, err := config.GenerateCLIFlags(baseFlags, true)
if err != nil {
fmt.Println(err)
}
app := &cli.App{
Name: "livekit-server",
Usage: "High performance WebRTC server",
Description: "run without subcommands to start the server",
Flags: append(baseFlags, generatedFlags...),
Action: startServer,
Commands: []*cli.Command{
{
Name: "generate-keys",
Usage: "generates an API key and secret pair",
Action: generateKeys,
},
{
Name: "ports",
Usage: "print ports that server is configured to use",
Action: printPorts,
},
{
// this subcommand is deprecated, token generation is provided by CLI
Name: "create-join-token",
Hidden: true,
Usage: "create a room join token for development use",
Action: createToken,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "room",
Usage: "name of room to join",
Required: true,
},
&cli.StringFlag{
Name: "identity",
Usage: "identity of participant that holds the token",
Required: true,
},
&cli.BoolFlag{
Name: "recorder",
Usage: "creates a hidden participant that can only subscribe",
Required: false,
},
},
},
{
Name: "list-nodes",
Usage: "list all nodes",
Action: listNodes,
},
{
Name: "help-verbose",
Usage: "prints app help, including all generated configuration flags",
Action: helpVerbose,
},
},
Version: version.Version,
}
if err := app.Run(os.Args); err != nil {
fmt.Println(err)
}
}
func getConfig(c *cli.Context) (*config.Config, error) {
confString, err := getConfigString(c.String("config"), c.String("config-body"))
if err != nil {
return nil, err
}
strictMode := true
if c.Bool("disable-strict-config") {
strictMode = false
}
conf, err := config.NewConfig(confString, strictMode, c, baseFlags)
if err != nil {
return nil, err
}
config.InitLoggerFromConfig(&conf.Logging)
if conf.Development {
logger.Infow("starting in development mode")
if len(conf.Keys) == 0 {
logger.Infow("no keys provided, using placeholder keys",
"API Key", "devkey",
"API Secret", "secret",
)
conf.Keys = map[string]string{
"devkey": "secret",
}
shouldMatchRTCIP := false
// when dev mode and using shared keys, we'll bind to localhost by default
if conf.BindAddresses == nil {
conf.BindAddresses = []string{
"127.0.0.1",
"::1",
}
} else {
// if non-loopback addresses are provided, then we'll match RTC IP to bind address
// our IP discovery ignores loopback addresses
for _, addr := range conf.BindAddresses {
ip := net.ParseIP(addr)
if ip != nil && !ip.IsLoopback() && !ip.IsUnspecified() {
shouldMatchRTCIP = true
}
}
}
if shouldMatchRTCIP {
for _, bindAddr := range conf.BindAddresses {
conf.RTC.IPs.Includes = append(conf.RTC.IPs.Includes, bindAddr+"/24")
}
}
}
}
return conf, nil
}
func startServer(c *cli.Context) error {
conf, err := getConfig(c)
if err != nil {
return err
}
// validate API key length
err = conf.ValidateKeys()
if err != nil {
return err
}
if memProfile := c.String("memprofile"); memProfile != "" {
if f, err := os.Create(memProfile); err != nil {
return err
} else {
defer func() {
// run memory profile at termination
runtime.GC()
_ = pprof.WriteHeapProfile(f)
_ = f.Close()
}()
}
}
currentNode, err := routing.NewLocalNode(conf)
if err != nil {
return err
}
if err := prometheus.Init(string(currentNode.NodeID()), currentNode.NodeType()); err != nil {
return err
}
server, err := service.InitializeServer(conf, currentNode)
if err != nil {
return err
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
for i := 0; i < 2; i++ {
sig := <-sigChan
force := i > 0
logger.Infow("exit requested, shutting down", "signal", sig, "force", force)
go server.Stop(force)
}
}()
return server.Start()
}
func getConfigString(configFile string, inConfigBody string) (string, error) {
if inConfigBody != "" || configFile == "" {
return inConfigBody, nil
}
outConfigBody, err := os.ReadFile(configFile)
if err != nil {
return "", err
}
return string(outConfigBody), nil
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
type testStruct struct {
configFileName string
configBody string
expectedError error
expectedConfigBody string
}
func TestGetConfigString(t *testing.T) {
tests := []testStruct{
{"", "", nil, ""},
{"", "configBody", nil, "configBody"},
{"file", "configBody", nil, "configBody"},
{"file", "", nil, "fileContent"},
}
for _, test := range tests {
func() {
writeConfigFile(test, t)
defer os.Remove(test.configFileName)
configBody, err := getConfigString(test.configFileName, test.configBody)
require.Equal(t, test.expectedError, err)
require.Equal(t, test.expectedConfigBody, configBody)
}()
}
}
func TestShouldReturnErrorIfConfigFileDoesNotExist(t *testing.T) {
configBody, err := getConfigString("notExistingFile", "")
require.Error(t, err)
require.Empty(t, configBody)
}
func writeConfigFile(test testStruct, t *testing.T) {
if test.configFileName != "" {
d1 := []byte(test.expectedConfigBody)
err := os.WriteFile(test.configFileName, d1, 0o644)
require.NoError(t, err)
}
}
+319
View File
@@ -0,0 +1,319 @@
# Copyright 2024 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# main TCP port for RoomService and RTC endpoint
# for production setups, this port should be placed behind a load balancer with TLS
port: 7880
# when redis is set, LiveKit will automatically operate in a fully distributed fashion
# clients could connect to any node and be routed to the same room
redis:
address: redis.host:6379
# db: 0
# username: myuser
# password: mypassword
# To use sentinel remove the address key above and add the following
# sentinel_master_name: livekit
# sentinel_addresses:
# - livekit-redis-node-0.livekit-redis-headless:26379
# - livekit-redis-node-1.livekit-redis-headless:26379
# If you use a different set of credentials for sentinel add
# sentinel_username: user
# sentinel_password: pass
#
# To use TLS with redis
# tls:
# enabled: true
# # when set to true, LiveKit will not verify the server's certificate, defaults to true
# insecure: false
# server_name: myserver.com
# # file containing trusted root certificates for verification
# ca_cert_file: /path/to/ca.crt
# client_cert_file: /path/to/client.crt
# client_key_file: /path/to/client.key
#
# To use cluster remove the address key above and add the following
# cluster_addresses:
# - livekit-redis-node-0.livekit-redis-headless:6379
# - livekit-redis-node-1.livekit-redis-headless:6380
# And it will use the password key above as cluster password
# And the db key will not be used due to cluster mode not support it.
# WebRTC configuration
rtc:
# UDP ports to use for client traffic.
# this port range should be open for inbound traffic on the firewall
port_range_start: 50000
port_range_end: 60000
# when set, LiveKit enable WebRTC ICE over TCP when UDP isn't available
# this port *cannot* be behind load balancer or TLS, and must be exposed on the node
# WebRTC transports are encrypted and do not require additional encryption
# only 80/443 on public IP are allowed if less than 1024
tcp_port: 7881
# when set to true, attempts to discover the host's public IP via STUN
# this is useful for cloud environments such as AWS & Google where hosts have an internal IP
# that maps to an external one
use_external_ip: true
# # when set, LiveKit will attempt to use a UDP mux so all UDP traffic goes through
# # listed port(s). To maximize system performance, we recommend using a range of ports
# # greater or equal to the number of vCPUs on the machine.
# # port_range_start & end must not be set for this config to take effect
# udp_port: 7882-7892
# # when set to true, server will use a lite ice agent, that will speed up ice connection, but
# # might cause connect issue if server running behind NAT.
# use_ice_lite: true
# # optional STUN servers for LiveKit clients to use. Clients will be configured to use these STUN servers automatically.
# # by default LiveKit clients use Google's public STUN servers
# stun_servers:
# - server1
# # optional TURN servers for clients. This isn't necessary if using embedded TURN server (see below).
# turn_servers:
# - host: myhost.com
# port: 443
# # tls, tcp, or udp
# protocol: tls
# username: ""
# credential: ""
# # allows LiveKit to monitor congestion when sending streams and automatically
# # manage bandwidth utilization to avoid congestion/loss. Enabled by default
# congestion_control:
# enabled: true
# # in the unlikely event of highly congested networks, SFU may choose to pause some tracks
# # in order to allow others to stream smoothly. You can disable this behavior here
# allow_pause: true
# # allows automatic connection fallback to TCP and TURN/TLS (if configured) when UDP has been unstable, default true
# allow_tcp_fallback: true
# # number of packets to buffer in the SFU for video, defaults to 500
# packet_buffer_size_video: 500
# # number of packets to buffer in the SFU for audio, defaults to 200
# packet_buffer_size_audio: 200
# # minimum amount of time between pli/fir rtcp packets being sent to an individual
# # producer. Increasing these times can lead to longer black screens when new participants join,
# # while reducing them can lead to higher stream bitrate.
# pli_throttle:
# low_quality: 500ms
# mid_quality: 1s
# high_quality: 1s
# # when set, Livekit will collect loopback candidates, it is useful for some VM have public address mapped to its loopback interface.
# enable_loopback_candidate: true
# # network interface filter. If the machine has more than one network interface and you'd like it to use or skip specific interfaces
# # both inclusion and exclusion filters can be used together. If neither is defined (default), all interfaces on the machine will be used.
# # If both of them are set, then only include takes effect.
# interfaces:
# includes:
# - en0
# excludes:
# - docker0
# # ip address filter. If the machine has more than one ip address and you'd like it to use or skip specific ips,
# # both inclusion and exclusion CIDR filters can be used together. If neither is defined (default), all ip on the machine will be used.
# # If both of them are set, then only include takes effect.
# ips:
# includes:
# - 10.0.0.0/16
# excludes:
# - 192.168.1.0/24
# # Set to true to enable mDNS name candidate. This should be left disabled for most users.
# # when enabled, it will impact performance since each PeerConnection will process the same mDNS message independently
# use_mdns: true
# # Set to false to disable strict ACKs for peer connections where LiveKit is the dialing side,
# # ie. subscriber peer connections. Disabling strict ACKs will prevent clients that do not ACK
# # peer connections from getting kicked out of rooms by the monitor. Note that if strict ACKs
# # are disabled and clients don't ACK opened peer connections, only reliable, ordered delivery
# # will be available.
# strict_acks: true
# # enable batch write to merge network write system calls to reduce cpu usage. Outgoing packets
# # will be queued until length of queue equal to `batch_size` or time elapsed since last write exceeds `max_flush_interval`.
# batch_io:
# batch_size: 128
# max_flush_interval: 2ms
# # max number of bytes to buffer for data channel. 0 means unlimited.
# # when this limit is breached, data messages will be dropped till the buffered amount drops below this limit.
# data_channel_max_buffered_amount: 0
# when enabled, LiveKit will expose prometheus metrics on :6789/metrics
# prometheus_port: 6789
# API key / secret pairs.
# Keys are used for JWT authentication, server APIs would require a keypair in order to generate access tokens
# and make calls to the server
keys:
key1: secret1
key2: secret2
# Logging config
# logging:
# # log level, valid values: debug, info, warn, error
# level: info
# # log level for pion, default error
# pion_level: error
# # when set to true, emit json fields
# json: false
# # for production setups, enables sampling algorithm
# # https://github.com/uber-go/zap/blob/master/FAQ.md#why-sample-application-logs
# sample: false
# Default room config
# Each room created will inherit these settings. If rooms are created explicitly with CreateRoom, they will take
# precedence over defaults
# room:
# # allow rooms to be automatically created when participants join, defaults to true
# # auto_create: false
# # number of seconds to keep the room open if no one joins
# empty_timeout: 300
# # number of seconds to keep the room open after everyone leaves
# departure_timeout: 20
# # limit number of participants that can be in a room, 0 for no limit
# max_participants: 0
# # only accept specific codecs for clients publishing to this room
# # this is useful to standardize codecs across clients
# # other supported codecs are video/h264, video/vp9, video/av1, audio/red
# enabled_codecs:
# - mime: audio/opus
# - mime: video/vp8
# # allow tracks to be unmuted remotely, defaults to false
# # tracks can always be muted from the Room Service APIs
# enable_remote_unmute: true
# # control playout delay in ms of video track (and associated audio track)
# playout_delay:
# enabled: true
# min: 100
# max: 2000
# # improves A/V sync when playout_delay set to a value larger than 200ms. It will disables transceiver re-use
# # so not recommended for rooms with frequent subscription changes
# sync_streams: true
# Webhooks
# when configured, LiveKit notifies your URL handler with room events
# webhook:
# # the API key to use in order to sign the message
# # this must match one of the keys LiveKit is configured with
# api_key: <api_key>
# # list of URLs to be notified of room events
# urls:
# - https://your-host.com/handler
# Signal Relay
# since v1.4.0, a more reliable, psrpc based signal relay is available
# this gives us the ability to reliably proxy messages between a signal server and RTC node
# signal_relay:
# # amount of time a message delivery is tried before giving up
# retry_timeout: 30s
# # minimum amount of time to wait for RTC node to ack,
# # retries use exponentially increasing wait on every subsequent try
# # with an upper bound of max_retry_interval
# min_retry_interval: 500ms
# # maximum amount of time to wait for RTC node to ack
# max_retry_interval: 5s
# # number of messages to buffer before dropping
# stream_buffer_size: 1000
# PSRPC
# since v1.5.1, a more reliable, psrpc based internal rpc
# psrpc:
# # maximum number of rpc attempts
# max_attempts: 3
# # initial time to wait for calls to complete
# timeout: 500ms
# # amount of time added to the timeout after each failure
# backoff: 500ms
# # number of messages to buffer before dropping
# buffer_size: 1000
# customize audio level sensitivity
# audio:
# # minimum level to be considered active, 0-127, where 0 is loudest
# # defaults to 30
# active_level: 30
# # percentile to measure, a participant is considered active if it has exceeded the
# # ActiveLevel more than MinPercentile% of the time
# # defaults to 40
# min_percentile: 40
# # frequency in ms to notify changes to clients, defaults to 500
# update_interval: 500
# # to prevent speaker updates from too jumpy, smooth out values over N samples
# smooth_intervals: 4
# # enable red encoding downtrack for opus only audio up track
# active_red_encoding: true
# turn server
# turn:
# # Uses TLS. Requires cert and key pem files by either:
# # - using turn.secretName if deploying with our helm chart, or
# # - setting LIVEKIT_TURN_CERT and LIVEKIT_TURN_KEY env vars with file locations, or
# # - using cert_file and key_file below
# # defaults to false
# enabled: false
# # defaults to 3478 - recommended to 443 if not running HTTP3/QUIC server
# # only 53/80/443 are allowed if less than 1024
# udp_port: 3478
# # defaults to 5349 - if not using a load balancer, this must be set to 443
# tls_port: 5349
# # set UDP port range for TURN relay to connect to LiveKit SFU, by default it uses a any available port
# relay_range_start: 1024
# relay_range_end: 30000
# # set external_tls to true if using a L4 load balancer to terminate TLS. when enabled,
# # LiveKit expects unencrypted traffic on tls_port, and still advertise tls_port as a TURN/TLS candidate.
# external_tls: true
# # needs to match tls cert domain
# domain: turn.myhost.com
# # optional (set only if not using external TLS termination)
# # cert_file: /path/to/cert.pem
# # key_file: /path/to/key.pem
# ingress server
# ingress:
# # Prefix used to generate RTMP URLs for RTMP ingress.
# rtmp_base_url: "rtmp://my.domain.com/live"
# # Prefix used to generate WHIP URLs for WHIP ingress.
# whip_base_url: "http://my.domain.com/whip"
# Region of the current node. Required if using regionaware node selector
# region: us-west-2
# # node selector
# node_selector:
# # default: any. valid values: any, sysload, cpuload, regionaware
# kind: sysload
# # priority used for selection of node when multiple are available
# # default: random. valid values: random, sysload, cpuload, rooms, clients, tracks, bytespersec
# sort_by: sysload
# # used in sysload and regionaware
# # do not assign room to node if load per CPU exceeds sysload_limit
# sysload_limit: 0.7
# # used in regionaware
# # list of regions and their lat/lon coordinates
# regions:
# - name: us-west-2
# lat: 44.19434095976287
# lon: -123.0674908379146
# # node limits
# # set to -1 to disable a limit
# limit:
# # defaults to 400 tracks in & out per CPU, up to 8000
# num_tracks: -1
# # defaults to 1 GB/s, or just under 10 Gbps
# bytes_per_sec: 1_000_000_000
# # how many tracks (audio / video) that a single participant can subscribe at same time.
# # if the limit is exceeded, subscriptions will be pending until any subscribed track has been unsubscribed.
# # value less or equal than 0 means no limit.
# subscription_limit_video: 0
# subscription_limit_audio: 0
# # limit size of room and participant's metadata, 0 for no limit
# max_metadata_size: 0
# # limit size of participant attributes, 0 for no limit
# max_attributes_size: 0
# # limit length of room names
# max_room_name_length: 0
# # limit length of participant identity
# max_participant_identity_length: 0
+8
View File
@@ -0,0 +1,8 @@
# LiveKit Server Deployment
Deployment Guides:
- [Deploy to a VM](https://docs.livekit.io/deploy/vm)
- [Deploy to Kubernetes](https://docs.livekit.io/deploy/kubernetes)
Also included are Grafana charts for metrics gathered in Prometheus.
@@ -0,0 +1,531 @@
{
"__inputs": [],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "8.2.2"
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"exemplar": true,
"expr": "sum(livekit_room_total)",
"interval": "",
"legendFormat": "Rooms",
"refId": "A"
}
],
"thresholds": [],
"title": "Rooms",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"exemplar": true,
"expr": "sum(livekit_participant_total)",
"interval": "",
"legendFormat": "Participants",
"refId": "A"
}
],
"title": "Participants",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 9,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"exemplar": true,
"expr": "sum(irate(livekit_node_messages{type=\"signal\"}[5m]))",
"interval": "",
"legendFormat": "Signal",
"refId": "Signal"
},
{
"exemplar": true,
"expr": "sum(irate(livekit_node_messages{type=\"rtc\"}[5m]))",
"hide": false,
"interval": "",
"legendFormat": "RTC",
"refId": "RTC"
},
{
"exemplar": true,
"expr": "sum(irate(livekit_node_messages{status=\"failure\"}[5m]))",
"hide": false,
"interval": "",
"legendFormat": "Failure",
"refId": "Failure"
},
{
"exemplar": true,
"expr": "sum(irate(livekit_messagebus_messages{type=\"out\"}[5m]))",
"hide": false,
"interval": "",
"legendFormat": "Out",
"refId": "Out"
},
{
"exemplar": true,
"expr": "sum(irate(livekit_messagebus_messages{type=\"out\", status=\"failure\"}[5m]))",
"hide": false,
"interval": "",
"legendFormat": "Out Failure",
"refId": "Out Failure"
}
],
"thresholds": [
{
"colorMode": "critical",
"op": "gt",
"value": 0,
"visible": true
}
],
"title": "Message Rate",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 11,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"exemplar": true,
"expr": "sum(livekit_track_published_total)",
"interval": "",
"legendFormat": "Tracks",
"refId": "A"
}
],
"title": "Tracks Published",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 13,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"exemplar": true,
"expr": "sum(irate(livekit_packet_total[5m]))",
"interval": "",
"legendFormat": "Packets",
"refId": "Packets"
},
{
"exemplar": true,
"expr": "sum(irate(livekit_nack_total[5m]))",
"hide": false,
"interval": "",
"legendFormat": "NACK",
"refId": "NACK"
},
{
"exemplar": true,
"expr": "sum(irate(livekit_pli_total[5m]))",
"hide": false,
"interval": "",
"legendFormat": "PLI",
"refId": "PLI"
},
{
"exemplar": true,
"expr": "sum(irate(livekit_fir_total[5m]))",
"hide": false,
"interval": "",
"legendFormat": "FIR",
"refId": "FIR"
}
],
"title": "Network Rate",
"type": "timeseries"
}
],
"refresh": "5m",
"schemaVersion": 31,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "LiveKit Server Overview",
"uid": "z_GO3t5nz",
"version": 2
}
+144
View File
@@ -0,0 +1,144 @@
module github.com/livekit/livekit-server
go 1.23
toolchain go1.23.6
require (
github.com/bep/debounce v1.2.1
github.com/d5/tengo/v2 v2.17.0
github.com/dennwc/iters v1.0.1
github.com/dustin/go-humanize v1.0.1
github.com/elliotchance/orderedmap/v2 v2.7.0
github.com/florianl/go-tc v0.4.4
github.com/frostbyte73/core v0.1.1
github.com/gammazero/deque v1.0.0
github.com/gammazero/workerpool v1.1.3
github.com/google/uuid v1.6.0
github.com/google/wire v0.6.0
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jellydator/ttlcache/v3 v3.3.0
github.com/jxskiss/base62 v1.1.0
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1
github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564
github.com/livekit/protocol v1.34.0
github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126
github.com/mackerelio/go-osstat v0.2.5
github.com/magefile/mage v1.15.0
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/ory/dockertest/v3 v3.11.0
github.com/pion/datachannel v1.5.10
github.com/pion/dtls/v3 v3.0.4
github.com/pion/ice/v4 v4.0.7
github.com/pion/interceptor v0.1.37
github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.11
github.com/pion/sctp v1.8.36
github.com/pion/sdp/v3 v3.0.10
github.com/pion/transport/v3 v3.0.7
github.com/pion/turn/v4 v4.0.0
github.com/pion/webrtc/v4 v4.0.11
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.20.5
github.com/redis/go-redis/v9 v9.7.0
github.com/rs/cors v1.11.1
github.com/stretchr/testify v1.10.0
github.com/thoas/go-funk v0.9.3
github.com/twitchtv/twirp v8.1.3+incompatible
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff
github.com/urfave/cli/v2 v2.27.5
github.com/urfave/negroni/v3 v3.1.1
go.uber.org/atomic v1.11.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/sync v0.11.0
google.golang.org/protobuf v1.36.5
gopkg.in/yaml.v3 v3.0.1
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 // indirect
buf.build/go/protoyaml v0.3.1 // indirect
cel.dev/expr v0.19.0 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protovalidate-go v0.8.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v26.1.4+incompatible // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/cel-go v0.22.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/subcommands v1.2.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/lithammer/shortuuid/v4 v4.2.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mdlayher/netlink v1.7.1 // indirect
github.com/mdlayher/socket v0.4.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.39.0 // indirect
github.com/nats-io/nkeys v0.4.10 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runc v1.1.14 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect
google.golang.org/grpc v1.70.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+498
View File
@@ -0,0 +1,498 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 h1:ntAj16eF7AtUyzOOAFk5gvbAO52QmUKPKk7GmsIEORo=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1/go.mod h1:AxRT+qTj5PJCz2nyQzsR/qxAcveW5USRhJTt/edTO5w=
buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4=
buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg=
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bufbuild/protovalidate-go v0.8.0 h1:Xs3kCLCJ4tQiogJ0iOXm+ClKw/KviW3nLAryCGW2I3Y=
github.com/bufbuild/protovalidate-go v0.8.0/go.mod h1:JPWZInGm2y2NBg3vKDKdDIkvDjyLv31J3hLH5GIFc/Q=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao=
github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/d5/tengo/v2 v2.17.0 h1:BWUN9NoJzw48jZKiYDXDIF3QrIVZRm1uV1gTzeZ2lqM=
github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dennwc/iters v1.0.1 h1:XwMudE6xtS0ugEdum4HQ+iRi+5HSvaeKxJPM/VI3pJs=
github.com/dennwc/iters v1.0.1/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elliotchance/orderedmap/v2 v2.7.0 h1:WHuf0DRo63uLnldCPp9ojm3gskYwEdIIfAUVG5KhoOc=
github.com/elliotchance/orderedmap/v2 v2.7.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/florianl/go-tc v0.4.4 h1:q6lhEWEfyhGffRzdl3eIcNqX/yVIw0IJwXqa9Rdcctw=
github.com/florianl/go-tc v0.4.4/go.mod h1:uvp6pIlOw7Z8hhfnT5M4+V1hHVgZWRZwwMS8Z0JsRxc=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/frostbyte73/core v0.1.1 h1:ChhJOR7bAKOCPbA+lqDLE2cGKlCG5JXsDvvQr4YaJIA=
github.com/frostbyte73/core v0.1.1/go.mod h1:mhfOtR+xWAvwXiwor7jnqPMnu4fxbv1F2MwZ0BEpzZo=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
github.com/gammazero/workerpool v1.1.3 h1:WixN4xzukFoN0XSeXF6puqEqFTl2mECI9S6W44HWy9Q=
github.com/gammazero/workerpool v1.1.3/go.mod h1:wPjyBLDbyKnUn2XwwyD3EEwo9dHutia9/fwNmSHWACc=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40=
github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo=
github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa793TP5z5GNAn/VLPzlc0ewzWdeP/25gDfgQ=
github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58=
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564 h1:GX7KF/V9ExmcfT/2Bdia8aROjkxrgx7WpyH7w9MB4J4=
github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564/go.mod h1:36s+wwmU3O40IAhE+MjBWP3W71QRiEE9SfooSBvtBqY=
github.com/livekit/protocol v1.34.0 h1:hbIXgNW+JPiTcGjzNg1XgQg3Wqa2R5dBhzuy+LLEIS4=
github.com/livekit/protocol v1.34.0/go.mod h1:yXuQ7ucrLj91nbxL6/AHgtxdha1DGzLj1LkgvnT90So=
github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126 h1:fzuYpAQbCid7ySPpQWWePfQOWUrs8x6dJ0T3Wl07n+Y=
github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126/go.mod h1:X5WtEZ7OnEs72Fi5/J+i0on3964F1aynQpCalcgMqRo=
github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o=
github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU=
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc=
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.39.0 h1:2/yg2JQjiYYKLwDuBzV0FbB2sIV+eFNkEevlRi4n9lI=
github.com/nats-io/nats.go v1.39.0/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM=
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w=
github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA=
github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
github.com/pion/ice/v4 v4.0.7 h1:mnwuT3n3RE/9va41/9QJqN5+Bhc0H/x/ZyiVlWMw35M=
github.com/pion/ice/v4 v4.0.7/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.11 h1:0i7BNFH2n8LVp08q/dqM5iyZBXW4TITbD1+RwNqk/iY=
github.com/pion/webrtc/v4 v4.0.11/go.mod h1:C+5JA7KiyLyoKyGh7hVFD/HCAon3IB/tfniocpZ9JoU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff h1:NwMEGwb7JJ8wPjT8OPKP5hO1Xz6AQ7Z00+GLSJfW21s=
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw=
github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# LiveKit install script for Linux
set -u
set -o errtrace
set -o errexit
set -o pipefail
REPO="livekit"
INSTALL_PATH="/usr/local/bin"
log() { printf "%b\n" "$*"; }
abort() {
printf "%s\n" "$@" >&2
exit 1
}
# returns the latest version according to GH
# i.e. 1.0.0
get_latest_version()
{
latest_version=$(curl -s https://api.github.com/repos/livekit/$REPO/releases/latest | grep -oP '"tarball_url": ".*/tarball/v\K([^/]*)(?=")')
printf "%s" "$latest_version"
}
# Ensure bash is used
if [ -z "${BASH_VERSION:-}" ]
then
abort "This script requires bash"
fi
# Check if $INSTALL_PATH exists
if [ ! -d ${INSTALL_PATH} ]
then
abort "Could not install, ${INSTALL_PATH} doesn't exist"
fi
# Needs SUDO if no permissions to write
SUDO_PREFIX=""
if [ ! -w ${INSTALL_PATH} ]
then
SUDO_PREFIX="sudo"
log "sudo is required to install to ${INSTALL_PATH}"
fi
# Check cURL is installed
if ! command -v curl >/dev/null
then
abort "cURL is required and is not found"
fi
# OS check
OS="$(uname)"
if [[ "${OS}" == "Darwin" ]]
then
abort "Installer not supported on MacOS, please install using Homebrew."
elif [[ "${OS}" != "Linux" ]]
then
abort "Installer is only supported on Linux."
fi
ARCH="$(uname -m)"
# fix arch on linux
if [[ "${ARCH}" == "aarch64" ]]
then
ARCH="arm64"
elif [[ "${ARCH}" == "x86_64" ]]
then
ARCH="amd64"
fi
VERSION=$(get_latest_version)
ARCHIVE_URL="https://github.com/livekit/$REPO/releases/download/v${VERSION}/${REPO}_${VERSION}_linux_${ARCH}.tar.gz"
# Ensure version follows SemVer
if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
then
abort "Invalid version: ${VERSION}"
fi
log "Installing ${REPO} ${VERSION}"
log "Downloading from ${ARCHIVE_URL}..."
curl -s -L "${ARCHIVE_URL}" | ${SUDO_PREFIX} tar xzf - -C "${INSTALL_PATH}" --wildcards --no-anchored "$REPO*"
log "\nlivekit-server is installed to $INSTALL_PATH\n"
+229
View File
@@ -0,0 +1,229 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build mage
// +build mage
package main
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/magefile/mage/mg"
"github.com/livekit/livekit-server/version"
"github.com/livekit/mageutil"
_ "github.com/livekit/psrpc"
)
const (
goChecksumFile = ".checksumgo"
imageName = "livekit/livekit-server"
)
// Default target to run when none is specified
// If not set, running mage will list available targets
var (
Default = Build
checksummer = mageutil.NewChecksummer(".", goChecksumFile, ".go", ".mod")
)
func init() {
checksummer.IgnoredPaths = []string{
"pkg/service/wire_gen.go",
"pkg/rtc/types/typesfakes",
}
}
// explicitly reinstall all deps
func Deps() error {
return installTools(true)
}
// builds LiveKit server
func Build() error {
mg.Deps(generateWire)
if !checksummer.IsChanged() {
fmt.Println("up to date")
return nil
}
fmt.Println("building...")
if err := os.MkdirAll("bin", 0755); err != nil {
return err
}
if err := mageutil.RunDir(context.Background(), "cmd/server", "go build -o ../../bin/livekit-server"); err != nil {
return err
}
checksummer.WriteChecksum()
return nil
}
// builds binary that runs on linux amd64
func BuildLinux() error {
mg.Deps(generateWire)
if !checksummer.IsChanged() {
fmt.Println("up to date")
return nil
}
fmt.Println("building...")
if err := os.MkdirAll("bin", 0755); err != nil {
return err
}
cmd := mageutil.CommandDir(context.Background(), "cmd/server", "go build -buildvcs=false -o ../../bin/livekit-server-amd64")
cmd.Env = []string{
"GOOS=linux",
"GOARCH=amd64",
"HOME=" + os.Getenv("HOME"),
"GOPATH=" + os.Getenv("GOPATH"),
}
if err := cmd.Run(); err != nil {
return err
}
checksummer.WriteChecksum()
return nil
}
func Deadlock() error {
ctx := context.Background()
if err := mageutil.InstallTool("golang.org/x/tools/cmd/goimports", "latest", false); err != nil {
return err
}
if err := mageutil.Run(ctx, "go get github.com/sasha-s/go-deadlock"); err != nil {
return err
}
if err := mageutil.Pipe("grep -rl sync.Mutex ./pkg", "xargs sed -i -e s/sync.Mutex/deadlock.Mutex/g"); err != nil {
return err
}
if err := mageutil.Pipe("grep -rl sync.RWMutex ./pkg", "xargs sed -i -e s/sync.RWMutex/deadlock.RWMutex/g"); err != nil {
return err
}
if err := mageutil.Pipe("grep -rl deadlock.Mutex\\|deadlock.RWMutex ./pkg", "xargs goimports -w"); err != nil {
return err
}
if err := mageutil.Run(ctx, "go mod tidy"); err != nil {
return err
}
return nil
}
func Sync() error {
if err := mageutil.Pipe("grep -rl deadlock.Mutex ./pkg", "xargs sed -i -e s/deadlock.Mutex/sync.Mutex/g"); err != nil {
return err
}
if err := mageutil.Pipe("grep -rl deadlock.RWMutex ./pkg", "xargs sed -i -e s/deadlock.RWMutex/sync.RWMutex/g"); err != nil {
return err
}
if err := mageutil.Pipe("grep -rl sync.Mutex\\|sync.RWMutex ./pkg", "xargs goimports -w"); err != nil {
return err
}
if err := mageutil.Run(context.Background(), "go mod tidy"); err != nil {
return err
}
return nil
}
// builds and publish snapshot docker image
func PublishDocker() error {
// don't publish snapshot versions as latest or minor version
if !strings.Contains(version.Version, "SNAPSHOT") {
return errors.New("Cannot publish non-snapshot versions")
}
versionImg := fmt.Sprintf("%s:v%s", imageName, version.Version)
cmd := exec.Command("docker", "buildx", "build",
"--push", "--platform", "linux/amd64,linux/arm64",
"--tag", versionImg,
".")
mageutil.ConnectStd(cmd)
if err := cmd.Run(); err != nil {
return err
}
return nil
}
// run unit tests, skipping integration
func Test() error {
mg.Deps(generateWire, setULimit)
return mageutil.Run(context.Background(), "go test -short ./... -count=1")
}
// run all tests including integration
func TestAll() error {
mg.Deps(generateWire, setULimit)
return mageutil.Run(context.Background(), "go test ./... -count=1 -timeout=4m -v")
}
// cleans up builds
func Clean() {
fmt.Println("cleaning...")
os.RemoveAll("bin")
os.Remove(goChecksumFile)
}
// regenerate code
func Generate() error {
mg.Deps(installDeps, generateWire)
fmt.Println("generating...")
return mageutil.Run(context.Background(), "go generate ./...")
}
// code generation for wiring
func generateWire() error {
mg.Deps(installDeps)
if !checksummer.IsChanged() {
return nil
}
fmt.Println("wiring...")
wire, err := mageutil.GetToolPath("wire")
if err != nil {
return err
}
cmd := exec.Command(wire)
cmd.Dir = "pkg/service"
mageutil.ConnectStd(cmd)
if err := cmd.Run(); err != nil {
return err
}
return nil
}
// implicitly install deps
func installDeps() error {
return installTools(false)
}
func installTools(force bool) error {
tools := map[string]string{
"github.com/google/wire/cmd/wire": "latest",
}
for t, v := range tools {
if err := mageutil.InstallTool(t, v, force); err != nil {
return err
}
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build mage && !windows
// +build mage,!windows
package main
import (
"syscall"
)
func setULimit() error {
// raise ulimit on unix
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return err
}
rLimit.Max = 10000
rLimit.Cur = 10000
return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build mage
// +build mage
package main
func setULimit() error {
return nil
}
+183
View File
@@ -0,0 +1,183 @@
package agent_test
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/agent/testutils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/protocol/utils/must"
"github.com/livekit/psrpc"
)
func TestAgent(t *testing.T) {
t.Run("dispatched jobs are assigned to a worker", func(t *testing.T) {
bus := psrpc.NewLocalMessageBus()
client := must.Get(rpc.NewAgentInternalClient(bus))
server := testutils.NewTestServer(bus)
t.Cleanup(server.Close)
worker := server.SimulateAgentWorker()
worker.Register("test", livekit.JobType_JT_ROOM)
jobAssignments := worker.JobAssignments.Observe()
job := &livekit.Job{
Id: guid.New(guid.AgentJobPrefix),
DispatchId: guid.New(guid.AgentDispatchPrefix),
Type: livekit.JobType_JT_ROOM,
Room: &livekit.Room{},
AgentName: "test",
}
_, err := client.JobRequest(context.Background(), "test", agent.RoomAgentTopic, job)
require.NoError(t, err)
select {
case a := <-jobAssignments.Events():
require.EqualValues(t, job.Id, a.Job.Id)
case <-time.After(time.Second):
require.Fail(t, "job assignment timeout")
}
})
}
func testBatchJobRequest(t require.TestingT, batchSize int, totalJobs int, client rpc.AgentInternalClient, workers []*testutils.AgentWorker) <-chan struct{} {
var assigned atomic.Uint32
done := make(chan struct{})
for _, w := range workers {
assignments := w.JobAssignments.Observe()
go func() {
defer assignments.Stop()
for {
select {
case <-done:
case <-assignments.Events():
if assigned.Inc() == uint32(totalJobs) {
close(done)
}
}
}
}()
}
// wait for agent registration
time.Sleep(100 * time.Millisecond)
var wg sync.WaitGroup
for i := 0; i < totalJobs; i += batchSize {
wg.Add(1)
go func(start int) {
defer wg.Done()
for j := start; j < start+batchSize && j < totalJobs; j++ {
job := &livekit.Job{
Id: guid.New(guid.AgentJobPrefix),
DispatchId: guid.New(guid.AgentDispatchPrefix),
Type: livekit.JobType_JT_ROOM,
Room: &livekit.Room{},
AgentName: "test",
}
_, err := client.JobRequest(context.Background(), "test", agent.RoomAgentTopic, job)
require.NoError(t, err)
}
}(i)
}
wg.Wait()
return done
}
func TestAgentLoadBalancing(t *testing.T) {
t.Run("jobs are distributed normally with baseline worker load", func(t *testing.T) {
totalWorkers := 5
totalJobs := 100
bus := psrpc.NewLocalMessageBus()
client := must.Get(rpc.NewAgentInternalClient(bus))
t.Cleanup(client.Close)
server := testutils.NewTestServer(bus)
t.Cleanup(server.Close)
agents := make([]*testutils.AgentWorker, totalWorkers)
for i := 0; i < totalWorkers; i++ {
agents[i] = server.SimulateAgentWorker(
testutils.WithLabel(fmt.Sprintf("agent-%d", i)),
testutils.WithJobLoad(testutils.NewStableJobLoad(0.01)),
)
agents[i].Register("test", livekit.JobType_JT_ROOM)
}
select {
case <-testBatchJobRequest(t, 10, totalJobs, client, agents):
case <-time.After(time.Second):
require.Fail(t, "job assignment timeout")
}
jobCount := make(map[string]int)
for _, w := range agents {
jobCount[w.Label] = len(w.Jobs())
}
// check that jobs are distributed normally
for i := 0; i < totalWorkers; i++ {
label := fmt.Sprintf("agent-%d", i)
require.GreaterOrEqual(t, jobCount[label], 0)
require.Less(t, jobCount[label], 35) // three std deviations from the mean is 32
}
})
t.Run("jobs are distributed with variable and overloaded worker load", func(t *testing.T) {
totalWorkers := 4
totalJobs := 15
bus := psrpc.NewLocalMessageBus()
client := must.Get(rpc.NewAgentInternalClient(bus))
t.Cleanup(client.Close)
server := testutils.NewTestServer(bus)
t.Cleanup(server.Close)
agents := make([]*testutils.AgentWorker, totalWorkers)
for i := 0; i < totalWorkers; i++ {
label := fmt.Sprintf("agent-%d", i)
if i%2 == 0 {
// make sure we have some workers that can accept jobs
agents[i] = server.SimulateAgentWorker(testutils.WithLabel(label))
} else {
agents[i] = server.SimulateAgentWorker(testutils.WithLabel(label), testutils.WithDefaultWorkerLoad(0.9))
}
agents[i].Register("test", livekit.JobType_JT_ROOM)
}
select {
case <-testBatchJobRequest(t, 1, totalJobs, client, agents):
case <-time.After(time.Second):
require.Fail(t, "job assignment timeout")
}
jobCount := make(map[string]int)
for _, w := range agents {
jobCount[w.Label] = len(w.Jobs())
}
for i := 0; i < totalWorkers; i++ {
label := fmt.Sprintf("agent-%d", i)
if i%2 == 0 {
require.GreaterOrEqual(t, jobCount[label], 2)
} else {
require.Equal(t, 0, jobCount[label])
}
require.GreaterOrEqual(t, jobCount[label], 0)
}
})
}
+336
View File
@@ -0,0 +1,336 @@
// Copyright 2024 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package agent
import (
"context"
"fmt"
"sync"
"time"
"github.com/gammazero/workerpool"
"google.golang.org/protobuf/types/known/emptypb"
serverutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
)
const (
EnabledCacheTTL = 1 * time.Minute
RoomAgentTopic = "room"
PublisherAgentTopic = "publisher"
ParticipantAgentTopic = "participant"
DefaultHandlerNamespace = ""
CheckEnabledTimeout = 5 * time.Second
)
var jobTypeTopics = map[livekit.JobType]string{
livekit.JobType_JT_ROOM: RoomAgentTopic,
livekit.JobType_JT_PUBLISHER: PublisherAgentTopic,
livekit.JobType_JT_PARTICIPANT: ParticipantAgentTopic,
}
type Client interface {
// LaunchJob starts a room or participant job on an agent.
// it will launch a job once for each worker in each namespace
LaunchJob(ctx context.Context, desc *JobRequest) *serverutils.IncrementalDispatcher[*livekit.Job]
TerminateJob(ctx context.Context, jobID string, reason rpc.JobTerminateReason) (*livekit.JobState, error)
Stop() error
}
type JobRequest struct {
DispatchId string
JobType livekit.JobType
Room *livekit.Room
// only set for participant jobs
Participant *livekit.ParticipantInfo
Metadata string
AgentName string
}
type agentClient struct {
client rpc.AgentInternalClient
mu sync.RWMutex
// cache response to avoid constantly checking with controllers
// cache is invalidated with AgentRegistered updates
roomNamespaces *serverutils.IncrementalDispatcher[string] // deprecated
publisherNamespaces *serverutils.IncrementalDispatcher[string] // deprecated
participantNamespaces *serverutils.IncrementalDispatcher[string] // deprecated
roomAgentNames *serverutils.IncrementalDispatcher[string]
publisherAgentNames *serverutils.IncrementalDispatcher[string]
participantAgentNames *serverutils.IncrementalDispatcher[string]
enabledExpiresAt time.Time
workers *workerpool.WorkerPool
invalidateSub psrpc.Subscription[*emptypb.Empty]
subDone chan struct{}
}
func NewAgentClient(bus psrpc.MessageBus) (Client, error) {
client, err := rpc.NewAgentInternalClient(bus)
if err != nil {
return nil, err
}
c := &agentClient{
client: client,
workers: workerpool.New(50),
subDone: make(chan struct{}),
}
sub, err := c.client.SubscribeWorkerRegistered(context.Background(), DefaultHandlerNamespace)
if err != nil {
return nil, err
}
c.invalidateSub = sub
go func() {
// invalidate cache
for range sub.Channel() {
c.mu.Lock()
c.roomNamespaces = nil
c.publisherNamespaces = nil
c.participantNamespaces = nil
c.roomAgentNames = nil
c.publisherAgentNames = nil
c.participantAgentNames = nil
c.mu.Unlock()
}
c.subDone <- struct{}{}
}()
return c, nil
}
func (c *agentClient) LaunchJob(ctx context.Context, desc *JobRequest) *serverutils.IncrementalDispatcher[*livekit.Job] {
var wg sync.WaitGroup
ret := serverutils.NewIncrementalDispatcher[*livekit.Job]()
defer func() {
c.workers.Submit(func() {
wg.Wait()
ret.Done()
})
}()
jobTypeTopic, ok := jobTypeTopics[desc.JobType]
if !ok {
return ret
}
dispatcher := c.getDispatcher(desc.AgentName, desc.JobType)
if dispatcher == nil {
logger.Infow("not dispatching agent job since no worker is available",
"agentName", desc.AgentName,
"jobType", desc.JobType,
"room", desc.Room.Name,
"roomID", desc.Room.Sid)
return ret
}
dispatcher.ForEach(func(curNs string) {
topic := GetAgentTopic(desc.AgentName, curNs)
wg.Add(1)
c.workers.Submit(func() {
defer wg.Done()
// The cached agent parameters do not provide the exact combination of available job type/agent name/namespace, so some of the JobRequest RPC may not trigger any worker
job := &livekit.Job{
Id: utils.NewGuid(utils.AgentJobPrefix),
DispatchId: desc.DispatchId,
Type: desc.JobType,
Room: desc.Room,
Participant: desc.Participant,
Namespace: curNs,
AgentName: desc.AgentName,
Metadata: desc.Metadata,
}
resp, err := c.client.JobRequest(context.Background(), topic, jobTypeTopic, job)
if err != nil {
logger.Infow("failed to send job request", "error", err, "namespace", curNs, "jobType", desc.JobType, "agentName", desc.AgentName)
return
}
job.State = resp.State
ret.Add(job)
})
})
return ret
}
func (c *agentClient) TerminateJob(ctx context.Context, jobID string, reason rpc.JobTerminateReason) (*livekit.JobState, error) {
resp, err := c.client.JobTerminate(context.Background(), jobID, &rpc.JobTerminateRequest{
JobId: jobID,
Reason: reason,
})
if err != nil {
logger.Infow("failed to send job request", "error", err, "jobID", jobID)
return nil, err
}
return resp.State, nil
}
func (c *agentClient) getDispatcher(agName string, jobType livekit.JobType) *serverutils.IncrementalDispatcher[string] {
c.mu.Lock()
if time.Since(c.enabledExpiresAt) > EnabledCacheTTL || c.roomNamespaces == nil ||
c.publisherNamespaces == nil || c.participantNamespaces == nil || c.roomAgentNames == nil || c.publisherAgentNames == nil || c.participantAgentNames == nil {
c.enabledExpiresAt = time.Now()
c.roomNamespaces = serverutils.NewIncrementalDispatcher[string]()
c.publisherNamespaces = serverutils.NewIncrementalDispatcher[string]()
c.participantNamespaces = serverutils.NewIncrementalDispatcher[string]()
c.roomAgentNames = serverutils.NewIncrementalDispatcher[string]()
c.publisherAgentNames = serverutils.NewIncrementalDispatcher[string]()
c.participantAgentNames = serverutils.NewIncrementalDispatcher[string]()
go c.checkEnabled(c.roomNamespaces, c.publisherNamespaces, c.participantNamespaces, c.roomAgentNames, c.publisherAgentNames, c.participantAgentNames)
}
var target *serverutils.IncrementalDispatcher[string]
var agentNames *serverutils.IncrementalDispatcher[string]
switch jobType {
case livekit.JobType_JT_ROOM:
target = c.roomNamespaces
agentNames = c.roomAgentNames
case livekit.JobType_JT_PUBLISHER:
target = c.publisherNamespaces
agentNames = c.publisherAgentNames
case livekit.JobType_JT_PARTICIPANT:
target = c.participantNamespaces
agentNames = c.participantAgentNames
}
c.mu.Unlock()
if agName == "" {
// if no agent name is given, we would need to dispatch backwards compatible mode
// which means dispatching to each of the namespaces
return target
}
done := make(chan *serverutils.IncrementalDispatcher[string], 1)
c.workers.Submit(func() {
agentNames.ForEach(func(ag string) {
if ag == agName {
select {
case done <- target:
default:
}
}
})
select {
case done <- nil:
default:
}
})
return <-done
}
func (c *agentClient) checkEnabled(roomNamespaces, publisherNamespaces, participantNamespaces, roomAgentNames, publisherAgentNames, participantAgentNames *serverutils.IncrementalDispatcher[string]) {
defer roomNamespaces.Done()
defer publisherNamespaces.Done()
defer participantNamespaces.Done()
defer roomAgentNames.Done()
defer publisherAgentNames.Done()
defer participantAgentNames.Done()
resChan, err := c.client.CheckEnabled(context.Background(), &rpc.CheckEnabledRequest{}, psrpc.WithRequestTimeout(CheckEnabledTimeout))
if err != nil {
logger.Errorw("failed to check enabled", err)
return
}
roomNSMap := make(map[string]bool)
publisherNSMap := make(map[string]bool)
participantNSMap := make(map[string]bool)
roomAgMap := make(map[string]bool)
publisherAgMap := make(map[string]bool)
participantAgMap := make(map[string]bool)
for r := range resChan {
if r.Result.GetRoomEnabled() {
for _, ns := range r.Result.GetNamespaces() {
if _, ok := roomNSMap[ns]; !ok {
roomNamespaces.Add(ns)
roomNSMap[ns] = true
}
}
for _, ag := range r.Result.GetAgentNames() {
if _, ok := roomAgMap[ag]; !ok {
roomAgentNames.Add(ag)
roomAgMap[ag] = true
}
}
}
if r.Result.GetPublisherEnabled() {
for _, ns := range r.Result.GetNamespaces() {
if _, ok := publisherNSMap[ns]; !ok {
publisherNamespaces.Add(ns)
publisherNSMap[ns] = true
}
}
for _, ag := range r.Result.GetAgentNames() {
if _, ok := publisherAgMap[ag]; !ok {
publisherAgentNames.Add(ag)
publisherAgMap[ag] = true
}
}
}
if r.Result.GetParticipantEnabled() {
for _, ns := range r.Result.GetNamespaces() {
if _, ok := participantNSMap[ns]; !ok {
participantNamespaces.Add(ns)
participantNSMap[ns] = true
}
}
for _, ag := range r.Result.GetAgentNames() {
if _, ok := participantAgMap[ag]; !ok {
participantAgentNames.Add(ag)
participantAgMap[ag] = true
}
}
}
}
}
func (c *agentClient) Stop() error {
_ = c.invalidateSub.Close()
<-c.subDone
return nil
}
func GetAgentTopic(agentName, namespace string) string {
if agentName == "" {
// Backward compatibility
return namespace
} else if namespace == "" {
// Forward compatibility once the namespace field is removed from the worker SDK
return agentName
} else {
return fmt.Sprintf("%s_%s", agentName, namespace)
}
}
@@ -0,0 +1,483 @@
package testutils
import (
"context"
"errors"
"io"
"math"
"math/rand/v2"
"sync"
"time"
"github.com/frostbyte73/core"
"github.com/gammazero/deque"
"golang.org/x/exp/maps"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/service"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils/events"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/protocol/utils/must"
"github.com/livekit/protocol/utils/options"
"github.com/livekit/psrpc"
)
type AgentService interface {
HandleConnection(context.Context, agent.SignalConn, agent.WorkerProtocolVersion)
DrainConnections(time.Duration)
}
type TestServer struct {
AgentService
}
func NewTestServer(bus psrpc.MessageBus) *TestServer {
localNode, _ := routing.NewLocalNode(nil)
return NewTestServerWithService(must.Get(service.NewAgentService(
&config.Config{Region: "test"},
localNode,
bus,
auth.NewSimpleKeyProvider("test", "verysecretsecret"),
)))
}
func NewTestServerWithService(s AgentService) *TestServer {
return &TestServer{s}
}
type SimulatedWorkerOptions struct {
Context context.Context
Label string
SupportResume bool
DefaultJobLoad float32
JobLoadThreshold float32
DefaultWorkerLoad float32
HandleAvailability func(AgentJobRequest)
HandleAssignment func(*livekit.Job) JobLoad
}
type SimulatedWorkerOption func(*SimulatedWorkerOptions)
func WithContext(ctx context.Context) SimulatedWorkerOption {
return func(o *SimulatedWorkerOptions) {
o.Context = ctx
}
}
func WithLabel(label string) SimulatedWorkerOption {
return func(o *SimulatedWorkerOptions) {
o.Label = label
}
}
func WithJobAvailabilityHandler(h func(AgentJobRequest)) SimulatedWorkerOption {
return func(o *SimulatedWorkerOptions) {
o.HandleAvailability = h
}
}
func WithJobAssignmentHandler(h func(*livekit.Job) JobLoad) SimulatedWorkerOption {
return func(o *SimulatedWorkerOptions) {
o.HandleAssignment = h
}
}
func WithJobLoad(l JobLoad) SimulatedWorkerOption {
return WithJobAssignmentHandler(func(j *livekit.Job) JobLoad { return l })
}
func WithDefaultWorkerLoad(load float32) SimulatedWorkerOption {
return func(o *SimulatedWorkerOptions) {
o.DefaultWorkerLoad = load
}
}
func (h *TestServer) SimulateAgentWorker(opts ...SimulatedWorkerOption) *AgentWorker {
o := &SimulatedWorkerOptions{
Context: context.Background(),
Label: guid.New("TEST_AGENT_"),
DefaultJobLoad: 0.1,
JobLoadThreshold: 0.8,
DefaultWorkerLoad: 0.0,
HandleAvailability: func(r AgentJobRequest) { r.Accept() },
HandleAssignment: func(j *livekit.Job) JobLoad { return nil },
}
options.Apply(o, opts)
w := &AgentWorker{
workerMessages: make(chan *livekit.WorkerMessage, 1),
jobs: map[string]*AgentJob{},
SimulatedWorkerOptions: o,
RegisterWorkerResponses: events.NewObserverList[*livekit.RegisterWorkerResponse](),
AvailabilityRequests: events.NewObserverList[*livekit.AvailabilityRequest](),
JobAssignments: events.NewObserverList[*livekit.JobAssignment](),
JobTerminations: events.NewObserverList[*livekit.JobTermination](),
WorkerPongs: events.NewObserverList[*livekit.WorkerPong](),
}
w.ctx, w.cancel = context.WithCancel(context.Background())
if o.DefaultWorkerLoad > 0.0 {
w.sendStatus()
}
ctx := service.WithAPIKey(o.Context, &auth.ClaimGrants{}, "test")
go h.HandleConnection(ctx, w, agent.CurrentProtocol)
return w
}
func (h *TestServer) Close() {
h.DrainConnections(1)
}
var _ agent.SignalConn = (*AgentWorker)(nil)
type JobLoad interface {
Load() float32
}
type AgentJob struct {
*livekit.Job
JobLoad
}
type AgentJobRequest struct {
w *AgentWorker
*livekit.AvailabilityRequest
}
func (r AgentJobRequest) Accept() {
identity := guid.New("PI_")
r.w.SendAvailability(&livekit.AvailabilityResponse{
JobId: r.Job.Id,
Available: true,
SupportsResume: r.w.SupportResume,
ParticipantName: identity,
ParticipantIdentity: identity,
})
}
func (r AgentJobRequest) Reject() {
r.w.SendAvailability(&livekit.AvailabilityResponse{
JobId: r.Job.Id,
Available: false,
})
}
type AgentWorker struct {
*SimulatedWorkerOptions
fuse core.Fuse
mu sync.Mutex
ctx context.Context
cancel context.CancelFunc
workerMessages chan *livekit.WorkerMessage
serverMessages deque.Deque[*livekit.ServerMessage]
jobs map[string]*AgentJob
RegisterWorkerResponses *events.ObserverList[*livekit.RegisterWorkerResponse]
AvailabilityRequests *events.ObserverList[*livekit.AvailabilityRequest]
JobAssignments *events.ObserverList[*livekit.JobAssignment]
JobTerminations *events.ObserverList[*livekit.JobTermination]
WorkerPongs *events.ObserverList[*livekit.WorkerPong]
}
func (w *AgentWorker) statusWorker() {
t := time.NewTicker(2 * time.Second)
defer t.Stop()
for !w.fuse.IsBroken() {
w.sendStatus()
<-t.C
}
}
func (w *AgentWorker) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
w.fuse.Break()
return nil
}
func (w *AgentWorker) SetReadDeadline(t time.Time) error {
w.mu.Lock()
defer w.mu.Unlock()
if !w.fuse.IsBroken() {
cancel := w.cancel
if t.IsZero() {
w.ctx, w.cancel = context.WithCancel(context.Background())
} else {
w.ctx, w.cancel = context.WithDeadline(context.Background(), t)
}
cancel()
}
return nil
}
func (w *AgentWorker) ReadWorkerMessage() (*livekit.WorkerMessage, int, error) {
for {
w.mu.Lock()
ctx := w.ctx
w.mu.Unlock()
select {
case <-w.fuse.Watch():
return nil, 0, io.EOF
case <-ctx.Done():
if err := ctx.Err(); errors.Is(err, context.DeadlineExceeded) {
return nil, 0, err
}
case m := <-w.workerMessages:
return m, 0, nil
}
}
}
func (w *AgentWorker) WriteServerMessage(m *livekit.ServerMessage) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
w.serverMessages.PushBack(m)
if w.serverMessages.Len() == 1 {
go w.handleServerMessages()
}
return 0, nil
}
func (w *AgentWorker) handleServerMessages() {
w.mu.Lock()
for w.serverMessages.Len() != 0 {
m := w.serverMessages.Front()
w.mu.Unlock()
switch m := m.Message.(type) {
case *livekit.ServerMessage_Register:
w.handleRegister(m.Register)
case *livekit.ServerMessage_Availability:
w.handleAvailability(m.Availability)
case *livekit.ServerMessage_Assignment:
w.handleAssignment(m.Assignment)
case *livekit.ServerMessage_Termination:
w.handleTermination(m.Termination)
case *livekit.ServerMessage_Pong:
w.handlePong(m.Pong)
}
w.mu.Lock()
w.serverMessages.PopFront()
}
w.mu.Unlock()
}
func (w *AgentWorker) handleRegister(m *livekit.RegisterWorkerResponse) {
w.RegisterWorkerResponses.Emit(m)
}
func (w *AgentWorker) handleAvailability(m *livekit.AvailabilityRequest) {
w.AvailabilityRequests.Emit(m)
if w.HandleAvailability != nil {
w.HandleAvailability(AgentJobRequest{w, m})
} else {
AgentJobRequest{w, m}.Accept()
}
}
func (w *AgentWorker) handleAssignment(m *livekit.JobAssignment) {
w.JobAssignments.Emit(m)
var load JobLoad
if w.HandleAssignment != nil {
load = w.HandleAssignment(m.Job)
}
if load == nil {
load = NewStableJobLoad(w.DefaultJobLoad)
}
w.mu.Lock()
defer w.mu.Unlock()
w.jobs[m.Job.Id] = &AgentJob{m.Job, load}
}
func (w *AgentWorker) handleTermination(m *livekit.JobTermination) {
w.JobTerminations.Emit(m)
w.mu.Lock()
defer w.mu.Unlock()
delete(w.jobs, m.JobId)
}
func (w *AgentWorker) handlePong(m *livekit.WorkerPong) {
w.WorkerPongs.Emit(m)
}
func (w *AgentWorker) sendMessage(m *livekit.WorkerMessage) {
select {
case <-w.fuse.Watch():
case w.workerMessages <- m:
}
}
func (w *AgentWorker) SendRegister(m *livekit.RegisterWorkerRequest) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Register{
Register: m,
}})
}
func (w *AgentWorker) SendAvailability(m *livekit.AvailabilityResponse) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Availability{
Availability: m,
}})
}
func (w *AgentWorker) SendUpdateWorker(m *livekit.UpdateWorkerStatus) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateWorker{
UpdateWorker: m,
}})
}
func (w *AgentWorker) SendUpdateJob(m *livekit.UpdateJobStatus) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateJob{
UpdateJob: m,
}})
}
func (w *AgentWorker) SendPing(m *livekit.WorkerPing) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Ping{
Ping: m,
}})
}
func (w *AgentWorker) SendSimulateJob(m *livekit.SimulateJobRequest) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_SimulateJob{
SimulateJob: m,
}})
}
func (w *AgentWorker) SendMigrateJob(m *livekit.MigrateJobRequest) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_MigrateJob{
MigrateJob: m,
}})
}
func (w *AgentWorker) sendStatus() {
w.mu.Lock()
var load float32
jobCount := len(w.jobs)
if len(w.jobs) == 0 {
load = w.DefaultWorkerLoad
} else {
for _, j := range w.jobs {
load += j.Load()
}
}
w.mu.Unlock()
status := livekit.WorkerStatus_WS_AVAILABLE
if load > w.JobLoadThreshold {
status = livekit.WorkerStatus_WS_FULL
}
w.SendUpdateWorker(&livekit.UpdateWorkerStatus{
Status: &status,
Load: load,
JobCount: uint32(jobCount),
})
}
func (w *AgentWorker) Register(agentName string, jobType livekit.JobType) {
w.SendRegister(&livekit.RegisterWorkerRequest{
Type: jobType,
AgentName: agentName,
})
go w.statusWorker()
}
func (w *AgentWorker) SimulateRoomJob(roomName string) {
w.SendSimulateJob(&livekit.SimulateJobRequest{
Type: livekit.JobType_JT_ROOM,
Room: &livekit.Room{
Sid: guid.New(guid.RoomPrefix),
Name: roomName,
},
})
}
func (w *AgentWorker) Jobs() []*AgentJob {
w.mu.Lock()
defer w.mu.Unlock()
return maps.Values(w.jobs)
}
type stableJobLoad struct {
load float32
}
func NewStableJobLoad(load float32) JobLoad {
return stableJobLoad{load}
}
func (s stableJobLoad) Load() float32 {
return s.load
}
type periodicJobLoad struct {
amplitude float64
period time.Duration
epoch time.Time
}
func NewPeriodicJobLoad(max float32, period time.Duration) JobLoad {
return periodicJobLoad{
amplitude: float64(max / 2),
period: period,
epoch: time.Now().Add(-time.Duration(rand.Int64N(int64(period)))),
}
}
func (s periodicJobLoad) Load() float32 {
a := math.Sin(time.Since(s.epoch).Seconds() / s.period.Seconds() * math.Pi * 2)
return float32(s.amplitude + a*s.amplitude)
}
type uniformRandomJobLoad struct {
min, max float32
rng func() float64
}
func NewUniformRandomJobLoad(min, max float32) JobLoad {
return uniformRandomJobLoad{min, max, rand.Float64}
}
func NewUniformRandomJobLoadWithRNG(min, max float32, rng *rand.Rand) JobLoad {
return uniformRandomJobLoad{min, max, rng.Float64}
}
func (s uniformRandomJobLoad) Load() float32 {
return rand.Float32()*(s.max-s.min) + s.min
}
type normalRandomJobLoad struct {
mean, stddev float64
rng func() float64
}
func NewNormalRandomJobLoad(mean, stddev float64) JobLoad {
return normalRandomJobLoad{mean, stddev, rand.Float64}
}
func NewNormalRandomJobLoadWithRNG(mean, stddev float64, rng *rand.Rand) JobLoad {
return normalRandomJobLoad{mean, stddev, rng.Float64}
}
func (s normalRandomJobLoad) Load() float32 {
u := 1 - s.rng()
v := s.rng()
z := math.Sqrt(-2*math.Log(u)) * math.Cos(2*math.Pi*v)
return float32(max(0, z*s.stddev+s.mean))
}
+559
View File
@@ -0,0 +1,559 @@
// Copyright 2024 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package agent
import (
"context"
"errors"
"fmt"
"sync"
"time"
"google.golang.org/protobuf/proto"
pagent "github.com/livekit/protocol/agent"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
)
var (
ErrUnimplementedWrorkerSignal = errors.New("unimplemented worker signal")
ErrUnknownWorkerSignal = errors.New("unknown worker signal")
ErrUnknownJobType = errors.New("unknown job type")
ErrJobNotFound = psrpc.NewErrorf(psrpc.NotFound, "no running job for given jobID")
ErrWorkerClosed = errors.New("worker closed")
ErrWorkerNotAvailable = errors.New("worker not available")
ErrAvailabilityTimeout = errors.New("agent worker availability timeout")
ErrDuplicateJobAssignment = errors.New("duplicate job assignment")
)
type WorkerProtocolVersion int
const CurrentProtocol = 1
const (
RegisterTimeout = 10 * time.Second
AssignJobTimeout = 10 * time.Second
)
type SignalConn interface {
WriteServerMessage(msg *livekit.ServerMessage) (int, error)
ReadWorkerMessage() (*livekit.WorkerMessage, int, error)
SetReadDeadline(time.Time) error
Close() error
}
func JobStatusIsEnded(s livekit.JobStatus) bool {
return s == livekit.JobStatus_JS_SUCCESS || s == livekit.JobStatus_JS_FAILED
}
type WorkerSignalHandler interface {
HandleRegister(*livekit.RegisterWorkerRequest) error
HandleAvailability(*livekit.AvailabilityResponse) error
HandleUpdateJob(*livekit.UpdateJobStatus) error
HandleSimulateJob(*livekit.SimulateJobRequest) error
HandlePing(*livekit.WorkerPing) error
HandleUpdateWorker(*livekit.UpdateWorkerStatus) error
HandleMigrateJob(*livekit.MigrateJobRequest) error
}
func DispatchWorkerSignal(req *livekit.WorkerMessage, h WorkerSignalHandler) error {
switch m := req.Message.(type) {
case *livekit.WorkerMessage_Register:
return h.HandleRegister(m.Register)
case *livekit.WorkerMessage_Availability:
return h.HandleAvailability(m.Availability)
case *livekit.WorkerMessage_UpdateJob:
return h.HandleUpdateJob(m.UpdateJob)
case *livekit.WorkerMessage_SimulateJob:
return h.HandleSimulateJob(m.SimulateJob)
case *livekit.WorkerMessage_Ping:
return h.HandlePing(m.Ping)
case *livekit.WorkerMessage_UpdateWorker:
return h.HandleUpdateWorker(m.UpdateWorker)
case *livekit.WorkerMessage_MigrateJob:
return h.HandleMigrateJob(m.MigrateJob)
default:
return ErrUnknownWorkerSignal
}
}
var _ WorkerSignalHandler = (*UnimplementedWorkerSignalHandler)(nil)
type UnimplementedWorkerSignalHandler struct{}
func (UnimplementedWorkerSignalHandler) HandleRegister(*livekit.RegisterWorkerRequest) error {
return fmt.Errorf("%w: Register", ErrUnimplementedWrorkerSignal)
}
func (UnimplementedWorkerSignalHandler) HandleAvailability(*livekit.AvailabilityResponse) error {
return fmt.Errorf("%w: Availability", ErrUnimplementedWrorkerSignal)
}
func (UnimplementedWorkerSignalHandler) HandleUpdateJob(*livekit.UpdateJobStatus) error {
return fmt.Errorf("%w: UpdateJob", ErrUnimplementedWrorkerSignal)
}
func (UnimplementedWorkerSignalHandler) HandleSimulateJob(*livekit.SimulateJobRequest) error {
return fmt.Errorf("%w: SimulateJob", ErrUnimplementedWrorkerSignal)
}
func (UnimplementedWorkerSignalHandler) HandlePing(*livekit.WorkerPing) error {
return fmt.Errorf("%w: Ping", ErrUnimplementedWrorkerSignal)
}
func (UnimplementedWorkerSignalHandler) HandleUpdateWorker(*livekit.UpdateWorkerStatus) error {
return fmt.Errorf("%w: UpdateWorker", ErrUnimplementedWrorkerSignal)
}
func (UnimplementedWorkerSignalHandler) HandleMigrateJob(*livekit.MigrateJobRequest) error {
return fmt.Errorf("%w: MigrateJob", ErrUnimplementedWrorkerSignal)
}
type WorkerPingHandler struct {
UnimplementedWorkerSignalHandler
conn SignalConn
}
func (h WorkerPingHandler) HandlePing(ping *livekit.WorkerPing) error {
_, err := h.conn.WriteServerMessage(&livekit.ServerMessage{
Message: &livekit.ServerMessage_Pong{
Pong: &livekit.WorkerPong{
LastTimestamp: ping.Timestamp,
Timestamp: time.Now().UnixMilli(),
},
},
})
return err
}
type WorkerRegistration struct {
Protocol WorkerProtocolVersion
ID string
Version string
AgentName string
Namespace string
JobType livekit.JobType
Permissions *livekit.ParticipantPermission
}
var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
type WorkerRegisterer struct {
WorkerPingHandler
serverInfo *livekit.ServerInfo
protocol WorkerProtocolVersion
deadline time.Time
registration WorkerRegistration
registered bool
}
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, protocol WorkerProtocolVersion) *WorkerRegisterer {
return &WorkerRegisterer{
WorkerPingHandler: WorkerPingHandler{conn: conn},
serverInfo: serverInfo,
protocol: protocol,
deadline: time.Now().Add(RegisterTimeout),
}
}
func (h *WorkerRegisterer) Deadline() time.Time {
return h.deadline
}
func (h *WorkerRegisterer) Registration() WorkerRegistration {
return h.registration
}
func (h *WorkerRegisterer) Registered() bool {
return h.registered
}
func (h *WorkerRegisterer) HandleRegister(req *livekit.RegisterWorkerRequest) error {
if !livekit.IsJobType(req.GetType()) {
return ErrUnknownJobType
}
permissions := req.AllowedPermissions
if permissions == nil {
permissions = &livekit.ParticipantPermission{
CanSubscribe: true,
CanPublish: true,
CanPublishData: true,
CanUpdateMetadata: true,
}
}
h.registration = WorkerRegistration{
Protocol: h.protocol,
ID: guid.New(guid.AgentWorkerPrefix),
Version: req.Version,
AgentName: req.AgentName,
Namespace: req.GetNamespace(),
JobType: req.GetType(),
Permissions: permissions,
}
h.registered = true
_, err := h.conn.WriteServerMessage(&livekit.ServerMessage{
Message: &livekit.ServerMessage_Register{
Register: &livekit.RegisterWorkerResponse{
WorkerId: h.registration.ID,
ServerInfo: h.serverInfo,
},
},
})
return err
}
var _ WorkerSignalHandler = (*Worker)(nil)
type Worker struct {
WorkerPingHandler
WorkerRegistration
apiKey string
apiSecret string
logger logger.Logger
ctx context.Context
cancel context.CancelFunc
closed chan struct{}
mu sync.Mutex
load float32
status livekit.WorkerStatus
runningJobs map[livekit.JobID]*livekit.Job
availability map[livekit.JobID]chan *livekit.AvailabilityResponse
}
func NewWorker(
registration WorkerRegistration,
apiKey string,
apiSecret string,
conn SignalConn,
logger logger.Logger,
) *Worker {
ctx, cancel := context.WithCancel(context.Background())
return &Worker{
WorkerPingHandler: WorkerPingHandler{conn: conn},
WorkerRegistration: registration,
apiKey: apiKey,
apiSecret: apiSecret,
logger: logger.WithValues(
"workerID", registration.ID,
"agentName", registration.AgentName,
"jobType", registration.JobType.String(),
),
ctx: ctx,
cancel: cancel,
closed: make(chan struct{}),
runningJobs: make(map[livekit.JobID]*livekit.Job),
availability: make(map[livekit.JobID]chan *livekit.AvailabilityResponse),
}
}
func (w *Worker) sendRequest(req *livekit.ServerMessage) {
if _, err := w.conn.WriteServerMessage(req); err != nil {
w.logger.Warnw("error writing to websocket", err)
}
}
func (w *Worker) Status() livekit.WorkerStatus {
w.mu.Lock()
defer w.mu.Unlock()
return w.status
}
func (w *Worker) Load() float32 {
w.mu.Lock()
defer w.mu.Unlock()
return w.load
}
func (w *Worker) Logger() logger.Logger {
return w.logger
}
func (w *Worker) RunningJobs() map[livekit.JobID]*livekit.Job {
w.mu.Lock()
defer w.mu.Unlock()
jobs := make(map[livekit.JobID]*livekit.Job, len(w.runningJobs))
for k, v := range w.runningJobs {
jobs[k] = v
}
return jobs
}
func (w *Worker) RunningJobCount() int {
w.mu.Lock()
defer w.mu.Unlock()
return len(w.runningJobs)
}
func (w *Worker) GetJobState(jobID livekit.JobID) (*livekit.JobState, error) {
w.mu.Lock()
defer w.mu.Unlock()
j, ok := w.runningJobs[jobID]
if !ok {
return nil, ErrJobNotFound
}
return utils.CloneProto(j.State), nil
}
func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobState, error) {
availCh := make(chan *livekit.AvailabilityResponse, 1)
job = utils.CloneProto(job)
jobID := livekit.JobID(job.Id)
w.mu.Lock()
if _, ok := w.availability[jobID]; ok {
w.mu.Unlock()
return nil, ErrDuplicateJobAssignment
}
w.availability[jobID] = availCh
w.mu.Unlock()
defer func() {
w.mu.Lock()
delete(w.availability, jobID)
w.mu.Unlock()
}()
if job.State == nil {
job.State = &livekit.JobState{}
}
now := time.Now()
job.State.UpdatedAt = now.UnixNano()
job.State.StartedAt = now.UnixNano()
job.State.Status = livekit.JobStatus_JS_RUNNING
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Availability{
Availability: &livekit.AvailabilityRequest{Job: job},
}})
timeout := time.NewTimer(AssignJobTimeout)
defer timeout.Stop()
// See handleAvailability for the response
select {
case res := <-availCh:
if !res.Available {
return nil, ErrWorkerNotAvailable
}
job.State.ParticipantIdentity = res.ParticipantIdentity
token, err := pagent.BuildAgentToken(
w.apiKey,
w.apiSecret,
job.Room.Name,
res.ParticipantIdentity,
res.ParticipantName,
res.ParticipantMetadata,
res.ParticipantAttributes,
w.Permissions,
)
if err != nil {
w.logger.Errorw("failed to build agent token", err)
return nil, err
}
// In OSS, Url is nil, and the used API Key is the same as the one used to connect the worker
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Assignment{
Assignment: &livekit.JobAssignment{Job: job, Url: nil, Token: token},
}})
state := utils.CloneProto(job.State)
w.mu.Lock()
w.runningJobs[jobID] = job
w.mu.Unlock()
// TODO sweep jobs that are never started. We can't do this until all SDKs actually update the the JOB state
return state, nil
case <-timeout.C:
return nil, ErrAvailabilityTimeout
case <-w.ctx.Done():
return nil, ErrWorkerClosed
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (w *Worker) TerminateJob(jobID livekit.JobID, reason rpc.JobTerminateReason) (*livekit.JobState, error) {
w.mu.Lock()
_, ok := w.runningJobs[jobID]
w.mu.Unlock()
if !ok {
return nil, ErrJobNotFound
}
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Termination{
Termination: &livekit.JobTermination{
JobId: string(jobID),
},
}})
status := livekit.JobStatus_JS_SUCCESS
errorStr := ""
if reason == rpc.JobTerminateReason_AGENT_LEFT_ROOM {
status = livekit.JobStatus_JS_FAILED
errorStr = "agent worker left the room"
}
return w.UpdateJobStatus(&livekit.UpdateJobStatus{
JobId: string(jobID),
Status: status,
Error: errorStr,
})
}
func (w *Worker) UpdateMetadata(metadata string) {
w.logger.Debugw("worker metadata updated", nil, "metadata", metadata)
}
func (w *Worker) IsClosed() bool {
select {
case <-w.closed:
return true
default:
return false
}
}
func (w *Worker) Close() {
w.mu.Lock()
if w.IsClosed() {
w.mu.Unlock()
return
}
w.logger.Infow("closing worker", "workerID", w.ID, "jobType", w.JobType, "agentName", w.AgentName)
close(w.closed)
w.cancel()
_ = w.conn.Close()
w.mu.Unlock()
}
func (w *Worker) HandleAvailability(res *livekit.AvailabilityResponse) error {
w.mu.Lock()
defer w.mu.Unlock()
jobID := livekit.JobID(res.JobId)
availCh, ok := w.availability[jobID]
if !ok {
w.logger.Warnw("received availability response for unknown job", nil, "jobID", jobID)
return nil
}
availCh <- res
delete(w.availability, jobID)
return nil
}
func (w *Worker) HandleUpdateJob(update *livekit.UpdateJobStatus) error {
_, err := w.UpdateJobStatus(update)
if err != nil {
// treating this as a debug message only
// this can happen if the Room closes first, which would delete the agent dispatch
// that would mark the job as successful. subsequent updates from the same worker
// would not be able to find the same jobID.
w.logger.Debugw("received job update for unknown job", "jobID", update.JobId)
}
return nil
}
func (w *Worker) UpdateJobStatus(update *livekit.UpdateJobStatus) (*livekit.JobState, error) {
w.mu.Lock()
defer w.mu.Unlock()
jobID := livekit.JobID(update.JobId)
job, ok := w.runningJobs[jobID]
if !ok {
return nil, psrpc.NewErrorf(psrpc.NotFound, "received job update for unknown job")
}
now := time.Now()
job.State.UpdatedAt = now.UnixNano()
if job.State.Status == livekit.JobStatus_JS_PENDING && update.Status != livekit.JobStatus_JS_PENDING {
job.State.StartedAt = now.UnixNano()
}
job.State.Status = update.Status
job.State.Error = update.Error
if JobStatusIsEnded(update.Status) {
job.State.EndedAt = now.UnixNano()
delete(w.runningJobs, jobID)
w.logger.Infow("job ended", "jobID", update.JobId, "status", update.Status, "error", update.Error)
}
return proto.Clone(job.State).(*livekit.JobState), nil
}
func (w *Worker) HandleSimulateJob(simulate *livekit.SimulateJobRequest) error {
jobType := livekit.JobType_JT_ROOM
if simulate.Participant != nil {
jobType = livekit.JobType_JT_PUBLISHER
}
job := &livekit.Job{
Id: guid.New(guid.AgentJobPrefix),
Type: jobType,
Room: simulate.Room,
Participant: simulate.Participant,
Namespace: w.Namespace,
AgentName: w.AgentName,
}
go func() {
_, err := w.AssignJob(w.ctx, job)
if err != nil {
w.logger.Errorw("unable to simulate job", err, "jobID", job.Id)
}
}()
return nil
}
func (w *Worker) HandleUpdateWorker(update *livekit.UpdateWorkerStatus) error {
w.mu.Lock()
defer w.mu.Unlock()
if status := update.Status; status != nil && w.status != *status {
w.status = *status
w.Logger().Debugw("worker status changed", "status", w.status)
}
w.load = update.GetLoad()
return nil
}
func (w *Worker) HandleMigrateJob(req *livekit.MigrateJobRequest) error {
// TODO(theomonnom): On OSS this is not implemented
// We could maybe just move a specific job to another worker
return nil
}
@@ -0,0 +1,46 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientconfiguration
import (
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/livekit"
)
// StaticConfigurations list specific device-side limitations that should be disabled at a global level
var StaticConfigurations = []ConfigurationItem{
// {
// Match: &ScriptMatch{Expr: `c.protocol <= 5 || c.browser == "firefox"`},
// Configuration: &livekit.ClientConfiguration{ResumeConnection: livekit.ClientConfigSetting_DISABLED},
// Merge: false,
// },
{
Match: &ScriptMatch{Expr: `c.browser == "safari"`},
Configuration: &livekit.ClientConfiguration{DisabledCodecs: &livekit.DisabledCodecs{Codecs: []*livekit.Codec{
{Mime: mime.MimeTypeAV1.String()},
}}},
Merge: false,
},
{
Match: &ScriptMatch{Expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android") ||
((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`},
Configuration: &livekit.ClientConfiguration{
DisabledCodecs: &livekit.DisabledCodecs{
Publish: []*livekit.Codec{{Mime: mime.MimeTypeH264.String()}},
},
},
Merge: false,
},
}
@@ -0,0 +1,117 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientconfiguration
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
)
func TestScriptMatchConfiguration(t *testing.T) {
t.Run("no merge", func(t *testing.T) {
confs := []ConfigurationItem{
{
Match: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
Configuration: &livekit.ClientConfiguration{
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
},
},
}
cm := NewStaticClientConfigurationManager(confs)
conf := cm.GetConfiguration(&livekit.ClientInfo{Protocol: 4})
require.Nil(t, conf)
conf = cm.GetConfiguration(&livekit.ClientInfo{Protocol: 6, Browser: "firefox"})
require.Nil(t, conf)
conf = cm.GetConfiguration(&livekit.ClientInfo{Protocol: 6, Browser: "chrome"})
require.Equal(t, conf.ResumeConnection, livekit.ClientConfigSetting_ENABLED)
})
t.Run("merge", func(t *testing.T) {
confs := []ConfigurationItem{
{
Match: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
Configuration: &livekit.ClientConfiguration{
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
},
Merge: true,
},
{
Match: &ScriptMatch{Expr: `c.sdk == "android"`},
Configuration: &livekit.ClientConfiguration{
Video: &livekit.VideoConfiguration{
HardwareEncoder: livekit.ClientConfigSetting_DISABLED,
},
},
Merge: true,
},
}
cm := NewStaticClientConfigurationManager(confs)
conf := cm.GetConfiguration(&livekit.ClientInfo{Protocol: 4})
require.Nil(t, conf)
conf = cm.GetConfiguration(&livekit.ClientInfo{Protocol: 6, Browser: "firefox"})
require.Nil(t, conf)
conf = cm.GetConfiguration(&livekit.ClientInfo{Protocol: 6, Browser: "chrome", Sdk: 3})
require.Equal(t, conf.ResumeConnection, livekit.ClientConfigSetting_ENABLED)
require.Equal(t, conf.Video.HardwareEncoder, livekit.ClientConfigSetting_DISABLED)
})
}
func TestScriptMatch(t *testing.T) {
client := &livekit.ClientInfo{
Protocol: 6,
Browser: "chrome",
Sdk: 3, // android
DeviceModel: "12345",
}
type testcase struct {
name string
expr string
result bool
err bool
}
cases := []testcase{
{name: "simple match", expr: `c.protocol > 5`, result: true},
{name: "invalid expr", expr: `cc.protocol > 5`, err: true},
{name: "unexist field", expr: `c.protocols > 5`, err: true},
{name: "combined condition", expr: `c.protocol > 5 && (c.sdk=="android" || c.sdk=="ios")`, result: true},
{name: "combined condition2", expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android) || ((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`, result: false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
match := &ScriptMatch{Expr: c.expr}
m, err := match.Match(client)
if c.err {
require.Error(t, err)
} else {
require.Equal(t, c.result, m)
}
})
}
}
@@ -0,0 +1,92 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientconfiguration
import (
"context"
"errors"
"strings"
"github.com/d5/tengo/v2"
"github.com/livekit/protocol/livekit"
)
type Match interface {
Match(clientInfo *livekit.ClientInfo) (bool, error)
}
type ScriptMatch struct {
Expr string
}
// use result of eval script expression for match.
// expression examples:
// protocol bigger than 5 : c.protocol > 5
// browser if firefox: c.browser == "firefox"
// combined rule : c.protocol > 5 && c.browser == "firefox"
func (m *ScriptMatch) Match(clientInfo *livekit.ClientInfo) (bool, error) {
res, err := tengo.Eval(context.TODO(), m.Expr, map[string]interface{}{"c": &clientObject{info: clientInfo}})
if err != nil {
return false, err
}
if val, ok := res.(bool); ok {
return val, nil
}
return false, errors.New("invalid match expression result")
}
type clientObject struct {
tengo.ObjectImpl
info *livekit.ClientInfo
}
func (c *clientObject) TypeName() string {
return "clientObject"
}
func (c *clientObject) String() string {
return c.info.String()
}
func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error) {
field, ok := index.(*tengo.String)
if !ok {
return nil, tengo.ErrInvalidIndexType
}
switch field.Value {
case "sdk":
return &tengo.String{Value: strings.ToLower(c.info.Sdk.String())}, nil
case "version":
return &tengo.String{Value: c.info.Version}, nil
case "protocol":
return &tengo.Int{Value: int64(c.info.Protocol)}, nil
case "os":
return &tengo.String{Value: strings.ToLower(c.info.Os)}, nil
case "os_version":
return &tengo.String{Value: c.info.OsVersion}, nil
case "device_model":
return &tengo.String{Value: strings.ToLower(c.info.DeviceModel)}, nil
case "browser":
return &tengo.String{Value: strings.ToLower(c.info.Browser)}, nil
case "browser_version":
return &tengo.String{Value: c.info.BrowserVersion}, nil
case "address":
return &tengo.String{Value: c.info.Address}, nil
}
return &tengo.Undefined{}, nil
}
@@ -0,0 +1,71 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientconfiguration
import (
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
protoutils "github.com/livekit/protocol/utils"
)
type ConfigurationItem struct {
Match
Configuration *livekit.ClientConfiguration
Merge bool
}
type StaticClientConfigurationManager struct {
confs []ConfigurationItem
}
func NewStaticClientConfigurationManager(confs []ConfigurationItem) *StaticClientConfigurationManager {
return &StaticClientConfigurationManager{confs: confs}
}
func (s *StaticClientConfigurationManager) GetConfiguration(clientInfo *livekit.ClientInfo) *livekit.ClientConfiguration {
var matchedConf []*livekit.ClientConfiguration
for _, c := range s.confs {
matched, err := c.Match.Match(clientInfo)
if err != nil {
logger.Errorw("matchrule failed", err,
"clientInfo", logger.Proto(utils.ClientInfoWithoutAddress(clientInfo)),
)
continue
}
if !matched {
continue
}
if !c.Merge {
return c.Configuration
}
matchedConf = append(matchedConf, c.Configuration)
}
var conf *livekit.ClientConfiguration
for k, v := range matchedConf {
if k == 0 {
conf = protoutils.CloneProto(matchedConf[0])
} else {
// TODO : there is a problem use protobuf merge, we don't have flag to indicate 'no value',
// don't override default behavior or other configuration's field. So a bool value = false or
// a int value = 0 will override same field in other configuration
proto.Merge(conf, v)
}
}
return conf
}
@@ -0,0 +1,23 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clientconfiguration
import (
"github.com/livekit/protocol/livekit"
)
type ClientConfigurationManager interface {
GetConfiguration(clientInfo *livekit.ClientInfo) *livekit.ClientConfiguration
}
+761
View File
@@ -0,0 +1,761 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"os"
"reflect"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"github.com/livekit/livekit-server/pkg/metric"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe"
"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
redisLiveKit "github.com/livekit/protocol/redis"
"github.com/livekit/protocol/rpc"
)
const (
generatedCLIFlagUsage = "generated"
)
var (
ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0")
ErrKeysNotSet = errors.New("one of key-file or keys must be provided")
)
type Config struct {
Port uint32 `yaml:"port,omitempty"`
BindAddresses []string `yaml:"bind_addresses,omitempty"`
// PrometheusPort is deprecated
PrometheusPort uint32 `yaml:"prometheus_port,omitempty"`
Prometheus PrometheusConfig `yaml:"prometheus,omitempty"`
RTC RTCConfig `yaml:"rtc,omitempty"`
Redis redisLiveKit.RedisConfig `yaml:"redis,omitempty"`
Audio sfu.AudioConfig `yaml:"audio,omitempty"`
Video VideoConfig `yaml:"video,omitempty"`
Room RoomConfig `yaml:"room,omitempty"`
TURN TURNConfig `yaml:"turn,omitempty"`
Ingress IngressConfig `yaml:"ingress,omitempty"`
SIP SIPConfig `yaml:"sip,omitempty"`
WebHook WebHookConfig `yaml:"webhook,omitempty"`
NodeSelector NodeSelectorConfig `yaml:"node_selector,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
Keys map[string]string `yaml:"keys,omitempty"`
Region string `yaml:"region,omitempty"`
SignalRelay SignalRelayConfig `yaml:"signal_relay,omitempty"`
PSRPC rpc.PSRPCConfig `yaml:"psrpc,omitempty"`
// Deprecated: LogLevel is deprecated
LogLevel string `yaml:"log_level,omitempty"`
Logging LoggingConfig `yaml:"logging,omitempty"`
Limit LimitConfig `yaml:"limit,omitempty"`
Development bool `yaml:"development,omitempty"`
Metric metric.MetricConfig `yaml:"metric,omitempty"`
}
type RTCConfig struct {
rtcconfig.RTCConfig `yaml:",inline"`
TURNServers []TURNServer `yaml:"turn_servers,omitempty"`
// Deprecated
StrictACKs bool `yaml:"strict_acks,omitempty"`
// Deprecated: use PacketBufferSizeVideo and PacketBufferSizeAudio
PacketBufferSize int `yaml:"packet_buffer_size,omitempty"`
// Number of packets to buffer for NACK - video
PacketBufferSizeVideo int `yaml:"packet_buffer_size_video,omitempty"`
// Number of packets to buffer for NACK - audio
PacketBufferSizeAudio int `yaml:"packet_buffer_size_audio,omitempty"`
// Throttle periods for pli/fir rtcp packets
PLIThrottle sfu.PLIThrottleConfig `yaml:"pli_throttle,omitempty"`
CongestionControl CongestionControlConfig `yaml:"congestion_control,omitempty"`
// allow TCP and TURN/TLS fallback
AllowTCPFallback *bool `yaml:"allow_tcp_fallback,omitempty"`
// force a reconnect on a publication error
ReconnectOnPublicationError *bool `yaml:"reconnect_on_publication_error,omitempty"`
// force a reconnect on a subscription error
ReconnectOnSubscriptionError *bool `yaml:"reconnect_on_subscription_error,omitempty"`
// force a reconnect on a data channel error
ReconnectOnDataChannelError *bool `yaml:"reconnect_on_data_channel_error,omitempty"`
// Deprecated
DataChannelMaxBufferedAmount uint64 `yaml:"data_channel_max_buffered_amount,omitempty"`
// Threshold of data channel writing to be considered too slow, data packet could
// be dropped for a slow data channel to avoid blocking the room.
DatachannelSlowThreshold int `yaml:"datachannel_slow_threshold,omitempty"`
ForwardStats ForwardStatsConfig `yaml:"forward_stats,omitempty"`
}
type TURNServer struct {
Host string `yaml:"host,omitempty"`
Port int `yaml:"port,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
Username string `yaml:"username,omitempty"`
Credential string `yaml:"credential,omitempty"`
}
type CongestionControlConfig struct {
Enabled bool `yaml:"enabled,omitempty"`
AllowPause bool `yaml:"allow_pause,omitempty"`
StreamAllocator streamallocator.StreamAllocatorConfig `yaml:"stream_allocator,omitempty"`
RemoteBWE remotebwe.RemoteBWEConfig `yaml:"remote_bwe,omitempty"`
UseSendSideBWEInterceptor bool `yaml:"use_send_side_bwe_interceptor,omitempty"`
UseSendSideBWE bool `yaml:"use_send_side_bwe,omitempty"`
SendSideBWE sendsidebwe.SendSideBWEConfig `yaml:"send_side_bwe,omitempty"`
}
type PlayoutDelayConfig struct {
Enabled bool `yaml:"enabled,omitempty"`
Min int `yaml:"min,omitempty"`
Max int `yaml:"max,omitempty"`
}
type VideoConfig struct {
DynacastPauseDelay time.Duration `yaml:"dynacast_pause_delay,omitempty"`
StreamTrackerManager sfu.StreamTrackerManagerConfig `yaml:"stream_tracker_manager,omitempty"`
}
type RoomConfig struct {
// enable rooms to be automatically created
AutoCreate bool `yaml:"auto_create,omitempty"`
EnabledCodecs []CodecSpec `yaml:"enabled_codecs,omitempty"`
MaxParticipants uint32 `yaml:"max_participants,omitempty"`
EmptyTimeout uint32 `yaml:"empty_timeout,omitempty"`
DepartureTimeout uint32 `yaml:"departure_timeout,omitempty"`
EnableRemoteUnmute bool `yaml:"enable_remote_unmute,omitempty"`
PlayoutDelay PlayoutDelayConfig `yaml:"playout_delay,omitempty"`
SyncStreams bool `yaml:"sync_streams,omitempty"`
CreateRoomEnabled bool `yaml:"create_room_enabled,omitempty"`
CreateRoomTimeout time.Duration `yaml:"create_room_timeout,omitempty"`
CreateRoomAttempts int `yaml:"create_room_attempts,omitempty"`
// deprecated, moved to limits
MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"`
// deprecated, moved to limits
MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"`
// deprecated, moved to limits
MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"`
RoomConfigurations map[string]*livekit.RoomConfiguration `yaml:"room_configurations,omitempty"`
}
type CodecSpec struct {
Mime string `yaml:"mime,omitempty"`
FmtpLine string `yaml:"fmtp_line,omitempty"`
}
type LoggingConfig struct {
logger.Config `yaml:",inline"`
PionLevel string `yaml:"pion_level,omitempty"`
}
type TURNConfig struct {
Enabled bool `yaml:"enabled,omitempty"`
Domain string `yaml:"domain,omitempty"`
CertFile string `yaml:"cert_file,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
TLSPort int `yaml:"tls_port,omitempty"`
UDPPort int `yaml:"udp_port,omitempty"`
RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"`
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
ExternalTLS bool `yaml:"external_tls,omitempty"`
}
type WebHookConfig struct {
URLs []string `yaml:"urls,omitempty"`
// key to use for webhook
APIKey string `yaml:"api_key,omitempty"`
}
type NodeSelectorConfig struct {
Kind string `yaml:"kind,omitempty"`
SortBy string `yaml:"sort_by,omitempty"`
CPULoadLimit float32 `yaml:"cpu_load_limit,omitempty"`
SysloadLimit float32 `yaml:"sysload_limit,omitempty"`
Regions []RegionConfig `yaml:"regions,omitempty"`
}
type SignalRelayConfig struct {
RetryTimeout time.Duration `yaml:"retry_timeout,omitempty"`
MinRetryInterval time.Duration `yaml:"min_retry_interval,omitempty"`
MaxRetryInterval time.Duration `yaml:"max_retry_interval,omitempty"`
StreamBufferSize int `yaml:"stream_buffer_size,omitempty"`
ConnectAttempts int `yaml:"connect_attempts,omitempty"`
}
// RegionConfig lists available regions and their latitude/longitude, so the selector would prefer
// regions that are closer
type RegionConfig struct {
Name string `yaml:"name,omitempty"`
Lat float64 `yaml:"lat,omitempty"`
Lon float64 `yaml:"lon,omitempty"`
}
type LimitConfig struct {
NumTracks int32 `yaml:"num_tracks,omitempty"`
BytesPerSec float32 `yaml:"bytes_per_sec,omitempty"`
SubscriptionLimitVideo int32 `yaml:"subscription_limit_video,omitempty"`
SubscriptionLimitAudio int32 `yaml:"subscription_limit_audio,omitempty"`
MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"`
// total size of all attributes on a participant
MaxAttributesSize uint32 `yaml:"max_attributes_size,omitempty"`
MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"`
MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"`
MaxParticipantNameLength int `yaml:"max_participant_name_length,omitempty"`
}
func (l LimitConfig) CheckRoomNameLength(name string) bool {
return l.MaxRoomNameLength == 0 || len(name) <= l.MaxRoomNameLength
}
func (l LimitConfig) CheckParticipantNameLength(name string) bool {
return l.MaxParticipantNameLength == 0 || len(name) <= l.MaxParticipantNameLength
}
func (l LimitConfig) CheckMetadataSize(metadata string) bool {
return l.MaxMetadataSize == 0 || uint32(len(metadata)) <= l.MaxMetadataSize
}
func (l LimitConfig) CheckAttributesSize(attributes map[string]string) bool {
if l.MaxAttributesSize == 0 {
return true
}
total := 0
for k, v := range attributes {
total += len(k) + len(v)
}
return uint32(total) <= l.MaxAttributesSize
}
type IngressConfig struct {
RTMPBaseURL string `yaml:"rtmp_base_url,omitempty"`
WHIPBaseURL string `yaml:"whip_base_url,omitempty"`
}
type SIPConfig struct{}
type APIConfig struct {
// amount of time to wait for API to execute, default 2s
ExecutionTimeout time.Duration `yaml:"execution_timeout,omitempty"`
// min amount of time to wait before checking for operation complete
CheckInterval time.Duration `yaml:"check_interval,omitempty"`
// max amount of time to wait before checking for operation complete
MaxCheckInterval time.Duration `yaml:"max_check_interval,omitempty"`
}
type PrometheusConfig struct {
Port uint32 `yaml:"port,omitempty"`
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
}
type ForwardStatsConfig struct {
SummaryInterval time.Duration `yaml:"summary_interval,omitempty"`
ReportInterval time.Duration `yaml:"report_interval,omitempty"`
ReportWindow time.Duration `yaml:"report_window,omitempty"`
}
func DefaultAPIConfig() APIConfig {
return APIConfig{
ExecutionTimeout: 2 * time.Second,
CheckInterval: 100 * time.Millisecond,
MaxCheckInterval: 300 * time.Second,
}
}
var DefaultConfig = Config{
Port: 7880,
RTC: RTCConfig{
RTCConfig: rtcconfig.RTCConfig{
UseExternalIP: false,
TCPPort: 7881,
ICEPortRangeStart: 0,
ICEPortRangeEnd: 0,
STUNServers: []string{},
},
PacketBufferSize: 500,
PacketBufferSizeVideo: 500,
PacketBufferSizeAudio: 200,
PLIThrottle: sfu.DefaultPLIThrottleConfig,
CongestionControl: CongestionControlConfig{
Enabled: true,
AllowPause: false,
StreamAllocator: streamallocator.DefaultStreamAllocatorConfig,
RemoteBWE: remotebwe.DefaultRemoteBWEConfig,
UseSendSideBWEInterceptor: false,
UseSendSideBWE: false,
SendSideBWE: sendsidebwe.DefaultSendSideBWEConfig,
},
},
Audio: sfu.DefaultAudioConfig,
Video: VideoConfig{
DynacastPauseDelay: 5 * time.Second,
StreamTrackerManager: sfu.DefaultStreamTrackerManagerConfig,
},
Redis: redisLiveKit.RedisConfig{},
Room: RoomConfig{
AutoCreate: true,
EnabledCodecs: []CodecSpec{
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeRED.String()},
{Mime: mime.MimeTypeVP8.String()},
{Mime: mime.MimeTypeH264.String()},
{Mime: mime.MimeTypeVP9.String()},
{Mime: mime.MimeTypeAV1.String()},
{Mime: mime.MimeTypeRTX.String()},
},
EmptyTimeout: 5 * 60,
DepartureTimeout: 20,
CreateRoomEnabled: true,
CreateRoomTimeout: 10 * time.Second,
CreateRoomAttempts: 3,
},
Limit: LimitConfig{
MaxMetadataSize: 64000,
MaxAttributesSize: 64000,
MaxRoomNameLength: 256,
MaxParticipantIdentityLength: 256,
MaxParticipantNameLength: 256,
},
Logging: LoggingConfig{
PionLevel: "error",
},
TURN: TURNConfig{
Enabled: false,
},
NodeSelector: NodeSelectorConfig{
Kind: "any",
SortBy: "random",
SysloadLimit: 0.9,
CPULoadLimit: 0.9,
},
SignalRelay: SignalRelayConfig{
RetryTimeout: 7500 * time.Millisecond,
MinRetryInterval: 500 * time.Millisecond,
MaxRetryInterval: 4 * time.Second,
StreamBufferSize: 1000,
ConnectAttempts: 3,
},
PSRPC: rpc.DefaultPSRPCConfig,
Keys: map[string]string{},
Metric: metric.DefaultMetricConfig,
}
func NewConfig(confString string, strictMode bool, c *cli.Context, baseFlags []cli.Flag) (*Config, error) {
// start with defaults
marshalled, err := yaml.Marshal(&DefaultConfig)
if err != nil {
return nil, err
}
var conf Config
err = yaml.Unmarshal(marshalled, &conf)
if err != nil {
return nil, err
}
if confString != "" {
decoder := yaml.NewDecoder(strings.NewReader(confString))
decoder.KnownFields(strictMode)
if err := decoder.Decode(&conf); err != nil {
return nil, fmt.Errorf("could not parse config: %v", err)
}
}
if c != nil {
if err := conf.updateFromCLI(c, baseFlags); err != nil {
return nil, err
}
}
if err := conf.RTC.Validate(conf.Development); err != nil {
return nil, fmt.Errorf("could not validate RTC config: %v", err)
}
// expand env vars in filenames
file, err := homedir.Expand(os.ExpandEnv(conf.KeyFile))
if err != nil {
return nil, err
}
conf.KeyFile = file
// set defaults for Turn relay if none are set
if conf.TURN.RelayPortRangeStart == 0 || conf.TURN.RelayPortRangeEnd == 0 {
// to make it easier to run in dev mode/docker, default to two ports
if conf.Development {
conf.TURN.RelayPortRangeStart = 30000
conf.TURN.RelayPortRangeEnd = 30002
} else {
conf.TURN.RelayPortRangeStart = 30000
conf.TURN.RelayPortRangeEnd = 40000
}
}
if conf.LogLevel != "" {
conf.Logging.Level = conf.LogLevel
}
if conf.Logging.Level == "" && conf.Development {
conf.Logging.Level = "debug"
}
if conf.Logging.PionLevel != "" {
if conf.Logging.ComponentLevels == nil {
conf.Logging.ComponentLevels = map[string]string{}
}
conf.Logging.ComponentLevels["transport.pion"] = conf.Logging.PionLevel
conf.Logging.ComponentLevels["pion"] = conf.Logging.PionLevel
}
// copy over legacy limits
if conf.Room.MaxMetadataSize != 0 {
conf.Limit.MaxMetadataSize = conf.Room.MaxMetadataSize
}
if conf.Room.MaxParticipantIdentityLength != 0 {
conf.Limit.MaxParticipantIdentityLength = conf.Room.MaxParticipantIdentityLength
}
if conf.Room.MaxRoomNameLength != 0 {
conf.Limit.MaxRoomNameLength = conf.Room.MaxRoomNameLength
}
return &conf, nil
}
func (conf *Config) IsTURNSEnabled() bool {
if conf.TURN.Enabled && conf.TURN.TLSPort != 0 {
return true
}
for _, s := range conf.RTC.TURNServers {
if s.Protocol == "tls" {
return true
}
}
return false
}
type configNode struct {
TypeNode reflect.Value
TagPrefix string
}
func (conf *Config) ToCLIFlagNames(existingFlags []cli.Flag) map[string]reflect.Value {
existingFlagNames := map[string]bool{}
for _, flag := range existingFlags {
for _, flagName := range flag.Names() {
existingFlagNames[flagName] = true
}
}
flagNames := map[string]reflect.Value{}
var currNode configNode
nodes := []configNode{{reflect.ValueOf(conf).Elem(), ""}}
for len(nodes) > 0 {
currNode, nodes = nodes[0], nodes[1:]
for i := 0; i < currNode.TypeNode.NumField(); i++ {
// inspect yaml tag from struct field to get path
field := currNode.TypeNode.Type().Field(i)
yamlTagArray := strings.SplitN(field.Tag.Get("yaml"), ",", 2)
yamlTag := yamlTagArray[0]
isInline := false
if len(yamlTagArray) > 1 && yamlTagArray[1] == "inline" {
isInline = true
}
if (yamlTag == "" && (!isInline || currNode.TagPrefix == "")) || yamlTag == "-" {
continue
}
yamlPath := yamlTag
if currNode.TagPrefix != "" {
if isInline {
yamlPath = currNode.TagPrefix
} else {
yamlPath = fmt.Sprintf("%s.%s", currNode.TagPrefix, yamlTag)
}
}
if existingFlagNames[yamlPath] {
continue
}
// map flag name to value
value := currNode.TypeNode.Field(i)
if value.Kind() == reflect.Struct {
nodes = append(nodes, configNode{value, yamlPath})
} else {
flagNames[yamlPath] = value
}
}
}
return flagNames
}
func (conf *Config) ValidateKeys() error {
// prefer keyfile if set
if conf.KeyFile != "" {
var otherFilter os.FileMode = 0o007
if st, err := os.Stat(conf.KeyFile); err != nil {
return err
} else if st.Mode().Perm()&otherFilter != 0o000 {
return ErrKeyFileIncorrectPermission
}
f, err := os.Open(conf.KeyFile)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
decoder := yaml.NewDecoder(f)
conf.Keys = map[string]string{}
if err = decoder.Decode(conf.Keys); err != nil {
return err
}
}
if len(conf.Keys) == 0 {
return ErrKeysNotSet
}
if !conf.Development {
for key, secret := range conf.Keys {
if len(secret) < 32 {
logger.Errorw("secret is too short, should be at least 32 characters for security", nil, "apiKey", key)
}
}
}
return nil
}
func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error) {
blankConfig := &Config{}
flags := make([]cli.Flag, 0)
for name, value := range blankConfig.ToCLIFlagNames(existingFlags) {
kind := value.Kind()
if kind == reflect.Ptr {
kind = value.Type().Elem().Kind()
}
var flag cli.Flag
envVar := fmt.Sprintf("LIVEKIT_%s", strings.ToUpper(strings.Replace(name, ".", "_", -1)))
switch kind {
case reflect.Bool:
flag = &cli.BoolFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.String:
flag = &cli.StringFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.Int, reflect.Int32:
flag = &cli.IntFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.Int64:
flag = &cli.Int64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
flag = &cli.UintFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.Uint64:
flag = &cli.Uint64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.Float32:
flag = &cli.Float64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.Float64:
flag = &cli.Float64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
}
case reflect.Slice:
// TODO
continue
case reflect.Map:
// TODO
continue
case reflect.Struct:
// TODO
continue
default:
return flags, fmt.Errorf("cli flag generation unsupported for config type: %s is a %s", name, kind.String())
}
flags = append(flags, flag)
}
return flags, nil
}
func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
generatedFlagNames := conf.ToCLIFlagNames(baseFlags)
for _, flag := range c.App.Flags {
flagName := flag.Names()[0]
// the `c.App.Name != "test"` check is needed because `c.IsSet(...)` is always false in unit tests
if !c.IsSet(flagName) && c.App.Name != "test" {
continue
}
configValue, ok := generatedFlagNames[flagName]
if !ok {
continue
}
kind := configValue.Kind()
if kind == reflect.Ptr {
// instantiate value to be set
configValue.Set(reflect.New(configValue.Type().Elem()))
kind = configValue.Type().Elem().Kind()
configValue = configValue.Elem()
}
switch kind {
case reflect.Bool:
configValue.SetBool(c.Bool(flagName))
case reflect.String:
configValue.SetString(c.String(flagName))
case reflect.Int, reflect.Int32, reflect.Int64:
configValue.SetInt(c.Int64(flagName))
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
configValue.SetUint(c.Uint64(flagName))
case reflect.Float32:
configValue.SetFloat(c.Float64(flagName))
case reflect.Float64:
configValue.SetFloat(c.Float64(flagName))
// case reflect.Slice:
// // TODO
// case reflect.Map:
// // TODO
default:
return fmt.Errorf("unsupported generated cli flag type for config: %s is a %s", flagName, kind.String())
}
}
if c.IsSet("dev") {
conf.Development = c.Bool("dev")
}
if c.IsSet("key-file") {
conf.KeyFile = c.String("key-file")
}
if c.IsSet("keys") {
if err := conf.unmarshalKeys(c.String("keys")); err != nil {
return errors.New("Could not parse keys, it needs to be exactly, \"key: secret\", including the space")
}
}
if c.IsSet("region") {
conf.Region = c.String("region")
}
if c.IsSet("redis-host") {
conf.Redis.Address = c.String("redis-host")
}
if c.IsSet("redis-password") {
conf.Redis.Password = c.String("redis-password")
}
if c.IsSet("turn-cert") {
conf.TURN.CertFile = c.String("turn-cert")
}
if c.IsSet("turn-key") {
conf.TURN.KeyFile = c.String("turn-key")
}
if c.IsSet("node-ip") {
conf.RTC.NodeIP = c.String("node-ip")
}
if c.IsSet("udp-port") {
conf.RTC.UDPPort.UnmarshalString(c.String("udp-port"))
}
if c.IsSet("bind") {
conf.BindAddresses = c.StringSlice("bind")
}
return nil
}
func (conf *Config) unmarshalKeys(keys string) error {
temp := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(keys), temp); err != nil {
return err
}
conf.Keys = make(map[string]string, len(temp))
for key, val := range temp {
if secret, ok := val.(string); ok {
conf.Keys[key] = secret
}
}
return nil
}
// Note: only pass in logr.Logger with default depth
func SetLogger(l logger.Logger) {
logger.SetLogger(l, "livekit")
}
func InitLoggerFromConfig(config *LoggingConfig) {
logger.InitFromConfig(&config.Config, "livekit")
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"flag"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"github.com/livekit/livekit-server/pkg/config/configtest"
)
func TestConfig_UnmarshalKeys(t *testing.T) {
conf, err := NewConfig("", true, nil, nil)
require.NoError(t, err)
require.NoError(t, conf.unmarshalKeys("key1: secret1"))
require.Equal(t, "secret1", conf.Keys["key1"])
}
func TestConfig_DefaultsKept(t *testing.T) {
const content = `room:
empty_timeout: 10`
conf, err := NewConfig(content, true, nil, nil)
require.NoError(t, err)
require.Equal(t, true, conf.Room.AutoCreate)
require.Equal(t, uint32(10), conf.Room.EmptyTimeout)
}
func TestConfig_UnknownKeys(t *testing.T) {
const content = `unknown: 10
room:
empty_timeout: 10`
_, err := NewConfig(content, true, nil, nil)
require.Error(t, err)
}
func TestGeneratedFlags(t *testing.T) {
generatedFlags, err := GenerateCLIFlags(nil, false)
require.NoError(t, err)
app := cli.NewApp()
app.Name = "test"
app.Flags = append(app.Flags, generatedFlags...)
set := flag.NewFlagSet("test", 0)
set.Bool("rtc.use_ice_lite", true, "") // bool
set.String("redis.address", "localhost:6379", "") // string
set.Uint("prometheus.port", 9999, "") // uint32
set.Bool("rtc.allow_tcp_fallback", true, "") // pointer
set.Bool("rtc.reconnect_on_publication_error", true, "") // pointer
set.Bool("rtc.reconnect_on_subscription_error", false, "") // pointer
c := cli.NewContext(app, set, nil)
conf, err := NewConfig("", true, c, nil)
require.NoError(t, err)
require.True(t, conf.RTC.UseICELite)
require.Equal(t, "localhost:6379", conf.Redis.Address)
require.Equal(t, uint32(9999), conf.Prometheus.Port)
require.NotNil(t, conf.RTC.AllowTCPFallback)
require.True(t, *conf.RTC.AllowTCPFallback)
require.NotNil(t, conf.RTC.ReconnectOnPublicationError)
require.True(t, *conf.RTC.ReconnectOnPublicationError)
require.NotNil(t, conf.RTC.ReconnectOnSubscriptionError)
require.False(t, *conf.RTC.ReconnectOnSubscriptionError)
}
func TestYAMLTag(t *testing.T) {
require.NoError(t, configtest.CheckYAMLTags(Config{}))
}
@@ -0,0 +1,69 @@
package configtest
import (
"fmt"
"reflect"
"slices"
"strings"
"go.uber.org/multierr"
"google.golang.org/protobuf/proto"
)
var protoMessageType = reflect.TypeOf((*proto.Message)(nil)).Elem()
func checkYAMLTags(t reflect.Type, seen map[reflect.Type]struct{}) error {
if _, ok := seen[t]; ok {
return nil
}
seen[t] = struct{}{}
switch t.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.Pointer:
return checkYAMLTags(t.Elem(), seen)
case reflect.Struct:
if reflect.PointerTo(t).Implements(protoMessageType) {
// ignore protobuf messages
return nil
}
var errs error
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if !field.IsExported() {
// ignore unexported fields
continue
}
if field.Type.Kind() == reflect.Bool {
// ignore boolean fields
continue
}
if field.Tag.Get("config") == "allowempty" {
// ignore configured exceptions
continue
}
parts := strings.Split(field.Tag.Get("yaml"), ",")
if parts[0] == "-" {
// ignore unparsed fields
continue
}
if !slices.Contains(parts, "omitempty") && !slices.Contains(parts, "inline") {
errs = multierr.Append(errs, fmt.Errorf("%s/%s.%s missing omitempty tag", t.PkgPath(), t.Name(), field.Name))
}
errs = multierr.Append(errs, checkYAMLTags(field.Type, seen))
}
return errs
default:
return nil
}
}
func CheckYAMLTags(config any) error {
return checkYAMLTags(reflect.TypeOf(config), map[reflect.Type]struct{}{})
}
@@ -0,0 +1,31 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metric
// ------------------------------------------------
type MetricConfig struct {
Timestamper MetricTimestamperConfig `yaml:"timestamper_config,omitempty"`
Collector MetricsCollectorConfig `yaml:"collector,omitempty"`
Reporter MetricsReporterConfig `yaml:"reporter,omitempty"`
}
var (
DefaultMetricConfig = MetricConfig{
Timestamper: DefaultMetricTimestamperConfig,
Collector: DefaultMetricsCollectorConfig,
Reporter: DefaultMetricsReporterConfig,
}
)
@@ -0,0 +1,119 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metric
import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/sfu/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
"google.golang.org/protobuf/types/known/timestamppb"
)
// ------------------------------------------------
type MetricTimestamperConfig struct {
OneWayDelayEstimatorMinInterval time.Duration `yaml:"one_way_delay_estimator_min_interval,omitempty"`
OneWayDelayEstimatorMaxBatch int `yaml:"one_way_delay_estimator_max_batch,omitempty"`
}
var (
DefaultMetricTimestamperConfig = MetricTimestamperConfig{
OneWayDelayEstimatorMinInterval: 5 * time.Second,
OneWayDelayEstimatorMaxBatch: 100,
}
)
// ------------------------------------------------
type MetricTimestamperParams struct {
Config MetricTimestamperConfig
Logger logger.Logger
}
type MetricTimestamper struct {
params MetricTimestamperParams
lock sync.Mutex
owdEstimator *utils.OWDEstimator
lastOWDEstimatorRunAt time.Time
batchesSinceLastOWDEstimatorRun int
}
func NewMetricTimestamper(params MetricTimestamperParams) *MetricTimestamper {
return &MetricTimestamper{
params: params,
owdEstimator: utils.NewOWDEstimator(utils.OWDEstimatorParamsDefault),
lastOWDEstimatorRunAt: time.Now().Add(-params.Config.OneWayDelayEstimatorMinInterval),
}
}
func (m *MetricTimestamper) Process(batch *livekit.MetricsBatch) {
if m == nil {
return
}
// run OWD estimation periodically
estimatedOWDNanos := m.maybeRunOWDEstimator(batch)
// normalize all time stamps and add estimated OWD
// NOTE: all timestamps will be re-mapped. If the time series or event happened some time
// in the past and the OWD estimation has changed since, those samples will get the updated
// OWD estimation applied. So, they may have more uncertainty in addition to the uncertainty
// of OWD estimation process.
batch.NormalizedTimestamp = timestamppb.New(time.Unix(0, batch.TimestampMs*1e6+estimatedOWDNanos))
for _, ts := range batch.TimeSeries {
for _, sample := range ts.Samples {
sample.NormalizedTimestamp = timestamppb.New(time.Unix(0, sample.TimestampMs*1e6+estimatedOWDNanos))
}
}
for _, ev := range batch.Events {
ev.NormalizedStartTimestamp = timestamppb.New(time.Unix(0, ev.StartTimestampMs*1e6+estimatedOWDNanos))
endTimestampMs := ev.GetEndTimestampMs()
if endTimestampMs != 0 {
ev.NormalizedEndTimestamp = timestamppb.New(time.Unix(0, endTimestampMs*1e6+estimatedOWDNanos))
}
}
m.params.Logger.Debugw("timestamped metrics batch", "batch", logger.Proto(batch))
}
func (m *MetricTimestamper) maybeRunOWDEstimator(batch *livekit.MetricsBatch) int64 {
m.lock.Lock()
defer m.lock.Unlock()
if time.Since(m.lastOWDEstimatorRunAt) < m.params.Config.OneWayDelayEstimatorMinInterval &&
m.batchesSinceLastOWDEstimatorRun < m.params.Config.OneWayDelayEstimatorMaxBatch {
m.batchesSinceLastOWDEstimatorRun++
return m.owdEstimator.EstimatedPropagationDelay()
}
senderClockTime := batch.GetTimestampMs()
if senderClockTime == 0 {
m.batchesSinceLastOWDEstimatorRun++
return m.owdEstimator.EstimatedPropagationDelay()
}
m.lastOWDEstimatorRunAt = time.Now()
m.batchesSinceLastOWDEstimatorRun = 1
estimatedOWDNs, _ := m.owdEstimator.Update(senderClockTime*1e6, mono.UnixNano())
return estimatedOWDNs
}
@@ -0,0 +1,225 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metric
import (
"sync"
"time"
"github.com/frostbyte73/core"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
"github.com/livekit/protocol/utils"
)
type MetricsCollectorProvider interface {
MetricsCollectorTimeToCollectMetrics()
MetricsCollectorBatchReady(mb *livekit.MetricsBatch)
}
// --------------------------------------------------------
type MetricsCollectorConfig struct {
SamplingIntervalMs uint32 `yaml:"sampling_interval_ms,omitempty" json:"sampling_interval_ms,omitempty"`
BatchIntervalMs uint32 `yaml:"batch_interval_ms,omitempty" json:"batch_interval_ms,omitempty"`
}
var (
DefaultMetricsCollectorConfig = MetricsCollectorConfig{
SamplingIntervalMs: 3 * 1000,
BatchIntervalMs: 10 * 1000,
}
)
// --------------------------------------------------------
type MetricsCollectorParams struct {
ParticipantIdentity livekit.ParticipantIdentity
Config MetricsCollectorConfig
Provider MetricsCollectorProvider
Logger logger.Logger
}
type MetricsCollector struct {
params MetricsCollectorParams
lock sync.RWMutex
mbb *utils.MetricsBatchBuilder
publisherRTTMetricId map[livekit.ParticipantIdentity]int
subscriberRTTMetricId int
relayRTTMetricId map[livekit.ParticipantIdentity]int
stop core.Fuse
}
func NewMetricsCollector(params MetricsCollectorParams) *MetricsCollector {
mc := &MetricsCollector{
params: params,
}
mc.reset()
go mc.worker()
return mc
}
func (mc *MetricsCollector) Stop() {
if mc != nil {
mc.stop.Break()
}
}
func (mc *MetricsCollector) AddPublisherRTT(participantIdentity livekit.ParticipantIdentity, rtt float32) {
mc.lock.Lock()
defer mc.lock.Unlock()
metricId, ok := mc.publisherRTTMetricId[participantIdentity]
if !ok {
var err error
metricId, err = mc.createTimeSeriesMetric(livekit.MetricLabel_PUBLISHER_RTT, participantIdentity)
if err != nil {
mc.params.Logger.Warnw("could not add time series metric for publisher RTT", err)
return
}
mc.publisherRTTMetricId[participantIdentity] = metricId
}
mc.addTimeSeriesMetricSample(metricId, rtt)
}
func (mc *MetricsCollector) AddSubscriberRTT(rtt float32) {
mc.lock.Lock()
defer mc.lock.Unlock()
if mc.subscriberRTTMetricId == utils.MetricsBatchBuilderInvalidTimeSeriesMetricId {
var err error
mc.subscriberRTTMetricId, err = mc.createTimeSeriesMetric(livekit.MetricLabel_SUBSCRIBER_RTT, mc.params.ParticipantIdentity)
if err != nil {
mc.params.Logger.Warnw("could not add time series metric for publisher RTT", err)
return
}
}
mc.addTimeSeriesMetricSample(mc.subscriberRTTMetricId, rtt)
}
func (mc *MetricsCollector) AddRelayRTT(participantIdentity livekit.ParticipantIdentity, rtt float32) {
mc.lock.Lock()
defer mc.lock.Unlock()
metricId, ok := mc.relayRTTMetricId[participantIdentity]
if !ok {
var err error
metricId, err = mc.createTimeSeriesMetric(livekit.MetricLabel_SERVER_MESH_RTT, participantIdentity)
if err != nil {
mc.params.Logger.Warnw("could not add time series metric for server mesh RTT", err)
return
}
mc.relayRTTMetricId[participantIdentity] = metricId
}
mc.addTimeSeriesMetricSample(metricId, rtt)
}
func (mc *MetricsCollector) getMetricsBatchAndReset() *livekit.MetricsBatch {
mc.lock.Lock()
mbb := mc.mbb
mc.reset()
mc.lock.Unlock()
if mbb.IsEmpty() {
return nil
}
now := mono.Now()
mbb.SetTime(now, now)
return mbb.ToProto()
}
func (mc *MetricsCollector) reset() {
mc.mbb = utils.NewMetricsBatchBuilder()
mc.mbb.SetRestrictedLabels(utils.MetricRestrictedLabels{
LabelRanges: []utils.MetricLabelRange{
{
StartInclusive: livekit.MetricLabel_CLIENT_VIDEO_SUBSCRIBER_FREEZE_COUNT,
EndInclusive: livekit.MetricLabel_CLIENT_VIDEO_PUBLISHER_QUALITY_LIMITATION_DURATION_OTHER,
},
},
ParticipantIdentity: mc.params.ParticipantIdentity,
})
mc.publisherRTTMetricId = make(map[livekit.ParticipantIdentity]int)
mc.subscriberRTTMetricId = utils.MetricsBatchBuilderInvalidTimeSeriesMetricId
mc.relayRTTMetricId = make(map[livekit.ParticipantIdentity]int)
}
func (mc *MetricsCollector) createTimeSeriesMetric(
label livekit.MetricLabel,
participantIdentity livekit.ParticipantIdentity,
) (int, error) {
return mc.mbb.AddTimeSeriesMetric(utils.TimeSeriesMetric{
MetricLabel: label,
ParticipantIdentity: participantIdentity,
},
)
}
func (mc *MetricsCollector) addTimeSeriesMetricSample(metricId int, value float32) {
now := mono.Now()
if err := mc.mbb.AddMetricSamplesToTimeSeriesMetric(metricId, []utils.MetricSample{
{
At: now,
NormalizedAt: now,
Value: value,
},
}); err != nil {
mc.params.Logger.Warnw("could not add metric sample", err, "metricId", metricId)
}
}
func (mc *MetricsCollector) worker() {
samplingIntervalMs := mc.params.Config.SamplingIntervalMs
if samplingIntervalMs == 0 {
samplingIntervalMs = DefaultMetricsCollectorConfig.SamplingIntervalMs
}
samplingTicker := time.NewTicker(time.Duration(samplingIntervalMs) * time.Millisecond)
defer samplingTicker.Stop()
batchIntervalMs := mc.params.Config.BatchIntervalMs
if batchIntervalMs < samplingIntervalMs {
batchIntervalMs = samplingIntervalMs
}
batchTicker := time.NewTicker(time.Duration(batchIntervalMs) * time.Millisecond)
defer batchTicker.Stop()
for {
select {
case <-samplingTicker.C:
mc.params.Provider.MetricsCollectorTimeToCollectMetrics()
case <-batchTicker.C:
if mb := mc.getMetricsBatchAndReset(); mb != nil {
mc.params.Provider.MetricsCollectorBatchReady(mb)
}
case <-mc.stop.Watch():
return
}
}
}
@@ -0,0 +1,138 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metric
import (
"sync"
"time"
"github.com/frostbyte73/core"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
"github.com/livekit/protocol/utils"
)
type MetricsReporterConsumer interface {
MetricsReporterBatchReady(mb *livekit.MetricsBatch)
}
// --------------------------------------------------------
type MetricsReporterConfig struct {
ReportingIntervalMs uint32 `yaml:"reporting_interval_ms,omitempty" json:"reporting_interval_ms,omitempty"`
}
var (
DefaultMetricsReporterConfig = MetricsReporterConfig{
ReportingIntervalMs: 10 * 1000,
}
)
// --------------------------------------------------------
type MetricsReporterParams struct {
ParticipantIdentity livekit.ParticipantIdentity
Config MetricsReporterConfig
Consumer MetricsReporterConsumer
Logger logger.Logger
}
type MetricsReporter struct {
params MetricsReporterParams
lock sync.RWMutex
mbb *utils.MetricsBatchBuilder
stop core.Fuse
}
func NewMetricsReporter(params MetricsReporterParams) *MetricsReporter {
mr := &MetricsReporter{
params: params,
}
mr.reset()
go mr.worker()
return mr
}
func (mr *MetricsReporter) Stop() {
if mr != nil {
mr.stop.Break()
}
}
func (mr *MetricsReporter) Merge(other *livekit.MetricsBatch) {
if mr == nil {
return
}
mr.lock.Lock()
defer mr.lock.Unlock()
mr.mbb.Merge(other)
}
func (mr *MetricsReporter) getMetricsBatchAndReset() *livekit.MetricsBatch {
mr.lock.Lock()
mbb := mr.mbb
mr.reset()
mr.lock.Unlock()
if mbb.IsEmpty() {
return nil
}
now := mono.Now()
mbb.SetTime(now, now)
return mbb.ToProto()
}
func (mr *MetricsReporter) reset() {
mr.mbb = utils.NewMetricsBatchBuilder()
mr.mbb.SetRestrictedLabels(utils.MetricRestrictedLabels{
LabelRanges: []utils.MetricLabelRange{
{
StartInclusive: livekit.MetricLabel_CLIENT_VIDEO_SUBSCRIBER_FREEZE_COUNT,
EndInclusive: livekit.MetricLabel_CLIENT_VIDEO_PUBLISHER_QUALITY_LIMITATION_DURATION_OTHER,
},
},
ParticipantIdentity: mr.params.ParticipantIdentity,
})
}
func (mr *MetricsReporter) worker() {
reportingIntervalMs := mr.params.Config.ReportingIntervalMs
if reportingIntervalMs == 0 {
reportingIntervalMs = DefaultMetricsReporterConfig.ReportingIntervalMs
}
reportingTicker := time.NewTicker(time.Duration(reportingIntervalMs) * time.Millisecond)
defer reportingTicker.Stop()
for {
select {
case <-reportingTicker.C:
if mb := mr.getMetricsBatchAndReset(); mb != nil {
mr.params.Consumer.MetricsReporterBatchReady(mb)
}
case <-mr.stop.Watch():
return
}
}
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"errors"
)
var (
ErrNotFound = errors.New("could not find object")
ErrIPNotSet = errors.New("ip address is required and not set")
ErrHandlerNotDefined = errors.New("handler not defined")
ErrIncorrectRTCNode = errors.New("current node isn't the RTC node for the room")
ErrNodeNotFound = errors.New("could not locate the node")
ErrNodeLimitReached = errors.New("reached configured limit for node")
ErrInvalidRouterMessage = errors.New("invalid router message")
ErrChannelClosed = errors.New("channel closed")
ErrChannelFull = errors.New("channel is full")
// errors when starting signal connection
ErrRequestChannelClosed = errors.New("request channel closed")
ErrCouldNotMigrateParticipant = errors.New("could not migrate participant")
ErrClientInfoNotSet = errors.New("client info not set")
)
+286
View File
@@ -0,0 +1,286 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"context"
"encoding/json"
"github.com/redis/go-redis/v9"
"go.uber.org/atomic"
"go.uber.org/zap/zapcore"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
)
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
// MessageSink is an abstraction for writing protobuf messages and having them read by a MessageSource,
// potentially on a different node via a transport
//
//counterfeiter:generate . MessageSink
type MessageSink interface {
WriteMessage(msg proto.Message) error
IsClosed() bool
Close()
ConnectionID() livekit.ConnectionID
}
// ----------
type NullMessageSink struct {
connID livekit.ConnectionID
isClosed atomic.Bool
}
func NewNullMessageSink(connID livekit.ConnectionID) *NullMessageSink {
return &NullMessageSink{
connID: connID,
}
}
func (n *NullMessageSink) WriteMessage(_msg proto.Message) error {
return nil
}
func (n *NullMessageSink) IsClosed() bool {
return n.isClosed.Load()
}
func (n *NullMessageSink) Close() {
n.isClosed.Store(true)
}
func (n *NullMessageSink) ConnectionID() livekit.ConnectionID {
return n.connID
}
// ------------------------------------------------
//counterfeiter:generate . MessageSource
type MessageSource interface {
// ReadChan exposes a one way channel to make it easier to use with select
ReadChan() <-chan proto.Message
IsClosed() bool
Close()
ConnectionID() livekit.ConnectionID
}
// ----------
type NullMessageSource struct {
connID livekit.ConnectionID
msgChan chan proto.Message
isClosed atomic.Bool
}
func NewNullMessageSource(connID livekit.ConnectionID) *NullMessageSource {
return &NullMessageSource{
connID: connID,
msgChan: make(chan proto.Message, 0),
}
}
func (n *NullMessageSource) ReadChan() <-chan proto.Message {
return n.msgChan
}
func (n *NullMessageSource) IsClosed() bool {
return n.isClosed.Load()
}
func (n *NullMessageSource) Close() {
if !n.isClosed.Swap(true) {
close(n.msgChan)
}
}
func (n *NullMessageSource) ConnectionID() livekit.ConnectionID {
return n.connID
}
// ------------------------------------------------
// Router allows multiple nodes to coordinate the participant session
//
//counterfeiter:generate . Router
type Router interface {
MessageRouter
RegisterNode() error
UnregisterNode() error
RemoveDeadNodes() error
ListNodes() ([]*livekit.Node, error)
GetNodeForRoom(ctx context.Context, roomName livekit.RoomName) (*livekit.Node, error)
SetNodeForRoom(ctx context.Context, roomName livekit.RoomName, nodeId livekit.NodeID) error
ClearRoomState(ctx context.Context, roomName livekit.RoomName) error
GetRegion() string
Start() error
Drain()
Stop()
}
type StartParticipantSignalResults struct {
ConnectionID livekit.ConnectionID
RequestSink MessageSink
ResponseSource MessageSource
NodeID livekit.NodeID
NodeSelectionReason string
}
type MessageRouter interface {
// CreateRoom starts an rtc room
CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (res *livekit.Room, err error)
// StartParticipantSignal participant signal connection is ready to start
StartParticipantSignal(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit) (res StartParticipantSignalResults, err error)
}
func CreateRouter(
rc redis.UniversalClient,
node LocalNode,
signalClient SignalClient,
roomManagerClient RoomManagerClient,
kps rpc.KeepalivePubSub,
) Router {
lr := NewLocalRouter(node, signalClient, roomManagerClient)
if rc != nil {
return NewRedisRouter(lr, rc, kps)
}
// local routing and store
logger.Infow("using single-node routing")
return lr
}
// ------------------------------------------------
type ParticipantInit struct {
Identity livekit.ParticipantIdentity
Name livekit.ParticipantName
Reconnect bool
ReconnectReason livekit.ReconnectReason
AutoSubscribe bool
Client *livekit.ClientInfo
Grants *auth.ClaimGrants
Region string
AdaptiveStream bool
ID livekit.ParticipantID
SubscriberAllowPause *bool
DisableICELite bool
CreateRoom *livekit.CreateRoomRequest
}
func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
if pi == nil {
return nil
}
logBoolPtr := func(prop string, val *bool) {
if val == nil {
e.AddString(prop, "not-set")
} else {
e.AddBool(prop, *val)
}
}
e.AddString("Identity", string(pi.Identity))
logBoolPtr("Reconnect", &pi.Reconnect)
e.AddString("ReconnectReason", pi.ReconnectReason.String())
logBoolPtr("AutoSubscribe", &pi.AutoSubscribe)
e.AddObject("Client", logger.Proto(utils.ClientInfoWithoutAddress(pi.Client)))
e.AddObject("Grants", pi.Grants)
e.AddString("Region", pi.Region)
logBoolPtr("AdaptiveStream", &pi.AdaptiveStream)
e.AddString("ID", string(pi.ID))
logBoolPtr("SubscriberAllowPause", pi.SubscriberAllowPause)
logBoolPtr("DisableICELite", &pi.DisableICELite)
e.AddObject("CreateRoom", logger.Proto(pi.CreateRoom))
return nil
}
func (pi *ParticipantInit) ToStartSession(roomName livekit.RoomName, connectionID livekit.ConnectionID) (*livekit.StartSession, error) {
claims, err := json.Marshal(pi.Grants)
if err != nil {
return nil, err
}
ss := &livekit.StartSession{
RoomName: string(roomName),
Identity: string(pi.Identity),
Name: string(pi.Name),
// connection id is to allow the RTC node to identify where to route the message back to
ConnectionId: string(connectionID),
Reconnect: pi.Reconnect,
ReconnectReason: pi.ReconnectReason,
AutoSubscribe: pi.AutoSubscribe,
Client: pi.Client,
GrantsJson: string(claims),
AdaptiveStream: pi.AdaptiveStream,
ParticipantId: string(pi.ID),
DisableIceLite: pi.DisableICELite,
CreateRoom: pi.CreateRoom,
}
if pi.SubscriberAllowPause != nil {
subscriberAllowPause := *pi.SubscriberAllowPause
ss.SubscriberAllowPause = &subscriberAllowPause
}
return ss, nil
}
func ParticipantInitFromStartSession(ss *livekit.StartSession, region string) (*ParticipantInit, error) {
claims := &auth.ClaimGrants{}
if err := json.Unmarshal([]byte(ss.GrantsJson), claims); err != nil {
return nil, err
}
pi := &ParticipantInit{
Identity: livekit.ParticipantIdentity(ss.Identity),
Name: livekit.ParticipantName(ss.Name),
Reconnect: ss.Reconnect,
ReconnectReason: ss.ReconnectReason,
Client: ss.Client,
AutoSubscribe: ss.AutoSubscribe,
Grants: claims,
Region: region,
AdaptiveStream: ss.AdaptiveStream,
ID: livekit.ParticipantID(ss.ParticipantId),
DisableICELite: ss.DisableIceLite,
CreateRoom: ss.CreateRoom,
}
if ss.SubscriberAllowPause != nil {
subscriberAllowPause := *ss.SubscriberAllowPause
pi.SubscriberAllowPause = &subscriberAllowPause
}
// TODO: clean up after 1.7 eol
if pi.CreateRoom == nil {
pi.CreateRoom = &livekit.CreateRoomRequest{
Name: ss.RoomName,
}
}
return pi, nil
}
+171
View File
@@ -0,0 +1,171 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"context"
"sync"
"time"
"go.uber.org/atomic"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
var _ Router = (*LocalRouter)(nil)
// a router of messages on the same node, basic implementation for local testing
type LocalRouter struct {
currentNode LocalNode
signalClient SignalClient
roomManagerClient RoomManagerClient
lock sync.RWMutex
// channels for each participant
requestChannels map[string]*MessageChannel
responseChannels map[string]*MessageChannel
isStarted atomic.Bool
}
func NewLocalRouter(
currentNode LocalNode,
signalClient SignalClient,
roomManagerClient RoomManagerClient,
) *LocalRouter {
return &LocalRouter{
currentNode: currentNode,
signalClient: signalClient,
roomManagerClient: roomManagerClient,
requestChannels: make(map[string]*MessageChannel),
responseChannels: make(map[string]*MessageChannel),
}
}
func (r *LocalRouter) GetNodeForRoom(_ context.Context, _ livekit.RoomName) (*livekit.Node, error) {
return r.currentNode.Clone(), nil
}
func (r *LocalRouter) SetNodeForRoom(_ context.Context, _ livekit.RoomName, _ livekit.NodeID) error {
return nil
}
func (r *LocalRouter) ClearRoomState(_ context.Context, _ livekit.RoomName) error {
return nil
}
func (r *LocalRouter) RegisterNode() error {
return nil
}
func (r *LocalRouter) UnregisterNode() error {
return nil
}
func (r *LocalRouter) RemoveDeadNodes() error {
return nil
}
func (r *LocalRouter) GetNode(nodeID livekit.NodeID) (*livekit.Node, error) {
if nodeID == r.currentNode.NodeID() {
return r.currentNode.Clone(), nil
}
return nil, ErrNotFound
}
func (r *LocalRouter) ListNodes() ([]*livekit.Node, error) {
return []*livekit.Node{
r.currentNode.Clone(),
}, nil
}
func (r *LocalRouter) CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (res *livekit.Room, err error) {
return r.CreateRoomWithNodeID(ctx, req, r.currentNode.NodeID())
}
func (r *LocalRouter) CreateRoomWithNodeID(ctx context.Context, req *livekit.CreateRoomRequest, nodeID livekit.NodeID) (res *livekit.Room, err error) {
return r.roomManagerClient.CreateRoom(ctx, nodeID, req)
}
func (r *LocalRouter) StartParticipantSignal(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit) (res StartParticipantSignalResults, err error) {
return r.StartParticipantSignalWithNodeID(ctx, roomName, pi, r.currentNode.NodeID())
}
func (r *LocalRouter) StartParticipantSignalWithNodeID(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit, nodeID livekit.NodeID) (res StartParticipantSignalResults, err error) {
connectionID, reqSink, resSource, err := r.signalClient.StartParticipantSignal(ctx, roomName, pi, nodeID)
if err != nil {
logger.Errorw("could not handle new participant", err,
"room", roomName,
"participant", pi.Identity,
"connID", connectionID,
)
} else {
return StartParticipantSignalResults{
ConnectionID: connectionID,
RequestSink: reqSink,
ResponseSource: resSource,
NodeID: nodeID,
}, nil
}
return
}
func (r *LocalRouter) Start() error {
if r.isStarted.Swap(true) {
return nil
}
go r.statsWorker()
// go r.memStatsWorker()
return nil
}
func (r *LocalRouter) Drain() {
r.currentNode.SetState(livekit.NodeState_SHUTTING_DOWN)
}
func (r *LocalRouter) Stop() {}
func (r *LocalRouter) GetRegion() string {
return r.currentNode.Region()
}
func (r *LocalRouter) statsWorker() {
for {
if !r.isStarted.Load() {
return
}
// update every 10 seconds
<-time.After(statsUpdateInterval)
r.currentNode.UpdateNodeStats()
}
}
/*
func (r *LocalRouter) memStatsWorker() {
ticker := time.NewTicker(time.Second * 30)
defer ticker.Stop()
for {
<-ticker.C
var m runtime.MemStats
runtime.ReadMemStats(&m)
logger.Infow("memstats",
"mallocs", m.Mallocs, "frees", m.Frees, "m-f", m.Mallocs-m.Frees,
"hinuse", m.HeapInuse, "halloc", m.HeapAlloc, "frag", m.HeapInuse-m.HeapAlloc,
)
}
}
*/
@@ -0,0 +1,94 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"sync"
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/proto"
)
const DefaultMessageChannelSize = 200
type MessageChannel struct {
connectionID livekit.ConnectionID
msgChan chan proto.Message
onClose func()
isClosed bool
lock sync.RWMutex
}
func NewDefaultMessageChannel(connectionID livekit.ConnectionID) *MessageChannel {
return NewMessageChannel(connectionID, DefaultMessageChannelSize)
}
func NewMessageChannel(connectionID livekit.ConnectionID, size int) *MessageChannel {
return &MessageChannel{
connectionID: connectionID,
// allow some buffer to avoid blocked writes
msgChan: make(chan proto.Message, size),
}
}
func (m *MessageChannel) OnClose(f func()) {
m.onClose = f
}
func (m *MessageChannel) IsClosed() bool {
m.lock.RLock()
defer m.lock.RUnlock()
return m.isClosed
}
func (m *MessageChannel) WriteMessage(msg proto.Message) error {
m.lock.RLock()
defer m.lock.RUnlock()
if m.isClosed {
return ErrChannelClosed
}
select {
case m.msgChan <- msg:
// published
return nil
default:
// channel is full
return ErrChannelFull
}
}
func (m *MessageChannel) ReadChan() <-chan proto.Message {
return m.msgChan
}
func (m *MessageChannel) Close() {
m.lock.Lock()
if m.isClosed {
m.lock.Unlock()
return
}
m.isClosed = true
close(m.msgChan)
m.lock.Unlock()
if m.onClose != nil {
m.onClose()
}
}
func (m *MessageChannel) ConnectionID() livekit.ConnectionID {
return m.connectionID
}
@@ -0,0 +1,50 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing_test
import (
"sync"
"testing"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/routing"
)
func TestMessageChannel_WriteMessageClosed(t *testing.T) {
// ensure it doesn't panic when written to after closing
m := routing.NewMessageChannel(livekit.ConnectionID("test"), routing.DefaultMessageChannelSize)
go func() {
for msg := range m.ReadChan() {
if msg == nil {
return
}
}
}()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
_ = m.WriteMessage(&livekit.SignalRequest{})
}
}()
_ = m.WriteMessage(&livekit.SignalRequest{})
m.Close()
_ = m.WriteMessage(&livekit.SignalRequest{})
wg.Wait()
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"runtime"
"sync"
"time"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
)
type LocalNode interface {
Clone() *livekit.Node
SetNodeID(nodeID livekit.NodeID)
NodeID() livekit.NodeID
NodeType() livekit.NodeType
NodeIP() string
Region() string
SetState(state livekit.NodeState)
SetStats(stats *livekit.NodeStats)
UpdateNodeStats() bool
SecondsSinceNodeStatsUpdate() float64
}
type LocalNodeImpl struct {
lock sync.RWMutex
node *livekit.Node
// previous stats for computing averages
prevStats *livekit.NodeStats
}
func NewLocalNode(conf *config.Config) (*LocalNodeImpl, error) {
nodeID := guid.New(utils.NodePrefix)
if conf != nil && conf.RTC.NodeIP == "" {
return nil, ErrIPNotSet
}
l := &LocalNodeImpl{
node: &livekit.Node{
Id: nodeID,
NumCpus: uint32(runtime.NumCPU()),
State: livekit.NodeState_SERVING,
Stats: &livekit.NodeStats{
StartedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
},
},
}
if conf != nil {
l.node.Ip = conf.RTC.NodeIP
l.node.Region = conf.Region
}
return l, nil
}
func NewLocalNodeFromNodeProto(node *livekit.Node) (*LocalNodeImpl, error) {
return &LocalNodeImpl{node: utils.CloneProto(node)}, nil
}
func (l *LocalNodeImpl) Clone() *livekit.Node {
l.lock.RLock()
defer l.lock.RUnlock()
return utils.CloneProto(l.node)
}
// for testing only
func (l *LocalNodeImpl) SetNodeID(nodeID livekit.NodeID) {
l.lock.Lock()
defer l.lock.Unlock()
l.node.Id = string(nodeID)
}
func (l *LocalNodeImpl) NodeID() livekit.NodeID {
l.lock.RLock()
defer l.lock.RUnlock()
return livekit.NodeID(l.node.Id)
}
func (l *LocalNodeImpl) NodeType() livekit.NodeType {
l.lock.RLock()
defer l.lock.RUnlock()
return l.node.Type
}
func (l *LocalNodeImpl) NodeIP() string {
l.lock.RLock()
defer l.lock.RUnlock()
return l.node.Ip
}
func (l *LocalNodeImpl) Region() string {
l.lock.RLock()
defer l.lock.RUnlock()
return l.node.Region
}
func (l *LocalNodeImpl) SetState(state livekit.NodeState) {
l.lock.Lock()
defer l.lock.Unlock()
l.node.State = state
}
// for testing only
func (l *LocalNodeImpl) SetStats(stats *livekit.NodeStats) {
l.lock.Lock()
defer l.lock.Unlock()
l.node.Stats = utils.CloneProto(stats)
}
func (l *LocalNodeImpl) UpdateNodeStats() bool {
l.lock.Lock()
defer l.lock.Unlock()
if l.prevStats == nil {
l.prevStats = l.node.Stats
}
updated, computedAvg, err := prometheus.GetUpdatedNodeStats(l.node.Stats, l.prevStats)
if err != nil {
logger.Errorw("could not update node stats", err)
return false
}
l.node.Stats = updated
if computedAvg {
l.prevStats = updated
}
return true
}
func (l *LocalNodeImpl) SecondsSinceNodeStatsUpdate() float64 {
l.lock.RLock()
defer l.lock.RUnlock()
return time.Since(time.Unix(l.node.Stats.UpdatedAt, 0)).Seconds()
}
+257
View File
@@ -0,0 +1,257 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"bytes"
"context"
"runtime/pprof"
"time"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/livekit-server/pkg/routing/selector"
)
const (
// expire participant mappings after a day
participantMappingTTL = 24 * time.Hour
statsUpdateInterval = 2 * time.Second
statsMaxDelaySeconds = float64(30)
// hash of node_id => Node proto
NodesKey = "nodes"
// hash of room_name => node_id
NodeRoomKey = "room_node_map"
)
var _ Router = (*RedisRouter)(nil)
// RedisRouter uses Redis pub/sub to route signaling messages across different nodes
// It relies on the RTC node to be the primary driver of the participant connection.
// Because
type RedisRouter struct {
*LocalRouter
rc redis.UniversalClient
kps rpc.KeepalivePubSub
ctx context.Context
isStarted atomic.Bool
cancel func()
}
func NewRedisRouter(lr *LocalRouter, rc redis.UniversalClient, kps rpc.KeepalivePubSub) *RedisRouter {
rr := &RedisRouter{
LocalRouter: lr,
rc: rc,
kps: kps,
}
rr.ctx, rr.cancel = context.WithCancel(context.Background())
return rr
}
func (r *RedisRouter) RegisterNode() error {
data, err := proto.Marshal(r.currentNode.Clone())
if err != nil {
return err
}
if err := r.rc.HSet(r.ctx, NodesKey, string(r.currentNode.NodeID()), data).Err(); err != nil {
return errors.Wrap(err, "could not register node")
}
return nil
}
func (r *RedisRouter) UnregisterNode() error {
// could be called after Stop(), so we'd want to use an unrelated context
return r.rc.HDel(context.Background(), NodesKey, string(r.currentNode.NodeID())).Err()
}
func (r *RedisRouter) RemoveDeadNodes() error {
nodes, err := r.ListNodes()
if err != nil {
return err
}
for _, n := range nodes {
if !selector.IsAvailable(n) {
if err := r.rc.HDel(context.Background(), NodesKey, n.Id).Err(); err != nil {
return err
}
}
}
return nil
}
// GetNodeForRoom finds the node where the room is hosted at
func (r *RedisRouter) GetNodeForRoom(_ context.Context, roomName livekit.RoomName) (*livekit.Node, error) {
nodeID, err := r.rc.HGet(r.ctx, NodeRoomKey, string(roomName)).Result()
if err == redis.Nil {
return nil, ErrNotFound
} else if err != nil {
return nil, errors.Wrap(err, "could not get node for room")
}
return r.GetNode(livekit.NodeID(nodeID))
}
func (r *RedisRouter) SetNodeForRoom(_ context.Context, roomName livekit.RoomName, nodeID livekit.NodeID) error {
return r.rc.HSet(r.ctx, NodeRoomKey, string(roomName), string(nodeID)).Err()
}
func (r *RedisRouter) ClearRoomState(_ context.Context, roomName livekit.RoomName) error {
if err := r.rc.HDel(context.Background(), NodeRoomKey, string(roomName)).Err(); err != nil {
return errors.Wrap(err, "could not clear room state")
}
return nil
}
func (r *RedisRouter) GetNode(nodeID livekit.NodeID) (*livekit.Node, error) {
data, err := r.rc.HGet(r.ctx, NodesKey, string(nodeID)).Result()
if err == redis.Nil {
return nil, ErrNotFound
} else if err != nil {
return nil, err
}
n := livekit.Node{}
if err = proto.Unmarshal([]byte(data), &n); err != nil {
return nil, err
}
return &n, nil
}
func (r *RedisRouter) ListNodes() ([]*livekit.Node, error) {
items, err := r.rc.HVals(r.ctx, NodesKey).Result()
if err != nil {
return nil, errors.Wrap(err, "could not list nodes")
}
nodes := make([]*livekit.Node, 0, len(items))
for _, item := range items {
n := livekit.Node{}
if err := proto.Unmarshal([]byte(item), &n); err != nil {
return nil, err
}
nodes = append(nodes, &n)
}
return nodes, nil
}
func (r *RedisRouter) CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (res *livekit.Room, err error) {
rtcNode, err := r.GetNodeForRoom(ctx, livekit.RoomName(req.Name))
if err != nil {
return
}
return r.CreateRoomWithNodeID(ctx, req, livekit.NodeID(rtcNode.Id))
}
// StartParticipantSignal signal connection sets up paths to the RTC node, and starts to route messages to that message queue
func (r *RedisRouter) StartParticipantSignal(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit) (res StartParticipantSignalResults, err error) {
rtcNode, err := r.GetNodeForRoom(ctx, roomName)
if err != nil {
return
}
return r.StartParticipantSignalWithNodeID(ctx, roomName, pi, livekit.NodeID(rtcNode.Id))
}
func (r *RedisRouter) Start() error {
if r.isStarted.Swap(true) {
return nil
}
workerStarted := make(chan error)
go r.statsWorker()
go r.keepaliveWorker(workerStarted)
// wait until worker is running
return <-workerStarted
}
func (r *RedisRouter) Drain() {
r.currentNode.SetState(livekit.NodeState_SHUTTING_DOWN)
if err := r.RegisterNode(); err != nil {
logger.Errorw("failed to mark as draining", err, "nodeID", r.currentNode.NodeID())
}
}
func (r *RedisRouter) Stop() {
if !r.isStarted.Swap(false) {
return
}
logger.Debugw("stopping RedisRouter")
_ = r.UnregisterNode()
r.cancel()
}
// update node stats and cleanup
func (r *RedisRouter) statsWorker() {
goroutineDumped := false
for r.ctx.Err() == nil {
// update periodically
select {
case <-time.After(statsUpdateInterval):
r.kps.PublishPing(r.ctx, r.currentNode.NodeID(), &rpc.KeepalivePing{Timestamp: time.Now().Unix()})
delaySeconds := r.currentNode.SecondsSinceNodeStatsUpdate()
if delaySeconds > statsMaxDelaySeconds {
if !goroutineDumped {
goroutineDumped = true
buf := bytes.NewBuffer(nil)
_ = pprof.Lookup("goroutine").WriteTo(buf, 2)
logger.Errorw("status update delayed, possible deadlock", nil,
"delay", delaySeconds,
"goroutines", buf.String())
}
} else {
goroutineDumped = false
}
case <-r.ctx.Done():
return
}
}
}
func (r *RedisRouter) keepaliveWorker(startedChan chan error) {
pings, err := r.kps.SubscribePing(r.ctx, r.currentNode.NodeID())
if err != nil {
startedChan <- err
return
}
close(startedChan)
for ping := range pings.Channel() {
if time.Since(time.Unix(ping.Timestamp, 0)) > statsUpdateInterval {
logger.Infow("keep alive too old, skipping", "timestamp", ping.Timestamp)
continue
}
if !r.currentNode.UpdateNodeStats() {
continue
}
// TODO: check stats against config.Limit values
if err := r.RegisterNode(); err != nil {
logger.Errorw("could not update node", err)
}
}
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"context"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware"
)
//counterfeiter:generate . RoomManagerClient
type RoomManagerClient interface {
rpc.TypedRoomManagerClient
}
type roomManagerClient struct {
config config.RoomConfig
client rpc.TypedRoomManagerClient
}
func NewRoomManagerClient(clientParams rpc.ClientParams, config config.RoomConfig) (RoomManagerClient, error) {
c, err := rpc.NewTypedRoomManagerClient(
clientParams.Bus,
psrpc.WithClientChannelSize(clientParams.BufferSize),
middleware.WithClientMetrics(clientParams.Observer),
rpc.WithClientLogger(clientParams.Logger),
)
if err != nil {
return nil, err
}
return &roomManagerClient{
config: config,
client: c,
}, nil
}
func (c *roomManagerClient) CreateRoom(ctx context.Context, nodeID livekit.NodeID, req *livekit.CreateRoomRequest, opts ...psrpc.RequestOption) (*livekit.Room, error) {
return c.client.CreateRoom(ctx, nodeID, req, append(opts, psrpc.WithRequestInterceptors(middleware.NewRPCRetryInterceptor(middleware.RetryOptions{
MaxAttempts: c.config.CreateRoomAttempts,
Timeout: c.config.CreateRoomTimeout,
})))...)
}
func (c *roomManagerClient) Close() {
c.client.Close()
}
@@ -0,0 +1,273 @@
// Code generated by counterfeiter. DO NOT EDIT.
package routingfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/proto"
)
type FakeMessageSink struct {
CloseStub func()
closeMutex sync.RWMutex
closeArgsForCall []struct {
}
ConnectionIDStub func() livekit.ConnectionID
connectionIDMutex sync.RWMutex
connectionIDArgsForCall []struct {
}
connectionIDReturns struct {
result1 livekit.ConnectionID
}
connectionIDReturnsOnCall map[int]struct {
result1 livekit.ConnectionID
}
IsClosedStub func() bool
isClosedMutex sync.RWMutex
isClosedArgsForCall []struct {
}
isClosedReturns struct {
result1 bool
}
isClosedReturnsOnCall map[int]struct {
result1 bool
}
WriteMessageStub func(proto.Message) error
writeMessageMutex sync.RWMutex
writeMessageArgsForCall []struct {
arg1 proto.Message
}
writeMessageReturns struct {
result1 error
}
writeMessageReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeMessageSink) Close() {
fake.closeMutex.Lock()
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
}{})
stub := fake.CloseStub
fake.recordInvocation("Close", []interface{}{})
fake.closeMutex.Unlock()
if stub != nil {
fake.CloseStub()
}
}
func (fake *FakeMessageSink) CloseCallCount() int {
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
return len(fake.closeArgsForCall)
}
func (fake *FakeMessageSink) CloseCalls(stub func()) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = stub
}
func (fake *FakeMessageSink) ConnectionID() livekit.ConnectionID {
fake.connectionIDMutex.Lock()
ret, specificReturn := fake.connectionIDReturnsOnCall[len(fake.connectionIDArgsForCall)]
fake.connectionIDArgsForCall = append(fake.connectionIDArgsForCall, struct {
}{})
stub := fake.ConnectionIDStub
fakeReturns := fake.connectionIDReturns
fake.recordInvocation("ConnectionID", []interface{}{})
fake.connectionIDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeMessageSink) ConnectionIDCallCount() int {
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
return len(fake.connectionIDArgsForCall)
}
func (fake *FakeMessageSink) ConnectionIDCalls(stub func() livekit.ConnectionID) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = stub
}
func (fake *FakeMessageSink) ConnectionIDReturns(result1 livekit.ConnectionID) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
fake.connectionIDReturns = struct {
result1 livekit.ConnectionID
}{result1}
}
func (fake *FakeMessageSink) ConnectionIDReturnsOnCall(i int, result1 livekit.ConnectionID) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
if fake.connectionIDReturnsOnCall == nil {
fake.connectionIDReturnsOnCall = make(map[int]struct {
result1 livekit.ConnectionID
})
}
fake.connectionIDReturnsOnCall[i] = struct {
result1 livekit.ConnectionID
}{result1}
}
func (fake *FakeMessageSink) IsClosed() bool {
fake.isClosedMutex.Lock()
ret, specificReturn := fake.isClosedReturnsOnCall[len(fake.isClosedArgsForCall)]
fake.isClosedArgsForCall = append(fake.isClosedArgsForCall, struct {
}{})
stub := fake.IsClosedStub
fakeReturns := fake.isClosedReturns
fake.recordInvocation("IsClosed", []interface{}{})
fake.isClosedMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeMessageSink) IsClosedCallCount() int {
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
return len(fake.isClosedArgsForCall)
}
func (fake *FakeMessageSink) IsClosedCalls(stub func() bool) {
fake.isClosedMutex.Lock()
defer fake.isClosedMutex.Unlock()
fake.IsClosedStub = stub
}
func (fake *FakeMessageSink) IsClosedReturns(result1 bool) {
fake.isClosedMutex.Lock()
defer fake.isClosedMutex.Unlock()
fake.IsClosedStub = nil
fake.isClosedReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeMessageSink) IsClosedReturnsOnCall(i int, result1 bool) {
fake.isClosedMutex.Lock()
defer fake.isClosedMutex.Unlock()
fake.IsClosedStub = nil
if fake.isClosedReturnsOnCall == nil {
fake.isClosedReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.isClosedReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeMessageSink) WriteMessage(arg1 proto.Message) error {
fake.writeMessageMutex.Lock()
ret, specificReturn := fake.writeMessageReturnsOnCall[len(fake.writeMessageArgsForCall)]
fake.writeMessageArgsForCall = append(fake.writeMessageArgsForCall, struct {
arg1 proto.Message
}{arg1})
stub := fake.WriteMessageStub
fakeReturns := fake.writeMessageReturns
fake.recordInvocation("WriteMessage", []interface{}{arg1})
fake.writeMessageMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeMessageSink) WriteMessageCallCount() int {
fake.writeMessageMutex.RLock()
defer fake.writeMessageMutex.RUnlock()
return len(fake.writeMessageArgsForCall)
}
func (fake *FakeMessageSink) WriteMessageCalls(stub func(proto.Message) error) {
fake.writeMessageMutex.Lock()
defer fake.writeMessageMutex.Unlock()
fake.WriteMessageStub = stub
}
func (fake *FakeMessageSink) WriteMessageArgsForCall(i int) proto.Message {
fake.writeMessageMutex.RLock()
defer fake.writeMessageMutex.RUnlock()
argsForCall := fake.writeMessageArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeMessageSink) WriteMessageReturns(result1 error) {
fake.writeMessageMutex.Lock()
defer fake.writeMessageMutex.Unlock()
fake.WriteMessageStub = nil
fake.writeMessageReturns = struct {
result1 error
}{result1}
}
func (fake *FakeMessageSink) WriteMessageReturnsOnCall(i int, result1 error) {
fake.writeMessageMutex.Lock()
defer fake.writeMessageMutex.Unlock()
fake.WriteMessageStub = nil
if fake.writeMessageReturnsOnCall == nil {
fake.writeMessageReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.writeMessageReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeMessageSink) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
fake.writeMessageMutex.RLock()
defer fake.writeMessageMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeMessageSink) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ routing.MessageSink = new(FakeMessageSink)
@@ -0,0 +1,264 @@
// Code generated by counterfeiter. DO NOT EDIT.
package routingfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/proto"
)
type FakeMessageSource struct {
CloseStub func()
closeMutex sync.RWMutex
closeArgsForCall []struct {
}
ConnectionIDStub func() livekit.ConnectionID
connectionIDMutex sync.RWMutex
connectionIDArgsForCall []struct {
}
connectionIDReturns struct {
result1 livekit.ConnectionID
}
connectionIDReturnsOnCall map[int]struct {
result1 livekit.ConnectionID
}
IsClosedStub func() bool
isClosedMutex sync.RWMutex
isClosedArgsForCall []struct {
}
isClosedReturns struct {
result1 bool
}
isClosedReturnsOnCall map[int]struct {
result1 bool
}
ReadChanStub func() <-chan proto.Message
readChanMutex sync.RWMutex
readChanArgsForCall []struct {
}
readChanReturns struct {
result1 <-chan proto.Message
}
readChanReturnsOnCall map[int]struct {
result1 <-chan proto.Message
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeMessageSource) Close() {
fake.closeMutex.Lock()
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
}{})
stub := fake.CloseStub
fake.recordInvocation("Close", []interface{}{})
fake.closeMutex.Unlock()
if stub != nil {
fake.CloseStub()
}
}
func (fake *FakeMessageSource) CloseCallCount() int {
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
return len(fake.closeArgsForCall)
}
func (fake *FakeMessageSource) CloseCalls(stub func()) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = stub
}
func (fake *FakeMessageSource) ConnectionID() livekit.ConnectionID {
fake.connectionIDMutex.Lock()
ret, specificReturn := fake.connectionIDReturnsOnCall[len(fake.connectionIDArgsForCall)]
fake.connectionIDArgsForCall = append(fake.connectionIDArgsForCall, struct {
}{})
stub := fake.ConnectionIDStub
fakeReturns := fake.connectionIDReturns
fake.recordInvocation("ConnectionID", []interface{}{})
fake.connectionIDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeMessageSource) ConnectionIDCallCount() int {
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
return len(fake.connectionIDArgsForCall)
}
func (fake *FakeMessageSource) ConnectionIDCalls(stub func() livekit.ConnectionID) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = stub
}
func (fake *FakeMessageSource) ConnectionIDReturns(result1 livekit.ConnectionID) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
fake.connectionIDReturns = struct {
result1 livekit.ConnectionID
}{result1}
}
func (fake *FakeMessageSource) ConnectionIDReturnsOnCall(i int, result1 livekit.ConnectionID) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
if fake.connectionIDReturnsOnCall == nil {
fake.connectionIDReturnsOnCall = make(map[int]struct {
result1 livekit.ConnectionID
})
}
fake.connectionIDReturnsOnCall[i] = struct {
result1 livekit.ConnectionID
}{result1}
}
func (fake *FakeMessageSource) IsClosed() bool {
fake.isClosedMutex.Lock()
ret, specificReturn := fake.isClosedReturnsOnCall[len(fake.isClosedArgsForCall)]
fake.isClosedArgsForCall = append(fake.isClosedArgsForCall, struct {
}{})
stub := fake.IsClosedStub
fakeReturns := fake.isClosedReturns
fake.recordInvocation("IsClosed", []interface{}{})
fake.isClosedMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeMessageSource) IsClosedCallCount() int {
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
return len(fake.isClosedArgsForCall)
}
func (fake *FakeMessageSource) IsClosedCalls(stub func() bool) {
fake.isClosedMutex.Lock()
defer fake.isClosedMutex.Unlock()
fake.IsClosedStub = stub
}
func (fake *FakeMessageSource) IsClosedReturns(result1 bool) {
fake.isClosedMutex.Lock()
defer fake.isClosedMutex.Unlock()
fake.IsClosedStub = nil
fake.isClosedReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeMessageSource) IsClosedReturnsOnCall(i int, result1 bool) {
fake.isClosedMutex.Lock()
defer fake.isClosedMutex.Unlock()
fake.IsClosedStub = nil
if fake.isClosedReturnsOnCall == nil {
fake.isClosedReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.isClosedReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeMessageSource) ReadChan() <-chan proto.Message {
fake.readChanMutex.Lock()
ret, specificReturn := fake.readChanReturnsOnCall[len(fake.readChanArgsForCall)]
fake.readChanArgsForCall = append(fake.readChanArgsForCall, struct {
}{})
stub := fake.ReadChanStub
fakeReturns := fake.readChanReturns
fake.recordInvocation("ReadChan", []interface{}{})
fake.readChanMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeMessageSource) ReadChanCallCount() int {
fake.readChanMutex.RLock()
defer fake.readChanMutex.RUnlock()
return len(fake.readChanArgsForCall)
}
func (fake *FakeMessageSource) ReadChanCalls(stub func() <-chan proto.Message) {
fake.readChanMutex.Lock()
defer fake.readChanMutex.Unlock()
fake.ReadChanStub = stub
}
func (fake *FakeMessageSource) ReadChanReturns(result1 <-chan proto.Message) {
fake.readChanMutex.Lock()
defer fake.readChanMutex.Unlock()
fake.ReadChanStub = nil
fake.readChanReturns = struct {
result1 <-chan proto.Message
}{result1}
}
func (fake *FakeMessageSource) ReadChanReturnsOnCall(i int, result1 <-chan proto.Message) {
fake.readChanMutex.Lock()
defer fake.readChanMutex.Unlock()
fake.ReadChanStub = nil
if fake.readChanReturnsOnCall == nil {
fake.readChanReturnsOnCall = make(map[int]struct {
result1 <-chan proto.Message
})
}
fake.readChanReturnsOnCall[i] = struct {
result1 <-chan proto.Message
}{result1}
}
func (fake *FakeMessageSource) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
fake.readChanMutex.RLock()
defer fake.readChanMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeMessageSource) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ routing.MessageSource = new(FakeMessageSource)
@@ -0,0 +1,155 @@
// Code generated by counterfeiter. DO NOT EDIT.
package routingfakes
import (
"context"
"sync"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/protocol/livekit"
"github.com/livekit/psrpc"
)
type FakeRoomManagerClient struct {
CloseStub func()
closeMutex sync.RWMutex
closeArgsForCall []struct {
}
CreateRoomStub func(context.Context, livekit.NodeID, *livekit.CreateRoomRequest, ...psrpc.RequestOption) (*livekit.Room, error)
createRoomMutex sync.RWMutex
createRoomArgsForCall []struct {
arg1 context.Context
arg2 livekit.NodeID
arg3 *livekit.CreateRoomRequest
arg4 []psrpc.RequestOption
}
createRoomReturns struct {
result1 *livekit.Room
result2 error
}
createRoomReturnsOnCall map[int]struct {
result1 *livekit.Room
result2 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeRoomManagerClient) Close() {
fake.closeMutex.Lock()
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
}{})
stub := fake.CloseStub
fake.recordInvocation("Close", []interface{}{})
fake.closeMutex.Unlock()
if stub != nil {
fake.CloseStub()
}
}
func (fake *FakeRoomManagerClient) CloseCallCount() int {
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
return len(fake.closeArgsForCall)
}
func (fake *FakeRoomManagerClient) CloseCalls(stub func()) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = stub
}
func (fake *FakeRoomManagerClient) CreateRoom(arg1 context.Context, arg2 livekit.NodeID, arg3 *livekit.CreateRoomRequest, arg4 ...psrpc.RequestOption) (*livekit.Room, error) {
fake.createRoomMutex.Lock()
ret, specificReturn := fake.createRoomReturnsOnCall[len(fake.createRoomArgsForCall)]
fake.createRoomArgsForCall = append(fake.createRoomArgsForCall, struct {
arg1 context.Context
arg2 livekit.NodeID
arg3 *livekit.CreateRoomRequest
arg4 []psrpc.RequestOption
}{arg1, arg2, arg3, arg4})
stub := fake.CreateRoomStub
fakeReturns := fake.createRoomReturns
fake.recordInvocation("CreateRoom", []interface{}{arg1, arg2, arg3, arg4})
fake.createRoomMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3, arg4...)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeRoomManagerClient) CreateRoomCallCount() int {
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
return len(fake.createRoomArgsForCall)
}
func (fake *FakeRoomManagerClient) CreateRoomCalls(stub func(context.Context, livekit.NodeID, *livekit.CreateRoomRequest, ...psrpc.RequestOption) (*livekit.Room, error)) {
fake.createRoomMutex.Lock()
defer fake.createRoomMutex.Unlock()
fake.CreateRoomStub = stub
}
func (fake *FakeRoomManagerClient) CreateRoomArgsForCall(i int) (context.Context, livekit.NodeID, *livekit.CreateRoomRequest, []psrpc.RequestOption) {
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
argsForCall := fake.createRoomArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeRoomManagerClient) CreateRoomReturns(result1 *livekit.Room, result2 error) {
fake.createRoomMutex.Lock()
defer fake.createRoomMutex.Unlock()
fake.CreateRoomStub = nil
fake.createRoomReturns = struct {
result1 *livekit.Room
result2 error
}{result1, result2}
}
func (fake *FakeRoomManagerClient) CreateRoomReturnsOnCall(i int, result1 *livekit.Room, result2 error) {
fake.createRoomMutex.Lock()
defer fake.createRoomMutex.Unlock()
fake.CreateRoomStub = nil
if fake.createRoomReturnsOnCall == nil {
fake.createRoomReturnsOnCall = make(map[int]struct {
result1 *livekit.Room
result2 error
})
}
fake.createRoomReturnsOnCall[i] = struct {
result1 *livekit.Room
result2 error
}{result1, result2}
}
func (fake *FakeRoomManagerClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeRoomManagerClient) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ routing.RoomManagerClient = new(FakeRoomManagerClient)
@@ -0,0 +1,893 @@
// Code generated by counterfeiter. DO NOT EDIT.
package routingfakes
import (
"context"
"sync"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/protocol/livekit"
)
type FakeRouter struct {
ClearRoomStateStub func(context.Context, livekit.RoomName) error
clearRoomStateMutex sync.RWMutex
clearRoomStateArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
clearRoomStateReturns struct {
result1 error
}
clearRoomStateReturnsOnCall map[int]struct {
result1 error
}
CreateRoomStub func(context.Context, *livekit.CreateRoomRequest) (*livekit.Room, error)
createRoomMutex sync.RWMutex
createRoomArgsForCall []struct {
arg1 context.Context
arg2 *livekit.CreateRoomRequest
}
createRoomReturns struct {
result1 *livekit.Room
result2 error
}
createRoomReturnsOnCall map[int]struct {
result1 *livekit.Room
result2 error
}
DrainStub func()
drainMutex sync.RWMutex
drainArgsForCall []struct {
}
GetNodeForRoomStub func(context.Context, livekit.RoomName) (*livekit.Node, error)
getNodeForRoomMutex sync.RWMutex
getNodeForRoomArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
getNodeForRoomReturns struct {
result1 *livekit.Node
result2 error
}
getNodeForRoomReturnsOnCall map[int]struct {
result1 *livekit.Node
result2 error
}
GetRegionStub func() string
getRegionMutex sync.RWMutex
getRegionArgsForCall []struct {
}
getRegionReturns struct {
result1 string
}
getRegionReturnsOnCall map[int]struct {
result1 string
}
ListNodesStub func() ([]*livekit.Node, error)
listNodesMutex sync.RWMutex
listNodesArgsForCall []struct {
}
listNodesReturns struct {
result1 []*livekit.Node
result2 error
}
listNodesReturnsOnCall map[int]struct {
result1 []*livekit.Node
result2 error
}
RegisterNodeStub func() error
registerNodeMutex sync.RWMutex
registerNodeArgsForCall []struct {
}
registerNodeReturns struct {
result1 error
}
registerNodeReturnsOnCall map[int]struct {
result1 error
}
RemoveDeadNodesStub func() error
removeDeadNodesMutex sync.RWMutex
removeDeadNodesArgsForCall []struct {
}
removeDeadNodesReturns struct {
result1 error
}
removeDeadNodesReturnsOnCall map[int]struct {
result1 error
}
SetNodeForRoomStub func(context.Context, livekit.RoomName, livekit.NodeID) error
setNodeForRoomMutex sync.RWMutex
setNodeForRoomArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.NodeID
}
setNodeForRoomReturns struct {
result1 error
}
setNodeForRoomReturnsOnCall map[int]struct {
result1 error
}
StartStub func() error
startMutex sync.RWMutex
startArgsForCall []struct {
}
startReturns struct {
result1 error
}
startReturnsOnCall map[int]struct {
result1 error
}
StartParticipantSignalStub func(context.Context, livekit.RoomName, routing.ParticipantInit) (routing.StartParticipantSignalResults, error)
startParticipantSignalMutex sync.RWMutex
startParticipantSignalArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 routing.ParticipantInit
}
startParticipantSignalReturns struct {
result1 routing.StartParticipantSignalResults
result2 error
}
startParticipantSignalReturnsOnCall map[int]struct {
result1 routing.StartParticipantSignalResults
result2 error
}
StopStub func()
stopMutex sync.RWMutex
stopArgsForCall []struct {
}
UnregisterNodeStub func() error
unregisterNodeMutex sync.RWMutex
unregisterNodeArgsForCall []struct {
}
unregisterNodeReturns struct {
result1 error
}
unregisterNodeReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeRouter) ClearRoomState(arg1 context.Context, arg2 livekit.RoomName) error {
fake.clearRoomStateMutex.Lock()
ret, specificReturn := fake.clearRoomStateReturnsOnCall[len(fake.clearRoomStateArgsForCall)]
fake.clearRoomStateArgsForCall = append(fake.clearRoomStateArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.ClearRoomStateStub
fakeReturns := fake.clearRoomStateReturns
fake.recordInvocation("ClearRoomState", []interface{}{arg1, arg2})
fake.clearRoomStateMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRouter) ClearRoomStateCallCount() int {
fake.clearRoomStateMutex.RLock()
defer fake.clearRoomStateMutex.RUnlock()
return len(fake.clearRoomStateArgsForCall)
}
func (fake *FakeRouter) ClearRoomStateCalls(stub func(context.Context, livekit.RoomName) error) {
fake.clearRoomStateMutex.Lock()
defer fake.clearRoomStateMutex.Unlock()
fake.ClearRoomStateStub = stub
}
func (fake *FakeRouter) ClearRoomStateArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.clearRoomStateMutex.RLock()
defer fake.clearRoomStateMutex.RUnlock()
argsForCall := fake.clearRoomStateArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeRouter) ClearRoomStateReturns(result1 error) {
fake.clearRoomStateMutex.Lock()
defer fake.clearRoomStateMutex.Unlock()
fake.ClearRoomStateStub = nil
fake.clearRoomStateReturns = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) ClearRoomStateReturnsOnCall(i int, result1 error) {
fake.clearRoomStateMutex.Lock()
defer fake.clearRoomStateMutex.Unlock()
fake.ClearRoomStateStub = nil
if fake.clearRoomStateReturnsOnCall == nil {
fake.clearRoomStateReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.clearRoomStateReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) CreateRoom(arg1 context.Context, arg2 *livekit.CreateRoomRequest) (*livekit.Room, error) {
fake.createRoomMutex.Lock()
ret, specificReturn := fake.createRoomReturnsOnCall[len(fake.createRoomArgsForCall)]
fake.createRoomArgsForCall = append(fake.createRoomArgsForCall, struct {
arg1 context.Context
arg2 *livekit.CreateRoomRequest
}{arg1, arg2})
stub := fake.CreateRoomStub
fakeReturns := fake.createRoomReturns
fake.recordInvocation("CreateRoom", []interface{}{arg1, arg2})
fake.createRoomMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeRouter) CreateRoomCallCount() int {
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
return len(fake.createRoomArgsForCall)
}
func (fake *FakeRouter) CreateRoomCalls(stub func(context.Context, *livekit.CreateRoomRequest) (*livekit.Room, error)) {
fake.createRoomMutex.Lock()
defer fake.createRoomMutex.Unlock()
fake.CreateRoomStub = stub
}
func (fake *FakeRouter) CreateRoomArgsForCall(i int) (context.Context, *livekit.CreateRoomRequest) {
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
argsForCall := fake.createRoomArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeRouter) CreateRoomReturns(result1 *livekit.Room, result2 error) {
fake.createRoomMutex.Lock()
defer fake.createRoomMutex.Unlock()
fake.CreateRoomStub = nil
fake.createRoomReturns = struct {
result1 *livekit.Room
result2 error
}{result1, result2}
}
func (fake *FakeRouter) CreateRoomReturnsOnCall(i int, result1 *livekit.Room, result2 error) {
fake.createRoomMutex.Lock()
defer fake.createRoomMutex.Unlock()
fake.CreateRoomStub = nil
if fake.createRoomReturnsOnCall == nil {
fake.createRoomReturnsOnCall = make(map[int]struct {
result1 *livekit.Room
result2 error
})
}
fake.createRoomReturnsOnCall[i] = struct {
result1 *livekit.Room
result2 error
}{result1, result2}
}
func (fake *FakeRouter) Drain() {
fake.drainMutex.Lock()
fake.drainArgsForCall = append(fake.drainArgsForCall, struct {
}{})
stub := fake.DrainStub
fake.recordInvocation("Drain", []interface{}{})
fake.drainMutex.Unlock()
if stub != nil {
fake.DrainStub()
}
}
func (fake *FakeRouter) DrainCallCount() int {
fake.drainMutex.RLock()
defer fake.drainMutex.RUnlock()
return len(fake.drainArgsForCall)
}
func (fake *FakeRouter) DrainCalls(stub func()) {
fake.drainMutex.Lock()
defer fake.drainMutex.Unlock()
fake.DrainStub = stub
}
func (fake *FakeRouter) GetNodeForRoom(arg1 context.Context, arg2 livekit.RoomName) (*livekit.Node, error) {
fake.getNodeForRoomMutex.Lock()
ret, specificReturn := fake.getNodeForRoomReturnsOnCall[len(fake.getNodeForRoomArgsForCall)]
fake.getNodeForRoomArgsForCall = append(fake.getNodeForRoomArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.GetNodeForRoomStub
fakeReturns := fake.getNodeForRoomReturns
fake.recordInvocation("GetNodeForRoom", []interface{}{arg1, arg2})
fake.getNodeForRoomMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeRouter) GetNodeForRoomCallCount() int {
fake.getNodeForRoomMutex.RLock()
defer fake.getNodeForRoomMutex.RUnlock()
return len(fake.getNodeForRoomArgsForCall)
}
func (fake *FakeRouter) GetNodeForRoomCalls(stub func(context.Context, livekit.RoomName) (*livekit.Node, error)) {
fake.getNodeForRoomMutex.Lock()
defer fake.getNodeForRoomMutex.Unlock()
fake.GetNodeForRoomStub = stub
}
func (fake *FakeRouter) GetNodeForRoomArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.getNodeForRoomMutex.RLock()
defer fake.getNodeForRoomMutex.RUnlock()
argsForCall := fake.getNodeForRoomArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeRouter) GetNodeForRoomReturns(result1 *livekit.Node, result2 error) {
fake.getNodeForRoomMutex.Lock()
defer fake.getNodeForRoomMutex.Unlock()
fake.GetNodeForRoomStub = nil
fake.getNodeForRoomReturns = struct {
result1 *livekit.Node
result2 error
}{result1, result2}
}
func (fake *FakeRouter) GetNodeForRoomReturnsOnCall(i int, result1 *livekit.Node, result2 error) {
fake.getNodeForRoomMutex.Lock()
defer fake.getNodeForRoomMutex.Unlock()
fake.GetNodeForRoomStub = nil
if fake.getNodeForRoomReturnsOnCall == nil {
fake.getNodeForRoomReturnsOnCall = make(map[int]struct {
result1 *livekit.Node
result2 error
})
}
fake.getNodeForRoomReturnsOnCall[i] = struct {
result1 *livekit.Node
result2 error
}{result1, result2}
}
func (fake *FakeRouter) GetRegion() string {
fake.getRegionMutex.Lock()
ret, specificReturn := fake.getRegionReturnsOnCall[len(fake.getRegionArgsForCall)]
fake.getRegionArgsForCall = append(fake.getRegionArgsForCall, struct {
}{})
stub := fake.GetRegionStub
fakeReturns := fake.getRegionReturns
fake.recordInvocation("GetRegion", []interface{}{})
fake.getRegionMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRouter) GetRegionCallCount() int {
fake.getRegionMutex.RLock()
defer fake.getRegionMutex.RUnlock()
return len(fake.getRegionArgsForCall)
}
func (fake *FakeRouter) GetRegionCalls(stub func() string) {
fake.getRegionMutex.Lock()
defer fake.getRegionMutex.Unlock()
fake.GetRegionStub = stub
}
func (fake *FakeRouter) GetRegionReturns(result1 string) {
fake.getRegionMutex.Lock()
defer fake.getRegionMutex.Unlock()
fake.GetRegionStub = nil
fake.getRegionReturns = struct {
result1 string
}{result1}
}
func (fake *FakeRouter) GetRegionReturnsOnCall(i int, result1 string) {
fake.getRegionMutex.Lock()
defer fake.getRegionMutex.Unlock()
fake.GetRegionStub = nil
if fake.getRegionReturnsOnCall == nil {
fake.getRegionReturnsOnCall = make(map[int]struct {
result1 string
})
}
fake.getRegionReturnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *FakeRouter) ListNodes() ([]*livekit.Node, error) {
fake.listNodesMutex.Lock()
ret, specificReturn := fake.listNodesReturnsOnCall[len(fake.listNodesArgsForCall)]
fake.listNodesArgsForCall = append(fake.listNodesArgsForCall, struct {
}{})
stub := fake.ListNodesStub
fakeReturns := fake.listNodesReturns
fake.recordInvocation("ListNodes", []interface{}{})
fake.listNodesMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeRouter) ListNodesCallCount() int {
fake.listNodesMutex.RLock()
defer fake.listNodesMutex.RUnlock()
return len(fake.listNodesArgsForCall)
}
func (fake *FakeRouter) ListNodesCalls(stub func() ([]*livekit.Node, error)) {
fake.listNodesMutex.Lock()
defer fake.listNodesMutex.Unlock()
fake.ListNodesStub = stub
}
func (fake *FakeRouter) ListNodesReturns(result1 []*livekit.Node, result2 error) {
fake.listNodesMutex.Lock()
defer fake.listNodesMutex.Unlock()
fake.ListNodesStub = nil
fake.listNodesReturns = struct {
result1 []*livekit.Node
result2 error
}{result1, result2}
}
func (fake *FakeRouter) ListNodesReturnsOnCall(i int, result1 []*livekit.Node, result2 error) {
fake.listNodesMutex.Lock()
defer fake.listNodesMutex.Unlock()
fake.ListNodesStub = nil
if fake.listNodesReturnsOnCall == nil {
fake.listNodesReturnsOnCall = make(map[int]struct {
result1 []*livekit.Node
result2 error
})
}
fake.listNodesReturnsOnCall[i] = struct {
result1 []*livekit.Node
result2 error
}{result1, result2}
}
func (fake *FakeRouter) RegisterNode() error {
fake.registerNodeMutex.Lock()
ret, specificReturn := fake.registerNodeReturnsOnCall[len(fake.registerNodeArgsForCall)]
fake.registerNodeArgsForCall = append(fake.registerNodeArgsForCall, struct {
}{})
stub := fake.RegisterNodeStub
fakeReturns := fake.registerNodeReturns
fake.recordInvocation("RegisterNode", []interface{}{})
fake.registerNodeMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRouter) RegisterNodeCallCount() int {
fake.registerNodeMutex.RLock()
defer fake.registerNodeMutex.RUnlock()
return len(fake.registerNodeArgsForCall)
}
func (fake *FakeRouter) RegisterNodeCalls(stub func() error) {
fake.registerNodeMutex.Lock()
defer fake.registerNodeMutex.Unlock()
fake.RegisterNodeStub = stub
}
func (fake *FakeRouter) RegisterNodeReturns(result1 error) {
fake.registerNodeMutex.Lock()
defer fake.registerNodeMutex.Unlock()
fake.RegisterNodeStub = nil
fake.registerNodeReturns = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) RegisterNodeReturnsOnCall(i int, result1 error) {
fake.registerNodeMutex.Lock()
defer fake.registerNodeMutex.Unlock()
fake.RegisterNodeStub = nil
if fake.registerNodeReturnsOnCall == nil {
fake.registerNodeReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.registerNodeReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) RemoveDeadNodes() error {
fake.removeDeadNodesMutex.Lock()
ret, specificReturn := fake.removeDeadNodesReturnsOnCall[len(fake.removeDeadNodesArgsForCall)]
fake.removeDeadNodesArgsForCall = append(fake.removeDeadNodesArgsForCall, struct {
}{})
stub := fake.RemoveDeadNodesStub
fakeReturns := fake.removeDeadNodesReturns
fake.recordInvocation("RemoveDeadNodes", []interface{}{})
fake.removeDeadNodesMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRouter) RemoveDeadNodesCallCount() int {
fake.removeDeadNodesMutex.RLock()
defer fake.removeDeadNodesMutex.RUnlock()
return len(fake.removeDeadNodesArgsForCall)
}
func (fake *FakeRouter) RemoveDeadNodesCalls(stub func() error) {
fake.removeDeadNodesMutex.Lock()
defer fake.removeDeadNodesMutex.Unlock()
fake.RemoveDeadNodesStub = stub
}
func (fake *FakeRouter) RemoveDeadNodesReturns(result1 error) {
fake.removeDeadNodesMutex.Lock()
defer fake.removeDeadNodesMutex.Unlock()
fake.RemoveDeadNodesStub = nil
fake.removeDeadNodesReturns = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) RemoveDeadNodesReturnsOnCall(i int, result1 error) {
fake.removeDeadNodesMutex.Lock()
defer fake.removeDeadNodesMutex.Unlock()
fake.RemoveDeadNodesStub = nil
if fake.removeDeadNodesReturnsOnCall == nil {
fake.removeDeadNodesReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.removeDeadNodesReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) SetNodeForRoom(arg1 context.Context, arg2 livekit.RoomName, arg3 livekit.NodeID) error {
fake.setNodeForRoomMutex.Lock()
ret, specificReturn := fake.setNodeForRoomReturnsOnCall[len(fake.setNodeForRoomArgsForCall)]
fake.setNodeForRoomArgsForCall = append(fake.setNodeForRoomArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.NodeID
}{arg1, arg2, arg3})
stub := fake.SetNodeForRoomStub
fakeReturns := fake.setNodeForRoomReturns
fake.recordInvocation("SetNodeForRoom", []interface{}{arg1, arg2, arg3})
fake.setNodeForRoomMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRouter) SetNodeForRoomCallCount() int {
fake.setNodeForRoomMutex.RLock()
defer fake.setNodeForRoomMutex.RUnlock()
return len(fake.setNodeForRoomArgsForCall)
}
func (fake *FakeRouter) SetNodeForRoomCalls(stub func(context.Context, livekit.RoomName, livekit.NodeID) error) {
fake.setNodeForRoomMutex.Lock()
defer fake.setNodeForRoomMutex.Unlock()
fake.SetNodeForRoomStub = stub
}
func (fake *FakeRouter) SetNodeForRoomArgsForCall(i int) (context.Context, livekit.RoomName, livekit.NodeID) {
fake.setNodeForRoomMutex.RLock()
defer fake.setNodeForRoomMutex.RUnlock()
argsForCall := fake.setNodeForRoomArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeRouter) SetNodeForRoomReturns(result1 error) {
fake.setNodeForRoomMutex.Lock()
defer fake.setNodeForRoomMutex.Unlock()
fake.SetNodeForRoomStub = nil
fake.setNodeForRoomReturns = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) SetNodeForRoomReturnsOnCall(i int, result1 error) {
fake.setNodeForRoomMutex.Lock()
defer fake.setNodeForRoomMutex.Unlock()
fake.SetNodeForRoomStub = nil
if fake.setNodeForRoomReturnsOnCall == nil {
fake.setNodeForRoomReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.setNodeForRoomReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) Start() error {
fake.startMutex.Lock()
ret, specificReturn := fake.startReturnsOnCall[len(fake.startArgsForCall)]
fake.startArgsForCall = append(fake.startArgsForCall, struct {
}{})
stub := fake.StartStub
fakeReturns := fake.startReturns
fake.recordInvocation("Start", []interface{}{})
fake.startMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRouter) StartCallCount() int {
fake.startMutex.RLock()
defer fake.startMutex.RUnlock()
return len(fake.startArgsForCall)
}
func (fake *FakeRouter) StartCalls(stub func() error) {
fake.startMutex.Lock()
defer fake.startMutex.Unlock()
fake.StartStub = stub
}
func (fake *FakeRouter) StartReturns(result1 error) {
fake.startMutex.Lock()
defer fake.startMutex.Unlock()
fake.StartStub = nil
fake.startReturns = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) StartReturnsOnCall(i int, result1 error) {
fake.startMutex.Lock()
defer fake.startMutex.Unlock()
fake.StartStub = nil
if fake.startReturnsOnCall == nil {
fake.startReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.startReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) StartParticipantSignal(arg1 context.Context, arg2 livekit.RoomName, arg3 routing.ParticipantInit) (routing.StartParticipantSignalResults, error) {
fake.startParticipantSignalMutex.Lock()
ret, specificReturn := fake.startParticipantSignalReturnsOnCall[len(fake.startParticipantSignalArgsForCall)]
fake.startParticipantSignalArgsForCall = append(fake.startParticipantSignalArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 routing.ParticipantInit
}{arg1, arg2, arg3})
stub := fake.StartParticipantSignalStub
fakeReturns := fake.startParticipantSignalReturns
fake.recordInvocation("StartParticipantSignal", []interface{}{arg1, arg2, arg3})
fake.startParticipantSignalMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeRouter) StartParticipantSignalCallCount() int {
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
return len(fake.startParticipantSignalArgsForCall)
}
func (fake *FakeRouter) StartParticipantSignalCalls(stub func(context.Context, livekit.RoomName, routing.ParticipantInit) (routing.StartParticipantSignalResults, error)) {
fake.startParticipantSignalMutex.Lock()
defer fake.startParticipantSignalMutex.Unlock()
fake.StartParticipantSignalStub = stub
}
func (fake *FakeRouter) StartParticipantSignalArgsForCall(i int) (context.Context, livekit.RoomName, routing.ParticipantInit) {
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
argsForCall := fake.startParticipantSignalArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeRouter) StartParticipantSignalReturns(result1 routing.StartParticipantSignalResults, result2 error) {
fake.startParticipantSignalMutex.Lock()
defer fake.startParticipantSignalMutex.Unlock()
fake.StartParticipantSignalStub = nil
fake.startParticipantSignalReturns = struct {
result1 routing.StartParticipantSignalResults
result2 error
}{result1, result2}
}
func (fake *FakeRouter) StartParticipantSignalReturnsOnCall(i int, result1 routing.StartParticipantSignalResults, result2 error) {
fake.startParticipantSignalMutex.Lock()
defer fake.startParticipantSignalMutex.Unlock()
fake.StartParticipantSignalStub = nil
if fake.startParticipantSignalReturnsOnCall == nil {
fake.startParticipantSignalReturnsOnCall = make(map[int]struct {
result1 routing.StartParticipantSignalResults
result2 error
})
}
fake.startParticipantSignalReturnsOnCall[i] = struct {
result1 routing.StartParticipantSignalResults
result2 error
}{result1, result2}
}
func (fake *FakeRouter) Stop() {
fake.stopMutex.Lock()
fake.stopArgsForCall = append(fake.stopArgsForCall, struct {
}{})
stub := fake.StopStub
fake.recordInvocation("Stop", []interface{}{})
fake.stopMutex.Unlock()
if stub != nil {
fake.StopStub()
}
}
func (fake *FakeRouter) StopCallCount() int {
fake.stopMutex.RLock()
defer fake.stopMutex.RUnlock()
return len(fake.stopArgsForCall)
}
func (fake *FakeRouter) StopCalls(stub func()) {
fake.stopMutex.Lock()
defer fake.stopMutex.Unlock()
fake.StopStub = stub
}
func (fake *FakeRouter) UnregisterNode() error {
fake.unregisterNodeMutex.Lock()
ret, specificReturn := fake.unregisterNodeReturnsOnCall[len(fake.unregisterNodeArgsForCall)]
fake.unregisterNodeArgsForCall = append(fake.unregisterNodeArgsForCall, struct {
}{})
stub := fake.UnregisterNodeStub
fakeReturns := fake.unregisterNodeReturns
fake.recordInvocation("UnregisterNode", []interface{}{})
fake.unregisterNodeMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRouter) UnregisterNodeCallCount() int {
fake.unregisterNodeMutex.RLock()
defer fake.unregisterNodeMutex.RUnlock()
return len(fake.unregisterNodeArgsForCall)
}
func (fake *FakeRouter) UnregisterNodeCalls(stub func() error) {
fake.unregisterNodeMutex.Lock()
defer fake.unregisterNodeMutex.Unlock()
fake.UnregisterNodeStub = stub
}
func (fake *FakeRouter) UnregisterNodeReturns(result1 error) {
fake.unregisterNodeMutex.Lock()
defer fake.unregisterNodeMutex.Unlock()
fake.UnregisterNodeStub = nil
fake.unregisterNodeReturns = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) UnregisterNodeReturnsOnCall(i int, result1 error) {
fake.unregisterNodeMutex.Lock()
defer fake.unregisterNodeMutex.Unlock()
fake.UnregisterNodeStub = nil
if fake.unregisterNodeReturnsOnCall == nil {
fake.unregisterNodeReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.unregisterNodeReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeRouter) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.clearRoomStateMutex.RLock()
defer fake.clearRoomStateMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
fake.drainMutex.RLock()
defer fake.drainMutex.RUnlock()
fake.getNodeForRoomMutex.RLock()
defer fake.getNodeForRoomMutex.RUnlock()
fake.getRegionMutex.RLock()
defer fake.getRegionMutex.RUnlock()
fake.listNodesMutex.RLock()
defer fake.listNodesMutex.RUnlock()
fake.registerNodeMutex.RLock()
defer fake.registerNodeMutex.RUnlock()
fake.removeDeadNodesMutex.RLock()
defer fake.removeDeadNodesMutex.RUnlock()
fake.setNodeForRoomMutex.RLock()
defer fake.setNodeForRoomMutex.RUnlock()
fake.startMutex.RLock()
defer fake.startMutex.RUnlock()
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
fake.stopMutex.RLock()
defer fake.stopMutex.RUnlock()
fake.unregisterNodeMutex.RLock()
defer fake.unregisterNodeMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeRouter) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ routing.Router = new(FakeRouter)
@@ -0,0 +1,199 @@
// Code generated by counterfeiter. DO NOT EDIT.
package routingfakes
import (
"context"
"sync"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/protocol/livekit"
)
type FakeSignalClient struct {
ActiveCountStub func() int
activeCountMutex sync.RWMutex
activeCountArgsForCall []struct {
}
activeCountReturns struct {
result1 int
}
activeCountReturnsOnCall map[int]struct {
result1 int
}
StartParticipantSignalStub func(context.Context, livekit.RoomName, routing.ParticipantInit, livekit.NodeID) (livekit.ConnectionID, routing.MessageSink, routing.MessageSource, error)
startParticipantSignalMutex sync.RWMutex
startParticipantSignalArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 routing.ParticipantInit
arg4 livekit.NodeID
}
startParticipantSignalReturns struct {
result1 livekit.ConnectionID
result2 routing.MessageSink
result3 routing.MessageSource
result4 error
}
startParticipantSignalReturnsOnCall map[int]struct {
result1 livekit.ConnectionID
result2 routing.MessageSink
result3 routing.MessageSource
result4 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeSignalClient) ActiveCount() int {
fake.activeCountMutex.Lock()
ret, specificReturn := fake.activeCountReturnsOnCall[len(fake.activeCountArgsForCall)]
fake.activeCountArgsForCall = append(fake.activeCountArgsForCall, struct {
}{})
stub := fake.ActiveCountStub
fakeReturns := fake.activeCountReturns
fake.recordInvocation("ActiveCount", []interface{}{})
fake.activeCountMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeSignalClient) ActiveCountCallCount() int {
fake.activeCountMutex.RLock()
defer fake.activeCountMutex.RUnlock()
return len(fake.activeCountArgsForCall)
}
func (fake *FakeSignalClient) ActiveCountCalls(stub func() int) {
fake.activeCountMutex.Lock()
defer fake.activeCountMutex.Unlock()
fake.ActiveCountStub = stub
}
func (fake *FakeSignalClient) ActiveCountReturns(result1 int) {
fake.activeCountMutex.Lock()
defer fake.activeCountMutex.Unlock()
fake.ActiveCountStub = nil
fake.activeCountReturns = struct {
result1 int
}{result1}
}
func (fake *FakeSignalClient) ActiveCountReturnsOnCall(i int, result1 int) {
fake.activeCountMutex.Lock()
defer fake.activeCountMutex.Unlock()
fake.ActiveCountStub = nil
if fake.activeCountReturnsOnCall == nil {
fake.activeCountReturnsOnCall = make(map[int]struct {
result1 int
})
}
fake.activeCountReturnsOnCall[i] = struct {
result1 int
}{result1}
}
func (fake *FakeSignalClient) StartParticipantSignal(arg1 context.Context, arg2 livekit.RoomName, arg3 routing.ParticipantInit, arg4 livekit.NodeID) (livekit.ConnectionID, routing.MessageSink, routing.MessageSource, error) {
fake.startParticipantSignalMutex.Lock()
ret, specificReturn := fake.startParticipantSignalReturnsOnCall[len(fake.startParticipantSignalArgsForCall)]
fake.startParticipantSignalArgsForCall = append(fake.startParticipantSignalArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 routing.ParticipantInit
arg4 livekit.NodeID
}{arg1, arg2, arg3, arg4})
stub := fake.StartParticipantSignalStub
fakeReturns := fake.startParticipantSignalReturns
fake.recordInvocation("StartParticipantSignal", []interface{}{arg1, arg2, arg3, arg4})
fake.startParticipantSignalMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3, arg4)
}
if specificReturn {
return ret.result1, ret.result2, ret.result3, ret.result4
}
return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3, fakeReturns.result4
}
func (fake *FakeSignalClient) StartParticipantSignalCallCount() int {
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
return len(fake.startParticipantSignalArgsForCall)
}
func (fake *FakeSignalClient) StartParticipantSignalCalls(stub func(context.Context, livekit.RoomName, routing.ParticipantInit, livekit.NodeID) (livekit.ConnectionID, routing.MessageSink, routing.MessageSource, error)) {
fake.startParticipantSignalMutex.Lock()
defer fake.startParticipantSignalMutex.Unlock()
fake.StartParticipantSignalStub = stub
}
func (fake *FakeSignalClient) StartParticipantSignalArgsForCall(i int) (context.Context, livekit.RoomName, routing.ParticipantInit, livekit.NodeID) {
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
argsForCall := fake.startParticipantSignalArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeSignalClient) StartParticipantSignalReturns(result1 livekit.ConnectionID, result2 routing.MessageSink, result3 routing.MessageSource, result4 error) {
fake.startParticipantSignalMutex.Lock()
defer fake.startParticipantSignalMutex.Unlock()
fake.StartParticipantSignalStub = nil
fake.startParticipantSignalReturns = struct {
result1 livekit.ConnectionID
result2 routing.MessageSink
result3 routing.MessageSource
result4 error
}{result1, result2, result3, result4}
}
func (fake *FakeSignalClient) StartParticipantSignalReturnsOnCall(i int, result1 livekit.ConnectionID, result2 routing.MessageSink, result3 routing.MessageSource, result4 error) {
fake.startParticipantSignalMutex.Lock()
defer fake.startParticipantSignalMutex.Unlock()
fake.StartParticipantSignalStub = nil
if fake.startParticipantSignalReturnsOnCall == nil {
fake.startParticipantSignalReturnsOnCall = make(map[int]struct {
result1 livekit.ConnectionID
result2 routing.MessageSink
result3 routing.MessageSource
result4 error
})
}
fake.startParticipantSignalReturnsOnCall[i] = struct {
result1 livekit.ConnectionID
result2 routing.MessageSink
result3 routing.MessageSource
result4 error
}{result1, result2, result3, result4}
}
func (fake *FakeSignalClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.activeCountMutex.RLock()
defer fake.activeCountMutex.RUnlock()
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeSignalClient) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ routing.SignalClient = new(FakeSignalClient)
@@ -0,0 +1,33 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector
import (
"github.com/livekit/protocol/livekit"
)
// AnySelector selects any available node with no limitations
type AnySelector struct {
SortBy string
}
func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
nodes = GetAvailableNodes(nodes)
if len(nodes) == 0 {
return nil, ErrNoAvailableNodes
}
return SelectSortedNode(nodes, s.SortBy)
}
@@ -0,0 +1,54 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector
import (
"github.com/livekit/protocol/livekit"
)
// CPULoadSelector eliminates nodes that have CPU usage higher than CPULoadLimit
// then selects a node from nodes that are not overloaded
type CPULoadSelector struct {
CPULoadLimit float32
SortBy string
}
func (s *CPULoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
nodes = GetAvailableNodes(nodes)
if len(nodes) == 0 {
return nil, ErrNoAvailableNodes
}
nodesLowLoad := make([]*livekit.Node, 0)
for _, node := range nodes {
stats := node.Stats
if stats.CpuLoad < s.CPULoadLimit {
nodesLowLoad = append(nodesLowLoad, node)
}
}
if len(nodesLowLoad) > 0 {
nodes = nodesLowLoad
}
return nodes, nil
}
func (s *CPULoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
nodes, err := s.filterNodes(nodes)
if err != nil {
return nil, err
}
return SelectSortedNode(nodes, s.SortBy)
}
@@ -0,0 +1,51 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/routing/selector"
)
func TestCPULoadSelector_SelectNode(t *testing.T) {
sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random"}
var nodes []*livekit.Node
_, err := sel.SelectNode(nodes)
require.Error(t, err, "should error no available nodes")
// Select a node with high load when no nodes with low load are available
nodes = []*livekit.Node{nodeLoadHigh}
if _, err := sel.SelectNode(nodes); err != nil {
t.Error(err)
}
// Select a node with low load when available
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
for i := 0; i < 5; i++ {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
}
if node != nodeLoadLow {
t.Error("selected the wrong node")
}
}
}
@@ -0,0 +1,25 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector
import "errors"
var (
ErrNoAvailableNodes = errors.New("could not find any available nodes")
ErrCurrentRegionNotSet = errors.New("current region cannot be blank")
ErrCurrentRegionUnknownLatLon = errors.New("unknown lat and lon for the current region")
ErrSortByNotSet = errors.New("sort by option cannot be blank")
ErrSortByUnknown = errors.New("unknown sort by option")
)
@@ -0,0 +1,64 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector
import (
"errors"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/config"
)
var ErrUnsupportedSelector = errors.New("unsupported node selector")
// NodeSelector selects an appropriate node to run the current session
type NodeSelector interface {
SelectNode(nodes []*livekit.Node) (*livekit.Node, error)
}
func CreateNodeSelector(conf *config.Config) (NodeSelector, error) {
kind := conf.NodeSelector.Kind
if kind == "" {
kind = "any"
}
switch kind {
case "any":
return &AnySelector{conf.NodeSelector.SortBy}, nil
case "cpuload":
return &CPULoadSelector{
CPULoadLimit: conf.NodeSelector.CPULoadLimit,
SortBy: conf.NodeSelector.SortBy,
}, nil
case "sysload":
return &SystemLoadSelector{
SysloadLimit: conf.NodeSelector.SysloadLimit,
SortBy: conf.NodeSelector.SortBy,
}, nil
case "regionaware":
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy)
if err != nil {
return nil, err
}
s.SysloadLimit = conf.NodeSelector.SysloadLimit
return s, nil
case "random":
logger.Warnw("random node selector is deprecated, please switch to \"any\" or another selector", nil)
return &AnySelector{conf.NodeSelector.SortBy}, nil
default:
return nil, ErrUnsupportedSelector
}
}
@@ -0,0 +1,125 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector
import (
"math"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/config"
)
// RegionAwareSelector prefers available nodes that are closest to the region of the current instance
type RegionAwareSelector struct {
SystemLoadSelector
CurrentRegion string
regionDistances map[string]float64
regions []config.RegionConfig
SortBy string
}
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string) (*RegionAwareSelector, error) {
if currentRegion == "" {
return nil, ErrCurrentRegionNotSet
}
// build internal map of distances
s := &RegionAwareSelector{
CurrentRegion: currentRegion,
regionDistances: make(map[string]float64),
regions: regions,
SortBy: sortBy,
}
var currentRC *config.RegionConfig
for _, region := range regions {
if region.Name == currentRegion {
currentRC = &region
break
}
}
if currentRC == nil && len(regions) > 0 {
return nil, ErrCurrentRegionUnknownLatLon
}
if currentRC != nil {
for _, region := range regions {
s.regionDistances[region.Name] = distanceBetween(currentRC.Lat, currentRC.Lon, region.Lat, region.Lon)
}
}
return s, nil
}
func (s *RegionAwareSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
nodes, err := s.SystemLoadSelector.filterNodes(nodes)
if err != nil {
return nil, err
}
// find nodes nearest to current region
var nearestNodes []*livekit.Node
nearestRegion := ""
minDist := math.MaxFloat64
for _, node := range nodes {
if node.Region == nearestRegion {
nearestNodes = append(nearestNodes, node)
continue
}
if dist, ok := s.regionDistances[node.Region]; ok {
if dist < minDist {
minDist = dist
nearestRegion = node.Region
nearestNodes = nearestNodes[:0]
nearestNodes = append(nearestNodes, node)
}
}
}
if len(nearestNodes) > 0 {
nodes = nearestNodes
}
return SelectSortedNode(nodes, s.SortBy)
}
// haversine(θ) function
func hsin(theta float64) float64 {
return math.Pow(math.Sin(theta/2), 2)
}
var piBy180 = math.Pi / 180
// Haversine Distance Formula
// http://en.wikipedia.org/wiki/Haversine_formula
// from https://gist.github.com/cdipaolo/d3f8db3848278b49db68
func distanceBetween(lat1, lon1, lat2, lon2 float64) float64 {
// convert to radians
// must cast radius as float to multiply later
var la1, lo1, la2, lo2, r float64
la1 = lat1 * piBy180
lo1 = lon1 * piBy180
la2 = lat2 * piBy180
lo2 = lon2 * piBy180
r = 6378100 // Earth radius in METERS
// calculate
h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1)
return 2 * r * math.Asin(math.Sqrt(h))
}
@@ -0,0 +1,165 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing/selector"
)
const (
loadLimit = 0.5
regionWest = "us-west"
regionEast = "us-east"
regionSeattle = "seattle"
sortBy = "random"
)
func TestRegionAwareRouting(t *testing.T) {
rc := []config.RegionConfig{
{
Name: regionWest,
Lat: 37.64046607830567,
Lon: -120.88026233189062,
},
{
Name: regionEast,
Lat: 40.68914362140307,
Lon: -74.04445748616385,
},
{
Name: regionSeattle,
Lat: 47.620426730945454,
Lon: -122.34938468973702,
},
}
t.Run("works without region config", func(t *testing.T) {
nodes := []*livekit.Node{
newTestNodeInRegion("", false),
}
s, err := selector.NewRegionAwareSelector(regionEast, nil, sortBy)
require.NoError(t, err)
node, err := s.SelectNode(nodes)
require.NoError(t, err)
require.NotNil(t, node)
})
t.Run("picks available nodes in same region", func(t *testing.T) {
expectedNode := newTestNodeInRegion(regionEast, true)
nodes := []*livekit.Node{
newTestNodeInRegion(regionSeattle, true),
newTestNodeInRegion(regionWest, true),
expectedNode,
newTestNodeInRegion(regionEast, false),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
require.NoError(t, err)
s.SysloadLimit = loadLimit
node, err := s.SelectNode(nodes)
require.NoError(t, err)
require.Equal(t, expectedNode, node)
})
t.Run("picks available nodes in same region when current node is first in the list", func(t *testing.T) {
expectedNode := newTestNodeInRegion(regionEast, true)
nodes := []*livekit.Node{
expectedNode,
newTestNodeInRegion(regionSeattle, true),
newTestNodeInRegion(regionWest, true),
newTestNodeInRegion(regionEast, false),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
require.NoError(t, err)
s.SysloadLimit = loadLimit
node, err := s.SelectNode(nodes)
require.NoError(t, err)
require.Equal(t, expectedNode, node)
})
t.Run("picks closest node in a diff region", func(t *testing.T) {
expectedNode := newTestNodeInRegion(regionWest, true)
nodes := []*livekit.Node{
newTestNodeInRegion(regionSeattle, false),
expectedNode,
newTestNodeInRegion(regionEast, true),
}
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
require.NoError(t, err)
s.SysloadLimit = loadLimit
node, err := s.SelectNode(nodes)
require.NoError(t, err)
require.Equal(t, expectedNode, node)
})
t.Run("handles multiple nodes in same region", func(t *testing.T) {
expectedNode := newTestNodeInRegion(regionWest, true)
nodes := []*livekit.Node{
newTestNodeInRegion(regionSeattle, false),
newTestNodeInRegion(regionEast, true),
newTestNodeInRegion(regionEast, true),
expectedNode,
expectedNode,
}
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
require.NoError(t, err)
s.SysloadLimit = loadLimit
node, err := s.SelectNode(nodes)
require.NoError(t, err)
require.Equal(t, expectedNode, node)
})
t.Run("functions when current region is full", func(t *testing.T) {
nodes := []*livekit.Node{
newTestNodeInRegion(regionWest, true),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
require.NoError(t, err)
node, err := s.SelectNode(nodes)
require.NoError(t, err)
require.NotNil(t, node)
})
}
func newTestNodeInRegion(region string, available bool) *livekit.Node {
load := float32(0.4)
if !available {
load = 1.0
}
return &livekit.Node{
Id: guid.New(utils.NodePrefix),
Region: region,
State: livekit.NodeState_SERVING,
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix(),
NumCpus: 1,
LoadAvgLast1Min: load,
},
}
}
@@ -0,0 +1,64 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector_test
import (
"testing"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/routing/selector"
)
func SortByTest(t *testing.T, sortBy string) {
sel := selector.SystemLoadSelector{SortBy: sortBy}
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
for i := 0; i < 5; i++ {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
}
if node != nodeLoadLow {
t.Error("selected the wrong node for SortBy:", sortBy)
}
}
}
func TestSortByErrors(t *testing.T) {
sel := selector.SystemLoadSelector{}
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
// Test unset sort by option error
_, err := sel.SelectNode(nodes)
if err != selector.ErrSortByNotSet {
t.Error("shouldn't allow empty sortBy")
}
// Test unknown sort by option error
sel.SortBy = "testFail"
_, err = sel.SelectNode(nodes)
if err != selector.ErrSortByUnknown {
t.Error("shouldn't allow unknown sortBy")
}
}
func TestSortBy(t *testing.T) {
sortByTests := []string{"sysload", "cpuload", "rooms", "clients", "tracks", "bytespersec"}
for _, sortBy := range sortByTests {
SortByTest(t, sortBy)
}
}
@@ -0,0 +1,53 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector
import (
"github.com/livekit/protocol/livekit"
)
// SystemLoadSelector eliminates nodes that surpass has a per-cpu node higher than SysloadLimit
// then selects a node from nodes that are not overloaded
type SystemLoadSelector struct {
SysloadLimit float32
SortBy string
}
func (s *SystemLoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
nodes = GetAvailableNodes(nodes)
if len(nodes) == 0 {
return nil, ErrNoAvailableNodes
}
nodesLowLoad := make([]*livekit.Node, 0)
for _, node := range nodes {
if GetNodeSysload(node) < s.SysloadLimit {
nodesLowLoad = append(nodesLowLoad, node)
}
}
if len(nodesLowLoad) > 0 {
nodes = nodesLowLoad
}
return nodes, nil
}
func (s *SystemLoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
nodes, err := s.filterNodes(nodes)
if err != nil {
return nil, err
}
return SelectSortedNode(nodes, s.SortBy)
}
@@ -0,0 +1,102 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/routing/selector"
)
var (
nodeLoadLow = &livekit.Node{
State: livekit.NodeState_SERVING,
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix(),
NumCpus: 1,
CpuLoad: 0.1,
LoadAvgLast1Min: 0.0,
NumRooms: 1,
NumClients: 2,
NumTracksIn: 4,
NumTracksOut: 8,
BytesInPerSec: 1000,
BytesOutPerSec: 2000,
},
}
nodeLoadMedium = &livekit.Node{
State: livekit.NodeState_SERVING,
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix(),
NumCpus: 1,
CpuLoad: 0.5,
LoadAvgLast1Min: 0.5,
NumRooms: 5,
NumClients: 10,
NumTracksIn: 20,
NumTracksOut: 200,
BytesInPerSec: 5000,
BytesOutPerSec: 10000,
},
}
nodeLoadHigh = &livekit.Node{
State: livekit.NodeState_SERVING,
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix(),
NumCpus: 1,
CpuLoad: 0.99,
LoadAvgLast1Min: 2.0,
NumRooms: 10,
NumClients: 20,
NumTracksIn: 40,
NumTracksOut: 800,
BytesInPerSec: 10000,
BytesOutPerSec: 40000,
},
}
)
func TestSystemLoadSelector_SelectNode(t *testing.T) {
sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random"}
var nodes []*livekit.Node
_, err := sel.SelectNode(nodes)
require.Error(t, err, "should error no available nodes")
// Select a node with high load when no nodes with low load are available
nodes = []*livekit.Node{nodeLoadHigh}
if _, err := sel.SelectNode(nodes); err != nil {
t.Error(err)
}
// Select a node with low load when available
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
for i := 0; i < 5; i++ {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
}
if node != nodeLoadLow {
t.Error("selected the wrong node")
}
}
}
@@ -0,0 +1,115 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector
import (
"sort"
"time"
"github.com/thoas/go-funk"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/config"
)
const AvailableSeconds = 5
// checks if a node has been updated recently to be considered for selection
func IsAvailable(node *livekit.Node) bool {
if node.Stats == nil {
// available till stats are available
return true
}
delta := time.Now().Unix() - node.Stats.UpdatedAt
return int(delta) < AvailableSeconds
}
func GetAvailableNodes(nodes []*livekit.Node) []*livekit.Node {
return funk.Filter(nodes, func(node *livekit.Node) bool {
return IsAvailable(node) && node.State == livekit.NodeState_SERVING
}).([]*livekit.Node)
}
func GetNodeSysload(node *livekit.Node) float32 {
stats := node.Stats
numCpus := stats.NumCpus
if numCpus == 0 {
numCpus = 1
}
return stats.LoadAvgLast1Min / float32(numCpus)
}
// TODO: check remote node configured limit, instead of this node's config
func LimitsReached(limitConfig config.LimitConfig, nodeStats *livekit.NodeStats) bool {
if nodeStats == nil {
return false
}
if limitConfig.NumTracks > 0 && limitConfig.NumTracks <= nodeStats.NumTracksIn+nodeStats.NumTracksOut {
return true
}
if limitConfig.BytesPerSec > 0 && limitConfig.BytesPerSec <= nodeStats.BytesInPerSec+nodeStats.BytesOutPerSec {
return true
}
return false
}
func SelectSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
if sortBy == "" {
return nil, ErrSortByNotSet
}
// Return a node based on what it should be sorted by for priority
switch sortBy {
case "random":
idx := funk.RandomInt(0, len(nodes))
return nodes[idx], nil
case "sysload":
sort.Slice(nodes, func(i, j int) bool {
return GetNodeSysload(nodes[i]) < GetNodeSysload(nodes[j])
})
return nodes[0], nil
case "cpuload":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.CpuLoad < nodes[j].Stats.CpuLoad
})
return nodes[0], nil
case "rooms":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.NumRooms < nodes[j].Stats.NumRooms
})
return nodes[0], nil
case "clients":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.NumClients < nodes[j].Stats.NumClients
})
return nodes[0], nil
case "tracks":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.NumTracksIn+nodes[i].Stats.NumTracksOut < nodes[j].Stats.NumTracksIn+nodes[j].Stats.NumTracksOut
})
return nodes[0], nil
case "bytespersec":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.BytesInPerSec+nodes[i].Stats.BytesOutPerSec < nodes[j].Stats.BytesInPerSec+nodes[j].Stats.BytesOutPerSec
})
return nodes[0], nil
default:
return nil, ErrSortByUnknown
}
}
@@ -0,0 +1,46 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selector_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/routing/selector"
)
func TestIsAvailable(t *testing.T) {
t.Run("still available", func(t *testing.T) {
n := &livekit.Node{
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix() - 3,
},
}
require.True(t, selector.IsAvailable(n))
})
t.Run("expired", func(t *testing.T) {
n := &livekit.Node{
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix() - 20,
},
}
require.False(t, selector.IsAvailable(n))
})
}
+368
View File
@@ -0,0 +1,368 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"context"
"errors"
"sync"
"time"
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware"
)
var ErrSignalWriteFailed = errors.New("signal write failed")
var ErrSignalMessageDropped = errors.New("signal message dropped")
//counterfeiter:generate . SignalClient
type SignalClient interface {
ActiveCount() int
StartParticipantSignal(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit, nodeID livekit.NodeID) (connectionID livekit.ConnectionID, reqSink MessageSink, resSource MessageSource, err error)
}
type signalClient struct {
nodeID livekit.NodeID
config config.SignalRelayConfig
client rpc.TypedSignalClient
active atomic.Int32
}
func NewSignalClient(nodeID livekit.NodeID, bus psrpc.MessageBus, config config.SignalRelayConfig) (SignalClient, error) {
c, err := rpc.NewTypedSignalClient(
nodeID,
bus,
middleware.WithClientMetrics(rpc.PSRPCMetricsObserver{}),
psrpc.WithClientChannelSize(config.StreamBufferSize),
)
if err != nil {
return nil, err
}
return &signalClient{
nodeID: nodeID,
config: config,
client: c,
}, nil
}
func (r *signalClient) ActiveCount() int {
return int(r.active.Load())
}
func (r *signalClient) StartParticipantSignal(
ctx context.Context,
roomName livekit.RoomName,
pi ParticipantInit,
nodeID livekit.NodeID,
) (
connectionID livekit.ConnectionID,
reqSink MessageSink,
resSource MessageSource,
err error,
) {
connectionID = livekit.ConnectionID(guid.New("CO_"))
ss, err := pi.ToStartSession(roomName, connectionID)
if err != nil {
return
}
l := logger.GetLogger().WithValues(
"room", roomName,
"reqNodeID", nodeID,
"participant", pi.Identity,
"connID", connectionID,
)
l.Debugw("starting signal connection")
stream, err := r.client.RelaySignal(ctx, nodeID)
if err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
return
}
err = stream.Send(&rpc.RelaySignalRequest{StartSession: ss})
if err != nil {
stream.Close(err)
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
return
}
sink := NewSignalMessageSink(SignalSinkParams[*rpc.RelaySignalRequest, *rpc.RelaySignalResponse]{
Logger: l,
Stream: stream,
Config: r.config,
Writer: signalRequestMessageWriter{},
CloseOnFailure: true,
BlockOnClose: true,
ConnectionID: connectionID,
})
resChan := NewDefaultMessageChannel(connectionID)
go func() {
r.active.Inc()
defer r.active.Dec()
err := CopySignalStreamToMessageChannel[*rpc.RelaySignalRequest, *rpc.RelaySignalResponse](
stream,
resChan,
signalResponseMessageReader{},
r.config,
)
l.Debugw("signal stream closed", "error", err)
resChan.Close()
}()
return connectionID, sink, resChan, nil
}
type signalRequestMessageWriter struct{}
func (e signalRequestMessageWriter) Write(seq uint64, close bool, msgs []proto.Message) *rpc.RelaySignalRequest {
r := &rpc.RelaySignalRequest{
Seq: seq,
Requests: make([]*livekit.SignalRequest, 0, len(msgs)),
Close: close,
}
for _, m := range msgs {
r.Requests = append(r.Requests, m.(*livekit.SignalRequest))
}
return r
}
type signalResponseMessageReader struct{}
func (e signalResponseMessageReader) Read(rm *rpc.RelaySignalResponse) ([]proto.Message, error) {
msgs := make([]proto.Message, 0, len(rm.Responses))
for _, m := range rm.Responses {
msgs = append(msgs, m)
}
return msgs, nil
}
type RelaySignalMessage interface {
proto.Message
GetSeq() uint64
GetClose() bool
}
type SignalMessageWriter[SendType RelaySignalMessage] interface {
Write(seq uint64, close bool, msgs []proto.Message) SendType
}
type SignalMessageReader[RecvType RelaySignalMessage] interface {
Read(msg RecvType) ([]proto.Message, error)
}
func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
stream psrpc.Stream[SendType, RecvType],
ch *MessageChannel,
reader SignalMessageReader[RecvType],
config config.SignalRelayConfig,
) error {
r := &signalMessageReader[SendType, RecvType]{
reader: reader,
config: config,
}
for msg := range stream.Channel() {
res, err := r.Read(msg)
if err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
return err
}
for _, r := range res {
if err = ch.WriteMessage(r); err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
return err
}
prometheus.MessageCounter.WithLabelValues("signal", "success").Add(1)
}
if msg.GetClose() {
return stream.Close(nil)
}
}
return stream.Err()
}
type signalMessageReader[SendType, RecvType RelaySignalMessage] struct {
seq uint64
reader SignalMessageReader[RecvType]
config config.SignalRelayConfig
}
func (r *signalMessageReader[SendType, RecvType]) Read(msg RecvType) ([]proto.Message, error) {
res, err := r.reader.Read(msg)
if err != nil {
return nil, err
}
if r.seq < msg.GetSeq() {
return nil, ErrSignalMessageDropped
}
if r.seq > msg.GetSeq() {
n := int(r.seq - msg.GetSeq())
if n > len(res) {
n = len(res)
}
res = res[n:]
}
r.seq += uint64(len(res))
return res, nil
}
type SignalSinkParams[SendType, RecvType RelaySignalMessage] struct {
Stream psrpc.Stream[SendType, RecvType]
Logger logger.Logger
Config config.SignalRelayConfig
Writer SignalMessageWriter[SendType]
CloseOnFailure bool
BlockOnClose bool
ConnectionID livekit.ConnectionID
}
func NewSignalMessageSink[SendType, RecvType RelaySignalMessage](params SignalSinkParams[SendType, RecvType]) MessageSink {
return &signalMessageSink[SendType, RecvType]{
SignalSinkParams: params,
}
}
type signalMessageSink[SendType, RecvType RelaySignalMessage] struct {
SignalSinkParams[SendType, RecvType]
mu sync.Mutex
seq uint64
queue []proto.Message
writing bool
draining bool
}
func (s *signalMessageSink[SendType, RecvType]) Close() {
s.mu.Lock()
s.draining = true
if !s.writing {
s.writing = true
go s.write()
}
s.mu.Unlock()
// conditionally block while closing to wait for outgoing messages to drain
//
// on media the signal sink shares a goroutine with other signal connection
// attempts from the same participant so blocking delays establishing new
// sessions during reconnect.
//
// on controller closing without waiting for the outstanding messages to
// drain causes leave messages to be dropped from the write queue. when
// this happens other participants in the room aren't notified about the
// departure until the participant times out.
if s.BlockOnClose {
<-s.Stream.Context().Done()
}
}
func (s *signalMessageSink[SendType, RecvType]) IsClosed() bool {
return s.Stream.Err() != nil
}
func (s *signalMessageSink[SendType, RecvType]) write() {
interval := s.Config.MinRetryInterval
deadline := time.Now().Add(s.Config.RetryTimeout)
var err error
s.mu.Lock()
for {
close := s.draining
if (!close && len(s.queue) == 0) || s.IsClosed() {
break
}
msg, n := s.Writer.Write(s.seq, close, s.queue), len(s.queue)
s.mu.Unlock()
err = s.Stream.Send(msg, psrpc.WithTimeout(interval))
if err != nil {
if time.Now().After(deadline) {
s.Logger.Warnw("could not send signal message", err)
s.mu.Lock()
s.seq += uint64(len(s.queue))
s.queue = nil
break
}
interval *= 2
if interval > s.Config.MaxRetryInterval {
interval = s.Config.MaxRetryInterval
}
}
s.mu.Lock()
if err == nil {
interval = s.Config.MinRetryInterval
deadline = time.Now().Add(s.Config.RetryTimeout)
s.seq += uint64(n)
s.queue = s.queue[n:]
if close {
break
}
}
}
s.writing = false
if s.draining {
s.Stream.Close(nil)
}
if err != nil && s.CloseOnFailure {
s.Stream.Close(ErrSignalWriteFailed)
}
s.mu.Unlock()
}
func (s *signalMessageSink[SendType, RecvType]) WriteMessage(msg proto.Message) error {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.Stream.Err(); err != nil {
return err
} else if s.draining {
return psrpc.ErrStreamClosed
}
s.queue = append(s.queue, msg)
if !s.writing {
s.writing = true
go s.write()
}
return nil
}
func (s *signalMessageSink[SendType, RecvType]) ConnectionID() livekit.ConnectionID {
return s.SignalSinkParams.ConnectionID
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"strconv"
"strings"
"github.com/livekit/protocol/livekit"
)
type ClientInfo struct {
*livekit.ClientInfo
}
func (c ClientInfo) isFirefox() bool {
return c.ClientInfo != nil && (strings.EqualFold(c.ClientInfo.Browser, "firefox") || strings.EqualFold(c.ClientInfo.Browser, "firefox mobile"))
}
func (c ClientInfo) isSafari() bool {
return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Browser, "safari")
}
func (c ClientInfo) isGo() bool {
return c.ClientInfo != nil && c.ClientInfo.Sdk == livekit.ClientInfo_GO
}
func (c ClientInfo) isLinux() bool {
return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Os, "linux")
}
func (c ClientInfo) isAndroid() bool {
return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Os, "android")
}
func (c ClientInfo) SupportsAudioRED() bool {
return !c.isFirefox() && !c.isSafari()
}
func (c ClientInfo) SupportPrflxOverRelay() bool {
return !c.isFirefox()
}
// GoSDK(pion) relies on rtp packets to fire ontrack event, browsers and native (libwebrtc) rely on sdp
func (c ClientInfo) FireTrackByRTPPacket() bool {
return c.isGo()
}
func (c ClientInfo) SupportsCodecChange() bool {
return c.ClientInfo != nil && c.ClientInfo.Sdk != livekit.ClientInfo_GO && c.ClientInfo.Sdk != livekit.ClientInfo_UNKNOWN
}
func (c ClientInfo) CanHandleReconnectResponse() bool {
if c.Sdk == livekit.ClientInfo_JS {
// JS handles Reconnect explicitly in 1.6.3, prior to 1.6.4 it could not handle unknown responses
if c.compareVersion("1.6.3") < 0 {
return false
}
}
return true
}
func (c ClientInfo) SupportsICETCP() bool {
if c.ClientInfo == nil {
return false
}
if c.ClientInfo.Sdk == livekit.ClientInfo_GO {
// Go does not support active TCP
return false
}
if c.ClientInfo.Sdk == livekit.ClientInfo_SWIFT {
// ICE/TCP added in 1.0.5
return c.compareVersion("1.0.5") >= 0
}
// most SDKs support ICE/TCP
return true
}
func (c ClientInfo) SupportsChangeRTPSenderEncodingActive() bool {
return !c.isFirefox()
}
func (c ClientInfo) ComplyWithCodecOrderInSDPAnswer() bool {
return !((c.isLinux() || c.isAndroid()) && c.isFirefox())
}
// Rust SDK can't decode unknown signal message (TrackSubscribed and ErrorResponse)
func (c ClientInfo) SupportTrackSubscribedEvent() bool {
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_RUST && c.ClientInfo.GetProtocol() < 10)
}
func (c ClientInfo) SupportErrorResponse() bool {
return c.SupportTrackSubscribedEvent()
}
func (c ClientInfo) SupportSctpZeroChecksum() bool {
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_UNKNOWN ||
(c.isGo() && c.compareVersion("2.4.0") < 0))
}
// compareVersion compares a semver against the current client SDK version
// returning 1 if current version is greater than version
// 0 if they are the same, and -1 if it's an earlier version
func (c ClientInfo) compareVersion(version string) int {
if c.ClientInfo == nil {
return -1
}
parts0 := strings.Split(c.ClientInfo.Version, ".")
parts1 := strings.Split(version, ".")
ints0 := make([]int, 3)
ints1 := make([]int, 3)
for i := 0; i < 3; i++ {
if len(parts0) > i {
ints0[i], _ = strconv.Atoi(parts0[i])
}
if len(parts1) > i {
ints1[i], _ = strconv.Atoi(parts1[i])
}
if ints0[i] > ints1[i] {
return 1
} else if ints0[i] < ints1[i] {
return -1
}
}
return 0
}
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright 2022 LiveKit, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rtc
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
)
func TestClientInfo_CompareVersion(t *testing.T) {
c := ClientInfo{
ClientInfo: &livekit.ClientInfo{
Version: "1",
},
}
require.Equal(t, 1, c.compareVersion("0.1.0"))
require.Equal(t, 0, c.compareVersion("1.0.0"))
require.Equal(t, -1, c.compareVersion("1.0.5"))
}
func TestClientInfo_SupportsICETCP(t *testing.T) {
t.Run("GO SDK cannot support TCP", func(t *testing.T) {
c := ClientInfo{
ClientInfo: &livekit.ClientInfo{
Sdk: livekit.ClientInfo_GO,
},
}
require.False(t, c.SupportsICETCP())
})
t.Run("Swift SDK cannot support TCP before 1.0.5", func(t *testing.T) {
c := ClientInfo{
ClientInfo: &livekit.ClientInfo{
Sdk: livekit.ClientInfo_SWIFT,
Version: "1.0.4",
},
}
require.False(t, c.SupportsICETCP())
c.Version = "1.0.5"
require.True(t, c.SupportsICETCP())
})
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
)
const (
frameMarking = "urn:ietf:params:rtp-hdrext:framemarking"
repairedRTPStreamID = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
)
type WebRTCConfig struct {
rtcconfig.WebRTCConfig
BufferFactory *buffer.Factory
Receiver ReceiverConfig
Publisher DirectionConfig
Subscriber DirectionConfig
}
type ReceiverConfig struct {
PacketBufferSizeVideo int
PacketBufferSizeAudio int
}
type RTPHeaderExtensionConfig struct {
Audio []string
Video []string
}
type RTCPFeedbackConfig struct {
Audio []webrtc.RTCPFeedback
Video []webrtc.RTCPFeedback
}
type DirectionConfig struct {
RTPHeaderExtension RTPHeaderExtensionConfig
RTCPFeedback RTCPFeedbackConfig
}
func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
rtcConf := conf.RTC
webRTCConfig, err := rtcconfig.NewWebRTCConfig(&rtcConf.RTCConfig, conf.Development)
if err != nil {
return nil, err
}
// we don't want to use active TCP on a server, clients should be dialing
webRTCConfig.SettingEngine.DisableActiveTCP(true)
if rtcConf.PacketBufferSize == 0 {
rtcConf.PacketBufferSize = 500
}
if rtcConf.PacketBufferSizeVideo == 0 {
rtcConf.PacketBufferSizeVideo = rtcConf.PacketBufferSize
}
if rtcConf.PacketBufferSizeAudio == 0 {
rtcConf.PacketBufferSizeAudio = rtcConf.PacketBufferSize
}
// publisher configuration
publisherConfig := DirectionConfig{
RTPHeaderExtension: RTPHeaderExtensionConfig{
Audio: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.AudioLevelURI,
//act.AbsCaptureTimeURI,
},
Video: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.TransportCCURI,
frameMarking,
dd.ExtensionURI,
repairedRTPStreamID,
//act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
Audio: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBNACK},
},
Video: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBTransportCC},
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
{Type: webrtc.TypeRTCPFBNACK},
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
},
},
}
return &WebRTCConfig{
WebRTCConfig: *webRTCConfig,
Receiver: ReceiverConfig{
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
},
Publisher: publisherConfig,
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
}, nil
}
func (c *WebRTCConfig) UpdateCongestionControl(ccConf config.CongestionControlConfig) {
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
}
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
c.BufferFactory = factory
c.SettingEngine.BufferFactory = factory.GetOrNew
}
func getSubscriberConfig(enableTWCC bool) DirectionConfig {
subscriberConfig := DirectionConfig{
RTPHeaderExtension: RTPHeaderExtensionConfig{
Video: []string{
dd.ExtensionURI,
//act.AbsCaptureTimeURI,
},
Audio: []string{
//act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
Audio: []webrtc.RTCPFeedback{
// always enable NACK for audio but disable it later for red enabled transceiver. https://github.com/pion/webrtc/pull/2972
{Type: webrtc.TypeRTCPFBNACK},
},
Video: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
{Type: webrtc.TypeRTCPFBNACK},
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
},
},
}
if enableTWCC {
subscriberConfig.RTPHeaderExtension.Video = append(subscriberConfig.RTPHeaderExtension.Video, sdp.TransportCCURI)
subscriberConfig.RTCPFeedback.Video = append(subscriberConfig.RTCPFeedback.Video, webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBTransportCC})
} else {
subscriberConfig.RTPHeaderExtension.Video = append(subscriberConfig.RTPHeaderExtension.Video, sdp.ABSSendTimeURI)
subscriberConfig.RTCPFeedback.Video = append(subscriberConfig.RTCPFeedback.Video, webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBGoogREMB})
}
return subscriberConfig
}
@@ -0,0 +1,349 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dynacast
import (
"sync"
"time"
"github.com/bep/debounce"
"golang.org/x/exp/maps"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/utils"
)
type DynacastManagerParams struct {
DynacastPauseDelay time.Duration
Logger logger.Logger
}
type DynacastManager struct {
params DynacastManagerParams
lock sync.RWMutex
regressedCodec map[mime.MimeType]struct{}
dynacastQuality map[mime.MimeType]*DynacastQuality
maxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
committedMaxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
maxSubscribedQualityDebounce func(func())
maxSubscribedQualityDebouncePending bool
qualityNotifyOpQueue *utils.OpsQueue
isClosed bool
onSubscribedMaxQualityChange func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)
}
func NewDynacastManager(params DynacastManagerParams) *DynacastManager {
if params.Logger == nil {
params.Logger = logger.GetLogger()
}
d := &DynacastManager{
params: params,
regressedCodec: make(map[mime.MimeType]struct{}),
dynacastQuality: make(map[mime.MimeType]*DynacastQuality),
maxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
committedMaxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
qualityNotifyOpQueue: utils.NewOpsQueue(utils.OpsQueueParams{
Name: "quality-notify",
MinSize: 64,
FlushOnStop: true,
Logger: params.Logger,
}),
}
if params.DynacastPauseDelay > 0 {
d.maxSubscribedQualityDebounce = debounce.New(params.DynacastPauseDelay)
}
d.qualityNotifyOpQueue.Start()
return d
}
func (d *DynacastManager) OnSubscribedMaxQualityChange(f func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)) {
d.lock.Lock()
d.onSubscribedMaxQualityChange = f
d.lock.Unlock()
}
func (d *DynacastManager) AddCodec(mime mime.MimeType) {
d.getOrCreateDynacastQuality(mime)
}
func (d *DynacastManager) HandleCodecRegression(fromMime, toMime mime.MimeType) {
fromDq := d.getOrCreateDynacastQuality(fromMime)
d.lock.Lock()
if d.isClosed {
d.lock.Unlock()
return
}
if fromDq == nil {
// should not happen as we have added the codec on setup receiver
d.params.Logger.Warnw("regression from codec not found", nil, "mime", fromMime)
d.lock.Unlock()
return
}
d.regressedCodec[fromMime] = struct{}{}
d.maxSubscribedQuality[fromMime] = livekit.VideoQuality_OFF
// if the new codec is not added, notify the publisher to start publishing
if _, ok := d.maxSubscribedQuality[toMime]; !ok {
d.maxSubscribedQuality[toMime] = livekit.VideoQuality_HIGH
}
d.lock.Unlock()
d.update(false)
fromDq.Stop()
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
}
func (d *DynacastManager) Restart() {
d.lock.Lock()
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
dqs := d.getDynacastQualitiesLocked()
d.lock.Unlock()
for _, dq := range dqs {
dq.Restart()
}
}
func (d *DynacastManager) Close() {
d.qualityNotifyOpQueue.Stop()
d.lock.Lock()
dqs := d.getDynacastQualitiesLocked()
d.dynacastQuality = make(map[mime.MimeType]*DynacastQuality)
d.isClosed = true
d.lock.Unlock()
for _, dq := range dqs {
dq.Stop()
}
}
// THere are situations like track unmute or streaming from a different node
// where subscribed quality needs to sent to the provider immediately.
// This bypasses any debouncing and forces a subscribed quality update
// with immediate effect.
func (d *DynacastManager) ForceUpdate() {
d.update(true)
}
// It is possible for tracks to be in pending close state. When track
// is waiting to be closed, a node is not streaming a track. This can
// be used to force an update announcing that subscribed quality is OFF,
// i.e. indicating not pulling track any more.
func (d *DynacastManager) ForceQuality(quality livekit.VideoQuality) {
d.lock.Lock()
defer d.lock.Unlock()
for mime := range d.committedMaxSubscribedQuality {
d.committedMaxSubscribedQuality[mime] = quality
}
d.enqueueSubscribedQualityChange()
}
func (d *DynacastManager) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, mime mime.MimeType, quality livekit.VideoQuality) {
dq := d.getOrCreateDynacastQuality(mime)
if dq != nil {
dq.NotifySubscriberMaxQuality(subscriberID, quality)
}
}
func (d *DynacastManager) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
for _, quality := range qualities {
dq := d.getOrCreateDynacastQuality(quality.CodecMime)
if dq != nil {
dq.NotifySubscriberNodeMaxQuality(nodeID, quality.Quality)
}
}
}
func (d *DynacastManager) getOrCreateDynacastQuality(mimeType mime.MimeType) *DynacastQuality {
d.lock.Lock()
defer d.lock.Unlock()
if d.isClosed || mimeType == mime.MimeTypeUnknown {
return nil
}
if dq := d.dynacastQuality[mimeType]; dq != nil {
return dq
}
dq := NewDynacastQuality(DynacastQualityParams{
MimeType: mimeType,
Logger: d.params.Logger,
})
dq.OnSubscribedMaxQualityChange(d.updateMaxQualityForMime)
dq.Start()
d.dynacastQuality[mimeType] = dq
return dq
}
func (d *DynacastManager) getDynacastQualitiesLocked() []*DynacastQuality {
return maps.Values(d.dynacastQuality)
}
func (d *DynacastManager) updateMaxQualityForMime(mime mime.MimeType, maxQuality livekit.VideoQuality) {
d.lock.Lock()
if _, ok := d.regressedCodec[mime]; !ok {
d.maxSubscribedQuality[mime] = maxQuality
}
d.lock.Unlock()
d.update(false)
}
func (d *DynacastManager) update(force bool) {
d.lock.Lock()
d.params.Logger.Debugw("processing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
if len(d.maxSubscribedQuality) == 0 {
// no mime has been added, nothing to update
d.lock.Unlock()
return
}
// add or remove of a mime triggers an update
changed := len(d.maxSubscribedQuality) != len(d.committedMaxSubscribedQuality)
downgradesOnly := !changed
if !changed {
for mime, quality := range d.maxSubscribedQuality {
if cq, ok := d.committedMaxSubscribedQuality[mime]; ok {
if cq != quality {
changed = true
}
if (cq == livekit.VideoQuality_OFF && quality != livekit.VideoQuality_OFF) || (cq != livekit.VideoQuality_OFF && quality != livekit.VideoQuality_OFF && cq < quality) {
downgradesOnly = false
}
}
}
}
if !force {
if !changed {
d.lock.Unlock()
return
}
if downgradesOnly && d.maxSubscribedQualityDebounce != nil {
if !d.maxSubscribedQualityDebouncePending {
d.params.Logger.Debugw("debouncing quality downgrade",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
d.maxSubscribedQualityDebounce(func() {
d.update(true)
})
d.maxSubscribedQualityDebouncePending = true
} else {
d.params.Logger.Debugw("quality downgrade waiting for debounce",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
}
d.lock.Unlock()
return
}
}
// clear debounce on send
if d.maxSubscribedQualityDebounce != nil {
d.maxSubscribedQualityDebounce(func() {})
d.maxSubscribedQualityDebouncePending = false
}
d.params.Logger.Debugw("committing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
// commit change
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality, len(d.maxSubscribedQuality))
for mime, quality := range d.maxSubscribedQuality {
d.committedMaxSubscribedQuality[mime] = quality
}
d.enqueueSubscribedQualityChange()
d.lock.Unlock()
}
func (d *DynacastManager) enqueueSubscribedQualityChange() {
if d.isClosed || d.onSubscribedMaxQualityChange == nil {
return
}
subscribedCodecs := make([]*livekit.SubscribedCodec, 0, len(d.committedMaxSubscribedQuality))
maxSubscribedQualities := make([]types.SubscribedCodecQuality, 0, len(d.committedMaxSubscribedQuality))
for mime, quality := range d.committedMaxSubscribedQuality {
maxSubscribedQualities = append(maxSubscribedQualities, types.SubscribedCodecQuality{
CodecMime: mime,
Quality: quality,
})
if quality == livekit.VideoQuality_OFF {
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedCodec{
Codec: mime.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
})
} else {
var subscribedQualities []*livekit.SubscribedQuality
for q := livekit.VideoQuality_LOW; q <= livekit.VideoQuality_HIGH; q++ {
subscribedQualities = append(subscribedQualities, &livekit.SubscribedQuality{
Quality: q,
Enabled: q <= quality,
})
}
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedCodec{
Codec: mime.String(),
Qualities: subscribedQualities,
})
}
}
d.params.Logger.Debugw(
"subscribedMaxQualityChange",
"subscribedCodecs", subscribedCodecs,
"maxSubscribedQualities", maxSubscribedQualities,
)
d.qualityNotifyOpQueue.Enqueue(func() {
d.onSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
})
}
@@ -0,0 +1,376 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dynacast
import (
"sort"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/livekit"
)
func TestSubscribedMaxQuality(t *testing.T) {
t.Run("subscribers muted", func(t *testing.T) {
dm := NewDynacastManager(DynacastManagerParams{})
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
// mute all subscribers of vp8
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
expectedSubscribedQualities := []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
})
t.Run("subscribers max quality", func(t *testing.T) {
dm := NewDynacastManager(DynacastManagerParams{
DynacastPauseDelay: 100 * time.Millisecond,
})
lock := sync.RWMutex{}
lock.Lock()
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
lock.Unlock()
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
})
dm.maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
mime.MimeTypeVP8: livekit.VideoQuality_LOW,
mime.MimeTypeAV1: livekit.VideoQuality_LOW,
}
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_MEDIUM)
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
expectedSubscribedQualities := []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// "s1" dropping to MEDIUM should disable HIGH layer
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_MEDIUM)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// "s1" , "s2" , "s3" dropping to LOW should disable HIGH & MEDIUM
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_LOW)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// muting "s2" only should not disable all qualities of vp8, no change of expected qualities
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
time.Sleep(100 * time.Millisecond)
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// muting "s1" and s3 also should disable all qualities
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_OFF)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// unmuting "s1" should enable vp8 previously set max quality
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// a higher quality from a different node should trigger that quality
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
{CodecMime: mime.MimeTypeVP8, Quality: livekit.VideoQuality_HIGH},
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_MEDIUM},
})
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
})
}
func TestCodecRegression(t *testing.T) {
dm := NewDynacastManager(DynacastManagerParams{})
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
expectedSubscribedQualities := []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
dm.HandleCodecRegression(mime.MimeTypeAV1, mime.MimeTypeVP8)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// av1 quality change should be forwarded to vp8
// av1 quality change of node should be ignored
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_HIGH},
})
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
}
func subscribedCodecsAsString(c1 []*livekit.SubscribedCodec) string {
sort.Slice(c1, func(i, j int) bool { return c1[i].Codec < c1[j].Codec })
var s1 string
for _, c := range c1 {
s1 += c.String()
}
return s1
}
@@ -0,0 +1,230 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dynacast
import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
const (
initialQualityUpdateWait = 10 * time.Second
)
type DynacastQualityParams struct {
MimeType mime.MimeType
Logger logger.Logger
}
// DynacastQuality manages max subscribed quality of a single receiver of a media track
type DynacastQuality struct {
params DynacastQualityParams
// quality level enable/disable
lock sync.RWMutex
initialized bool
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality
maxSubscribedQuality livekit.VideoQuality
maxQualityTimer *time.Timer
regressTo *DynacastQuality
onSubscribedMaxQualityChange func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)
}
func NewDynacastQuality(params DynacastQualityParams) *DynacastQuality {
return &DynacastQuality{
params: params,
maxSubscriberQuality: make(map[livekit.ParticipantID]livekit.VideoQuality),
maxSubscriberNodeQuality: make(map[livekit.NodeID]livekit.VideoQuality),
}
}
func (d *DynacastQuality) Start() {
d.reset()
}
func (d *DynacastQuality) Restart() {
d.reset()
}
func (d *DynacastQuality) Stop() {
d.stopMaxQualityTimer()
}
func (d *DynacastQuality) OnSubscribedMaxQualityChange(f func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)) {
d.lock.Lock()
defer d.lock.Unlock()
d.onSubscribedMaxQualityChange = f
}
func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber max quality",
"mime", d.params.MimeType,
"subscriberID", subscriberID,
"quality", quality.String(),
)
d.lock.Lock()
if r := d.regressTo; r != nil {
d.lock.Unlock()
r.NotifySubscriberMaxQuality(subscriberID, quality)
return
}
if quality == livekit.VideoQuality_OFF {
delete(d.maxSubscriberQuality, subscriberID)
} else {
d.maxSubscriberQuality[subscriberID] = quality
}
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber node max quality",
"mime", d.params.MimeType,
"subscriberNodeID", nodeID,
"quality", quality.String(),
)
d.lock.Lock()
if r := d.regressTo; r != nil {
// the downstream node will synthesize correct quality notify (its dynacast manager has codec regression), just ignore it
d.lock.Unlock()
r.params.Logger.Debugw("ignoring node quality change, regressed to another dynacast quality", "mime", d.params.MimeType)
return
}
if quality == livekit.VideoQuality_OFF {
delete(d.maxSubscriberNodeQuality, nodeID)
} else {
d.maxSubscriberNodeQuality[nodeID] = quality
}
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *DynacastQuality) RegressTo(other *DynacastQuality) {
d.lock.Lock()
d.regressTo = other
maxSubscriberQuality := d.maxSubscriberQuality
maxSubscriberNodeQuality := d.maxSubscriberNodeQuality
d.maxSubscriberQuality = make(map[livekit.ParticipantID]livekit.VideoQuality)
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
d.lock.Unlock()
other.lock.Lock()
for subID, quality := range maxSubscriberQuality {
if otherQuality, ok := other.maxSubscriberQuality[subID]; ok {
// no QUALITY_OFF in the map
if quality > otherQuality {
other.maxSubscriberQuality[subID] = quality
}
} else {
other.maxSubscriberQuality[subID] = quality
}
}
for nodeID, quality := range maxSubscriberNodeQuality {
if otherQuality, ok := other.maxSubscriberNodeQuality[nodeID]; ok {
// no QUALITY_OFF in the map
if quality > otherQuality {
other.maxSubscriberNodeQuality[nodeID] = quality
}
} else {
other.maxSubscriberNodeQuality[nodeID] = quality
}
}
other.lock.Unlock()
other.Restart()
}
func (d *DynacastQuality) reset() {
d.lock.Lock()
d.initialized = false
d.lock.Unlock()
d.startMaxQualityTimer()
}
func (d *DynacastQuality) updateQualityChange(force bool) {
d.lock.Lock()
maxSubscribedQuality := livekit.VideoQuality_OFF
for _, subQuality := range d.maxSubscriberQuality {
if maxSubscribedQuality == livekit.VideoQuality_OFF || (subQuality != livekit.VideoQuality_OFF && subQuality > maxSubscribedQuality) {
maxSubscribedQuality = subQuality
}
}
for _, nodeQuality := range d.maxSubscriberNodeQuality {
if maxSubscribedQuality == livekit.VideoQuality_OFF || (nodeQuality != livekit.VideoQuality_OFF && nodeQuality > maxSubscribedQuality) {
maxSubscribedQuality = nodeQuality
}
}
if maxSubscribedQuality == d.maxSubscribedQuality && d.initialized && !force {
d.lock.Unlock()
return
}
d.initialized = true
d.maxSubscribedQuality = maxSubscribedQuality
d.params.Logger.Debugw("notifying quality change",
"mime", d.params.MimeType,
"maxSubscriberQuality", d.maxSubscriberQuality,
"maxSubscriberNodeQuality", d.maxSubscriberNodeQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
"force", force,
)
onSubscribedMaxQualityChange := d.onSubscribedMaxQualityChange
d.lock.Unlock()
if onSubscribedMaxQualityChange != nil {
onSubscribedMaxQualityChange(d.params.MimeType, maxSubscribedQuality)
}
}
func (d *DynacastQuality) startMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
if d.maxQualityTimer != nil {
d.maxQualityTimer.Stop()
d.maxQualityTimer = nil
}
d.maxQualityTimer = time.AfterFunc(initialQualityUpdateWait, func() {
d.stopMaxQualityTimer()
d.updateQualityChange(true)
})
}
func (d *DynacastQuality) stopMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
if d.maxQualityTimer != nil {
d.maxQualityTimer.Stop()
d.maxQualityTimer = nil
}
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"context"
"errors"
"fmt"
"strings"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/webhook"
)
type EgressLauncher interface {
StartEgress(context.Context, *rpc.StartEgressRequest) (*livekit.EgressInfo, error)
}
func StartParticipantEgress(
ctx context.Context,
launcher EgressLauncher,
ts telemetry.TelemetryService,
opts *livekit.AutoParticipantEgress,
identity livekit.ParticipantIdentity,
roomName livekit.RoomName,
roomID livekit.RoomID,
) error {
if req, err := startParticipantEgress(ctx, launcher, opts, identity, roomName, roomID); err != nil {
// send egress failed webhook
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
Event: webhook.EventEgressEnded,
EgressInfo: &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Participant{Participant: req},
},
})
return err
}
return nil
}
func startParticipantEgress(
ctx context.Context,
launcher EgressLauncher,
opts *livekit.AutoParticipantEgress,
identity livekit.ParticipantIdentity,
roomName livekit.RoomName,
roomID livekit.RoomID,
) (*livekit.ParticipantEgressRequest, error) {
req := &livekit.ParticipantEgressRequest{
RoomName: string(roomName),
Identity: string(identity),
FileOutputs: opts.FileOutputs,
SegmentOutputs: opts.SegmentOutputs,
}
switch o := opts.Options.(type) {
case *livekit.AutoParticipantEgress_Preset:
req.Options = &livekit.ParticipantEgressRequest_Preset{Preset: o.Preset}
case *livekit.AutoParticipantEgress_Advanced:
req.Options = &livekit.ParticipantEgressRequest_Advanced{Advanced: o.Advanced}
}
if launcher == nil {
return req, errors.New("egress launcher not found")
}
_, err := launcher.StartEgress(ctx, &rpc.StartEgressRequest{
Request: &rpc.StartEgressRequest_Participant{
Participant: req,
},
RoomId: string(roomID),
})
return req, err
}
func StartTrackEgress(
ctx context.Context,
launcher EgressLauncher,
ts telemetry.TelemetryService,
opts *livekit.AutoTrackEgress,
track types.MediaTrack,
roomName livekit.RoomName,
roomID livekit.RoomID,
) error {
if req, err := startTrackEgress(ctx, launcher, opts, track, roomName, roomID); err != nil {
// send egress failed webhook
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
Event: webhook.EventEgressEnded,
EgressInfo: &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Track{Track: req},
},
})
return err
}
return nil
}
func startTrackEgress(
ctx context.Context,
launcher EgressLauncher,
opts *livekit.AutoTrackEgress,
track types.MediaTrack,
roomName livekit.RoomName,
roomID livekit.RoomID,
) (*livekit.TrackEgressRequest, error) {
output := &livekit.DirectFileOutput{
Filepath: getFilePath(opts.Filepath),
}
switch out := opts.Output.(type) {
case *livekit.AutoTrackEgress_Azure:
output.Output = &livekit.DirectFileOutput_Azure{Azure: out.Azure}
case *livekit.AutoTrackEgress_Gcp:
output.Output = &livekit.DirectFileOutput_Gcp{Gcp: out.Gcp}
case *livekit.AutoTrackEgress_S3:
output.Output = &livekit.DirectFileOutput_S3{S3: out.S3}
}
req := &livekit.TrackEgressRequest{
RoomName: string(roomName),
TrackId: string(track.ID()),
Output: &livekit.TrackEgressRequest_File{
File: output,
},
}
if launcher == nil {
return req, errors.New("egress launcher not found")
}
_, err := launcher.StartEgress(ctx, &rpc.StartEgressRequest{
Request: &rpc.StartEgressRequest_Track{
Track: req,
},
RoomId: string(roomID),
})
return req, err
}
func getFilePath(filepath string) string {
if filepath == "" || strings.HasSuffix(filepath, "/") || strings.Contains(filepath, "{track_id}") {
return filepath
}
idx := strings.Index(filepath, ".")
if idx == -1 {
return fmt.Sprintf("%s-{track_id}", filepath)
} else {
return fmt.Sprintf("%s-%s%s", filepath[:idx], "{track_id}", filepath[idx:])
}
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"errors"
)
var (
ErrRoomClosed = errors.New("room has already closed")
ErrParticipantSessionClosed = errors.New("participant session is already closed")
ErrPermissionDenied = errors.New("no permissions to access the room")
ErrMaxParticipantsExceeded = errors.New("room has exceeded its max participants")
ErrLimitExceeded = errors.New("node has exceeded its configured limit")
ErrAlreadyJoined = errors.New("a participant with the same identity is already in the room")
ErrDataChannelUnavailable = errors.New("data channel is not available")
ErrDataChannelBufferFull = errors.New("data channel buffer is full")
ErrTransportFailure = errors.New("transport failure")
ErrEmptyIdentity = errors.New("participant identity cannot be empty")
ErrEmptyParticipantID = errors.New("participant ID cannot be empty")
ErrMissingGrants = errors.New("VideoGrant is missing")
ErrInternalError = errors.New("internal error")
ErrNameExceedsLimits = errors.New("name length exceeds limits")
ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits")
ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits")
// Track subscription related
ErrNoTrackPermission = errors.New("participant is not allowed to subscribe to this track")
ErrNoSubscribePermission = errors.New("participant is not given permission to subscribe to tracks")
ErrTrackNotFound = errors.New("track cannot be found")
ErrTrackNotBound = errors.New("track not bound")
ErrSubscriptionLimitExceeded = errors.New("participant has exceeded its subscription limit")
ErrNoSubscribeMetricsPermission = errors.New("participant is not given permission to subscribe to metrics")
)
+217
View File
@@ -0,0 +1,217 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"fmt"
"strings"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/livekit"
)
var OpusCodecCapability = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeOpus.String(),
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
}
var RedCodecCapability = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRED.String(),
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "111/111",
}
var videoRTX = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRTX.String(),
ClockRate: 90000,
}
func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedback RTCPFeedbackConfig, filterOutH264HighProfile bool) error {
opusCodec := OpusCodecCapability
opusCodec.RTCPFeedback = rtcpFeedback.Audio
var opusPayload webrtc.PayloadType
if IsCodecEnabled(codecs, opusCodec) {
opusPayload = 111
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: opusCodec,
PayloadType: opusPayload,
}, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
if IsCodecEnabled(codecs, RedCodecCapability) {
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: RedCodecCapability,
PayloadType: 63,
}, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
}
rtxEnabled := IsCodecEnabled(codecs, videoRTX)
h264HighProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP8.String(),
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP9.String(),
ClockRate: 90000,
SDPFmtpLine: "profile-id=0",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 98,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP9.String(),
ClockRate: 90000,
SDPFmtpLine: "profile-id=1",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 100,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 125,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 108,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: h264HighProfileFmtp,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 123,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeAV1.String(),
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 35,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 116,
},
} {
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == h264HighProfileFmtp {
continue
}
if mime.IsMimeTypeStringRTX(codec.MimeType) {
continue
}
if IsCodecEnabled(codecs, codec.RTPCodecCapability) {
if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
if rtxEnabled {
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRTX.String(),
ClockRate: 90000,
SDPFmtpLine: fmt.Sprintf("apt=%d", codec.PayloadType),
},
PayloadType: codec.PayloadType + 1,
}, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
}
}
return nil
}
func registerHeaderExtensions(me *webrtc.MediaEngine, rtpHeaderExtension RTPHeaderExtensionConfig) error {
for _, extension := range rtpHeaderExtension.Video {
if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
for _, extension := range rtpHeaderExtension.Audio {
if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
return nil
}
func createMediaEngine(codecs []*livekit.Codec, config DirectionConfig, filterOutH264HighProfile bool) (*webrtc.MediaEngine, error) {
me := &webrtc.MediaEngine{}
if err := registerCodecs(me, codecs, config.RTCPFeedback, filterOutH264HighProfile); err != nil {
return nil, err
}
if err := registerHeaderExtensions(me, config.RTPHeaderExtension); err != nil {
return nil, err
}
return me, nil
}
func IsCodecEnabled(codecs []*livekit.Codec, cap webrtc.RTPCodecCapability) bool {
for _, codec := range codecs {
if !mime.IsMimeTypeStringEqual(codec.Mime, cap.MimeType) {
continue
}
if codec.FmtpLine == "" || strings.EqualFold(codec.FmtpLine, cap.SDPFmtpLine) {
return true
}
}
return false
}
func selectAlternativeVideoCodec(enabledCodecs []*livekit.Codec) string {
for _, c := range enabledCodecs {
if mime.IsMimeTypeStringVideo(c.Mime) {
return c.Mime
}
}
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
return mime.MimeTypeVP8.String()
}
@@ -0,0 +1,41 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"testing"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/livekit"
)
func TestIsCodecEnabled(t *testing.T) {
t.Run("empty fmtp requirement should match all", func(t *testing.T) {
enabledCodecs := []*livekit.Codec{{Mime: "video/h264"}}
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String(), SDPFmtpLine: "special"}))
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String()}))
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()}))
})
t.Run("when fmtp is provided, require match", func(t *testing.T) {
enabledCodecs := []*livekit.Codec{{Mime: "video/h264", FmtpLine: "special"}}
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String(), SDPFmtpLine: "special"}))
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String()}))
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()}))
})
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"sync"
"time"
"github.com/pion/rtcp"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/sfu"
)
const (
downLostUpdateDelta = time.Second
)
type MediaLossProxyParams struct {
Logger logger.Logger
}
type MediaLossProxy struct {
params MediaLossProxyParams
lock sync.Mutex
maxDownFracLost uint8
maxDownFracLostTs time.Time
maxDownFracLostValid bool
onMediaLossUpdate func(fractionalLoss uint8)
}
func NewMediaLossProxy(params MediaLossProxyParams) *MediaLossProxy {
return &MediaLossProxy{params: params}
}
func (m *MediaLossProxy) OnMediaLossUpdate(f func(fractionalLoss uint8)) {
m.lock.Lock()
m.onMediaLossUpdate = f
m.lock.Unlock()
}
func (m *MediaLossProxy) HandleMaxLossFeedback(_ *sfu.DownTrack, report *rtcp.ReceiverReport) {
m.lock.Lock()
for _, rr := range report.Reports {
m.maxDownFracLostValid = true
if m.maxDownFracLost < rr.FractionLost {
m.maxDownFracLost = rr.FractionLost
}
}
m.lock.Unlock()
m.maybeUpdateLoss()
}
func (m *MediaLossProxy) NotifySubscriberNodeMediaLoss(_nodeID livekit.NodeID, fractionalLoss uint8) {
m.lock.Lock()
m.maxDownFracLostValid = true
if m.maxDownFracLost < fractionalLoss {
m.maxDownFracLost = fractionalLoss
}
m.lock.Unlock()
m.maybeUpdateLoss()
}
func (m *MediaLossProxy) maybeUpdateLoss() {
var (
shouldUpdate bool
maxLost uint8
)
m.lock.Lock()
now := time.Now()
if now.Sub(m.maxDownFracLostTs) > downLostUpdateDelta && m.maxDownFracLostValid {
shouldUpdate = true
maxLost = m.maxDownFracLost
m.maxDownFracLost = 0
m.maxDownFracLostTs = now
m.maxDownFracLostValid = false
}
onMediaLossUpdate := m.onMediaLossUpdate
m.lock.Unlock()
if shouldUpdate {
if onMediaLossUpdate != nil {
onMediaLossUpdate(maxLost)
}
}
}
+488
View File
@@ -0,0 +1,488 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"context"
"math"
"sync"
"time"
"github.com/pion/rtcp"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/rtc/dynacast"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/connectionquality"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/telemetry"
util "github.com/livekit/mediatransportutil"
)
// MediaTrack represents a WebRTC track that needs to be forwarded
// Implements MediaTrack and PublishedTrack interface
type MediaTrack struct {
params MediaTrackParams
numUpTracks atomic.Uint32
buffer *buffer.Buffer
everSubscribed atomic.Bool
*MediaTrackReceiver
*MediaLossProxy
dynacastManager *dynacast.DynacastManager
lock sync.RWMutex
rttFromXR atomic.Bool
enableRegression bool
regressionTargetCodec mime.MimeType
regressionTargetCodecReceived bool
}
type MediaTrackParams struct {
SignalCid string
SdpCid string
ParticipantID livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
BufferFactory *buffer.Factory
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
PLIThrottleConfig sfu.PLIThrottleConfig
AudioConfig sfu.AudioConfig
VideoConfig config.VideoConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
SimTracks map[uint32]SimulcastTrackInfo
OnRTCP func([]rtcp.Packet)
ForwardStats *sfu.ForwardStats
OnTrackEverSubscribed func(livekit.TrackID)
}
func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
t := &MediaTrack{
params: params,
}
// TODO: disable codec regression until simulcast-codec clients knows that
if ti.BackupCodecPolicy == livekit.BackupCodecPolicy_REGRESSION && len(ti.Codecs) > 1 {
t.enableRegression = true
t.regressionTargetCodec = mime.NormalizeMimeType(ti.Codecs[1].MimeType)
t.params.Logger.Debugw("track enabled codec regression", "regressionCodec", t.regressionTargetCodec)
}
t.MediaTrackReceiver = NewMediaTrackReceiver(MediaTrackReceiverParams{
MediaTrack: t,
IsRelayed: false,
ParticipantID: params.ParticipantID,
ParticipantIdentity: params.ParticipantIdentity,
ParticipantVersion: params.ParticipantVersion,
ReceiverConfig: params.ReceiverConfig,
SubscriberConfig: params.SubscriberConfig,
AudioConfig: params.AudioConfig,
Telemetry: params.Telemetry,
Logger: params.Logger,
RegressionTargetCodec: t.regressionTargetCodec,
}, ti)
if ti.Type == livekit.TrackType_AUDIO {
t.MediaLossProxy = NewMediaLossProxy(MediaLossProxyParams{
Logger: params.Logger,
})
t.MediaLossProxy.OnMediaLossUpdate(func(fractionalLoss uint8) {
if t.buffer != nil {
t.buffer.SetLastFractionLostReport(fractionalLoss)
}
})
t.MediaTrackReceiver.OnMediaLossFeedback(t.MediaLossProxy.HandleMaxLossFeedback)
}
if ti.Type == livekit.TrackType_VIDEO {
t.dynacastManager = dynacast.NewDynacastManager(dynacast.DynacastManagerParams{
DynacastPauseDelay: params.VideoConfig.DynacastPauseDelay,
Logger: params.Logger,
})
t.MediaTrackReceiver.OnSetupReceiver(func(mime mime.MimeType) {
t.dynacastManager.AddCodec(mime)
})
t.MediaTrackReceiver.OnSubscriberMaxQualityChange(
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, layer int32) {
t.dynacastManager.NotifySubscriberMaxQuality(
subscriberID,
mimeType,
buffer.SpatialLayerToVideoQuality(layer, t.MediaTrackReceiver.TrackInfo()),
)
},
)
t.MediaTrackReceiver.OnCodecRegression(func(old, new webrtc.RTPCodecParameters) {
t.dynacastManager.HandleCodecRegression(mime.NormalizeMimeType(old.MimeType), mime.NormalizeMimeType(new.MimeType))
})
}
return t
}
func (t *MediaTrack) OnSubscribedMaxQualityChange(
f func(
trackID livekit.TrackID,
trackInfo *livekit.TrackInfo,
subscribedQualities []*livekit.SubscribedCodec,
maxSubscribedQualities []types.SubscribedCodecQuality,
) error,
) {
if t.dynacastManager == nil {
return
}
handler := func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality) {
if f != nil && !t.IsMuted() {
_ = f(t.ID(), t.ToProto(), subscribedQualities, maxSubscribedQualities)
}
for _, q := range maxSubscribedQualities {
receiver := t.Receiver(q.CodecMime)
if receiver != nil {
receiver.SetMaxExpectedSpatialLayer(buffer.VideoQualityToSpatialLayer(q.Quality, t.MediaTrackReceiver.TrackInfo()))
}
}
}
t.dynacastManager.OnSubscribedMaxQualityChange(handler)
}
func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriberNodeMaxQuality(nodeID, qualities)
}
}
func (t *MediaTrack) SignalCid() string {
return t.params.SignalCid
}
func (t *MediaTrack) HasSdpCid(cid string) bool {
if t.params.SdpCid == cid {
return true
}
ti := t.MediaTrackReceiver.TrackInfoClone()
for _, c := range ti.Codecs {
if c.Cid == cid {
return true
}
}
return false
}
func (t *MediaTrack) ToProto() *livekit.TrackInfo {
return t.MediaTrackReceiver.TrackInfoClone()
}
func (t *MediaTrack) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
t.MediaTrackReceiver.UpdateCodecCid(codecs)
}
// AddReceiver adds a new RTP receiver to the track, returns true when receiver represents a new codec
func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRemote, mid string) bool {
var newCodec bool
ssrc := uint32(track.SSRC())
buff, rtcpReader := t.params.BufferFactory.GetBufferPair(ssrc)
if buff == nil || rtcpReader == nil {
t.params.Logger.Errorw("could not retrieve buffer pair", nil)
return newCodec
}
var lastRR uint32
rtcpReader.OnPacket(func(bytes []byte) {
pkts, err := rtcp.Unmarshal(bytes)
if err != nil {
t.params.Logger.Errorw("could not unmarshal RTCP", err)
return
}
for _, pkt := range pkts {
switch pkt := pkt.(type) {
case *rtcp.SourceDescription:
case *rtcp.SenderReport:
if pkt.SSRC == uint32(track.SSRC()) {
buff.SetSenderReportData(pkt.RTPTime, pkt.NTPTime, pkt.PacketCount, pkt.OctetCount)
}
case *rtcp.ExtendedReport:
rttFromXR:
for _, report := range pkt.Reports {
if rr, ok := report.(*rtcp.DLRRReportBlock); ok {
for _, dlrrReport := range rr.Reports {
if dlrrReport.LastRR <= lastRR {
continue
}
nowNTP := util.ToNtpTime(time.Now())
nowNTP32 := uint32(nowNTP >> 16)
ntpDiff := nowNTP32 - dlrrReport.LastRR - dlrrReport.DLRR
rtt := uint32(math.Ceil(float64(ntpDiff) * 1000.0 / 65536.0))
buff.SetRTT(rtt)
t.rttFromXR.Store(true)
lastRR = dlrrReport.LastRR
break rttFromXR
}
}
}
}
}
})
ti := t.MediaTrackReceiver.TrackInfoClone()
t.lock.Lock()
var regressCodec bool
mimeType := mime.NormalizeMimeType(track.Codec().MimeType)
layer := buffer.RidToSpatialLayer(track.RID(), ti)
t.params.Logger.Debugw(
"AddReceiver",
"rid", track.RID(),
"layer", layer,
"ssrc", track.SSRC(),
"codec", track.Codec(),
)
wr := t.MediaTrackReceiver.Receiver(mimeType)
if wr == nil {
priority := -1
for idx, c := range ti.Codecs {
if mime.IsMimeTypeStringEqual(track.Codec().MimeType, c.MimeType) {
priority = idx
break
}
}
if priority < 0 {
switch len(ti.Codecs) {
case 0:
// audio track
priority = 0
case 1:
// older clients or non simulcast-codec, mime type only set later
if ti.Codecs[0].MimeType == "" {
priority = 0
}
}
}
if priority < 0 {
t.params.Logger.Warnw("could not find codec for webrtc receiver", nil, "webrtcCodec", mimeType, "track", logger.Proto(ti))
t.lock.Unlock()
return false
}
newWR := sfu.NewWebRTCReceiver(
receiver,
track,
ti,
LoggerWithCodecMime(t.params.Logger, mimeType),
t.params.OnRTCP,
t.params.VideoConfig.StreamTrackerManager,
sfu.WithPliThrottleConfig(t.params.PLIThrottleConfig),
sfu.WithAudioConfig(t.params.AudioConfig),
sfu.WithLoadBalanceThreshold(20),
sfu.WithStreamTrackers(),
sfu.WithForwardStats(t.params.ForwardStats),
)
newWR.OnCloseHandler(func() {
t.MediaTrackReceiver.SetClosing()
t.MediaTrackReceiver.ClearReceiver(mimeType, false)
if t.MediaTrackReceiver.TryClose() {
if t.dynacastManager != nil {
t.dynacastManager.Close()
}
}
})
// SIMULCAST-CODEC-TODO: these need to be receiver/mime aware, setting it up only for primary now
if priority == 0 {
newWR.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
key := telemetry.StatsKeyForTrack(livekit.StreamType_UPSTREAM, t.PublisherID(), t.ID(), ti.Source, ti.Type)
t.params.Telemetry.TrackStats(key, stat)
})
newWR.OnMaxLayerChange(t.onMaxLayerChange)
}
if t.PrimaryReceiver() == nil {
// primary codec published, set potential codecs
potentialCodecs := make([]webrtc.RTPCodecParameters, 0, len(ti.Codecs))
parameters := receiver.GetParameters()
for _, c := range ti.Codecs {
for _, nc := range parameters.Codecs {
if mime.IsMimeTypeStringEqual(nc.MimeType, c.MimeType) {
potentialCodecs = append(potentialCodecs, nc)
break
}
}
}
if len(potentialCodecs) > 0 {
t.params.Logger.Debugw("primary codec published, set potential codecs", "potential", potentialCodecs)
t.MediaTrackReceiver.SetPotentialCodecs(potentialCodecs, parameters.HeaderExtensions)
}
}
t.buffer = buff
t.MediaTrackReceiver.SetupReceiver(newWR, priority, mid)
for ssrc, info := range t.params.SimTracks {
if info.Mid == mid {
t.MediaTrackReceiver.SetLayerSsrc(mimeType, info.Rid, ssrc)
}
}
wr = newWR
newCodec = true
newWR.AddOnCodecStateChange(func(codec webrtc.RTPCodecParameters, state sfu.ReceiverCodecState) {
t.MediaTrackReceiver.HandleReceiverCodecChange(newWR, codec, state)
})
}
if newCodec && t.enableRegression {
if mimeType == t.regressionTargetCodec {
t.params.Logger.Infow("regression target codec received", "codec", mimeType)
t.regressionTargetCodecReceived = true
regressCodec = true
} else if t.regressionTargetCodecReceived {
regressCodec = true
}
}
t.lock.Unlock()
if err := wr.(*sfu.WebRTCReceiver).AddUpTrack(track, buff); err != nil {
t.params.Logger.Warnw(
"adding up track failed", err,
"rid", track.RID(),
"layer", layer,
"ssrc", track.SSRC(),
"newCodec", newCodec,
)
buff.Close()
return false
}
// LK-TODO: can remove this completely when VideoLayers protocol becomes the default as it has info from client or if we decide to use TrackInfo.Simulcast
if t.numUpTracks.Inc() > 1 || track.RID() != "" {
// cannot only rely on numUpTracks since we fire metadata events immediately after the first layer
t.SetSimulcast(true)
}
var bitrates int
if len(ti.Layers) > int(layer) {
bitrates = int(ti.Layers[layer].GetBitrate())
}
t.MediaTrackReceiver.SetLayerSsrc(mimeType, track.RID(), uint32(track.SSRC()))
if regressCodec {
for _, c := range ti.Codecs {
if mime.NormalizeMimeType(c.MimeType) == t.regressionTargetCodec {
continue
}
t.params.Logger.Debugw("suspending codec for codec regression", "codec", c.MimeType)
if r := t.MediaTrackReceiver.Receiver(mime.NormalizeMimeType(c.MimeType)); r != nil {
if rtcreceiver, ok := r.(*sfu.WebRTCReceiver); ok {
rtcreceiver.SetCodecState(sfu.ReceiverCodecStateSuspended)
}
}
}
}
buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, bitrates)
// if subscriber request fps before fps calculated, update them after fps updated.
buff.OnFpsChanged(func() {
t.MediaTrackSubscriptions.UpdateVideoLayers()
})
buff.OnFinalRtpStats(func(stats *livekit.RTPStats) {
t.params.Telemetry.TrackPublishRTPStats(
context.Background(),
t.params.ParticipantID,
t.ID(),
mimeType,
int(layer),
stats,
)
})
return newCodec
}
func (t *MediaTrack) GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality) {
receiver := t.PrimaryReceiver()
if rtcReceiver, ok := receiver.(*sfu.WebRTCReceiver); ok {
return rtcReceiver.GetConnectionScoreAndQuality()
}
return connectionquality.MaxMOS, livekit.ConnectionQuality_EXCELLENT
}
func (t *MediaTrack) SetRTT(rtt uint32) {
if !t.rttFromXR.Load() {
t.MediaTrackReceiver.SetRTT(rtt)
}
}
func (t *MediaTrack) HasPendingCodec() bool {
return t.MediaTrackReceiver.PrimaryReceiver() == nil
}
func (t *MediaTrack) onMaxLayerChange(maxLayer int32) {
t.MediaTrackReceiver.NotifyMaxLayerChange(maxLayer)
}
func (t *MediaTrack) Restart() {
t.MediaTrackReceiver.Restart()
if t.dynacastManager != nil {
t.dynacastManager.Restart()
}
}
func (t *MediaTrack) Close(isExpectedToResume bool) {
t.MediaTrackReceiver.SetClosing()
if t.dynacastManager != nil {
t.dynacastManager.Close()
}
t.MediaTrackReceiver.ClearAllReceivers(isExpectedToResume)
t.MediaTrackReceiver.Close(isExpectedToResume)
}
func (t *MediaTrack) SetMuted(muted bool) {
// update quality based on subscription if unmuting.
// This will queue up the current state, but subscriber
// driven changes could update it.
if !muted && t.dynacastManager != nil {
t.dynacastManager.ForceUpdate()
}
t.MediaTrackReceiver.SetMuted(muted)
}
// OnTrackSubscribed is called when the track is subscribed by a non-hidden subscriber
// this allows the publisher to know when they should start sending data
func (t *MediaTrack) OnTrackSubscribed() {
if !t.everSubscribed.Swap(true) && t.params.OnTrackEverSubscribed != nil {
go t.params.OnTrackEverSubscribed(t.ID())
}
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
)
func TestTrackInfo(t *testing.T) {
// ensures that persisted trackinfo is being returned
ti := livekit.TrackInfo{
Sid: "testsid",
Name: "testtrack",
Source: livekit.TrackSource_SCREEN_SHARE,
Type: livekit.TrackType_VIDEO,
Simulcast: false,
Width: 100,
Height: 80,
Muted: true,
}
mt := NewMediaTrack(MediaTrackParams{}, &ti)
outInfo := mt.ToProto()
require.Equal(t, ti.Muted, outInfo.Muted)
require.Equal(t, ti.Name, outInfo.Name)
require.Equal(t, ti.Name, mt.Name())
require.Equal(t, livekit.TrackID(ti.Sid), mt.ID())
require.Equal(t, ti.Type, outInfo.Type)
require.Equal(t, ti.Type, mt.Kind())
require.Equal(t, ti.Source, outInfo.Source)
require.Equal(t, ti.Width, outInfo.Width)
require.Equal(t, ti.Height, outInfo.Height)
require.Equal(t, ti.Simulcast, outInfo.Simulcast)
// make it simulcasted
mt.SetSimulcast(true)
require.True(t, mt.ToProto().Simulcast)
}
func TestGetQualityForDimension(t *testing.T) {
t.Run("landscape source", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 200))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(200, 250))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(700, 480))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(500, 1000))
})
t.Run("portrait source", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 540,
Height: 960,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(200, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(600, 900))
})
t.Run("layers provided", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 960,
Height: 540,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
},
},
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
})
t.Run("highest layer with smallest dimensions", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 1080,
Height: 720,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
},
},
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
mt = NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
},
},
})
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(120, 120))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
})
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,465 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"errors"
"sync"
"github.com/pion/rtcp"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/mime"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/telemetry"
)
var (
errAlreadySubscribed = errors.New("already subscribed")
errNotFound = errors.New("not found")
)
// MediaTrackSubscriptions manages subscriptions of a media track
type MediaTrackSubscriptions struct {
params MediaTrackSubscriptionsParams
subscribedTracksMu sync.RWMutex
subscribedTracks map[livekit.ParticipantID]types.SubscribedTrack
onDownTrackCreated func(downTrack *sfu.DownTrack)
onSubscriberMaxQualityChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)
}
type MediaTrackSubscriptionsParams struct {
MediaTrack types.MediaTrack
IsRelayed bool
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
}
func NewMediaTrackSubscriptions(params MediaTrackSubscriptionsParams) *MediaTrackSubscriptions {
return &MediaTrackSubscriptions{
params: params,
subscribedTracks: make(map[livekit.ParticipantID]types.SubscribedTrack),
}
}
func (t *MediaTrackSubscriptions) OnDownTrackCreated(f func(downTrack *sfu.DownTrack)) {
t.onDownTrackCreated = f
}
func (t *MediaTrackSubscriptions) OnSubscriberMaxQualityChange(f func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)) {
t.onSubscriberMaxQualityChange = f
}
func (t *MediaTrackSubscriptions) SetMuted(muted bool) {
// update mute of all subscribed tracks
for _, st := range t.getAllSubscribedTracks() {
st.SetPublisherMuted(muted)
}
}
func (t *MediaTrackSubscriptions) IsSubscriber(subID livekit.ParticipantID) bool {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
_, ok := t.subscribedTracks[subID]
return ok
}
// AddSubscriber subscribes sub to current mediaTrack
func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *WrappedReceiver) (types.SubscribedTrack, error) {
trackID := t.params.MediaTrack.ID()
subscriberID := sub.ID()
// don't subscribe to the same track multiple times
t.subscribedTracksMu.Lock()
if _, ok := t.subscribedTracks[subscriberID]; ok {
t.subscribedTracksMu.Unlock()
return nil, errAlreadySubscribed
}
t.subscribedTracksMu.Unlock()
var rtcpFeedback []webrtc.RTCPFeedback
var maxTrack int
switch t.params.MediaTrack.Kind() {
case livekit.TrackType_AUDIO:
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Audio
maxTrack = t.params.ReceiverConfig.PacketBufferSizeAudio
case livekit.TrackType_VIDEO:
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Video
maxTrack = t.params.ReceiverConfig.PacketBufferSizeVideo
}
codecs := wr.Codecs()
for _, c := range codecs {
c.RTCPFeedback = rtcpFeedback
}
streamID := wr.StreamID()
if sub.SupportsSyncStreamID() && t.params.MediaTrack.Stream() != "" {
streamID = PackSyncStreamID(t.params.MediaTrack.PublisherID(), t.params.MediaTrack.Stream())
}
var trailer []byte
if t.params.MediaTrack.IsEncrypted() {
trailer = sub.GetTrailer()
}
downTrack, err := sfu.NewDownTrack(sfu.DowntrackParams{
Codecs: codecs,
Source: t.params.MediaTrack.Source(),
Receiver: wr,
BufferFactory: sub.GetBufferFactory(),
SubID: subscriberID,
StreamID: streamID,
MaxTrack: maxTrack,
PlayoutDelayLimit: sub.GetPlayoutDelayConfig(),
Pacer: sub.GetPacer(),
Trailer: trailer,
Logger: LoggerWithTrack(sub.GetLogger().WithComponent(sutils.ComponentSub), trackID, t.params.IsRelayed),
RTCPWriter: sub.WriteSubscriberRTCP,
DisableSenderReportPassThrough: sub.GetDisableSenderReportPassThrough(),
SupportsCodecChange: sub.SupportsCodecChange(),
})
if err != nil {
return nil, err
}
if t.onDownTrackCreated != nil {
t.onDownTrackCreated(downTrack)
}
subTrack := NewSubscribedTrack(SubscribedTrackParams{
PublisherID: t.params.MediaTrack.PublisherID(),
PublisherIdentity: t.params.MediaTrack.PublisherIdentity(),
PublisherVersion: t.params.MediaTrack.PublisherVersion(),
Subscriber: sub,
MediaTrack: t.params.MediaTrack,
DownTrack: downTrack,
AdaptiveStream: sub.GetAdaptiveStream(),
})
if !sub.Hidden() {
subTrack.AddOnBind(func(err error) {
if err == nil {
t.params.MediaTrack.OnTrackSubscribed()
}
})
}
// Bind callback can happen from replaceTrack, so set it up early
var reusingTransceiver atomic.Bool
var dtState sfu.DownTrackState
downTrack.OnCodecNegotiated(func(codec webrtc.RTPCodecCapability) {
if !wr.DetermineReceiver(codec) {
if t.onSubscriberMaxQualityChange != nil {
go func() {
spatial := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.params.MediaTrack.ToProto())
t.onSubscriberMaxQualityChange(downTrack.SubscriberID(), mime.NormalizeMimeType(codec.MimeType), spatial)
}()
}
}
})
downTrack.OnBinding(func(err error) {
if err != nil {
go subTrack.Bound(err)
return
}
if reusingTransceiver.Load() {
sub.GetLogger().Debugw("seeding downtrack state", "trackID", trackID)
downTrack.SeedState(dtState)
}
if err = wr.AddDownTrack(downTrack); err != nil && err != sfu.ErrReceiverClosed {
sub.GetLogger().Errorw(
"could not add down track", err,
"publisher", subTrack.PublisherIdentity(),
"publisherID", subTrack.PublisherID(),
"trackID", trackID,
)
}
go subTrack.Bound(nil)
subTrack.SetPublisherMuted(t.params.MediaTrack.IsMuted())
})
downTrack.OnStatsUpdate(func(_ *sfu.DownTrack, stat *livekit.AnalyticsStat) {
key := telemetry.StatsKeyForTrack(livekit.StreamType_DOWNSTREAM, subscriberID, trackID, t.params.MediaTrack.Source(), t.params.MediaTrack.Kind())
t.params.Telemetry.TrackStats(key, stat)
})
downTrack.OnMaxLayerChanged(func(dt *sfu.DownTrack, layer int32) {
if t.onSubscriberMaxQualityChange != nil {
t.onSubscriberMaxQualityChange(dt.SubscriberID(), dt.Mime(), layer)
}
})
downTrack.OnRttUpdate(func(_ *sfu.DownTrack, rtt uint32) {
go sub.UpdateMediaRTT(rtt)
})
downTrack.AddReceiverReportListener(func(dt *sfu.DownTrack, report *rtcp.ReceiverReport) {
sub.HandleReceiverReport(dt, report)
})
var transceiver *webrtc.RTPTransceiver
var sender *webrtc.RTPSender
// try cached RTP senders for a chance to replace track
var existingTransceiver *webrtc.RTPTransceiver
replacedTrack := false
existingTransceiver, dtState = sub.GetCachedDownTrack(trackID)
if existingTransceiver != nil {
sub.GetLogger().Debugw(
"trying to use existing transceiver",
"publisher", subTrack.PublisherIdentity(),
"publisherID", subTrack.PublisherID(),
"trackID", trackID,
)
reusingTransceiver.Store(true)
rtpSender := existingTransceiver.Sender()
if rtpSender != nil {
// replaced track will bind immediately without negotiation, SetTransceiver first before bind
downTrack.SetTransceiver(existingTransceiver)
err := rtpSender.ReplaceTrack(downTrack)
if err == nil {
sender = rtpSender
transceiver = existingTransceiver
replacedTrack = true
sub.GetLogger().Debugw(
"track replaced",
"publisher", subTrack.PublisherIdentity(),
"publisherID", subTrack.PublisherID(),
"trackID", trackID,
)
}
}
if !replacedTrack {
// Could not re-use cached transceiver for this track.
// Stop the transceiver so that it is at least not active.
// It is not usable once stopped,
//
// Adding down track will create a new transceiver (or re-use
// an inactive existing one). In either case, a renegotiation
// will happen and that will notify remote of this stopped
// transceiver
existingTransceiver.Stop()
reusingTransceiver.Store(false)
}
}
// if cannot replace, find an unused transceiver or add new one
if transceiver == nil {
info := t.params.MediaTrack.ToProto()
addTrackParams := types.AddTrackParams{
Stereo: info.Stereo,
Red: !info.DisableRed,
}
if addTrackParams.Red && (len(codecs) == 1 && mime.IsMimeTypeStringOpus(codecs[0].MimeType)) {
addTrackParams.Red = false
}
sub.VerifySubscribeParticipantInfo(subTrack.PublisherID(), subTrack.PublisherVersion())
if sub.SupportsTransceiverReuse() {
//
// AddTrack will create a new transceiver or re-use an unused one
// if the attributes match. This prevents SDP from bloating
// because of dormant transceivers building up.
//
sender, transceiver, err = sub.AddTrackLocal(downTrack, addTrackParams)
if err != nil {
return nil, err
}
} else {
sender, transceiver, err = sub.AddTransceiverFromTrackLocal(downTrack, addTrackParams)
if err != nil {
return nil, err
}
}
}
// whether re-using or stopping remove transceiver from cache
// NOTE: safety net, if somehow a cached transceiver is re-used by a different track
sub.UncacheDownTrack(transceiver)
// negotiation isn't required if we've replaced track
// ONE-SHOT-SIGNALLING-MODE: this should not be needed, but that mode information is not available here,
// but it is not detrimental to set this, needs clean up when participants modes are separated out better.
subTrack.SetNeedsNegotiation(!replacedTrack)
subTrack.SetRTPSender(sender)
// it is possible that subscribed track is closed before subscription manager sets
// the `OnClose` callback. That handler in subscription manager removes the track
// from the peer connection.
//
// But, the subscription could be removed early if the published track is closed
// while adding subscription. In those cases, subscription manager would not have set
// the `OnClose` callback. So, set it here to handle cases of early close.
subTrack.OnClose(func(isExpectedToResume bool) {
if !isExpectedToResume {
if err := sub.RemoveTrackLocal(sender); err != nil {
t.params.Logger.Warnw("could not remove track from peer connection", err)
}
}
})
downTrack.SetTransceiver(transceiver)
downTrack.OnCloseHandler(func(isExpectedToResume bool) {
t.downTrackClosed(sub, subTrack, isExpectedToResume)
})
t.subscribedTracksMu.Lock()
t.subscribedTracks[subscriberID] = subTrack
t.subscribedTracksMu.Unlock()
return subTrack, nil
}
// RemoveSubscriber removes participant from subscription
// stop all forwarders to the client
func (t *MediaTrackSubscriptions) RemoveSubscriber(subscriberID livekit.ParticipantID, isExpectedToResume bool) error {
subTrack := t.getSubscribedTrack(subscriberID)
if subTrack == nil {
return errNotFound
}
t.params.Logger.Debugw("removing subscriber", "subscriberID", subscriberID, "isExpectedToResume", isExpectedToResume)
t.closeSubscribedTrack(subTrack, isExpectedToResume)
return nil
}
func (t *MediaTrackSubscriptions) closeSubscribedTrack(subTrack types.SubscribedTrack, isExpectedToResume bool) {
dt := subTrack.DownTrack()
if dt == nil {
return
}
if isExpectedToResume {
dt.CloseWithFlush(false)
} else {
// flushing blocks, avoid blocking when publisher removes all its subscribers
go dt.CloseWithFlush(true)
}
}
func (t *MediaTrackSubscriptions) GetAllSubscribers() []livekit.ParticipantID {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
subs := make([]livekit.ParticipantID, 0, len(t.subscribedTracks))
for id := range t.subscribedTracks {
subs = append(subs, id)
}
return subs
}
func (t *MediaTrackSubscriptions) GetAllSubscribersForMime(mime mime.MimeType) []livekit.ParticipantID {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
subs := make([]livekit.ParticipantID, 0, len(t.subscribedTracks))
for id, subTrack := range t.subscribedTracks {
if subTrack.DownTrack().Mime() != mime {
continue
}
subs = append(subs, id)
}
return subs
}
func (t *MediaTrackSubscriptions) GetNumSubscribers() int {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
return len(t.subscribedTracks)
}
func (t *MediaTrackSubscriptions) UpdateVideoLayers() {
for _, st := range t.getAllSubscribedTracks() {
st.UpdateVideoLayer()
}
}
func (t *MediaTrackSubscriptions) getSubscribedTrack(subscriberID livekit.ParticipantID) types.SubscribedTrack {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
return t.subscribedTracks[subscriberID]
}
func (t *MediaTrackSubscriptions) getAllSubscribedTracks() []types.SubscribedTrack {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
return t.getAllSubscribedTracksLocked()
}
func (t *MediaTrackSubscriptions) getAllSubscribedTracksLocked() []types.SubscribedTrack {
subTracks := make([]types.SubscribedTrack, 0, len(t.subscribedTracks))
for _, subTrack := range t.subscribedTracks {
subTracks = append(subTracks, subTrack)
}
return subTracks
}
func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} {
subscribedTrackInfo := make([]map[string]interface{}, 0)
for _, val := range t.getAllSubscribedTracks() {
if st, ok := val.(*SubscribedTrack); ok {
subscribedTrackInfo = append(subscribedTrackInfo, st.DownTrack().DebugInfo())
}
}
return subscribedTrackInfo
}
func (t *MediaTrackSubscriptions) downTrackClosed(
sub types.LocalParticipant,
subTrack types.SubscribedTrack,
isExpectedToResume bool,
) {
// Cache transceiver for potential re-use on resume.
// To ensure subscription manager does not re-subscribe before caching,
// delete the subscribed track only after caching.
if isExpectedToResume {
dt := subTrack.DownTrack()
tr := dt.GetTransceiver()
if tr != nil {
sub.CacheDownTrack(subTrack.ID(), tr, dt.GetState())
}
}
go func() {
t.subscribedTracksMu.Lock()
delete(t.subscribedTracks, sub.ID())
t.subscribedTracksMu.Unlock()
subTrack.Close(isExpectedToResume)
}()
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,716 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"fmt"
"strings"
"testing"
"time"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/routing/routingfakes"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
"github.com/livekit/livekit-server/pkg/testutils"
)
func TestIsReady(t *testing.T) {
tests := []struct {
state livekit.ParticipantInfo_State
ready bool
}{
{
state: livekit.ParticipantInfo_JOINING,
ready: false,
},
{
state: livekit.ParticipantInfo_JOINED,
ready: true,
},
{
state: livekit.ParticipantInfo_ACTIVE,
ready: true,
},
{
state: livekit.ParticipantInfo_DISCONNECTED,
ready: false,
},
}
for _, test := range tests {
t.Run(test.state.String(), func(t *testing.T) {
p := &ParticipantImpl{}
p.state.Store(test.state)
require.Equal(t, test.ready, p.IsReady())
})
}
}
func TestTrackPublishing(t *testing.T) {
t.Run("should send the correct events", func(t *testing.T) {
p := newParticipantForTest("test")
track := &typesfakes.FakeMediaTrack{}
track.IDReturns("id")
published := false
updated := false
p.OnTrackUpdated(func(p types.LocalParticipant, track types.MediaTrack) {
updated = true
})
p.OnTrackPublished(func(p types.LocalParticipant, track types.MediaTrack) {
published = true
})
p.UpTrackManager.AddPublishedTrack(track)
p.handleTrackPublished(track)
require.True(t, published)
require.False(t, updated)
require.Len(t, p.UpTrackManager.publishedTracks, 1)
})
t.Run("sends back trackPublished event", func(t *testing.T) {
p := newParticipantForTest("test")
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "webcam",
Type: livekit.TrackType_VIDEO,
Width: 1024,
Height: 768,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
res := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
require.IsType(t, &livekit.SignalResponse_TrackPublished{}, res.Message)
published := res.Message.(*livekit.SignalResponse_TrackPublished).TrackPublished
require.Equal(t, "cid", published.Cid)
require.Equal(t, "webcam", published.Track.Name)
require.Equal(t, livekit.TrackType_VIDEO, published.Track.Type)
require.Equal(t, uint32(1024), published.Track.Width)
require.Equal(t, uint32(768), published.Track.Height)
})
t.Run("should not allow adding of duplicate tracks", func(t *testing.T) {
p := newParticipantForTest("test")
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "duplicate",
Type: livekit.TrackType_AUDIO,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
})
t.Run("should queue adding of duplicate tracks if already published by client id in signalling", func(t *testing.T) {
p := newParticipantForTest("test")
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
track := &typesfakes.FakeLocalMediaTrack{}
track.SignalCidReturns("cid")
track.ToProtoReturns(&livekit.TrackInfo{})
// directly add to publishedTracks without lock - for testing purpose only
p.UpTrackManager.publishedTracks["cid"] = track
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))
// add again - it should be added to the queue
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))
// check SID is the same
require.Equal(t, p.pendingTracks["cid"].trackInfos[0].Sid, p.pendingTracks["cid"].trackInfos[1].Sid)
})
t.Run("should queue adding of duplicate tracks if already published by client id in sdp", func(t *testing.T) {
p := newParticipantForTest("test")
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
track := &typesfakes.FakeLocalMediaTrack{}
track.ToProtoReturns(&livekit.TrackInfo{})
track.HasSdpCidCalls(func(s string) bool { return s == "cid" })
// directly add to publishedTracks without lock - for testing purpose only
p.UpTrackManager.publishedTracks["cid"] = track
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))
// add again - it should be added to the queue
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))
// check SID is the same
require.Equal(t, p.pendingTracks["cid"].trackInfos[0].Sid, p.pendingTracks["cid"].trackInfos[1].Sid)
})
t.Run("should not allow adding disallowed sources", func(t *testing.T) {
p := newParticipantForTest("test")
p.SetPermission(&livekit.ParticipantPermission{
CanPublish: true,
CanPublishSources: []livekit.TrackSource{
livekit.TrackSource_CAMERA,
},
})
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Name: "webcam",
Source: livekit.TrackSource_CAMERA,
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid2",
Name: "rejected source",
Type: livekit.TrackType_AUDIO,
Source: livekit.TrackSource_MICROPHONE,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
})
}
func TestOutOfOrderUpdates(t *testing.T) {
p := newParticipantForTest("test")
p.updateState(livekit.ParticipantInfo_JOINED)
p.SetMetadata("initial metadata")
sink := p.getResponseSink().(*routingfakes.FakeMessageSink)
pi1 := p.ToProto()
p.SetMetadata("second update")
pi2 := p.ToProto()
require.Greater(t, pi2.Version, pi1.Version)
// send the second update first
require.NoError(t, p.SendParticipantUpdate([]*livekit.ParticipantInfo{pi2}))
require.NoError(t, p.SendParticipantUpdate([]*livekit.ParticipantInfo{pi1}))
// only sent once, and it's the earlier message
require.Equal(t, 1, sink.WriteMessageCallCount())
sent := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
require.Equal(t, "second update", sent.GetUpdate().Participants[0].Metadata)
}
// after disconnection, things should continue to function and not panic
func TestDisconnectTiming(t *testing.T) {
t.Run("Negotiate doesn't panic after channel closed", func(t *testing.T) {
p := newParticipantForTest("test")
msg := routing.NewMessageChannel(livekit.ConnectionID("test"), routing.DefaultMessageChannelSize)
p.params.Sink = msg
go func() {
for msg := range msg.ReadChan() {
t.Log("received message from chan", msg)
}
}()
track := &typesfakes.FakeMediaTrack{}
p.UpTrackManager.AddPublishedTrack(track)
p.handleTrackPublished(track)
// close channel and then try to Negotiate
msg.Close()
})
}
func TestCorrectJoinedAt(t *testing.T) {
p := newParticipantForTest("test")
info := p.ToProto()
require.NotZero(t, info.JoinedAt)
require.True(t, time.Now().Unix()-info.JoinedAt <= 1)
}
func TestMuteSetting(t *testing.T) {
t.Run("can set mute when track is pending", func(t *testing.T) {
p := newParticipantForTest("test")
ti := &livekit.TrackInfo{Sid: "testTrack"}
p.pendingTracks["cid"] = &pendingTrackInfo{trackInfos: []*livekit.TrackInfo{ti}}
p.SetTrackMuted(livekit.TrackID(ti.Sid), true, false)
require.True(t, p.pendingTracks["cid"].trackInfos[0].Muted)
})
t.Run("can publish a muted track", func(t *testing.T) {
p := newParticipantForTest("test")
p.AddTrack(&livekit.AddTrackRequest{
Cid: "cid",
Type: livekit.TrackType_AUDIO,
Muted: true,
})
_, ti, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
require.NotNil(t, ti)
require.True(t, ti.Muted)
})
}
func TestSubscriberAsPrimary(t *testing.T) {
t.Run("protocol 4 uses subs as primary", func(t *testing.T) {
p := newParticipantForTestWithOpts("test", &participantOpts{
permissions: &livekit.ParticipantPermission{
CanSubscribe: true,
CanPublish: true,
},
})
require.True(t, p.SubscriberAsPrimary())
})
t.Run("protocol 2 uses pub as primary", func(t *testing.T) {
p := newParticipantForTestWithOpts("test", &participantOpts{
protocolVersion: 2,
permissions: &livekit.ParticipantPermission{
CanSubscribe: true,
CanPublish: true,
},
})
require.False(t, p.SubscriberAsPrimary())
})
t.Run("publisher only uses pub as primary", func(t *testing.T) {
p := newParticipantForTestWithOpts("test", &participantOpts{
permissions: &livekit.ParticipantPermission{
CanSubscribe: false,
CanPublish: true,
},
})
require.False(t, p.SubscriberAsPrimary())
// ensure that it doesn't change after perms
p.SetPermission(&livekit.ParticipantPermission{
CanSubscribe: true,
CanPublish: true,
})
require.False(t, p.SubscriberAsPrimary())
})
}
func TestDisableCodecs(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: false,
clientConf: &livekit.ClientConfiguration{
DisabledCodecs: &livekit.DisabledCodecs{
Codecs: []*livekit.Codec{
{Mime: "video/h264"},
},
},
},
})
participant.SetMigrateState(types.MigrateStateComplete)
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
pc.SetLocalDescription(sdp)
codecs := transceiver.Receiver().GetParameters().Codecs
var found264 bool
for _, c := range codecs {
if mime.IsMimeTypeStringH264(c.MimeType) {
found264 = true
}
}
require.True(t, found264)
// negotiated codec should not contain h264
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var answer webrtc.SessionDescription
var answerReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer = FromProtoSessionDescription(res.GetAnswer())
answerReceived.Store(true)
}
}
return nil
})
participant.HandleOffer(sdp)
testutils.WithTimeout(t, func() string {
if answerReceived.Load() {
return ""
} else {
return "answer not received"
}
})
require.NoError(t, pc.SetRemoteDescription(answer), answer.SDP, sdp.SDP)
codecs = transceiver.Receiver().GetParameters().Codecs
found264 = false
for _, c := range codecs {
if mime.IsMimeTypeStringH264(c.MimeType) {
found264 = true
}
}
require.False(t, found264)
}
func TestDisablePublishCodec(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
clientConf: &livekit.ClientConfiguration{
DisabledCodecs: &livekit.DisabledCodecs{
Publish: []*livekit.Codec{
{Mime: "video/h264"},
},
},
},
})
for _, codec := range participant.enabledPublishCodecs {
require.False(t, mime.IsMimeTypeStringH264(codec.Mime))
}
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var publishReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if published := res.GetTrackPublished(); published != nil {
publishReceived.Store(true)
require.NotEmpty(t, published.Track.Codecs)
require.True(t, mime.IsMimeTypeStringVP8(published.Track.Codecs[0].MimeType))
}
}
return nil
})
// simulcast codec response should pick an alternative
participant.AddTrack(&livekit.AddTrackRequest{
Cid: "cid1",
Type: livekit.TrackType_VIDEO,
SimulcastCodecs: []*livekit.SimulcastCodec{{
Codec: "h264",
Cid: "cid1",
}},
})
require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)
// publishing a supported codec should not change
publishReceived.Store(false)
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if published := res.GetTrackPublished(); published != nil {
publishReceived.Store(true)
require.NotEmpty(t, published.Track.Codecs)
require.True(t, mime.IsMimeTypeStringVP8(published.Track.Codecs[0].MimeType))
}
}
return nil
})
participant.AddTrack(&livekit.AddTrackRequest{
Cid: "cid2",
Type: livekit.TrackType_VIDEO,
SimulcastCodecs: []*livekit.SimulcastCodec{{
Codec: "vp8",
Cid: "cid2",
}},
})
require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)
}
func TestPreferVideoCodecForPublisher(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
})
participant.SetMigrateState(types.MigrateStateComplete)
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
for i := 0; i < 2; i++ {
// publish h264 track without client preferred codec
trackCid := fmt.Sprintf("preferh264video%d", i)
participant.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
Name: "video",
Width: 1280,
Height: 720,
Source: livekit.TrackSource_CAMERA,
SimulcastCodecs: []*livekit.SimulcastCodec{
{
Codec: "h264",
Cid: trackCid,
},
},
})
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, trackCid, trackCid)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
codecs := transceiver.Receiver().GetParameters().Codecs
if i > 0 {
// the negotiated codecs order could be updated by first negotiation, reorder to make h264 not preferred
for mime.IsMimeTypeStringH264(codecs[0].MimeType) {
codecs = append(codecs[1:], codecs[0])
}
}
// h264 should not be preferred
require.False(t, mime.IsMimeTypeStringH264(codecs[0].MimeType), "codecs", codecs)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
require.NoError(t, pc.SetLocalDescription(sdp))
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var answer webrtc.SessionDescription
var answerReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer = FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
}
}
return nil
})
participant.HandleOffer(sdp)
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
var h264Preferred bool
parsed, err := answer.Unmarshal()
require.NoError(t, err)
var videoSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "video" {
if videoSectionIndex == i {
codecs, err := codecsFromMediaDescription(m)
require.NoError(t, err)
if mime.IsMimeTypeCodecStringH264(codecs[0].Name) {
h264Preferred = true
break
}
}
videoSectionIndex++
}
}
require.Truef(t, h264Preferred, "h264 should be preferred for video section %d, answer sdp: \n%s", i, answer.SDP)
}
}
func TestPreferAudioCodecForRed(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
})
participant.SetMigrateState(types.MigrateStateComplete)
me := webrtc.MediaEngine{}
me.RegisterDefaultCodecs()
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: RedCodecCapability,
PayloadType: 63,
}, webrtc.RTPCodecTypeAudio))
api := webrtc.NewAPI(webrtc.WithMediaEngine(&me))
pc, err := api.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
for i, disableRed := range []bool{false, true} {
t.Run(fmt.Sprintf("disableRed=%v", disableRed), func(t *testing.T) {
trackCid := fmt.Sprintf("audiotrack%d", i)
participant.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_AUDIO,
DisableRed: disableRed,
Cid: trackCid,
})
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, trackCid, trackCid)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
codecs := transceiver.Sender().GetParameters().Codecs
for i, c := range codecs {
if c.MimeType == "audio/opus" && i != 0 {
codecs[0], codecs[i] = codecs[i], codecs[0]
break
}
}
transceiver.SetCodecPreferences(codecs)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
pc.SetLocalDescription(sdp)
// opus should be preferred
require.Equal(t, codecs[0].MimeType, "audio/opus", sdp)
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var answer webrtc.SessionDescription
var answerReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer = FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
}
}
return nil
})
participant.HandleOffer(sdp)
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
var redPreferred bool
parsed, err := answer.Unmarshal()
require.NoError(t, err)
var audioSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "audio" {
if audioSectionIndex == i {
codecs, err := codecsFromMediaDescription(m)
require.NoError(t, err)
// nack is always enabled. if red is preferred, server will not generate nack request
var nackEnabled bool
for _, c := range codecs {
if c.Name == "opus" {
for _, fb := range c.RTCPFeedback {
if strings.Contains(fb, "nack") {
nackEnabled = true
break
}
}
}
}
require.True(t, nackEnabled, "nack should be enabled for opus")
if mime.IsMimeTypeCodecStringRED(codecs[0].Name) {
redPreferred = true
break
}
}
audioSectionIndex++
}
}
require.Equalf(t, !disableRed, redPreferred, "offer : \n%s\nanswer sdp: \n%s", sdp, answer.SDP)
})
}
}
type participantOpts struct {
permissions *livekit.ParticipantPermission
protocolVersion types.ProtocolVersion
publisher bool
clientConf *livekit.ClientConfiguration
clientInfo *livekit.ClientInfo
}
func newParticipantForTestWithOpts(identity livekit.ParticipantIdentity, opts *participantOpts) *ParticipantImpl {
if opts == nil {
opts = &participantOpts{}
}
if opts.protocolVersion == 0 {
opts.protocolVersion = 6
}
conf, _ := config.NewConfig("", true, nil, nil)
// disable mux, it doesn't play too well with unit test
conf.RTC.TCPPort = 0
rtcConf, err := NewWebRTCConfig(conf)
if err != nil {
panic(err)
}
ff := buffer.NewFactoryOfBufferFactory(500, 200)
rtcConf.SetBufferFactory(ff.CreateBufferFactory())
grants := &auth.ClaimGrants{
Video: &auth.VideoGrant{},
}
if opts.permissions != nil {
grants.Video.SetCanPublish(opts.permissions.CanPublish)
grants.Video.SetCanPublishData(opts.permissions.CanPublishData)
grants.Video.SetCanSubscribe(opts.permissions.CanSubscribe)
}
enabledCodecs := make([]*livekit.Codec, 0, len(conf.Room.EnabledCodecs))
for _, c := range conf.Room.EnabledCodecs {
enabledCodecs = append(enabledCodecs, &livekit.Codec{
Mime: c.Mime,
FmtpLine: c.FmtpLine,
})
}
sid := livekit.ParticipantID(guid.New(utils.ParticipantPrefix))
p, _ := NewParticipant(ParticipantParams{
SID: sid,
Identity: identity,
Config: rtcConf,
Sink: &routingfakes.FakeMessageSink{},
ProtocolVersion: opts.protocolVersion,
SessionStartTime: time.Now(),
PLIThrottleConfig: conf.RTC.PLIThrottle,
Grants: grants,
PublishEnabledCodecs: enabledCodecs,
SubscribeEnabledCodecs: enabledCodecs,
ClientConf: opts.clientConf,
ClientInfo: ClientInfo{ClientInfo: opts.clientInfo},
Logger: LoggerWithParticipant(logger.GetLogger(), identity, sid, false),
Telemetry: &telemetryfakes.FakeTelemetryService{},
VersionGenerator: utils.NewDefaultTimedVersionGenerator(),
})
p.isPublisher.Store(opts.publisher)
p.updateState(livekit.ParticipantInfo_ACTIVE)
return p
}
func newParticipantForTest(identity livekit.ParticipantIdentity) *ParticipantImpl {
return newParticipantForTestWithOpts(identity, nil)
}
+274
View File
@@ -0,0 +1,274 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"fmt"
"strconv"
"strings"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/livekit"
lksdp "github.com/livekit/protocol/sdp"
)
func (p *ParticipantImpl) setCodecPreferencesForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
offer = p.setCodecPreferencesOpusRedForPublisher(offer)
offer = p.setCodecPreferencesVideoForPublisher(offer)
return offer
}
func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
parsed, unmatchAudios, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "audio")
if err != nil || len(unmatchAudios) == 0 {
return offer
}
for _, unmatchAudio := range unmatchAudios {
streamID, ok := lksdp.ExtractStreamID(unmatchAudio)
if !ok {
continue
}
p.pendingTracksLock.RLock()
_, info, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
// if RED is disabled for this track, don't prefer RED codec in offer
disableRed := info != nil && info.DisableRed
p.pendingTracksLock.RUnlock()
codecs, err := codecsFromMediaDescription(unmatchAudio)
if err != nil {
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchAudio)
continue
}
var opusPayload uint8
for _, codec := range codecs {
if mime.IsMimeTypeCodecStringOpus(codec.Name) {
opusPayload = codec.PayloadType
break
}
}
if opusPayload == 0 {
continue
}
var preferredCodecs, leftCodecs []string
for _, codec := range codecs {
// codec contain opus/red
if !disableRed && mime.IsMimeTypeCodecStringRED(codec.Name) && strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
}
}
// ensure nack enabled for audio in publisher offer
var nackFound bool
for _, attr := range unmatchAudio.Attributes {
if attr.Key == "rtcp-fb" && strings.Contains(attr.Value, fmt.Sprintf("%d nack", opusPayload)) {
nackFound = true
break
}
}
if !nackFound {
unmatchAudio.Attributes = append(unmatchAudio.Attributes, sdp.Attribute{
Key: "rtcp-fb",
Value: fmt.Sprintf("%d nack", opusPayload),
})
}
// no opus/red found
if len(preferredCodecs) == 0 {
continue
}
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats[:0], preferredCodecs...)
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats, leftCodecs...)
}
bytes, err := parsed.Marshal()
if err != nil {
p.pubLogger.Errorw("failed to marshal offer", err)
return offer
}
return webrtc.SessionDescription{
Type: offer.Type,
SDP: string(bytes),
}
}
func (p *ParticipantImpl) setCodecPreferencesVideoForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
parsed, unmatchVideos, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "video")
if err != nil || len(unmatchVideos) == 0 {
return offer
}
// unmatched video is pending for publish, set codec preference
for _, unmatchVideo := range unmatchVideos {
streamID, ok := lksdp.ExtractStreamID(unmatchVideo)
if !ok {
continue
}
var info *livekit.TrackInfo
p.pendingTracksLock.RLock()
mt := p.getPublishedTrackBySdpCid(streamID)
if mt != nil {
info = mt.ToProto()
} else {
_, info, _, _ = p.getPendingTrack(streamID, livekit.TrackType_VIDEO, false)
}
if info == nil {
p.pendingTracksLock.RUnlock()
continue
}
var mimeType string
for _, c := range info.Codecs {
if c.Cid == streamID {
mimeType = c.MimeType
break
}
}
if mimeType == "" && len(info.Codecs) > 0 {
mimeType = info.Codecs[0].MimeType
}
p.pendingTracksLock.RUnlock()
if mimeType != "" {
codecs, err := codecsFromMediaDescription(unmatchVideo)
if err != nil {
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchVideo)
continue
}
var preferredCodecs, leftCodecs []string
for _, c := range codecs {
if mime.GetMimeTypeCodec(mimeType) == mime.NormalizeMimeTypeCodec(c.Name) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
}
}
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats[:0], preferredCodecs...)
// if the client don't comply with codec order in SDP answer, only keep preferred codecs to force client to use it
if p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer() {
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats, leftCodecs...)
}
}
}
bytes, err := parsed.Marshal()
if err != nil {
p.pubLogger.Errorw("failed to marshal offer", err)
return offer
}
return webrtc.SessionDescription{
Type: offer.Type,
SDP: string(bytes),
}
}
// configure publisher answer for audio track's dtx and stereo settings
func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescription) webrtc.SessionDescription {
offer := p.TransportManager.LastPublisherOffer()
parsedOffer, err := offer.Unmarshal()
if err != nil {
return answer
}
parsed, err := answer.Unmarshal()
if err != nil {
return answer
}
for _, m := range parsed.MediaDescriptions {
switch m.MediaName.Media {
case "audio":
_, ok := m.Attribute(sdp.AttrKeyInactive)
if ok {
continue
}
mid, ok := m.Attribute(sdp.AttrKeyMID)
if !ok {
continue
}
// find track info from offer's stream id
var ti *livekit.TrackInfo
for _, om := range parsedOffer.MediaDescriptions {
_, ok := om.Attribute(sdp.AttrKeyInactive)
if ok {
continue
}
omid, ok := om.Attribute(sdp.AttrKeyMID)
if ok && omid == mid {
streamID, ok := lksdp.ExtractStreamID(om)
if !ok {
continue
}
track, _ := p.getPublishedTrackBySdpCid(streamID).(*MediaTrack)
if track == nil {
p.pendingTracksLock.RLock()
_, ti, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
p.pendingTracksLock.RUnlock()
} else {
ti = track.ToProto()
}
break
}
}
if ti == nil || (ti.DisableDtx && !ti.Stereo) {
// no need to configure
continue
}
opusPT, err := parsed.GetPayloadTypeForCodec(sdp.Codec{Name: mime.MimeTypeCodecOpus.String()})
if err != nil {
p.pubLogger.Infow("failed to get opus payload type", "error", err, "trackID", ti.Sid)
continue
}
for i, attr := range m.Attributes {
if strings.HasPrefix(attr.String(), fmt.Sprintf("fmtp:%d", opusPT)) {
if !ti.DisableDtx {
attr.Value += ";usedtx=1"
}
if ti.Stereo {
attr.Value += ";stereo=1;maxaveragebitrate=510000"
}
m.Attributes[i] = attr
}
}
default:
continue
}
}
bytes, err := parsed.Marshal()
if err != nil {
p.pubLogger.Infow("failed to marshal answer", "error", err)
return answer
}
answer.SDP = string(bytes)
return answer
}
@@ -0,0 +1,401 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"fmt"
"time"
"github.com/pion/webrtc/v4"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
func (p *ParticipantImpl) getResponseSink() routing.MessageSink {
p.resSinkMu.Lock()
defer p.resSinkMu.Unlock()
return p.resSink
}
func (p *ParticipantImpl) SetResponseSink(sink routing.MessageSink) {
p.resSinkMu.Lock()
defer p.resSinkMu.Unlock()
p.resSink = sink
}
func (p *ParticipantImpl) SendJoinResponse(joinResponse *livekit.JoinResponse) error {
// keep track of participant updates and versions
p.updateLock.Lock()
for _, op := range joinResponse.OtherParticipants {
p.updateCache.Add(livekit.ParticipantID(op.Sid), participantUpdateInfo{
identity: livekit.ParticipantIdentity(op.Identity),
version: op.Version,
state: op.State,
updatedAt: time.Now(),
})
}
p.updateLock.Unlock()
// send Join response
err := p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Join{
Join: joinResponse,
},
})
if err != nil {
return err
}
// update state after sending message, so that no participant updates could slip through before JoinResponse is sent
p.updateLock.Lock()
if p.State() == livekit.ParticipantInfo_JOINING {
p.updateState(livekit.ParticipantInfo_JOINED)
}
queuedUpdates := p.queuedUpdates
p.queuedUpdates = nil
p.updateLock.Unlock()
if len(queuedUpdates) > 0 {
return p.SendParticipantUpdate(queuedUpdates)
}
return nil
}
func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.ParticipantInfo) error {
p.updateLock.Lock()
if p.IsDisconnected() {
p.updateLock.Unlock()
return nil
}
if !p.IsReady() {
// queue up updates
p.queuedUpdates = append(p.queuedUpdates, participantsToUpdate...)
p.updateLock.Unlock()
return nil
}
validUpdates := make([]*livekit.ParticipantInfo, 0, len(participantsToUpdate))
for _, pi := range participantsToUpdate {
isValid := true
pID := livekit.ParticipantID(pi.Sid)
if lastVersion, ok := p.updateCache.Get(pID); ok {
// this is a message delivered out of order, a more recent version of the message had already been
// sent.
if pi.Version < lastVersion.version {
p.params.Logger.Debugw(
"skipping outdated participant update",
"otherParticipant", pi.Identity,
"otherPID", pi.Sid,
"version", pi.Version,
"lastVersion", lastVersion,
)
isValid = false
}
}
if pi.Permission != nil && pi.Permission.Hidden && pi.Sid != string(p.params.SID) {
p.params.Logger.Debugw("skipping hidden participant update", "otherParticipant", pi.Identity)
isValid = false
}
if isValid {
p.updateCache.Add(pID, participantUpdateInfo{
identity: livekit.ParticipantIdentity(pi.Identity),
version: pi.Version,
state: pi.State,
updatedAt: time.Now(),
})
validUpdates = append(validUpdates, pi)
}
}
p.updateLock.Unlock()
if len(validUpdates) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: validUpdates,
},
},
})
}
// SendSpeakerUpdate notifies participant changes to speakers. only send members that have changed since last update
func (p *ParticipantImpl) SendSpeakerUpdate(speakers []*livekit.SpeakerInfo, force bool) error {
if !p.IsReady() {
return nil
}
var scopedSpeakers []*livekit.SpeakerInfo
if force {
scopedSpeakers = speakers
} else {
for _, s := range speakers {
participantID := livekit.ParticipantID(s.Sid)
if p.IsSubscribedTo(participantID) || participantID == p.ID() {
scopedSpeakers = append(scopedSpeakers, s)
}
}
}
if len(scopedSpeakers) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_SpeakersChanged{
SpeakersChanged: &livekit.SpeakersChanged{
Speakers: scopedSpeakers,
},
},
})
}
func (p *ParticipantImpl) SendRoomUpdate(room *livekit.Room) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RoomUpdate{
RoomUpdate: &livekit.RoomUpdate{
Room: room,
},
},
})
}
func (p *ParticipantImpl) SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_ConnectionQuality{
ConnectionQuality: update,
},
})
}
func (p *ParticipantImpl) SendRefreshToken(token string) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RefreshToken{
RefreshToken: token,
},
})
}
func (p *ParticipantImpl) SendRequestResponse(requestResponse *livekit.RequestResponse) error {
if requestResponse.RequestId == 0 || !p.params.ClientInfo.SupportErrorResponse() {
return nil
}
if requestResponse.Reason == livekit.RequestResponse_OK && !p.ProtocolVersion().SupportsNonErrorSignalResponse() {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RequestResponse{
RequestResponse: requestResponse,
},
})
}
func (p *ParticipantImpl) HandleReconnectAndSendResponse(reconnectReason livekit.ReconnectReason, reconnectResponse *livekit.ReconnectResponse) error {
p.TransportManager.HandleClientReconnect(reconnectReason)
if !p.params.ClientInfo.CanHandleReconnectResponse() {
return nil
}
if err := p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Reconnect{
Reconnect: reconnectResponse,
},
}); err != nil {
return err
}
if p.params.ProtocolVersion.SupportHandlesDisconnectedUpdate() {
return p.sendDisconnectUpdatesForReconnect()
}
return nil
}
func (p *ParticipantImpl) sendDisconnectUpdatesForReconnect() error {
lastSignalAt := p.TransportManager.LastSeenSignalAt()
var disconnectedParticipants []*livekit.ParticipantInfo
p.updateLock.Lock()
keys := p.updateCache.Keys()
for i := len(keys) - 1; i >= 0; i-- {
if info, ok := p.updateCache.Get(keys[i]); ok {
if info.updatedAt.Before(lastSignalAt) {
break
} else if info.state == livekit.ParticipantInfo_DISCONNECTED {
disconnectedParticipants = append(disconnectedParticipants, &livekit.ParticipantInfo{
Sid: string(keys[i]),
Identity: string(info.identity),
Version: info.version,
State: livekit.ParticipantInfo_DISCONNECTED,
})
}
}
}
p.updateLock.Unlock()
if len(disconnectedParticipants) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: disconnectedParticipants,
},
},
})
}
func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livekit.SignalTarget) error {
prevIC := p.icQueue[target].Swap(ic)
if prevIC == nil {
return nil
}
trickle := ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
p.params.Logger.Debugw("sending ICE candidate", "transport", target, "trickle", logger.Proto(trickle))
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Trickle{
Trickle: trickle,
},
})
}
func (p *ParticipantImpl) sendTrackMuted(trackID livekit.TrackID, muted bool) {
_ = p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Mute{
Mute: &livekit.MuteTrackRequest{
Sid: string(trackID),
Muted: muted,
},
},
})
}
func (p *ParticipantImpl) sendTrackUnpublished(trackID livekit.TrackID) {
_ = p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackUnpublished{
TrackUnpublished: &livekit.TrackUnpublishedResponse{
TrackSid: string(trackID),
},
},
})
}
func (p *ParticipantImpl) sendTrackHasBeenSubscribed(trackID livekit.TrackID) {
if !p.params.ClientInfo.SupportTrackSubscribedEvent() {
return
}
_ = p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackSubscribed{
TrackSubscribed: &livekit.TrackSubscribed{
TrackSid: string(trackID),
},
},
})
p.params.Logger.Debugw("track has been subscribed", "trackID", trackID)
}
func (p *ParticipantImpl) writeMessage(msg *livekit.SignalResponse) error {
if p.IsDisconnected() || (!p.IsReady() && msg.GetJoin() == nil) {
return nil
}
sink := p.getResponseSink()
if sink == nil {
p.params.Logger.Debugw("could not send message to participant", "messageType", fmt.Sprintf("%T", msg.Message))
return nil
}
err := sink.WriteMessage(msg)
if utils.ErrorIsOneOf(err, psrpc.Canceled, routing.ErrChannelClosed) {
p.params.Logger.Debugw(
"could not send message to participant",
"error", err,
"messageType", fmt.Sprintf("%T", msg.Message),
)
return nil
} else if err != nil {
p.params.Logger.Warnw(
"could not send message to participant", err,
"messageType", fmt.Sprintf("%T", msg.Message),
)
return err
}
return nil
}
// closes signal connection to notify client to resume/reconnect
func (p *ParticipantImpl) CloseSignalConnection(reason types.SignallingCloseReason) {
sink := p.getResponseSink()
if sink != nil {
p.params.Logger.Debugw("closing signal connection", "reason", reason, "connID", sink.ConnectionID())
sink.Close()
p.SetResponseSink(nil)
}
}
func (p *ParticipantImpl) sendLeaveRequest(
reason types.ParticipantCloseReason,
isExpectedToResume bool,
isExpectedToReconnect bool,
sendOnlyIfSupportingLeaveRequestWithAction bool,
) error {
var leave *livekit.LeaveRequest
if p.ProtocolVersion().SupportsRegionsInLeaveRequest() {
leave = &livekit.LeaveRequest{
Reason: reason.ToDisconnectReason(),
}
switch {
case isExpectedToResume:
leave.Action = livekit.LeaveRequest_RESUME
case isExpectedToReconnect:
leave.Action = livekit.LeaveRequest_RECONNECT
default:
leave.Action = livekit.LeaveRequest_DISCONNECT
}
if leave.Action != livekit.LeaveRequest_DISCONNECT && p.params.GetRegionSettings != nil {
// sending region settings even for RESUME just in case client wants to a full reconnect despite server saying RESUME
leave.Regions = p.params.GetRegionSettings(p.params.ClientInfo.Address)
}
} else {
if !sendOnlyIfSupportingLeaveRequestWithAction {
leave = &livekit.LeaveRequest{
CanReconnect: isExpectedToReconnect,
Reason: reason.ToDisconnectReason(),
}
}
}
if leave != nil {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Leave{
Leave: leave,
},
})
}
return nil
}
File diff suppressed because it is too large Load Diff
+835
View File
@@ -0,0 +1,835 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/webhook"
"github.com/livekit/livekit-server/version"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/audio"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
"github.com/livekit/livekit-server/pkg/testutils"
)
func init() {
prometheus.Init("test", livekit.NodeType_SERVER)
}
const (
numParticipants = 3
defaultDelay = 10 * time.Millisecond
audioUpdateInterval = 25
)
func init() {
config.InitLoggerFromConfig(&config.DefaultConfig.Logging)
roomUpdateInterval = defaultDelay
}
var iceServersForRoom = []*livekit.ICEServer{{Urls: []string{"stun:stun.l.google.com:19302"}}}
func TestJoinedState(t *testing.T) {
t.Run("new room should return joinedAt 0", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
require.Equal(t, int64(0), rm.FirstJoinedAt())
require.Equal(t, int64(0), rm.LastLeftAt())
})
t.Run("should be current time when a participant joins", func(t *testing.T) {
s := time.Now().Unix()
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
require.LessOrEqual(t, s, rm.FirstJoinedAt())
require.Equal(t, int64(0), rm.LastLeftAt())
})
t.Run("should be set when a participant leaves", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
p0 := rm.GetParticipants()[0]
s := time.Now().Unix()
rm.RemoveParticipant(p0.Identity(), p0.ID(), types.ParticipantCloseReasonClientRequestLeave)
require.LessOrEqual(t, s, rm.LastLeftAt())
})
t.Run("LastLeftAt should be set when there are still participants in the room", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
p0 := rm.GetParticipants()[0]
rm.RemoveParticipant(p0.Identity(), p0.ID(), types.ParticipantCloseReasonClientRequestLeave)
require.Greater(t, rm.LastLeftAt(), int64(0))
})
}
func TestRoomJoin(t *testing.T) {
t.Run("joining returns existing participant data", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: numParticipants})
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false)
_ = rm.Join(pNew, nil, nil, iceServersForRoom)
// expect new participant to get a JoinReply
res := pNew.SendJoinResponseArgsForCall(0)
require.Equal(t, livekit.RoomID(res.Room.Sid), rm.ID())
require.Len(t, res.OtherParticipants, numParticipants)
require.Len(t, rm.GetParticipants(), numParticipants+1)
require.NotEmpty(t, res.IceServers)
})
t.Run("subscribe to existing channels upon join", func(t *testing.T) {
numExisting := 3
rm := newRoomWithParticipants(t, testRoomOpts{num: numExisting})
p := NewMockParticipant("new", types.CurrentProtocol, false, false)
err := rm.Join(p, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
stateChangeCB := p.OnStateChangeArgsForCall(0)
require.NotNil(t, stateChangeCB)
stateChangeCB(p, livekit.ParticipantInfo_ACTIVE)
// it should become a subscriber when connectivity changes
numTracks := 0
for _, op := range rm.GetParticipants() {
if p == op {
continue
}
numTracks += len(op.GetPublishedTracks())
}
require.Equal(t, numTracks, p.SubscribeToTrackCallCount())
})
t.Run("participant state change is broadcasted to others", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: numParticipants})
var changedParticipant types.Participant
rm.OnParticipantChanged(func(participant types.LocalParticipant) {
changedParticipant = participant
})
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
disconnectedParticipant := participants[1].(*typesfakes.FakeLocalParticipant)
disconnectedParticipant.StateReturns(livekit.ParticipantInfo_DISCONNECTED)
rm.RemoveParticipant(p.Identity(), p.ID(), types.ParticipantCloseReasonClientRequestLeave)
time.Sleep(defaultDelay)
require.Equal(t, p, changedParticipant)
numUpdates := 0
for _, op := range participants {
if op == p || op == disconnectedParticipant {
require.Zero(t, p.SendParticipantUpdateCallCount())
continue
}
fakeP := op.(*typesfakes.FakeLocalParticipant)
require.Equal(t, 1, fakeP.SendParticipantUpdateCallCount())
numUpdates += 1
}
require.Equal(t, numParticipants-2, numUpdates)
})
t.Run("cannot exceed max participants", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
rm.lock.Lock()
rm.protoRoom.MaxParticipants = 1
rm.lock.Unlock()
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false)
err := rm.Join(p, nil, nil, iceServersForRoom)
require.Equal(t, ErrMaxParticipantsExceeded, err)
})
}
// various state changes to participant and that others are receiving update
func TestParticipantUpdate(t *testing.T) {
tests := []struct {
name string
sendToSender bool // should sender receive it
action func(p types.LocalParticipant)
}{
{
"track mutes are sent to everyone",
true,
func(p types.LocalParticipant) {
p.SetTrackMuted("", true, false)
},
},
{
"track metadata updates are sent to everyone",
true,
func(p types.LocalParticipant) {
p.SetMetadata("")
},
},
{
"track publishes are sent to existing participants",
true,
func(p types.LocalParticipant) {
p.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
})
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
// remember how many times send has been called for each
callCounts := make(map[livekit.ParticipantID]int)
for _, p := range rm.GetParticipants() {
fp := p.(*typesfakes.FakeLocalParticipant)
callCounts[p.ID()] = fp.SendParticipantUpdateCallCount()
}
sender := rm.GetParticipants()[0]
test.action(sender)
// go through the other participants, make sure they've received update
for _, p := range rm.GetParticipants() {
expected := callCounts[p.ID()]
if p != sender || test.sendToSender {
expected += 1
}
fp := p.(*typesfakes.FakeLocalParticipant)
require.Equal(t, expected, fp.SendParticipantUpdateCallCount())
}
})
}
}
func TestPushAndDequeueUpdates(t *testing.T) {
identity := "test_user"
publisher1v1 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
IsPublisher: true,
Version: 1,
JoinedAt: 0,
}
publisher1v2 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
IsPublisher: true,
Version: 2,
JoinedAt: 1,
}
publisher2 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "2",
IsPublisher: true,
Version: 1,
JoinedAt: 2,
}
subscriber1v1 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 1,
JoinedAt: 0,
}
subscriber1v2 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 2,
JoinedAt: 1,
}
requirePIEquals := func(t *testing.T, a, b *livekit.ParticipantInfo) {
require.Equal(t, a.Sid, b.Sid)
require.Equal(t, a.Identity, b.Identity)
require.Equal(t, a.Version, b.Version)
}
testCases := []struct {
name string
pi *livekit.ParticipantInfo
closeReason types.ParticipantCloseReason
immediate bool
existing *participantUpdate
expected []*participantUpdate
validate func(t *testing.T, rm *Room, updates []*participantUpdate)
}{
{
name: "publisher updates are immediate",
pi: publisher1v1,
expected: []*participantUpdate{{pi: publisher1v1}},
},
{
name: "subscriber updates are queued",
pi: subscriber1v1,
},
{
name: "last version is enqueued",
pi: subscriber1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)}, // clone the existing value since it can be modified when setting to disconnected
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
require.NotNil(t, queued)
requirePIEquals(t, subscriber1v2, queued.pi)
},
},
{
name: "latest version when immediate",
pi: subscriber1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
immediate: true,
expected: []*participantUpdate{{pi: subscriber1v2}},
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
require.Nil(t, queued)
},
},
{
name: "out of order updates are rejected",
pi: subscriber1v1,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v2)},
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
requirePIEquals(t, subscriber1v2, queued.pi)
},
},
{
name: "sid change is broadcasted immediately with synthsized disconnect",
pi: publisher2,
closeReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant, // just to test if update contain the close reason
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v2), closeReason: types.ParticipantCloseReasonStale},
expected: []*participantUpdate{
{
pi: &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 2,
State: livekit.ParticipantInfo_DISCONNECTED,
},
isSynthesizedDisconnect: true,
closeReason: types.ParticipantCloseReasonStale,
},
{pi: publisher2, closeReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
},
},
{
name: "when switching to publisher, queue is cleared",
pi: publisher1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
expected: []*participantUpdate{{pi: publisher1v2}},
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
require.Empty(t, rm.batchedUpdates)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
if tc.existing != nil {
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.pi.Identity)] = tc.existing
}
updates := rm.pushAndDequeueUpdates(tc.pi, tc.closeReason, tc.immediate)
require.Equal(t, len(tc.expected), len(updates))
for i, item := range tc.expected {
requirePIEquals(t, item.pi, updates[i].pi)
require.Equal(t, item.isSynthesizedDisconnect, updates[i].isSynthesizedDisconnect)
require.Equal(t, item.closeReason, updates[i].closeReason)
}
if tc.validate != nil {
tc.validate(t, rm, updates)
}
})
}
}
func TestRoomClosure(t *testing.T) {
t.Run("room closes after participant leaves", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
isClosed := false
rm.OnClose(func() {
isClosed = true
})
p := rm.GetParticipants()[0]
rm.lock.Lock()
// allows immediate close after
rm.protoRoom.EmptyTimeout = 0
rm.lock.Unlock()
rm.RemoveParticipant(p.Identity(), p.ID(), types.ParticipantCloseReasonClientRequestLeave)
time.Sleep(time.Duration(rm.ToProto().DepartureTimeout)*time.Second + defaultDelay)
rm.CloseIfEmpty()
require.Len(t, rm.GetParticipants(), 0)
require.True(t, isClosed)
require.Equal(t, ErrRoomClosed, rm.Join(p, nil, nil, iceServersForRoom))
})
t.Run("room does not close before empty timeout", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
isClosed := false
rm.OnClose(func() {
isClosed = true
})
require.NotZero(t, rm.protoRoom.EmptyTimeout)
rm.CloseIfEmpty()
require.False(t, isClosed)
})
t.Run("room closes after empty timeout", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
isClosed := false
rm.OnClose(func() {
isClosed = true
})
rm.lock.Lock()
rm.protoRoom.EmptyTimeout = 1
rm.lock.Unlock()
time.Sleep(1010 * time.Millisecond)
rm.CloseIfEmpty()
require.True(t, isClosed)
})
}
func TestNewTrack(t *testing.T) {
t.Run("new track should be added to ready participants", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
participants := rm.GetParticipants()
p0 := participants[0].(*typesfakes.FakeLocalParticipant)
p0.StateReturns(livekit.ParticipantInfo_JOINED)
p1 := participants[1].(*typesfakes.FakeLocalParticipant)
p1.StateReturns(livekit.ParticipantInfo_ACTIVE)
pub := participants[2].(*typesfakes.FakeLocalParticipant)
// pub adds track
track := NewMockTrack(livekit.TrackType_VIDEO, "webcam")
trackCB := pub.OnTrackPublishedArgsForCall(0)
require.NotNil(t, trackCB)
trackCB(pub, track)
// only p1 should've been subscribed to
require.Equal(t, 0, p0.SubscribeToTrackCallCount())
require.Equal(t, 1, p1.SubscribeToTrackCallCount())
})
}
func TestActiveSpeakers(t *testing.T) {
t.Parallel()
getActiveSpeakerUpdates := func(p *typesfakes.FakeLocalParticipant) [][]*livekit.SpeakerInfo {
var updates [][]*livekit.SpeakerInfo
numCalls := p.SendSpeakerUpdateCallCount()
for i := 0; i < numCalls; i++ {
infos, _ := p.SendSpeakerUpdateArgsForCall(i)
updates = append(updates, infos)
}
return updates
}
audioUpdateDuration := (audioUpdateInterval + 10) * time.Millisecond
t.Run("participant should not be getting audio updates (protocol 2)", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1, protocol: 2})
defer rm.Close(types.ParticipantCloseReasonNone)
p := rm.GetParticipants()[0].(*typesfakes.FakeLocalParticipant)
require.Empty(t, rm.GetActiveSpeakers())
time.Sleep(audioUpdateDuration)
updates := getActiveSpeakerUpdates(p)
require.Empty(t, updates)
})
t.Run("speakers should be sorted by loudness", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p2 := participants[1].(*typesfakes.FakeLocalParticipant)
p.GetAudioLevelReturns(20, true)
p2.GetAudioLevelReturns(10, true)
speakers := rm.GetActiveSpeakers()
require.Len(t, speakers, 2)
require.Equal(t, string(p.ID()), speakers[0].Sid)
require.Equal(t, string(p2.ID()), speakers[1].Sid)
})
t.Run("participants are getting audio updates (protocol 3+)", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: 3})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
time.Sleep(time.Millisecond) // let the first update cycle run
p.GetAudioLevelReturns(30, true)
speakers := rm.GetActiveSpeakers()
require.NotEmpty(t, speakers)
require.Equal(t, string(p.ID()), speakers[0].Sid)
testutils.WithTimeout(t, func() string {
for _, op := range participants {
op := op.(*typesfakes.FakeLocalParticipant)
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return fmt.Sprintf("%s did not get any audio updates", op.Identity())
}
}
return ""
})
// no longer speaking, send update with empty items
p.GetAudioLevelReturns(127, false)
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(p)
lastUpdate := updates[len(updates)-1]
if len(lastUpdate) == 0 {
return "did not get updates of speaker going quiet"
}
if lastUpdate[0].Active {
return "speaker should not have been active"
}
return ""
})
})
t.Run("audio level is smoothed", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: 3, audioSmoothIntervals: 3})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
op := participants[1].(*typesfakes.FakeLocalParticipant)
p.GetAudioLevelReturns(30, true)
convertedLevel := float32(audio.ConvertAudioLevel(30))
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return "no speaker updates received"
}
lastSpeakers := updates[len(updates)-1]
if len(lastSpeakers) == 0 {
return "no speakers in the update"
}
if lastSpeakers[0].Level > convertedLevel {
return ""
}
return "level mismatch"
})
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return "no updates received"
}
lastSpeakers := updates[len(updates)-1]
if len(lastSpeakers) == 0 {
return "no speakers found"
}
if lastSpeakers[0].Level > convertedLevel {
return ""
}
return "did not match expected levels"
})
p.GetAudioLevelReturns(127, false)
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return "no speaker updates received"
}
lastSpeakers := updates[len(updates)-1]
if len(lastSpeakers) == 1 && !lastSpeakers[0].Active {
return ""
}
return "speakers didn't go back to zero"
})
})
}
func TestDataChannel(t *testing.T) {
t.Parallel()
const (
curAPI = iota
legacySID
legacyIdentity
)
modes := []int{
curAPI, legacySID, legacyIdentity,
}
modeNames := []string{
"cur", "legacy sid", "legacy identity",
}
setSource := func(mode int, dp *livekit.DataPacket, p types.LocalParticipant) {
switch mode {
case curAPI:
dp.ParticipantIdentity = string(p.Identity())
case legacySID:
dp.GetUser().ParticipantSid = string(p.ID())
case legacyIdentity:
dp.GetUser().ParticipantIdentity = string(p.Identity())
}
}
setDest := func(mode int, dp *livekit.DataPacket, p types.LocalParticipant) {
switch mode {
case curAPI:
dp.DestinationIdentities = []string{string(p.Identity())}
case legacySID:
dp.GetUser().DestinationSids = []string{string(p.ID())}
case legacyIdentity:
dp.GetUser().DestinationIdentities = []string{string(p.Identity())}
}
}
t.Run("participants should receive data", func(t *testing.T) {
for _, mode := range modes {
mode := mode
t.Run(modeNames[mode], func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
packet := &livekit.DataPacket{
Kind: livekit.DataPacket_RELIABLE,
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{
Payload: []byte("message.."),
},
},
}
setSource(mode, packet, p)
packetExp := utils.CloneProto(packet)
if mode != legacySID {
packetExp.ParticipantIdentity = string(p.Identity())
packetExp.GetUser().ParticipantIdentity = string(p.Identity())
}
encoded, _ := proto.Marshal(packetExp)
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
// ensure everyone has received the packet
for _, op := range participants {
fp := op.(*typesfakes.FakeLocalParticipant)
if fp == p {
require.Zero(t, fp.SendDataPacketCallCount())
continue
}
require.Equal(t, 1, fp.SendDataPacketCallCount())
_, got := fp.SendDataPacketArgsForCall(0)
require.Equal(t, encoded, got)
}
})
}
})
t.Run("only one participant should receive the data", func(t *testing.T) {
for _, mode := range modes {
mode := mode
t.Run(modeNames[mode], func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 4})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p1 := participants[1].(*typesfakes.FakeLocalParticipant)
packet := &livekit.DataPacket{
Kind: livekit.DataPacket_RELIABLE,
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{
Payload: []byte("message to p1.."),
},
},
}
setSource(mode, packet, p)
setDest(mode, packet, p1)
packetExp := utils.CloneProto(packet)
if mode != legacySID {
packetExp.ParticipantIdentity = string(p.Identity())
packetExp.GetUser().ParticipantIdentity = string(p.Identity())
packetExp.DestinationIdentities = []string{string(p1.Identity())}
packetExp.GetUser().DestinationIdentities = []string{string(p1.Identity())}
}
encoded, _ := proto.Marshal(packetExp)
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
// only p1 should receive the data
for _, op := range participants {
fp := op.(*typesfakes.FakeLocalParticipant)
if fp != p1 {
require.Zero(t, fp.SendDataPacketCallCount())
}
}
require.Equal(t, 1, p1.SendDataPacketCallCount())
_, got := p1.SendDataPacketArgsForCall(0)
require.Equal(t, encoded, got)
})
}
})
t.Run("publishing disallowed", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p.CanPublishDataReturns(false)
packet := livekit.DataPacket{
Kind: livekit.DataPacket_RELIABLE,
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{
Payload: []byte{},
},
},
}
if p.CanPublishData() {
p.OnDataPacketArgsForCall(0)(p, packet.Kind, &packet)
}
// no one should've been sent packet
for _, op := range participants {
fp := op.(*typesfakes.FakeLocalParticipant)
require.Zero(t, fp.SendDataPacketCallCount())
}
})
}
func TestHiddenParticipants(t *testing.T) {
t.Run("other participants don't receive hidden updates", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, numHidden: 1})
defer rm.Close(types.ParticipantCloseReasonNone)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false)
rm.Join(pNew, nil, nil, iceServersForRoom)
// expect new participant to get a JoinReply
res := pNew.SendJoinResponseArgsForCall(0)
require.Equal(t, livekit.RoomID(res.Room.Sid), rm.ID())
require.Len(t, res.OtherParticipants, 2)
require.Len(t, rm.GetParticipants(), 4)
require.NotEmpty(t, res.IceServers)
require.Equal(t, "testregion", res.ServerInfo.Region)
})
t.Run("hidden participant subscribes to tracks", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false)
err := rm.Join(hidden, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
stateChangeCB := hidden.OnStateChangeArgsForCall(0)
require.NotNil(t, stateChangeCB)
stateChangeCB(hidden, livekit.ParticipantInfo_ACTIVE)
require.Eventually(t, func() bool { return hidden.SubscribeToTrackCallCount() == 2 }, 5*time.Second, 10*time.Millisecond)
})
}
func TestRoomUpdate(t *testing.T) {
t.Run("updates are sent when participant joined", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
defer rm.Close(types.ParticipantCloseReasonNone)
p1 := rm.GetParticipants()[0].(*typesfakes.FakeLocalParticipant)
require.Equal(t, 0, p1.SendRoomUpdateCallCount())
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false)
require.NoError(t, rm.Join(p2, nil, nil, iceServersForRoom))
// p1 should have received an update
time.Sleep(2 * defaultDelay)
require.LessOrEqual(t, 1, p1.SendRoomUpdateCallCount())
require.EqualValues(t, 2, p1.SendRoomUpdateArgsForCall(p1.SendRoomUpdateCallCount()-1).NumParticipants)
})
t.Run("participants should receive metadata update", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
defer rm.Close(types.ParticipantCloseReasonNone)
rm.SetMetadata("test metadata...")
// callbacks are updated from goroutine
time.Sleep(2 * defaultDelay)
for _, op := range rm.GetParticipants() {
fp := op.(*typesfakes.FakeLocalParticipant)
// room updates are now sent for both participant joining and room metadata
require.GreaterOrEqual(t, fp.SendRoomUpdateCallCount(), 1)
}
})
}
type testRoomOpts struct {
num int
numHidden int
protocol types.ProtocolVersion
audioSmoothIntervals uint32
}
func newRoomWithParticipants(t *testing.T, opts testRoomOpts) *Room {
rm := NewRoom(
&livekit.Room{Name: "room"},
nil,
WebRTCConfig{},
config.RoomConfig{
EmptyTimeout: 5 * 60,
DepartureTimeout: 1,
},
&sfu.AudioConfig{
AudioLevelConfig: audio.AudioLevelConfig{
UpdateInterval: audioUpdateInterval,
SmoothIntervals: opts.audioSmoothIntervals,
},
},
&livekit.ServerInfo{
Edition: livekit.ServerInfo_Standard,
Version: version.Version,
Protocol: types.CurrentProtocol,
NodeId: "testnode",
Region: "testregion",
},
telemetry.NewTelemetryService(webhook.NewDefaultNotifier("", "", nil), &telemetryfakes.FakeAnalyticsService{}),
nil, nil, nil,
)
for i := 0; i < opts.num+opts.numHidden; i++ {
identity := livekit.ParticipantIdentity(fmt.Sprintf("p%d", i))
participant := NewMockParticipant(identity, opts.protocol, i >= opts.num, true)
err := rm.Join(participant, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
participant.StateReturns(livekit.ParticipantInfo_ACTIVE)
participant.IsReadyReturns(true)
// each participant has a track
participant.GetPublishedTracksReturns([]types.MediaTrack{
&typesfakes.FakeMediaTrack{},
})
}
return rm
}
+125
View File
@@ -0,0 +1,125 @@
/*
* Copyright 2023 LiveKit, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rtc
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
)
// RoomTrackManager holds tracks that are published to the room
type RoomTrackManager struct {
lock sync.RWMutex
changedNotifier *utils.ChangeNotifierManager
removedNotifier *utils.ChangeNotifierManager
tracks map[livekit.TrackID]*TrackInfo
}
type TrackInfo struct {
Track types.MediaTrack
PublisherIdentity livekit.ParticipantIdentity
PublisherID livekit.ParticipantID
}
func NewRoomTrackManager() *RoomTrackManager {
return &RoomTrackManager{
tracks: make(map[livekit.TrackID]*TrackInfo),
changedNotifier: utils.NewChangeNotifierManager(),
removedNotifier: utils.NewChangeNotifierManager(),
}
}
func (r *RoomTrackManager) AddTrack(track types.MediaTrack, publisherIdentity livekit.ParticipantIdentity, publisherID livekit.ParticipantID) {
r.lock.Lock()
r.tracks[track.ID()] = &TrackInfo{
Track: track,
PublisherIdentity: publisherIdentity,
PublisherID: publisherID,
}
r.lock.Unlock()
r.NotifyTrackChanged(track.ID())
}
func (r *RoomTrackManager) RemoveTrack(track types.MediaTrack) {
r.lock.Lock()
// ensure we are removing the same track as added
info, ok := r.tracks[track.ID()]
if !ok || info.Track != track {
r.lock.Unlock()
return
}
delete(r.tracks, track.ID())
r.lock.Unlock()
n := r.removedNotifier.GetNotifier(string(track.ID()))
if n != nil {
n.NotifyChanged()
}
r.changedNotifier.RemoveNotifier(string(track.ID()), true)
r.removedNotifier.RemoveNotifier(string(track.ID()), true)
}
func (r *RoomTrackManager) GetTrackInfo(trackID livekit.TrackID) *TrackInfo {
r.lock.RLock()
defer r.lock.RUnlock()
info := r.tracks[trackID]
if info == nil {
return nil
}
// when track is about to close, do not resolve
if info.Track != nil && !info.Track.IsOpen() {
return nil
}
return info
}
func (r *RoomTrackManager) NotifyTrackChanged(trackID livekit.TrackID) {
n := r.changedNotifier.GetNotifier(string(trackID))
if n != nil {
n.NotifyChanged()
}
}
// HasObservers lets caller know if the current media track has any observers
// this is used to signal interest in the track. when another MediaTrack with the same
// trackID is being used, track is not considered to be observed.
func (r *RoomTrackManager) HasObservers(track types.MediaTrack) bool {
n := r.changedNotifier.GetNotifier(string(track.ID()))
if n == nil || !n.HasObservers() {
return false
}
info := r.GetTrackInfo(track.ID())
if info == nil || info.Track != track {
return false
}
return true
}
func (r *RoomTrackManager) GetOrCreateTrackChangeNotifier(trackID livekit.TrackID) *utils.ChangeNotifier {
return r.changedNotifier.GetOrCreateNotifier(string(trackID))
}
func (r *RoomTrackManager) GetOrCreateTrackRemoveNotifier(trackID livekit.TrackID) *utils.ChangeNotifier {
return r.removedNotifier.GetOrCreateNotifier(string(trackID))
}
+159
View File
@@ -0,0 +1,159 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
func HandleParticipantSignal(room types.Room, participant types.LocalParticipant, req *livekit.SignalRequest, pLogger logger.Logger) error {
participant.UpdateLastSeenSignal()
switch msg := req.GetMessage().(type) {
case *livekit.SignalRequest_Offer:
participant.HandleOffer(FromProtoSessionDescription(msg.Offer))
case *livekit.SignalRequest_Answer:
participant.HandleAnswer(FromProtoSessionDescription(msg.Answer))
case *livekit.SignalRequest_Trickle:
candidateInit, err := FromProtoTrickle(msg.Trickle)
if err != nil {
pLogger.Warnw("could not decode trickle", err)
return nil
}
participant.AddICECandidate(candidateInit, msg.Trickle.Target)
case *livekit.SignalRequest_AddTrack:
pLogger.Debugw("add track request", "trackID", msg.AddTrack.Cid)
participant.AddTrack(msg.AddTrack)
case *livekit.SignalRequest_Mute:
participant.SetTrackMuted(livekit.TrackID(msg.Mute.Sid), msg.Mute.Muted, false)
case *livekit.SignalRequest_Subscription:
// allow participant to indicate their interest in the subscription
// permission check happens later in SubscriptionManager
room.UpdateSubscriptions(
participant,
livekit.StringsAsIDs[livekit.TrackID](msg.Subscription.TrackSids),
msg.Subscription.ParticipantTracks,
msg.Subscription.Subscribe,
)
case *livekit.SignalRequest_TrackSetting:
for _, sid := range livekit.StringsAsIDs[livekit.TrackID](msg.TrackSetting.TrackSids) {
participant.UpdateSubscribedTrackSettings(sid, msg.TrackSetting)
}
case *livekit.SignalRequest_Leave:
reason := types.ParticipantCloseReasonClientRequestLeave
switch msg.Leave.Reason {
case livekit.DisconnectReason_CLIENT_INITIATED:
reason = types.ParticipantCloseReasonClientRequestLeave
case livekit.DisconnectReason_USER_UNAVAILABLE:
reason = types.ParticipantCloseReasonUserUnavailable
case livekit.DisconnectReason_USER_REJECTED:
reason = types.ParticipantCloseReasonUserRejected
}
pLogger.Debugw("client leaving room", "reason", reason)
room.RemoveParticipant(participant.Identity(), participant.ID(), reason)
case *livekit.SignalRequest_SubscriptionPermission:
err := room.UpdateSubscriptionPermission(participant, msg.SubscriptionPermission)
if err != nil {
pLogger.Warnw("could not update subscription permission", err,
"permissions", msg.SubscriptionPermission)
}
case *livekit.SignalRequest_SyncState:
err := room.SyncState(participant, msg.SyncState)
if err != nil {
pLogger.Warnw("could not sync state", err,
"state", msg.SyncState)
}
case *livekit.SignalRequest_Simulate:
err := room.SimulateScenario(participant, msg.Simulate)
if err != nil {
pLogger.Warnw("could not simulate scenario", err,
"simulate", msg.Simulate)
}
case *livekit.SignalRequest_PingReq:
if msg.PingReq.Rtt > 0 {
participant.UpdateSignalingRTT(uint32(msg.PingReq.Rtt))
}
case *livekit.SignalRequest_UpdateMetadata:
requestResponse := &livekit.RequestResponse{
RequestId: msg.UpdateMetadata.RequestId,
Reason: livekit.RequestResponse_OK,
}
if participant.ClaimGrants().Video.GetCanUpdateOwnMetadata() {
if err := participant.CheckMetadataLimits(
msg.UpdateMetadata.Name,
msg.UpdateMetadata.Metadata,
msg.UpdateMetadata.Attributes,
); err == nil {
if msg.UpdateMetadata.Name != "" {
participant.SetName(msg.UpdateMetadata.Name)
}
if msg.UpdateMetadata.Metadata != "" {
participant.SetMetadata(msg.UpdateMetadata.Metadata)
}
if msg.UpdateMetadata.Attributes != nil {
participant.SetAttributes(msg.UpdateMetadata.Attributes)
}
} else {
pLogger.Warnw("could not update metadata", err)
switch err {
case ErrNameExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds name length limit"
case ErrMetadataExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds metadata size limit"
case ErrAttributesExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds attributes size limit"
}
}
} else {
requestResponse.Reason = livekit.RequestResponse_NOT_ALLOWED
requestResponse.Message = "does not have permission to update own metadata"
}
participant.SendRequestResponse(requestResponse)
case *livekit.SignalRequest_UpdateAudioTrack:
if err := participant.UpdateAudioTrack(msg.UpdateAudioTrack); err != nil {
pLogger.Warnw("could not update audio track", err, "update", msg.UpdateAudioTrack)
}
case *livekit.SignalRequest_UpdateVideoTrack:
if err := participant.UpdateVideoTrack(msg.UpdateVideoTrack); err != nil {
pLogger.Warnw("could not update video track", err, "update", msg.UpdateVideoTrack)
}
}
return nil
}
+304
View File
@@ -0,0 +1,304 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"sync"
"time"
"github.com/bep/debounce"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
)
const (
subscriptionDebounceInterval = 100 * time.Millisecond
)
type SubscribedTrackParams struct {
PublisherID livekit.ParticipantID
PublisherIdentity livekit.ParticipantIdentity
PublisherVersion uint32
Subscriber types.LocalParticipant
MediaTrack types.MediaTrack
DownTrack *sfu.DownTrack
AdaptiveStream bool
}
type SubscribedTrack struct {
params SubscribedTrackParams
logger logger.Logger
sender atomic.Pointer[webrtc.RTPSender]
needsNegotiation atomic.Bool
versionGenerator utils.TimedVersionGenerator
settingsLock sync.Mutex
settings *livekit.UpdateTrackSettings
settingsVersion utils.TimedVersion
bindLock sync.Mutex
bound bool
onBindCallbacks []func(error)
onClose atomic.Value // func(bool)
debouncer func(func())
}
func NewSubscribedTrack(params SubscribedTrackParams) *SubscribedTrack {
s := &SubscribedTrack{
params: params,
logger: params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub).WithValues(
"trackID", params.DownTrack.ID(),
"publisherID", params.PublisherID,
"publisher", params.PublisherIdentity,
),
versionGenerator: utils.NewDefaultTimedVersionGenerator(),
debouncer: debounce.New(subscriptionDebounceInterval),
}
return s
}
func (t *SubscribedTrack) AddOnBind(f func(error)) {
t.bindLock.Lock()
bound := t.bound
if !bound {
t.onBindCallbacks = append(t.onBindCallbacks, f)
}
t.bindLock.Unlock()
if bound {
// fire immediately, do not need to persist since bind is a one time event
go f(nil)
}
}
// for DownTrack callback to notify us that it's bound
func (t *SubscribedTrack) Bound(err error) {
t.bindLock.Lock()
if err == nil {
t.bound = true
}
callbacks := t.onBindCallbacks
t.onBindCallbacks = nil
t.bindLock.Unlock()
if err == nil && t.MediaTrack().Kind() == livekit.TrackType_VIDEO {
// When AdaptiveStream is enabled, default the subscriber to LOW quality stream
// we would want LOW instead of OFF for a couple of reasons
// 1. when a subscriber unsubscribes from a track, we would forget their previously defined settings
// depending on client implementation, subscription on/off is kept separately from adaptive stream
// So when there are no changes to desired resolution, but the user re-subscribes, we may leave stream at OFF
// 2. when interacting with dynacast *and* adaptive stream. If the publisher was not publishing at the
// time of subscription, we might not be able to trigger adaptive stream updates on the client side
// (since there isn't any video frames coming through). this will leave the stream "stuck" on off, without
// a trigger to re-enable it
t.settingsLock.Lock()
if t.settings != nil {
if t.params.AdaptiveStream {
// remove `disabled` flag to force a visibility update
t.settings.Disabled = false
}
} else {
if t.params.AdaptiveStream {
t.settings = &livekit.UpdateTrackSettings{Quality: livekit.VideoQuality_LOW}
} else {
t.settings = &livekit.UpdateTrackSettings{Quality: livekit.VideoQuality_HIGH}
}
}
t.settingsLock.Unlock()
t.applySettings()
}
for _, cb := range callbacks {
go cb(err)
}
}
// for DownTrack callback to notify us that it's closed
func (t *SubscribedTrack) Close(isExpectedToResume bool) {
if onClose := t.onClose.Load(); onClose != nil {
go onClose.(func(bool))(isExpectedToResume)
}
}
func (t *SubscribedTrack) OnClose(f func(bool)) {
t.onClose.Store(f)
}
func (t *SubscribedTrack) IsBound() bool {
t.bindLock.Lock()
defer t.bindLock.Unlock()
return t.bound
}
func (t *SubscribedTrack) ID() livekit.TrackID {
return livekit.TrackID(t.params.DownTrack.ID())
}
func (t *SubscribedTrack) PublisherID() livekit.ParticipantID {
return t.params.PublisherID
}
func (t *SubscribedTrack) PublisherIdentity() livekit.ParticipantIdentity {
return t.params.PublisherIdentity
}
func (t *SubscribedTrack) PublisherVersion() uint32 {
return t.params.PublisherVersion
}
func (t *SubscribedTrack) SubscriberID() livekit.ParticipantID {
return t.params.Subscriber.ID()
}
func (t *SubscribedTrack) SubscriberIdentity() livekit.ParticipantIdentity {
return t.params.Subscriber.Identity()
}
func (t *SubscribedTrack) Subscriber() types.LocalParticipant {
return t.params.Subscriber
}
func (t *SubscribedTrack) DownTrack() *sfu.DownTrack {
return t.params.DownTrack
}
func (t *SubscribedTrack) MediaTrack() types.MediaTrack {
return t.params.MediaTrack
}
// has subscriber indicated it wants to mute this track
func (t *SubscribedTrack) IsMuted() bool {
t.settingsLock.Lock()
defer t.settingsLock.Unlock()
return t.isMutedLocked()
}
func (t *SubscribedTrack) isMutedLocked() bool {
if t.settings == nil {
return false
}
return t.settings.Disabled
}
func (t *SubscribedTrack) SetPublisherMuted(muted bool) {
t.DownTrack().PubMute(muted)
}
func (t *SubscribedTrack) UpdateSubscriberSettings(settings *livekit.UpdateTrackSettings, isImmediate bool) {
t.settingsLock.Lock()
if proto.Equal(t.settings, settings) {
t.settingsLock.Unlock()
return
}
isImmediate = isImmediate || (!settings.Disabled && settings.Disabled != t.isMutedLocked())
t.settings = utils.CloneProto(settings)
t.settingsLock.Unlock()
if isImmediate {
t.applySettings()
} else {
// avoid frequent changes to mute & video layers, unless it became visible
t.debouncer(t.applySettings)
}
}
func (t *SubscribedTrack) UpdateVideoLayer() {
t.applySettings()
}
func (t *SubscribedTrack) applySettings() {
t.settingsLock.Lock()
if t.settings == nil {
t.settingsLock.Unlock()
return
}
t.logger.Debugw("updating subscriber track settings", "settings", logger.Proto(t.settings))
t.settingsVersion = t.versionGenerator.Next()
settingsVersion := t.settingsVersion
t.settingsLock.Unlock()
dt := t.DownTrack()
spatial := buffer.InvalidLayerSpatial
temporal := buffer.InvalidLayerTemporal
if dt.Kind() == webrtc.RTPCodecTypeVideo {
mt := t.MediaTrack()
quality := t.settings.Quality
if t.settings.Width > 0 {
quality = mt.GetQualityForDimension(t.settings.Width, t.settings.Height)
}
spatial = buffer.VideoQualityToSpatialLayer(quality, mt.ToProto())
if t.settings.Fps > 0 {
temporal = mt.GetTemporalLayerForSpatialFps(spatial, t.settings.Fps, dt.Mime())
}
}
t.settingsLock.Lock()
if settingsVersion != t.settingsVersion {
// a newer settings has superceded this one
t.settingsLock.Unlock()
return
}
if t.settings.Disabled {
dt.Mute(true)
t.settingsLock.Unlock()
return
} else {
dt.Mute(false)
}
if dt.Kind() == webrtc.RTPCodecTypeVideo {
dt.SetMaxSpatialLayer(spatial)
if temporal != buffer.InvalidLayerTemporal {
dt.SetMaxTemporalLayer(temporal)
}
}
t.settingsLock.Unlock()
}
func (t *SubscribedTrack) NeedsNegotiation() bool {
return t.needsNegotiation.Load()
}
func (t *SubscribedTrack) SetNeedsNegotiation(needs bool) {
t.needsNegotiation.Store(needs)
}
func (t *SubscribedTrack) RTPSender() *webrtc.RTPSender {
return t.sender.Load()
}
func (t *SubscribedTrack) SetRTPSender(sender *webrtc.RTPSender) {
t.sender.Store(sender)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,545 @@
/*
* Copyright 2023 LiveKit, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rtc
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func init() {
reconcileInterval = 50 * time.Millisecond
notFoundTimeout = 200 * time.Millisecond
subscriptionTimeout = 200 * time.Millisecond
}
const (
subSettleTimeout = 600 * time.Millisecond
subCheckInterval = 10 * time.Millisecond
)
func TestSubscribe(t *testing.T) {
t.Run("happy path subscribe", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
subCount := atomic.Int32{}
failed := atomic.Bool{}
sm.params.OnTrackSubscribed = func(subTrack types.SubscribedTrack) {
subCount.Add(1)
}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
numParticipantSubscribed := atomic.Int32{}
numParticipantUnsubscribed := atomic.Int32{}
sm.OnSubscribeStatusChanged(func(pubID livekit.ParticipantID, subscribed bool) {
if subscribed {
numParticipantSubscribed.Add(1)
} else {
numParticipantUnsubscribed.Add(1)
}
})
sm.SubscribeToTrack("track")
s := sm.subscriptions["track"]
require.True(t, s.isDesired())
require.Eventually(t, func() bool {
return subCount.Load() == 1
}, subSettleTimeout, subCheckInterval, "track was not subscribed")
require.NotNil(t, s.getSubscribedTrack())
require.Len(t, sm.GetSubscribedTracks(), 1)
require.Eventually(t, func() bool {
return len(sm.GetSubscribedParticipants()) == 1
}, subSettleTimeout, subCheckInterval, "GetSubscribedParticipants should have returned one item")
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
// ensure telemetry events are sent
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
// ensure bound
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
require.Eventually(t, func() bool {
return !s.needsBind()
}, subSettleTimeout, subCheckInterval, "track was not bound")
// telemetry event should have been sent
require.Equal(t, 1, tm.TrackSubscribedCallCount())
time.Sleep(notFoundTimeout)
require.False(t, failed.Load())
resolver.SetPause(true)
// ensure its resilience after being closed
setTestSubscribedTrackClosed(t, s.getSubscribedTrack(), false)
require.Eventually(t, func() bool {
return s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "needs subscribe did not persist across track close")
resolver.SetPause(false)
require.Eventually(t, func() bool {
return s.isDesired() && !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "track was not resubscribed")
// was subscribed twice, unsubscribed once (due to close)
require.Eventually(t, func() bool {
return numParticipantSubscribed.Load() == 2
}, subSettleTimeout, subCheckInterval, "participant subscribe status was not updated twice")
require.Equal(t, int32(1), numParticipantUnsubscribed.Load())
})
t.Run("no track permission", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(false, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
failed := atomic.Bool{}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
sm.SubscribeToTrack("track")
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.getHasPermission()
}, subSettleTimeout, subCheckInterval, "should not have permission to subscribe")
time.Sleep(subscriptionTimeout)
// should not have called failed callbacks, isDesired remains unchanged
require.True(t, s.isDesired())
require.False(t, failed.Load())
require.True(t, s.needsSubscribe())
require.Len(t, sm.GetSubscribedTracks(), 0)
// trackSubscribed telemetry not sent
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
require.Equal(t, 0, tm.TrackSubscribedCallCount())
// give permissions now
resolver.lock.Lock()
resolver.hasPermission = true
resolver.lock.Unlock()
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "should be subscribed")
require.Len(t, sm.GetSubscribedTracks(), 1)
})
t.Run("publisher left", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
failed := atomic.Bool{}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
sm.SubscribeToTrack("track")
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "should be subscribed")
resolver.lock.Lock()
resolver.hasTrack = false
resolver.lock.Unlock()
// publisher triggers close
setTestSubscribedTrackClosed(t, s.getSubscribedTrack(), false)
require.Eventually(t, func() bool {
return !s.isDesired()
}, subSettleTimeout, subCheckInterval, "isDesired not set to false")
})
}
func TestUnsubscribe(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
unsubCount := atomic.Int32{}
sm.params.OnTrackUnsubscribed = func(subTrack types.SubscribedTrack) {
unsubCount.Add(1)
}
resolver := newTestResolver(true, true, "pub", "pubID")
s := &trackSubscription{
trackID: "track",
desired: true,
subscriberID: sm.params.Participant.ID(),
publisherID: "pubID",
publisherIdentity: "pub",
hasPermission: true,
bound: true,
logger: logger.GetLogger(),
}
// a bunch of unfortunate manual wiring
res := resolver.Resolve(nil, s.trackID)
res.TrackChangedNotifier.AddObserver(string(sm.params.Participant.ID()), func() {})
s.changedNotifier = res.TrackChangedNotifier
st, err := res.Track.AddSubscriber(sm.params.Participant)
require.NoError(t, err)
s.subscribedTrack = st
st.OnClose(func(isExpectedToResume bool) {
sm.handleSubscribedTrackClose(s, isExpectedToResume)
})
res.Track.(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) {
setTestSubscribedTrackClosed(t, st, isExpectedToResume)
})
sm.lock.Lock()
sm.subscriptions["track"] = s
sm.lock.Unlock()
require.False(t, s.needsSubscribe())
require.False(t, s.needsUnsubscribe())
// unsubscribe
sm.UnsubscribeFromTrack("track")
require.False(t, s.isDesired())
require.Eventually(t, func() bool {
if s.needsUnsubscribe() {
return false
}
if sm.pendingUnsubscribes.Load() != 0 {
return false
}
sm.lock.RLock()
subLen := len(sm.subscriptions)
sm.lock.RUnlock()
if subLen != 0 {
return false
}
return true
}, subSettleTimeout, subCheckInterval, "Track was not unsubscribed")
// no traces should be left
require.Len(t, sm.GetSubscribedTracks(), 0)
require.False(t, res.TrackChangedNotifier.HasObservers())
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackUnsubscribedCallCount())
}
func TestSubscribeStatusChanged(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
numParticipantSubscribed := atomic.Int32{}
numParticipantUnsubscribed := atomic.Int32{}
sm.OnSubscribeStatusChanged(func(pubID livekit.ParticipantID, subscribed bool) {
if subscribed {
numParticipantSubscribed.Add(1)
} else {
numParticipantUnsubscribed.Add(1)
}
})
sm.SubscribeToTrack("track1")
sm.SubscribeToTrack("track2")
s1 := sm.subscriptions["track1"]
s2 := sm.subscriptions["track2"]
require.Eventually(t, func() bool {
return !s1.needsSubscribe() && !s2.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "track1 and track2 should be subscribed")
st1 := s1.getSubscribedTrack()
st1.OnClose(func(isExpectedToResume bool) {
sm.handleSubscribedTrackClose(s1, isExpectedToResume)
})
st2 := s2.getSubscribedTrack()
st2.OnClose(func(isExpectedToResume bool) {
sm.handleSubscribedTrackClose(s2, isExpectedToResume)
})
st1.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) {
setTestSubscribedTrackClosed(t, st1, isExpectedToResume)
})
st2.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) {
setTestSubscribedTrackClosed(t, st2, isExpectedToResume)
})
require.Eventually(t, func() bool {
return numParticipantSubscribed.Load() == 1
}, subSettleTimeout, subCheckInterval, "should be subscribed to publisher")
require.Equal(t, int32(0), numParticipantUnsubscribed.Load())
require.True(t, sm.IsSubscribedTo("pubID"))
// now unsubscribe track2, no event should be fired
sm.UnsubscribeFromTrack("track2")
require.Eventually(t, func() bool {
return !s2.needsUnsubscribe()
}, subSettleTimeout, subCheckInterval, "track2 should be unsubscribed")
require.Equal(t, int32(0), numParticipantUnsubscribed.Load())
// unsubscribe track1, expect event
sm.UnsubscribeFromTrack("track1")
require.Eventually(t, func() bool {
return !s1.needsUnsubscribe()
}, subSettleTimeout, subCheckInterval, "track1 should be unsubscribed")
require.Eventually(t, func() bool {
return numParticipantUnsubscribed.Load() == 1
}, subSettleTimeout, subCheckInterval, "should be subscribed to publisher")
require.False(t, sm.IsSubscribedTo("pubID"))
}
// clients may send update subscribed settings prior to subscription events coming through
// settings should be persisted and used when the subscription does take place.
func TestUpdateSettingsBeforeSubscription(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
settings := &livekit.UpdateTrackSettings{
Disabled: true,
Width: 100,
Height: 100,
}
sm.UpdateSubscribedTrackSettings("track", settings)
sm.SubscribeToTrack("track")
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "Track should be subscribed")
st := s.getSubscribedTrack().(*typesfakes.FakeSubscribedTrack)
require.Eventually(t, func() bool {
return st.UpdateSubscriberSettingsCallCount() == 1
}, subSettleTimeout, subCheckInterval, "UpdateSubscriberSettings should be called once")
applied, _ := st.UpdateSubscriberSettingsArgsForCall(0)
require.Equal(t, settings.Disabled, applied.Disabled)
require.Equal(t, settings.Width, applied.Width)
require.Equal(t, settings.Height, applied.Height)
}
func TestSubscriptionLimits(t *testing.T) {
sm := newTestSubscriptionManagerWithParams(testSubscriptionParams{
SubscriptionLimitAudio: 1,
SubscriptionLimitVideo: 1,
})
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
subCount := atomic.Int32{}
failed := atomic.Bool{}
sm.params.OnTrackSubscribed = func(subTrack types.SubscribedTrack) {
subCount.Add(1)
}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
numParticipantSubscribed := atomic.Int32{}
numParticipantUnsubscribed := atomic.Int32{}
sm.OnSubscribeStatusChanged(func(pubID livekit.ParticipantID, subscribed bool) {
if subscribed {
numParticipantSubscribed.Add(1)
} else {
numParticipantUnsubscribed.Add(1)
}
})
sm.SubscribeToTrack("track")
s := sm.subscriptions["track"]
require.True(t, s.isDesired())
require.Eventually(t, func() bool {
return subCount.Load() == 1
}, subSettleTimeout, subCheckInterval, "track was not subscribed")
require.NotNil(t, s.getSubscribedTrack())
require.Len(t, sm.GetSubscribedTracks(), 1)
require.Eventually(t, func() bool {
return len(sm.GetSubscribedParticipants()) == 1
}, subSettleTimeout, subCheckInterval, "GetSubscribedParticipants should have returned one item")
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
// ensure telemetry events are sent
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
// ensure bound
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
require.Eventually(t, func() bool {
return !s.needsBind()
}, subSettleTimeout, subCheckInterval, "track was not bound")
// telemetry event should have been sent
require.Equal(t, 1, tm.TrackSubscribedCallCount())
// reach subscription limit, subscribe pending
sm.SubscribeToTrack("track2")
s2 := sm.subscriptions["track2"]
time.Sleep(subscriptionTimeout * 2)
require.True(t, s2.needsSubscribe())
require.Equal(t, 2, tm.TrackSubscribeRequestedCallCount())
require.Equal(t, 1, tm.TrackSubscribeFailedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
// unsubscribe track1, then track2 should be subscribed
sm.UnsubscribeFromTrack("track")
require.False(t, s.isDesired())
require.True(t, s.needsUnsubscribe())
// wait for unsubscribe to take effect
time.Sleep(reconcileInterval)
setTestSubscribedTrackClosed(t, s.getSubscribedTrack(), false)
require.Nil(t, s.getSubscribedTrack())
time.Sleep(reconcileInterval)
require.True(t, s2.isDesired())
require.False(t, s2.needsSubscribe())
require.EqualValues(t, 2, subCount.Load())
require.NotNil(t, s2.getSubscribedTrack())
require.Equal(t, 2, tm.TrackSubscribeRequestedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
// ensure bound
setTestSubscribedTrackBound(t, s2.getSubscribedTrack())
require.Eventually(t, func() bool {
return !s2.needsBind()
}, subSettleTimeout, subCheckInterval, "track was not bound")
// subscribe to track1 again, which should pending
sm.SubscribeToTrack("track")
s = sm.subscriptions["track"]
require.True(t, s.isDesired())
time.Sleep(subscriptionTimeout * 2)
require.True(t, s.needsSubscribe())
require.Equal(t, 3, tm.TrackSubscribeRequestedCallCount())
require.Equal(t, 2, tm.TrackSubscribeFailedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
}
type testSubscriptionParams struct {
SubscriptionLimitAudio int32
SubscriptionLimitVideo int32
}
func newTestSubscriptionManager() *SubscriptionManager {
return newTestSubscriptionManagerWithParams(testSubscriptionParams{})
}
func newTestSubscriptionManagerWithParams(params testSubscriptionParams) *SubscriptionManager {
p := &typesfakes.FakeLocalParticipant{}
p.CanSubscribeReturns(true)
p.IDReturns("subID")
p.IdentityReturns("sub")
p.KindReturns(livekit.ParticipantInfo_STANDARD)
return NewSubscriptionManager(SubscriptionManagerParams{
Participant: p,
Logger: logger.GetLogger(),
OnTrackSubscribed: func(subTrack types.SubscribedTrack) {},
OnTrackUnsubscribed: func(subTrack types.SubscribedTrack) {},
OnSubscriptionError: func(trackID livekit.TrackID, fatal bool, err error) {},
TrackResolver: func(sub types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
return types.MediaResolverResult{}
},
Telemetry: &telemetryfakes.FakeTelemetryService{},
SubscriptionLimitAudio: params.SubscriptionLimitAudio,
SubscriptionLimitVideo: params.SubscriptionLimitVideo,
})
}
type testResolver struct {
lock sync.Mutex
hasPermission bool
hasTrack bool
pubIdentity livekit.ParticipantIdentity
pubID livekit.ParticipantID
paused bool
}
func newTestResolver(hasPermission bool, hasTrack bool, pubIdentity livekit.ParticipantIdentity, pubID livekit.ParticipantID) *testResolver {
return &testResolver{
hasPermission: hasPermission,
hasTrack: hasTrack,
pubIdentity: pubIdentity,
pubID: pubID,
}
}
func (t *testResolver) SetPause(paused bool) {
t.lock.Lock()
defer t.lock.Unlock()
t.paused = paused
}
func (t *testResolver) Resolve(_subscriber types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
t.lock.Lock()
defer t.lock.Unlock()
res := types.MediaResolverResult{
TrackChangedNotifier: utils.NewChangeNotifier(),
TrackRemovedNotifier: utils.NewChangeNotifier(),
HasPermission: t.hasPermission,
PublisherID: t.pubID,
PublisherIdentity: t.pubIdentity,
}
if t.hasTrack && !t.paused {
mt := &typesfakes.FakeMediaTrack{}
st := &typesfakes.FakeSubscribedTrack{}
st.IDReturns(trackID)
st.PublisherIDReturns(t.pubID)
st.PublisherIdentityReturns(t.pubIdentity)
mt.AddSubscriberCalls(func(sub types.LocalParticipant) (types.SubscribedTrack, error) {
st.SubscriberReturns(sub)
return st, nil
})
st.MediaTrackReturns(mt)
res.Track = mt
}
return res
}
func setTestSubscribedTrackBound(t *testing.T, st types.SubscribedTrack) {
fst, ok := st.(*typesfakes.FakeSubscribedTrack)
require.True(t, ok)
for i := 0; i < fst.AddOnBindCallCount(); i++ {
fst.AddOnBindArgsForCall(i)(nil)
}
}
func setTestSubscribedTrackClosed(t *testing.T, st types.SubscribedTrack, isExpectedToResume bool) {
fst, ok := st.(*typesfakes.FakeSubscribedTrack)
require.True(t, ok)
fst.OnCloseArgsForCall(0)(isExpectedToResume)
}
@@ -0,0 +1,183 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package supervisor
import (
"sync"
"time"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
const (
monitorInterval = 1 * time.Second
)
type ParticipantSupervisorParams struct {
Logger logger.Logger
}
type trackMonitor struct {
opMon types.OperationMonitor
err error
}
type ParticipantSupervisor struct {
params ParticipantSupervisorParams
lock sync.RWMutex
isPublisherConnected bool
publications map[livekit.TrackID]*trackMonitor
isStopped atomic.Bool
onPublicationError func(trackID livekit.TrackID)
}
func NewParticipantSupervisor(params ParticipantSupervisorParams) *ParticipantSupervisor {
p := &ParticipantSupervisor{
params: params,
publications: make(map[livekit.TrackID]*trackMonitor),
}
go p.checkState()
return p
}
func (p *ParticipantSupervisor) Stop() {
p.isStopped.Store(true)
}
func (p *ParticipantSupervisor) OnPublicationError(f func(trackID livekit.TrackID)) {
p.lock.Lock()
defer p.lock.Unlock()
p.onPublicationError = f
}
func (p *ParticipantSupervisor) getOnPublicationError() func(trackID livekit.TrackID) {
p.lock.RLock()
defer p.lock.RUnlock()
return p.onPublicationError
}
func (p *ParticipantSupervisor) SetPublisherPeerConnectionConnected(isConnected bool) {
p.lock.Lock()
p.isPublisherConnected = isConnected
for _, pm := range p.publications {
pm.opMon.PostEvent(types.OperationMonitorEventPublisherPeerConnectionConnected, p.isPublisherConnected)
}
p.lock.Unlock()
}
func (p *ParticipantSupervisor) AddPublication(trackID livekit.TrackID) {
p.lock.Lock()
pm, ok := p.publications[trackID]
if !ok {
pm = &trackMonitor{
opMon: NewPublicationMonitor(
PublicationMonitorParams{
TrackID: trackID,
IsPeerConnectionConnected: p.isPublisherConnected,
Logger: p.params.Logger,
},
),
}
p.publications[trackID] = pm
}
pm.opMon.PostEvent(types.OperationMonitorEventAddPendingPublication, nil)
p.lock.Unlock()
}
func (p *ParticipantSupervisor) SetPublicationMute(trackID livekit.TrackID, isMuted bool) {
p.lock.Lock()
pm, ok := p.publications[trackID]
if ok {
pm.opMon.PostEvent(types.OperationMonitorEventSetPublicationMute, isMuted)
}
p.lock.Unlock()
}
func (p *ParticipantSupervisor) SetPublishedTrack(trackID livekit.TrackID, pubTrack types.LocalMediaTrack) {
p.lock.RLock()
pm, ok := p.publications[trackID]
if ok {
pm.opMon.PostEvent(types.OperationMonitorEventSetPublishedTrack, pubTrack)
}
p.lock.RUnlock()
}
func (p *ParticipantSupervisor) ClearPublishedTrack(trackID livekit.TrackID, pubTrack types.LocalMediaTrack) {
p.lock.RLock()
pm, ok := p.publications[trackID]
if ok {
pm.opMon.PostEvent(types.OperationMonitorEventClearPublishedTrack, pubTrack)
}
p.lock.RUnlock()
}
func (p *ParticipantSupervisor) checkState() {
ticker := time.NewTicker(monitorInterval)
defer ticker.Stop()
for !p.isStopped.Load() {
<-ticker.C
p.checkPublications()
}
}
func (p *ParticipantSupervisor) checkPublications() {
var erroredPublications []livekit.TrackID
var removablePublications []livekit.TrackID
p.lock.RLock()
for trackID, pm := range p.publications {
if err := pm.opMon.Check(); err != nil {
if pm.err == nil {
p.params.Logger.Errorw("supervisor error on publication", err, "trackID", trackID)
pm.err = err
erroredPublications = append(erroredPublications, trackID)
}
} else {
if pm.err != nil {
p.params.Logger.Infow("supervisor publication recovered", "trackID", trackID)
pm.err = err
}
if pm.opMon.IsIdle() {
removablePublications = append(removablePublications, trackID)
}
}
}
p.lock.RUnlock()
p.lock.Lock()
for _, trackID := range removablePublications {
delete(p.publications, trackID)
}
p.lock.Unlock()
if onPublicationError := p.getOnPublicationError(); onPublicationError != nil {
for _, trackID := range erroredPublications {
onPublicationError(trackID)
}
}
}
@@ -0,0 +1,190 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package supervisor
import (
"errors"
"sync"
"time"
"github.com/gammazero/deque"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
const (
publishWaitDuration = 30 * time.Second
)
var (
errPublishTimeout = errors.New("publish time out")
)
type publish struct {
isStart bool
}
type PublicationMonitorParams struct {
TrackID livekit.TrackID
IsPeerConnectionConnected bool
Logger logger.Logger
}
type PublicationMonitor struct {
params PublicationMonitorParams
lock sync.RWMutex
desiredPublishes deque.Deque[*publish]
isConnected bool
publishedTrack types.LocalMediaTrack
isMuted bool
unmutedAt time.Time
}
func NewPublicationMonitor(params PublicationMonitorParams) *PublicationMonitor {
p := &PublicationMonitor{
params: params,
isConnected: params.IsPeerConnectionConnected,
}
p.desiredPublishes.SetBaseCap(4)
return p
}
func (p *PublicationMonitor) PostEvent(ome types.OperationMonitorEvent, omd types.OperationMonitorData) {
switch ome {
case types.OperationMonitorEventPublisherPeerConnectionConnected:
p.setConnected(omd.(bool))
case types.OperationMonitorEventAddPendingPublication:
p.addPending()
case types.OperationMonitorEventSetPublicationMute:
p.setMute(omd.(bool))
case types.OperationMonitorEventSetPublishedTrack:
p.setPublishedTrack(omd.(types.LocalMediaTrack))
case types.OperationMonitorEventClearPublishedTrack:
p.clearPublishedTrack(omd.(types.LocalMediaTrack))
}
}
func (p *PublicationMonitor) addPending() {
p.lock.Lock()
p.desiredPublishes.PushBack(
&publish{
isStart: true,
},
)
// synthesize an end
p.desiredPublishes.PushBack(
&publish{
isStart: false,
},
)
p.update()
p.lock.Unlock()
}
func (p *PublicationMonitor) maybeStartMonitor() {
if p.isConnected && !p.isMuted {
p.unmutedAt = time.Now()
}
}
func (p *PublicationMonitor) setConnected(isConnected bool) {
p.lock.Lock()
p.isConnected = isConnected
p.maybeStartMonitor()
p.lock.Unlock()
}
func (p *PublicationMonitor) setMute(isMuted bool) {
p.lock.Lock()
p.isMuted = isMuted
p.maybeStartMonitor()
p.lock.Unlock()
}
func (p *PublicationMonitor) setPublishedTrack(pubTrack types.LocalMediaTrack) {
p.lock.Lock()
p.publishedTrack = pubTrack
p.update()
p.lock.Unlock()
}
func (p *PublicationMonitor) clearPublishedTrack(pubTrack types.LocalMediaTrack) {
p.lock.Lock()
if p.publishedTrack == pubTrack {
p.publishedTrack = nil
} else {
p.params.Logger.Errorw("supervisor: mismatched published track on clear", nil, "trackID", p.params.TrackID)
}
p.update()
p.lock.Unlock()
}
func (p *PublicationMonitor) Check() error {
p.lock.RLock()
var pub *publish
if p.desiredPublishes.Len() > 0 {
pub = p.desiredPublishes.Front()
}
isMuted := p.isMuted
unmutedAt := p.unmutedAt
p.lock.RUnlock()
if pub == nil {
return nil
}
if pub.isStart && !isMuted && !unmutedAt.IsZero() && time.Since(unmutedAt) > publishWaitDuration {
// timed out waiting for publish
return errPublishTimeout
}
// give more time for publish to happen
// NOTE: synthesized end events do not have a start time, so do not check them for time out
return nil
}
func (p *PublicationMonitor) IsIdle() bool {
p.lock.RLock()
defer p.lock.RUnlock()
return p.desiredPublishes.Len() == 0 && p.publishedTrack == nil
}
func (p *PublicationMonitor) update() {
for {
var pub *publish
if p.desiredPublishes.Len() > 0 {
pub = p.desiredPublishes.PopFront()
}
if pub == nil {
return
}
if (pub.isStart && p.publishedTrack == nil) || (!pub.isStart && p.publishedTrack != nil) {
// put it back as the condition is not satisfied
p.desiredPublishes.PushFront(pub)
return
}
}
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
)
func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.ProtocolVersion, hidden bool, publisher bool) *typesfakes.FakeLocalParticipant {
p := &typesfakes.FakeLocalParticipant{}
sid := guid.New(utils.ParticipantPrefix)
p.IDReturns(livekit.ParticipantID(sid))
p.IdentityReturns(identity)
p.StateReturns(livekit.ParticipantInfo_JOINED)
p.ProtocolVersionReturns(protocol)
p.CanSubscribeReturns(true)
p.CanPublishSourceReturns(!hidden)
p.CanPublishDataReturns(!hidden)
p.HiddenReturns(hidden)
p.ToProtoReturns(&livekit.ParticipantInfo{
Sid: sid,
Identity: string(identity),
State: livekit.ParticipantInfo_JOINED,
IsPublisher: publisher,
})
p.ToProtoWithVersionReturns(&livekit.ParticipantInfo{
Sid: sid,
Identity: string(identity),
State: livekit.ParticipantInfo_JOINED,
IsPublisher: publisher,
}, utils.TimedVersion(0))
p.SetMetadataCalls(func(m string) {
var f func(participant types.LocalParticipant)
if p.OnParticipantUpdateCallCount() > 0 {
f = p.OnParticipantUpdateArgsForCall(p.OnParticipantUpdateCallCount() - 1)
}
if f != nil {
f(p)
}
})
updateTrack := func() {
var f func(participant types.LocalParticipant, track types.MediaTrack)
if p.OnTrackUpdatedCallCount() > 0 {
f = p.OnTrackUpdatedArgsForCall(p.OnTrackUpdatedCallCount() - 1)
}
if f != nil {
f(p, NewMockTrack(livekit.TrackType_VIDEO, "testcam"))
}
}
p.SetTrackMutedCalls(func(sid livekit.TrackID, muted bool, fromServer bool) *livekit.TrackInfo {
updateTrack()
return nil
})
p.AddTrackCalls(func(req *livekit.AddTrackRequest) {
updateTrack()
})
p.GetLoggerReturns(logger.GetLogger())
return p
}
func NewMockTrack(kind livekit.TrackType, name string) *typesfakes.FakeMediaTrack {
t := &typesfakes.FakeMediaTrack{}
t.IDReturns(livekit.TrackID(guid.New(utils.TrackPrefix)))
t.KindReturns(kind)
t.NameReturns(name)
t.ToProtoReturns(&livekit.TrackInfo{
Type: kind,
Name: name,
})
return t
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
// Copyright 2024 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import (
"errors"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
"github.com/livekit/protocol/livekit"
)
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
var (
ErrNoICECandidateHandler = errors.New("no ICE candidate handler")
ErrNoOfferHandler = errors.New("no offer handler")
ErrNoAnswerHandler = errors.New("no answer handler")
)
//counterfeiter:generate . Handler
type Handler interface {
OnICECandidate(c *webrtc.ICECandidate, target livekit.SignalTarget) error
OnInitialConnected()
OnFullyEstablished()
OnFailed(isShortLived bool, iceConnectionInfo *types.ICEConnectionInfo)
OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver)
OnDataPacket(kind livekit.DataPacket_Kind, data []byte)
OnDataSendError(err error)
OnOffer(sd webrtc.SessionDescription) error
OnAnswer(sd webrtc.SessionDescription) error
OnNegotiationStateChanged(state NegotiationState)
OnNegotiationFailed()
OnStreamStateChange(update *streamallocator.StreamStateUpdate) error
}
type UnimplementedHandler struct{}
func (h UnimplementedHandler) OnICECandidate(c *webrtc.ICECandidate, target livekit.SignalTarget) error {
return ErrNoICECandidateHandler
}
func (h UnimplementedHandler) OnInitialConnected() {}
func (h UnimplementedHandler) OnFullyEstablished() {}
func (h UnimplementedHandler) OnFailed(isShortLived bool) {}
func (h UnimplementedHandler) OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {}
func (h UnimplementedHandler) OnDataPacket(kind livekit.DataPacket_Kind, data []byte) {}
func (h UnimplementedHandler) OnDataSendError(err error) {}
func (h UnimplementedHandler) OnOffer(sd webrtc.SessionDescription) error {
return ErrNoOfferHandler
}
func (h UnimplementedHandler) OnAnswer(sd webrtc.SessionDescription) error {
return ErrNoAnswerHandler
}
func (h UnimplementedHandler) OnNegotiationStateChanged(state NegotiationState) {}
func (h UnimplementedHandler) OnNegotiationFailed() {}
func (h UnimplementedHandler) OnStreamStateChange(update *streamallocator.StreamStateUpdate) error {
return nil
}
@@ -0,0 +1,40 @@
// Copyright 2024 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import "fmt"
type NegotiationState int
const (
NegotiationStateNone NegotiationState = iota
// waiting for remote description
NegotiationStateRemote
// need to Negotiate again
NegotiationStateRetry
)
func (n NegotiationState) String() string {
switch n {
case NegotiationStateNone:
return "NONE"
case NegotiationStateRemote:
return "WAITING_FOR_REMOTE"
case NegotiationStateRetry:
return "RETRY"
default:
return fmt.Sprintf("%d", int(n))
}
}

Some files were not shown because too many files have changed in this diff Show More