added livekit v1.9.11 sources
This commit is contained in:
commit
a077651f7a
373 changed files with 133407 additions and 0 deletions
61
livekit/.goreleaser.yaml
Normal file
61
livekit/.goreleaser.yaml
Normal 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"
|
||||
1501
livekit/CHANGELOG.md
Normal file
1501
livekit/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
44
livekit/Dockerfile
Normal file
44
livekit/Dockerfile
Normal 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.25-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
livekit/LICENSE
Normal file
202
livekit/LICENSE
Normal 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
livekit/NOTICE
Normal file
13
livekit/NOTICE
Normal 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.
|
||||
319
livekit/README.md
Normal file
319
livekit/README.md
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
<!--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.
|
||||
|
||||
[](https://github.com/livekit/livekit/stargazers/)
|
||||
[](https://livekit.io/join-slack)
|
||||
[](https://twitter.com/livekit)
|
||||
[](https://deepwiki.com/livekit/livekit)
|
||||
[](https://github.com/livekit/livekit/releases/latest)
|
||||
[](https://github.com/livekit/livekit/actions/workflows/buildtest.yaml)
|
||||
[](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> · <a href="https://github.com/livekit/client-sdk-esp32">ESP32</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> · <a href="https://github.com/livekit/components-flutter">Flutter</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
livekit/bootstrap.sh
Executable file
33
livekit/bootstrap.sh
Executable 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
|
||||
258
livekit/cmd/server/commands.go
Normal file
258
livekit/cmd/server/commands.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/urfave/cli/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"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"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
)
|
||||
|
||||
func generateKeys(_ context.Context, _ *cli.Command) error {
|
||||
apiKey := guid.New(utils.APIKeyPrefix)
|
||||
secret := utils.RandomSecret()
|
||||
fmt.Println("API Key: ", apiKey)
|
||||
fmt.Println("API Secret: ", secret)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printPorts(_ context.Context, c *cli.Command) 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(_ context.Context, c *cli.Command) error {
|
||||
generatedFlags, err := config.GenerateCLIFlags(baseFlags, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Flags = append(baseFlags, generatedFlags...)
|
||||
return cli.ShowAppHelp(c)
|
||||
}
|
||||
|
||||
func createToken(_ context.Context, c *cli.Command) 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(_ context.Context, c *cli.Command) 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
|
||||
rate := &livekit.NodeStatsRate{}
|
||||
if len(stats.Rates) > 0 {
|
||||
rate = stats.Rates[0]
|
||||
}
|
||||
|
||||
// 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(rate.BytesIn)), humanize.Bytes(uint64(rate.BytesOut)),
|
||||
humanize.Bytes(stats.BytesIn), humanize.Bytes(stats.BytesOut))
|
||||
packets := fmt.Sprintf("%s / %s\n%s / %s", humanize.Comma(int64(rate.PacketsIn)), humanize.Comma(int64(rate.PacketsOut)),
|
||||
strings.TrimSpace(humanize.SIWithDigits(float64(stats.PacketsIn), 2, "")), strings.TrimSpace(humanize.SIWithDigits(float64(stats.PacketsOut), 2, "")))
|
||||
sysPacketsDroppedPct := float32(0)
|
||||
if rate.SysPacketsOut+rate.SysPacketsDropped > 0 {
|
||||
sysPacketsDroppedPct = float32(rate.SysPacketsDropped) / float32(rate.SysPacketsDropped+rate.SysPacketsOut)
|
||||
}
|
||||
sysPackets := fmt.Sprintf("%.2f %%\n%v / %v", sysPacketsDroppedPct*100, float64(rate.SysPacketsOut), float64(rate.SysPacketsDropped))
|
||||
nacks := fmt.Sprintf("%.2f\n%s", rate.NackTotal, strings.TrimSpace(humanize.SIWithDigits(float64(stats.NackTotal), 2, "")))
|
||||
retransmit := fmt.Sprintf("%.2f\n%s", rate.RetransmitPacketsOut, 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
|
||||
}
|
||||
333
livekit/cmd/server/main.go
Normal file
333
livekit/cmd/server/main.go
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/tracer/jaeger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
||||
"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",
|
||||
Sources: cli.EnvVars("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)",
|
||||
Sources: cli.EnvVars("LIVEKIT_KEYS"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "region",
|
||||
Usage: "region of the current node. Used by regionaware node selector",
|
||||
Sources: cli.EnvVars("LIVEKIT_REGION"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "node-ip",
|
||||
Usage: "IP address of the current node, used to advertise to clients. Automatically determined by default",
|
||||
Sources: cli.EnvVars("NODE_IP"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "udp-port",
|
||||
Usage: "UDP port(s) to use for WebRTC traffic",
|
||||
Sources: cli.EnvVars("UDP_PORT"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "redis-host",
|
||||
Usage: "host (incl. port) to redis server",
|
||||
Sources: cli.EnvVars("REDIS_HOST"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "redis-password",
|
||||
Usage: "password to redis",
|
||||
Sources: cli.EnvVars("REDIS_PASSWORD"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "turn-cert",
|
||||
Usage: "tls cert file for TURN server",
|
||||
Sources: cli.EnvVars("LIVEKIT_TURN_CERT"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "turn-key",
|
||||
Usage: "tls key file for TURN server",
|
||||
Sources: cli.EnvVars("LIVEKIT_TURN_KEY"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "cpuprofile",
|
||||
Usage: "write CPU profile to `file`",
|
||||
},
|
||||
&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)
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
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 := cmd.Run(context.Background(), os.Args); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getConfig(c *cli.Command) (*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(ctx context.Context, c *cli.Command) error {
|
||||
conf, err := getConfig(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if url := conf.Trace.JaegerURL; url != "" {
|
||||
jaeger.Configure(ctx, url, "livekit")
|
||||
}
|
||||
|
||||
// validate API key length
|
||||
err = conf.ValidateKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cpuProfile := c.String("cpuprofile"); cpuProfile != "" {
|
||||
if f, err := os.Create(cpuProfile); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
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 := range 2 {
|
||||
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
livekit/cmd/server/main_test.go
Normal file
63
livekit/cmd/server/main_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
330
livekit/config-sample.yaml
Normal file
330
livekit/config-sample.yaml
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
# 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
|
||||
# # there are cases where the public IP determined via STUN is not the correct one
|
||||
# # in such cases, use this setting to set the public IP of the node
|
||||
# # use_external_ip takes precedence, for this to take effect, set use_external_ip to false
|
||||
# node_ip: <external-ip-of-node>
|
||||
# # 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
|
||||
# # Shared secret for TURN server authentication
|
||||
# secret: ""
|
||||
# ttl: 14400 # seconds
|
||||
# # Insecure username/password authentication
|
||||
# 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
|
||||
# # algorithm used to govern selecting from sorted nodes
|
||||
# # default: lowest. valid values: lowest, twochoice
|
||||
# algorithm: lowest
|
||||
# # 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
livekit/deploy/README.md
Normal file
8
livekit/deploy/README.md
Normal 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.
|
||||
531
livekit/deploy/grafana/livekit-server-overview.json
Normal file
531
livekit/deploy/grafana/livekit-server-overview.json
Normal file
|
|
@ -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
|
||||
}
|
||||
159
livekit/go.mod
Normal file
159
livekit/go.mod
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
module github.com/livekit/livekit-server
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/d5/tengo/v2 v2.17.0
|
||||
github.com/dennwc/iters v1.2.2
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elliotchance/orderedmap/v2 v2.7.0
|
||||
github.com/florianl/go-tc v0.4.5
|
||||
github.com/frostbyte73/core v0.1.1
|
||||
github.com/gammazero/deque v1.2.0
|
||||
github.com/gammazero/workerpool v1.1.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.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.4.0
|
||||
github.com/jxskiss/base62 v1.1.0
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
|
||||
github.com/livekit/mediatransportutil v0.0.0-20260113174415-2e8ba344fca3
|
||||
github.com/livekit/protocol v1.43.5-0.20260114074149-a8bb8204ce69
|
||||
github.com/livekit/psrpc v0.7.1
|
||||
github.com/mackerelio/go-osstat v0.2.6
|
||||
github.com/magefile/mage v1.15.0
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/pion/datachannel v1.6.0
|
||||
github.com/pion/dtls/v3 v3.0.10
|
||||
github.com/pion/ice/v4 v4.2.0
|
||||
github.com/pion/interceptor v0.1.43
|
||||
github.com/pion/rtcp v1.2.16
|
||||
github.com/pion/rtp v1.10.0
|
||||
github.com/pion/sctp v1.9.2
|
||||
github.com/pion/sdp/v3 v3.0.17
|
||||
github.com/pion/transport/v4 v4.0.1
|
||||
github.com/pion/turn/v4 v4.1.4
|
||||
github.com/pion/webrtc/v4 v4.2.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/thoas/go-funk v0.9.3
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible
|
||||
github.com/ua-parser/uap-go v0.0.0-20250326155420-f7f5a2f9f5bc
|
||||
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.1
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||
golang.org/x/mod v0.32.0
|
||||
golang.org/x/sync v0.19.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/nyaruka/phonenumbers v1.6.5 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
|
||||
buf.build/go/protovalidate v1.1.0 // indirect
|
||||
buf.build/go/protoyaml v0.6.0 // indirect
|
||||
cel.dev/expr v0.25.1 // 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.1 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // 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.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 v27.4.1+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.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
github.com/google/go-cmp v0.7.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 v1.0.2 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // 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/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.48.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.12 // 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.2.3 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||
github.com/pion/stun/v3 v3.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/urfave/cli/v3 v3.3.9
|
||||
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/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
507
livekit/go.sum
Normal file
507
livekit/go.sum
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||
buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=
|
||||
buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
|
||||
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
||||
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
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.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
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/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
||||
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||
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/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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
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/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
|
||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
||||
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
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.2.2 h1:XH2/Etihiy9ZvPOVCR+icQXeYlhbvS7k0qro4x/2qQo=
|
||||
github.com/dennwc/iters v1.2.2/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
github.com/docker/cli v27.4.1+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/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.5 h1:8lvecARs3c/vGee46j0ro8kco98ga9XjwWvXGwlzrXA=
|
||||
github.com/florianl/go-tc v0.4.5/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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gammazero/deque v1.2.0 h1:scEFO8Uidhw6KDU5qg1HA5fYwM0+us2qdeJqm43bitU=
|
||||
github.com/gammazero/deque v1.2.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
|
||||
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.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
||||
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/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.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
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 v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/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.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
|
||||
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.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5ATTo469PQPkqzdoU7be46ryiCDO3boc=
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
|
||||
github.com/livekit/mediatransportutil v0.0.0-20260113174415-2e8ba344fca3 h1:v1Xc/q/547TjLX7Nw5y2vXNnmV0XYFAbhTJrtErQeDA=
|
||||
github.com/livekit/mediatransportutil v0.0.0-20260113174415-2e8ba344fca3/go.mod h1:QBx/KHV6Vv00ggibg/WrOlqrkTciEA2Hc9DGWYr3Q9U=
|
||||
github.com/livekit/protocol v1.43.5-0.20260114074149-a8bb8204ce69 h1:cD82r488SxGYL5MX1lLuLLjmdnNoC+u5TIepxQmSB40=
|
||||
github.com/livekit/protocol v1.43.5-0.20260114074149-a8bb8204ce69/go.mod h1:BLJHYHErQTu3+fnmfGrzN6CbHxNYiooFIIYGYxXxotw=
|
||||
github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw=
|
||||
github.com/livekit/psrpc v0.7.1/go.mod h1:bZ4iHFQptTkbPnB0LasvRNu/OBYXEu1NA6O5BMFo9kk=
|
||||
github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0=
|
||||
github.com/mackerelio/go-osstat v0.2.6/go.mod h1:lRy8V9ZuHpuRVZh+vyTkODeDPl3/d5MgXHtLSaqG8bA=
|
||||
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.12.0 h1:aOeI7xAOVdK+R6xbVsZuU9HmCZYmQVmZgPf9xJUd2Sg=
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0/go.mod h1:0hZWbtfeCYUQeAQdPLUzETiBhUSns7O6LDj9vH88xKA=
|
||||
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/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/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
|
||||
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
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.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||
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/nyaruka/phonenumbers v1.6.5 h1:aBCaUhfpRA7hU6fsXk+p7KF1aNx4nQlq9hGeo2qdFg8=
|
||||
github.com/nyaruka/phonenumbers v1.6.5/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU=
|
||||
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.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
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.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
|
||||
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
|
||||
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
|
||||
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
|
||||
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
|
||||
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
|
||||
github.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ=
|
||||
github.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=
|
||||
github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
|
||||
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
|
||||
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
||||
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
||||
github.com/pion/webrtc/v4 v4.2.3 h1:RtdWDnkenNQGxUrZqWa5gSkTm5ncsLg5d+zu0M4cXt4=
|
||||
github.com/pion/webrtc/v4 v4.2.3/go.mod h1:7vsyFzRzaKP5IELUnj8zLcglPyIT6wWwqTppBZ1k6Kc=
|
||||
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.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
|
||||
github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
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.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
|
||||
github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
|
||||
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-20250326155420-f7f5a2f9f5bc h1:reH9QQKGFOq39MYOvU9+SYrB8uzXtWNo51fWK3g0gGc=
|
||||
github.com/ua-parser/uap-go v0.0.0-20250326155420-f7f5a2f9f5bc/go.mod h1:gwANdYmo9R8LLwGnyDFWK2PMsaXXX2HhAvCnb/UhZsM=
|
||||
github.com/urfave/cli/v3 v3.3.9 h1:54roEDJcTWuucl6MSQ3B+pQqt1ePh/xOQokhEYl5Gfs=
|
||||
github.com/urfave/cli/v3 v3.3.9/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
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/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/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/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=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 h1:4DKBrmaqeptdEzp21EfrOEh8LE7PJ5ywH6wydSbOfGY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 h1:IY6/YYRrFUk0JPp0xOVctvFIVuRnjccihY5kxf5g0TE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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.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
livekit/install-livekit.sh
Executable file
101
livekit/install-livekit.sh
Executable 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"
|
||||
233
livekit/magefile.go
Normal file
233
livekit/magefile.go
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// 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
|
||||
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
|
||||
}
|
||||
buildArch := os.Getenv("GOARCH")
|
||||
if len(buildArch) == 0 {
|
||||
buildArch = "amd64"
|
||||
}
|
||||
cmd := mageutil.CommandDir(context.Background(), "cmd/server", "go build -buildvcs=false -o ../../bin/livekit-server-" + buildArch)
|
||||
cmd.Env = []string{
|
||||
"GOOS=linux",
|
||||
"GOARCH=" + buildArch,
|
||||
"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
livekit/magefile_unix.go
Normal file
34
livekit/magefile_unix.go
Normal 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
livekit/magefile_windows.go
Normal file
22
livekit/magefile_windows.go
Normal 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
|
||||
}
|
||||
190
livekit/pkg/agent/agent_test.go
Normal file
190
livekit/pkg/agent/agent_test.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
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/auth"
|
||||
"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) {
|
||||
testAgentName := "test_agent"
|
||||
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(testAgentName, 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: testAgentName,
|
||||
}
|
||||
_, err := client.JobRequest(context.Background(), testAgentName, agent.RoomAgentTopic, job)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case a := <-jobAssignments.Events():
|
||||
require.EqualValues(t, job.Id, a.Job.Id)
|
||||
v, err := auth.ParseAPIToken(a.Token)
|
||||
require.NoError(t, err)
|
||||
_, claims, err := v.Verify(server.TestAPISecret)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testAgentName, claims.Attributes[agent.AgentNameAttributeKey])
|
||||
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 := range totalWorkers {
|
||||
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 := range totalWorkers {
|
||||
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 := range totalWorkers {
|
||||
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 := range totalWorkers {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
339
livekit/pkg/agent/client.go
Normal file
339
livekit/pkg/agent/client.go
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
// 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
|
||||
config Config
|
||||
|
||||
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, config Config) (Client, error) {
|
||||
client, err := rpc.NewAgentInternalClient(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &agentClient{
|
||||
client: client,
|
||||
config: config,
|
||||
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,
|
||||
EnableRecording: c.config.EnableUserDataRecording,
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
5
livekit/pkg/agent/config.go
Normal file
5
livekit/pkg/agent/config.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package agent
|
||||
|
||||
type Config struct {
|
||||
EnableUserDataRecording bool `yaml:"enable_user_data_recording"`
|
||||
}
|
||||
485
livekit/pkg/agent/testutils/server.go
Normal file
485
livekit/pkg/agent/testutils/server.go
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
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.WorkerRegistration)
|
||||
DrainConnections(time.Duration)
|
||||
}
|
||||
|
||||
type TestServer struct {
|
||||
AgentService
|
||||
TestAPIKey string
|
||||
TestAPISecret string
|
||||
}
|
||||
|
||||
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, "test", "verysecretsecret"}
|
||||
}
|
||||
|
||||
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.MakeWorkerRegistration())
|
||||
|
||||
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))
|
||||
}
|
||||
578
livekit/pkg/agent/worker.go
Normal file
578
livekit/pkg/agent/worker.go
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
// 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")
|
||||
)
|
||||
|
||||
const AgentNameAttributeKey = "lk.agent_name"
|
||||
|
||||
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
|
||||
AgentID string
|
||||
AgentName string
|
||||
Namespace string
|
||||
JobType livekit.JobType
|
||||
Permissions *livekit.ParticipantPermission
|
||||
ClientIP string
|
||||
}
|
||||
|
||||
func MakeWorkerRegistration() WorkerRegistration {
|
||||
return WorkerRegistration{
|
||||
ID: guid.New(guid.AgentWorkerPrefix),
|
||||
Protocol: CurrentProtocol,
|
||||
}
|
||||
}
|
||||
|
||||
var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
|
||||
|
||||
type WorkerRegisterer struct {
|
||||
WorkerPingHandler
|
||||
serverInfo *livekit.ServerInfo
|
||||
deadline time.Time
|
||||
|
||||
registration WorkerRegistration
|
||||
registered bool
|
||||
}
|
||||
|
||||
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, base WorkerRegistration) *WorkerRegisterer {
|
||||
return &WorkerRegisterer{
|
||||
WorkerPingHandler: WorkerPingHandler{conn: conn},
|
||||
serverInfo: serverInfo,
|
||||
registration: base,
|
||||
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.Version = req.Version
|
||||
h.registration.AgentName = req.AgentName
|
||||
h.registration.Namespace = req.GetNamespace()
|
||||
h.registration.JobType = req.GetType()
|
||||
h.registration.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.WorkerId = w.ID
|
||||
job.State.AgentId = w.AgentID
|
||||
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.Terminate {
|
||||
job.State.EndedAt = now.UnixNano()
|
||||
job.State.Status = livekit.JobStatus_JS_SUCCESS
|
||||
return job.State, nil
|
||||
}
|
||||
|
||||
if !res.Available {
|
||||
return nil, ErrWorkerNotAvailable
|
||||
}
|
||||
|
||||
job.State.ParticipantIdentity = res.ParticipantIdentity
|
||||
attributes := res.ParticipantAttributes
|
||||
if attributes == nil {
|
||||
attributes = make(map[string]string)
|
||||
}
|
||||
attributes[AgentNameAttributeKey] = w.AgentName
|
||||
|
||||
token, err := pagent.BuildAgentToken(
|
||||
w.apiKey,
|
||||
w.apiSecret,
|
||||
job.Room.Name,
|
||||
res.ParticipantIdentity,
|
||||
res.ParticipantName,
|
||||
res.ParticipantMetadata,
|
||||
attributes,
|
||||
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
|
||||
}
|
||||
64
livekit/pkg/clientconfiguration/conf.go
Normal file
64
livekit/pkg/clientconfiguration/conf.go
Normal file
|
|
@ -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 clientconfiguration
|
||||
|
||||
import (
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils/must"
|
||||
)
|
||||
|
||||
// StaticConfigurations list specific device-side limitations that should be disabled at a global level
|
||||
var StaticConfigurations = []ConfigurationItem{
|
||||
// {
|
||||
// Match: must.Get(NewScriptMatch(`c.protocol <= 5 || c.browser == "firefox"`)),
|
||||
// Configuration: &livekit.ClientConfiguration{ResumeConnection: livekit.ClientConfigSetting_DISABLED},
|
||||
// Merge: false,
|
||||
// },
|
||||
{
|
||||
Match: must.Get(NewScriptMatch(`c.browser == "safari"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
DisabledCodecs: &livekit.DisabledCodecs{
|
||||
Codecs: []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeAV1.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
Merge: true,
|
||||
},
|
||||
{
|
||||
Match: must.Get(NewScriptMatch(`c.browser == "safari" && c.browser_version > "18.3"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
DisabledCodecs: &livekit.DisabledCodecs{
|
||||
Publish: []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeVP9.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
Merge: true,
|
||||
},
|
||||
{
|
||||
Match: must.Get(NewScriptMatch(`(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,
|
||||
},
|
||||
}
|
||||
124
livekit/pkg/clientconfiguration/conf_test.go
Normal file
124
livekit/pkg/clientconfiguration/conf_test.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// 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"
|
||||
"github.com/livekit/protocol/utils/must"
|
||||
)
|
||||
|
||||
func TestScriptMatchConfiguration(t *testing.T) {
|
||||
t.Run("no merge", func(t *testing.T) {
|
||||
confs := []ConfigurationItem{
|
||||
{
|
||||
Match: must.Get(NewScriptMatch(`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: must.Get(NewScriptMatch(`c.protocol > 5 && c.browser != "firefox"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
|
||||
},
|
||||
Merge: true,
|
||||
},
|
||||
{
|
||||
Match: must.Get(NewScriptMatch(`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, err := NewScriptMatch(c.expr)
|
||||
if err != nil {
|
||||
if !c.err {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
m, err := match.Match(client)
|
||||
if c.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.Equal(t, c.result, m)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
173
livekit/pkg/clientconfiguration/match.go
Normal file
173
livekit/pkg/clientconfiguration/match.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/d5/tengo/v2"
|
||||
"github.com/d5/tengo/v2/token"
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type Match interface {
|
||||
Match(clientInfo *livekit.ClientInfo) (bool, error)
|
||||
}
|
||||
|
||||
type ScriptMatch struct {
|
||||
compiled *tengo.Compiled
|
||||
}
|
||||
|
||||
func NewScriptMatch(expr string) (*ScriptMatch, error) {
|
||||
script := tengo.NewScript(fmt.Appendf(nil, "__res__ := (%s)", expr))
|
||||
if err := script.Add("c", &clientObject{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compiled, err := script.Compile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ScriptMatch{compiled}, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
clone := m.compiled.Clone()
|
||||
if err := clone.Set("c", &clientObject{info: clientInfo}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := clone.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res := clone.Get("__res__").Value()
|
||||
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 &ruleSdkVersion{sdkVersion: 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 &ruleSdkVersion{sdkVersion: c.info.BrowserVersion}, nil
|
||||
case "address":
|
||||
return &tengo.String{Value: c.info.Address}, nil
|
||||
}
|
||||
return &tengo.Undefined{}, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
type ruleSdkVersion struct {
|
||||
tengo.ObjectImpl
|
||||
sdkVersion string
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) TypeName() string {
|
||||
return "sdkVersion"
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) String() string {
|
||||
return r.sdkVersion
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) BinaryOp(op token.Token, rhs tengo.Object) (tengo.Object, error) {
|
||||
if rhs, ok := rhs.(*tengo.String); ok {
|
||||
cmp := r.compare(rhs.Value)
|
||||
|
||||
isMatch := false
|
||||
switch op {
|
||||
case token.Greater:
|
||||
isMatch = cmp > 0
|
||||
case token.GreaterEq:
|
||||
isMatch = cmp >= 0
|
||||
default:
|
||||
return nil, tengo.ErrInvalidOperator
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
return tengo.TrueValue, nil
|
||||
}
|
||||
return tengo.FalseValue, nil
|
||||
}
|
||||
|
||||
return nil, tengo.ErrInvalidOperator
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) Equals(rhs tengo.Object) bool {
|
||||
if rhs, ok := rhs.(*tengo.String); ok {
|
||||
return r.compare(rhs.Value) == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) compare(rhsSdkVersion string) int {
|
||||
if !semver.IsValid("v"+r.sdkVersion) || !semver.IsValid("v"+rhsSdkVersion) {
|
||||
// if not valid semver, do string compare
|
||||
switch {
|
||||
case r.sdkVersion < rhsSdkVersion:
|
||||
return -1
|
||||
case r.sdkVersion > rhsSdkVersion:
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
return semver.Compare("v"+r.sdkVersion, "v"+rhsSdkVersion)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
71
livekit/pkg/clientconfiguration/staticconfiguration.go
Normal file
71
livekit/pkg/clientconfiguration/staticconfiguration.go
Normal file
|
|
@ -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
|
||||
}
|
||||
23
livekit/pkg/clientconfiguration/types.go
Normal file
23
livekit/pkg/clientconfiguration/types.go
Normal file
|
|
@ -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
|
||||
}
|
||||
796
livekit/pkg/config/config.go
Normal file
796
livekit/pkg/config/config.go
Normal file
|
|
@ -0,0 +1,796 @@
|
|||
// 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/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"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"
|
||||
"github.com/livekit/protocol/webhook"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/agent"
|
||||
"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/pacer"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
|
||||
)
|
||||
|
||||
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 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"`
|
||||
Agents agent.Config `yaml:"agents,omitempty"`
|
||||
|
||||
Development bool `yaml:"development,omitempty"`
|
||||
|
||||
Metric metric.MetricConfig `yaml:"metric,omitempty"`
|
||||
Trace TracingConfig `yaml:"trace,omitempty"`
|
||||
|
||||
NodeStats NodeStatsConfig `yaml:"node_stats,omitempty"`
|
||||
|
||||
EnableDataTracks bool `yaml:"enable_data_tracks,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"`
|
||||
|
||||
// Target latency for lossy data channels, used to drop packets to reduce latency.
|
||||
DatachannelLossyTargetLatency time.Duration `yaml:"datachannel_lossy_target_latency,omitempty"`
|
||||
|
||||
ForwardStats ForwardStatsConfig `yaml:"forward_stats,omitempty"`
|
||||
|
||||
// enable rtp stream restart detection for published tracks
|
||||
EnableRTPStreamRestartDetection bool `yaml:"enable_rtp_stream_restart_detection,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"`
|
||||
// Secret is used for TURN static auth secrets mechanism. When provided,
|
||||
// dynamic credentials are generated using HMAC-SHA1 instead of static Username/Credential
|
||||
Secret string `yaml:"secret,omitempty"`
|
||||
// TTL is the time-to-live in seconds for generated credentials when using Secret.
|
||||
// Defaults to 14400 seconds (4 hours) if not specified
|
||||
TTL int `yaml:"ttl,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"`
|
||||
SendSideBWEPacer string `yaml:"send_side_bwe_pacer,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"`
|
||||
|
||||
CodecRegressionThreshold int `yaml:"codec_regression_threshold,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"`
|
||||
// target room participant update batch chunk size in bytes
|
||||
UpdateBatchTargetSize int `yaml:"update_batch_target_size,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 NodeSelectorConfig struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
SortBy string `yaml:"sort_by,omitempty"`
|
||||
Algorithm string `yaml:"algorithm,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) CheckParticipantIdentityLength(identity string) bool {
|
||||
return l.MaxParticipantIdentityLength == 0 || len(identity) <= l.MaxParticipantIdentityLength
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type TracingConfig struct {
|
||||
// JaegerURL configures Jaeger as a global tracer.
|
||||
//
|
||||
// The following formats are supported: <hostname>, <host>:<port>, http(s)://<host>/<path>
|
||||
JaegerURL string `yaml:"jaeger_url,omitempty"`
|
||||
}
|
||||
|
||||
func DefaultAPIConfig() APIConfig {
|
||||
return APIConfig{
|
||||
ExecutionTimeout: 2 * time.Second,
|
||||
CheckInterval: 100 * time.Millisecond,
|
||||
MaxCheckInterval: 300 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
type NodeStatsConfig struct {
|
||||
StatsUpdateInterval time.Duration `yaml:"stats_update_interval,omitempty"`
|
||||
StatsRateMeasurementIntervals []time.Duration `yaml:"stats_rate_measurement_intervals,omitempty"`
|
||||
StatsMaxDelay time.Duration `yaml:"stats_max_delay,omitempty"`
|
||||
}
|
||||
|
||||
var DefaultNodeStatsConfig = NodeStatsConfig{
|
||||
StatsUpdateInterval: 2 * time.Second,
|
||||
StatsRateMeasurementIntervals: []time.Duration{10 * time.Second},
|
||||
StatsMaxDelay: 30 * 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,
|
||||
SendSideBWEPacer: string(pacer.PacerBehaviorNoQueue),
|
||||
SendSideBWE: sendsidebwe.DefaultSendSideBWEConfig,
|
||||
},
|
||||
},
|
||||
Audio: sfu.DefaultAudioConfig,
|
||||
Video: VideoConfig{
|
||||
DynacastPauseDelay: 5 * time.Second,
|
||||
StreamTrackerManager: sfu.DefaultStreamTrackerManagerConfig,
|
||||
CodecRegressionThreshold: 5,
|
||||
},
|
||||
Redis: redisLiveKit.RedisConfig{},
|
||||
Room: RoomConfig{
|
||||
AutoCreate: true,
|
||||
EnabledCodecs: []CodecSpec{
|
||||
{Mime: mime.MimeTypePCMU.String()},
|
||||
{Mime: mime.MimeTypePCMA.String()},
|
||||
{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.MimeTypeH265.String()},
|
||||
{Mime: mime.MimeTypeRTX.String()},
|
||||
},
|
||||
EmptyTimeout: 5 * 60,
|
||||
DepartureTimeout: 20,
|
||||
CreateRoomEnabled: true,
|
||||
CreateRoomTimeout: 10 * time.Second,
|
||||
CreateRoomAttempts: 3,
|
||||
UpdateBatchTargetSize: 128 * 1024,
|
||||
},
|
||||
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,
|
||||
Algorithm: "lowest",
|
||||
},
|
||||
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,
|
||||
WebHook: webhook.DefaultWebHookConfig,
|
||||
NodeStats: DefaultNodeStatsConfig,
|
||||
}
|
||||
|
||||
func NewConfig(confString string, strictMode bool, c *cli.Command, 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,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.String:
|
||||
flag = &cli.StringFlag{
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Int, reflect.Int32:
|
||||
flag = &cli.IntFlag{
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Int64:
|
||||
flag = &cli.Int64Flag{
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
flag = &cli.UintFlag{
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Uint64:
|
||||
flag = &cli.Uint64Flag{
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Float32:
|
||||
flag = &cli.Float64Flag{
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Float64:
|
||||
flag = &cli.Float64Flag{
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(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.Command, baseFlags []cli.Flag) error {
|
||||
generatedFlagNames := conf.ToCLIFlagNames(baseFlags)
|
||||
for _, flag := range c.Flags {
|
||||
flagName := flag.Names()[0]
|
||||
|
||||
if !c.IsSet(flagName) {
|
||||
continue
|
||||
}
|
||||
|
||||
configValue, ok := generatedFlagNames[flagName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if configValue.Kind() == reflect.Ptr {
|
||||
configValue.Set(reflect.New(configValue.Type().Elem()))
|
||||
configValue = configValue.Elem()
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(c.Value(flagName))
|
||||
if value.CanConvert(configValue.Type()) {
|
||||
configValue.Set(value.Convert(configValue.Type()))
|
||||
} else {
|
||||
return fmt.Errorf("unsupported generated cli flag type for config: %s (expected %s, got %s)", flagName, configValue.Type(), value.Type())
|
||||
}
|
||||
}
|
||||
|
||||
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]any)
|
||||
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")
|
||||
}
|
||||
85
livekit/pkg/config/config_test.go
Normal file
85
livekit/pkg/config/config_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"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)
|
||||
|
||||
c := &cli.Command{}
|
||||
c.Name = "test"
|
||||
c.Flags = append(c.Flags, generatedFlags...)
|
||||
|
||||
c.Set("rtc.use_ice_lite", "true")
|
||||
c.Set("redis.address", "localhost:6379")
|
||||
c.Set("prometheus.port", "9999")
|
||||
c.Set("rtc.allow_tcp_fallback", "true")
|
||||
c.Set("rtc.reconnect_on_publication_error", "true")
|
||||
c.Set("rtc.reconnect_on_subscription_error", "false")
|
||||
|
||||
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{}))
|
||||
}
|
||||
69
livekit/pkg/config/configtest/checkyamltag.go
Normal file
69
livekit/pkg/config/configtest/checkyamltag.go
Normal file
|
|
@ -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{}{})
|
||||
}
|
||||
31
livekit/pkg/metric/metric_config.go
Normal file
31
livekit/pkg/metric/metric_config.go
Normal file
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
119
livekit/pkg/metric/metric_timestamper.go
Normal file
119
livekit/pkg/metric/metric_timestamper.go
Normal file
|
|
@ -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/mediatransportutil/pkg/latency"
|
||||
"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 *latency.OWDEstimator
|
||||
lastOWDEstimatorRunAt time.Time
|
||||
batchesSinceLastOWDEstimatorRun int
|
||||
}
|
||||
|
||||
func NewMetricTimestamper(params MetricTimestamperParams) *MetricTimestamper {
|
||||
return &MetricTimestamper{
|
||||
params: params,
|
||||
owdEstimator: latency.NewOWDEstimator(latency.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
|
||||
}
|
||||
225
livekit/pkg/metric/metrics_collector.go
Normal file
225
livekit/pkg/metric/metrics_collector.go
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
138
livekit/pkg/metric/metrics_reporter.go
Normal file
138
livekit/pkg/metric/metrics_reporter.go
Normal 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 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
livekit/pkg/routing/errors.go
Normal file
36
livekit/pkg/routing/errors.go
Normal 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")
|
||||
)
|
||||
318
livekit/pkg/routing/interfaces.go
Normal file
318
livekit/pkg/routing/interfaces.go
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
// 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/config"
|
||||
"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),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
nodeStatsConfig config.NodeStatsConfig,
|
||||
) Router {
|
||||
lr := NewLocalRouter(node, signalClient, roomManagerClient, nodeStatsConfig)
|
||||
|
||||
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
|
||||
AutoSubscribeDataTrack *bool
|
||||
Client *livekit.ClientInfo
|
||||
Grants *auth.ClaimGrants
|
||||
Region string
|
||||
AdaptiveStream bool
|
||||
ID livekit.ParticipantID
|
||||
SubscriberAllowPause *bool
|
||||
DisableICELite bool
|
||||
CreateRoom *livekit.CreateRoomRequest
|
||||
AddTrackRequests []*livekit.AddTrackRequest
|
||||
PublisherOffer *livekit.SessionDescription
|
||||
SyncState *livekit.SyncState
|
||||
UseSinglePeerConnection bool
|
||||
}
|
||||
|
||||
func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
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)
|
||||
logBoolPtr("AutoSubscribeDataTrack", pi.AutoSubscribeDataTrack)
|
||||
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))
|
||||
e.AddArray("AddTrackRequests", logger.ProtoSlice(pi.AddTrackRequests))
|
||||
e.AddObject("PublisherOffer", logger.Proto(pi.PublisherOffer))
|
||||
e.AddObject("SyncState", logger.Proto(pi.SyncState))
|
||||
logBoolPtr("UseSinglePeerConnection", &pi.UseSinglePeerConnection)
|
||||
return nil
|
||||
}
|
||||
|
||||
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),
|
||||
ConnectionId: string(connectionID),
|
||||
Reconnect: pi.Reconnect,
|
||||
ReconnectReason: pi.ReconnectReason,
|
||||
AutoSubscribe: pi.AutoSubscribe,
|
||||
Client: pi.Client,
|
||||
GrantsJson: string(claims),
|
||||
AdaptiveStream: pi.AdaptiveStream,
|
||||
ParticipantId: string(pi.ID),
|
||||
DisableIceLite: pi.DisableICELite,
|
||||
CreateRoom: pi.CreateRoom,
|
||||
AddTrackRequests: pi.AddTrackRequests,
|
||||
PublisherOffer: pi.PublisherOffer,
|
||||
SyncState: pi.SyncState,
|
||||
UseSinglePeerConnection: pi.UseSinglePeerConnection,
|
||||
}
|
||||
if pi.AutoSubscribeDataTrack != nil {
|
||||
autoSubscribeDataTrack := *pi.AutoSubscribeDataTrack
|
||||
ss.AutoSubscribeDataTrack = &autoSubscribeDataTrack
|
||||
}
|
||||
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,
|
||||
AddTrackRequests: ss.AddTrackRequests,
|
||||
PublisherOffer: ss.PublisherOffer,
|
||||
SyncState: ss.SyncState,
|
||||
UseSinglePeerConnection: ss.UseSinglePeerConnection,
|
||||
}
|
||||
if ss.AutoSubscribeDataTrack != nil {
|
||||
autoSubscribeDataTrack := *ss.AutoSubscribeDataTrack
|
||||
pi.AutoSubscribeDataTrack = &autoSubscribeDataTrack
|
||||
}
|
||||
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
|
||||
}
|
||||
173
livekit/pkg/routing/localrouter.go
Normal file
173
livekit/pkg/routing/localrouter.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// 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"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"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
|
||||
nodeStatsConfig config.NodeStatsConfig
|
||||
|
||||
// channels for each participant
|
||||
requestChannels map[string]*MessageChannel
|
||||
responseChannels map[string]*MessageChannel
|
||||
isStarted atomic.Bool
|
||||
}
|
||||
|
||||
func NewLocalRouter(
|
||||
currentNode LocalNode,
|
||||
signalClient SignalClient,
|
||||
roomManagerClient RoomManagerClient,
|
||||
nodeStatsConfig config.NodeStatsConfig,
|
||||
) *LocalRouter {
|
||||
return &LocalRouter{
|
||||
currentNode: currentNode,
|
||||
signalClient: signalClient,
|
||||
roomManagerClient: roomManagerClient,
|
||||
nodeStatsConfig: nodeStatsConfig,
|
||||
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
|
||||
}
|
||||
<-time.After(r.nodeStatsConfig.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
*/
|
||||
94
livekit/pkg/routing/messagechannel.go
Normal file
94
livekit/pkg/routing/messagechannel.go
Normal file
|
|
@ -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
|
||||
}
|
||||
50
livekit/pkg/routing/messagechannel_test.go
Normal file
50
livekit/pkg/routing/messagechannel_test.go
Normal file
|
|
@ -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 range 100 {
|
||||
_ = m.WriteMessage(&livekit.SignalRequest{})
|
||||
}
|
||||
}()
|
||||
_ = m.WriteMessage(&livekit.SignalRequest{})
|
||||
m.Close()
|
||||
_ = m.WriteMessage(&livekit.SignalRequest{})
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
158
livekit/pkg/routing/node.go
Normal file
158
livekit/pkg/routing/node.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// 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/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
nodeStats *NodeStats
|
||||
}
|
||||
|
||||
func NewLocalNode(conf *config.Config) (*LocalNodeImpl, error) {
|
||||
nodeID := guid.New(utils.NodePrefix)
|
||||
if conf != nil && conf.RTC.NodeIP == "" {
|
||||
return nil, ErrIPNotSet
|
||||
}
|
||||
nowUnix := time.Now().Unix()
|
||||
l := &LocalNodeImpl{
|
||||
node: &livekit.Node{
|
||||
Id: nodeID,
|
||||
NumCpus: uint32(runtime.NumCPU()),
|
||||
State: livekit.NodeState_SERVING,
|
||||
Stats: &livekit.NodeStats{
|
||||
StartedAt: nowUnix,
|
||||
UpdatedAt: nowUnix,
|
||||
},
|
||||
},
|
||||
}
|
||||
var nsc *config.NodeStatsConfig
|
||||
if conf != nil {
|
||||
l.node.Ip = conf.RTC.NodeIP
|
||||
l.node.Region = conf.Region
|
||||
|
||||
nsc = &conf.NodeStats
|
||||
}
|
||||
l.nodeStats = NewNodeStats(nsc, nowUnix)
|
||||
|
||||
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()
|
||||
|
||||
stats, err := l.nodeStats.UpdateAndGetNodeStats()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
l.node.Stats = stats
|
||||
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()
|
||||
}
|
||||
82
livekit/pkg/routing/nodestats.go
Normal file
82
livekit/pkg/routing/nodestats.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// 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"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
||||
)
|
||||
|
||||
type NodeStats struct {
|
||||
config config.NodeStatsConfig
|
||||
startedAt int64
|
||||
|
||||
lock sync.Mutex
|
||||
statsHistory []*livekit.NodeStats
|
||||
statsHistoryWritePtr int
|
||||
}
|
||||
|
||||
func NewNodeStats(conf *config.NodeStatsConfig, startedAt int64) *NodeStats {
|
||||
n := &NodeStats{
|
||||
startedAt: startedAt,
|
||||
}
|
||||
n.UpdateConfig(conf)
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *NodeStats) UpdateConfig(conf *config.NodeStatsConfig) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if conf == nil {
|
||||
conf = &config.DefaultNodeStatsConfig
|
||||
}
|
||||
n.config = *conf
|
||||
|
||||
// set up stats history to be able to measure different rate windows
|
||||
var maxInterval time.Duration
|
||||
for _, rateInterval := range conf.StatsRateMeasurementIntervals {
|
||||
if rateInterval > maxInterval {
|
||||
maxInterval = rateInterval
|
||||
}
|
||||
}
|
||||
n.statsHistory = make([]*livekit.NodeStats, (maxInterval+conf.StatsUpdateInterval-1)/conf.StatsUpdateInterval)
|
||||
n.statsHistoryWritePtr = 0
|
||||
}
|
||||
|
||||
func (n *NodeStats) UpdateAndGetNodeStats() (*livekit.NodeStats, error) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
stats, err := prometheus.GetNodeStats(
|
||||
n.startedAt,
|
||||
append(n.statsHistory[n.statsHistoryWritePtr:], n.statsHistory[0:n.statsHistoryWritePtr]...),
|
||||
n.config.StatsRateMeasurementIntervals,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Errorw("could not update node stats", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.statsHistory[n.statsHistoryWritePtr] = stats
|
||||
n.statsHistoryWritePtr = (n.statsHistoryWritePtr + 1) % len(n.statsHistory)
|
||||
return stats, nil
|
||||
}
|
||||
252
livekit/pkg/routing/redisrouter.go
Normal file
252
livekit/pkg/routing/redisrouter.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
// 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 (
|
||||
// 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(r.nodeStatsConfig.StatsUpdateInterval):
|
||||
r.kps.PublishPing(r.ctx, r.currentNode.NodeID(), &rpc.KeepalivePing{Timestamp: time.Now().Unix()})
|
||||
|
||||
delaySeconds := r.currentNode.SecondsSinceNodeStatsUpdate()
|
||||
if delaySeconds > r.nodeStatsConfig.StatsMaxDelay.Seconds() {
|
||||
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)) > r.nodeStatsConfig.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
livekit/pkg/routing/roommanager.go
Normal file
63
livekit/pkg/routing/roommanager.go
Normal 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()
|
||||
}
|
||||
265
livekit/pkg/routing/routingfakes/fake_message_sink.go
Normal file
265
livekit/pkg/routing/routingfakes/fake_message_sink.go
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
// 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()
|
||||
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)
|
||||
256
livekit/pkg/routing/routingfakes/fake_message_source.go
Normal file
256
livekit/pkg/routing/routingfakes/fake_message_source.go
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// 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()
|
||||
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)
|
||||
151
livekit/pkg/routing/routingfakes/fake_room_manager_client.go
Normal file
151
livekit/pkg/routing/routingfakes/fake_room_manager_client.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// 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()
|
||||
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)
|
||||
867
livekit/pkg/routing/routingfakes/fake_router.go
Normal file
867
livekit/pkg/routing/routingfakes/fake_router.go
Normal file
|
|
@ -0,0 +1,867 @@
|
|||
// 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()
|
||||
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)
|
||||
195
livekit/pkg/routing/routingfakes/fake_signal_client.go
Normal file
195
livekit/pkg/routing/routingfakes/fake_signal_client.go
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
// 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()
|
||||
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)
|
||||
34
livekit/pkg/routing/selector/any.go
Normal file
34
livekit/pkg/routing/selector/any.go
Normal 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.
|
||||
|
||||
package selector
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
// AnySelector selects any available node with no limitations
|
||||
type AnySelector struct {
|
||||
SortBy string
|
||||
Algorithm 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, s.Algorithm)
|
||||
}
|
||||
287
livekit/pkg/routing/selector/any_test.go
Normal file
287
livekit/pkg/routing/selector/any_test.go
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
func createTestNode(id string, cpuLoad float32, numRooms int32, numClients int32, state livekit.NodeState) *livekit.Node {
|
||||
return &livekit.Node{
|
||||
Id: id,
|
||||
State: state,
|
||||
Stats: &livekit.NodeStats{
|
||||
UpdatedAt: time.Now().Unix() - 1, // Recent update to be considered available
|
||||
CpuLoad: cpuLoad,
|
||||
NumRooms: numRooms,
|
||||
NumClients: numClients,
|
||||
NumCpus: 4,
|
||||
LoadAvgLast1Min: cpuLoad * 4, // Simulate system load
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnySelector_SelectNode_TwoChoice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sortBy string
|
||||
algorithm string
|
||||
nodes []*livekit.Node
|
||||
wantErr string
|
||||
expected string
|
||||
notExpected string
|
||||
}{
|
||||
{
|
||||
name: "successful selection with cpuload sorting",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.8, 5, 10, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.3, 2, 5, livekit.NodeState_SERVING),
|
||||
createTestNode("node3", 0.6, 3, 8, livekit.NodeState_SERVING),
|
||||
createTestNode("node4", 0.9, 6, 12, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "", // Not determinstic selection, so no specific expected node
|
||||
notExpected: "node4", // Node with highest load should not be selected
|
||||
},
|
||||
{
|
||||
name: "successful selection with rooms sorting",
|
||||
sortBy: "rooms",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 8, 15, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.4, 2, 5, livekit.NodeState_SERVING),
|
||||
createTestNode("node3", 0.6, 12, 20, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "", // Not determinstic selection, so no specific expected node
|
||||
notExpected: "node3", // Node with highest room count should not be selected
|
||||
},
|
||||
{
|
||||
name: "successful selection with clients sorting",
|
||||
sortBy: "clients",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 3, 25, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.4, 2, 5, livekit.NodeState_SERVING),
|
||||
createTestNode("node3", 0.6, 4, 30, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "", // Not determinstic selection, so no specific expected node
|
||||
notExpected: "node3", // Node with highest clients should not be selected
|
||||
},
|
||||
{
|
||||
name: "empty nodes list",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{},
|
||||
wantErr: "could not find any available nodes",
|
||||
},
|
||||
{
|
||||
name: "no available nodes - all unavailable",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
{
|
||||
Id: "node1",
|
||||
State: livekit.NodeState_SERVING,
|
||||
Stats: &livekit.NodeStats{
|
||||
UpdatedAt: time.Now().Unix() - 10, // Too old
|
||||
CpuLoad: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "could not find any available nodes",
|
||||
},
|
||||
{
|
||||
name: "no available nodes - not serving",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
{
|
||||
Id: "node1",
|
||||
State: livekit.NodeState_SHUTTING_DOWN,
|
||||
Stats: &livekit.NodeStats{
|
||||
UpdatedAt: time.Now().Unix() - 1,
|
||||
CpuLoad: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "could not find any available nodes",
|
||||
},
|
||||
{
|
||||
name: "single available node",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 3, 10, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "node1", // Should select the only available node
|
||||
notExpected: "", // No other nodes to compare against
|
||||
},
|
||||
{
|
||||
name: "two available nodes",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.8, 5, 15, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.3, 2, 5, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "node2", // Should select the node with lower load
|
||||
notExpected: "node1", // Should not select the node with higher load
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector := &AnySelector{
|
||||
SortBy: tt.sortBy,
|
||||
Algorithm: tt.algorithm,
|
||||
}
|
||||
|
||||
node, err := selector.SelectNode(tt.nodes)
|
||||
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
require.Nil(t, node)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, node)
|
||||
require.NotEmpty(t, node.Id)
|
||||
|
||||
// Verify the selected node is one of the available nodes
|
||||
found := false
|
||||
availableNodes := GetAvailableNodes(tt.nodes)
|
||||
for _, availableNode := range availableNodes {
|
||||
if availableNode.Id == node.Id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "Selected node should be one of the available nodes")
|
||||
|
||||
if tt.expected != "" {
|
||||
require.Equal(t, tt.expected, node.Id, "Selected node should match expected")
|
||||
}
|
||||
if tt.notExpected != "" {
|
||||
require.NotEqual(t, tt.notExpected, node.Id, "Selected node should not match not expected")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnySelector_SelectNode_TwoChoice_Probabilistic_Behavior(t *testing.T) {
|
||||
// Test that two-choice algorithm favors nodes with lower metrics
|
||||
// This test runs multiple iterations to increase confidence in the probabilistic behavior
|
||||
selector := &AnySelector{
|
||||
SortBy: "cpuload",
|
||||
Algorithm: "twochoice",
|
||||
}
|
||||
|
||||
// Create nodes where node2 has significantly lower CPU load
|
||||
nodes := []*livekit.Node{
|
||||
createTestNode("node1", 0.95, 10, 20, livekit.NodeState_SERVING), // Very high load
|
||||
createTestNode("node2", 0.1, 1, 2, livekit.NodeState_SERVING), // Low load
|
||||
createTestNode("node3", 0.5, 9, 18, livekit.NodeState_SERVING), // Medium load
|
||||
createTestNode("node4", 0.85, 8, 16, livekit.NodeState_SERVING), // High load
|
||||
}
|
||||
|
||||
// Run multiple selections and count how often the low-load node is selected
|
||||
iterations := 1000
|
||||
lowLoadSelections := 0
|
||||
higestLoadSelections := 0
|
||||
|
||||
for range iterations {
|
||||
node, err := selector.SelectNode(nodes)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, node)
|
||||
|
||||
if node.Id == "node2" {
|
||||
lowLoadSelections++
|
||||
}
|
||||
if node.Id == "node1" {
|
||||
higestLoadSelections++
|
||||
}
|
||||
}
|
||||
|
||||
// The low-load node should be selected more often than pure random (25%)
|
||||
// Due to the two-choice algorithm favoring the better node
|
||||
selectionRate := float64(lowLoadSelections) / float64(iterations)
|
||||
require.Greater(t, selectionRate, 0.4, "Two-choice algorithm should favor the low-load node more than random selection")
|
||||
require.Equal(t, higestLoadSelections, 0, "Two-choice algorithm should never favor the highest load node")
|
||||
}
|
||||
|
||||
func TestAnySelector_SelectNode_InvalidParameters(t *testing.T) {
|
||||
nodes := []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 3, 10, livekit.NodeState_SERVING),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sortBy string
|
||||
algorithm string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty sortBy",
|
||||
sortBy: "",
|
||||
algorithm: "twochoice",
|
||||
wantErr: "sort by option cannot be blank",
|
||||
},
|
||||
{
|
||||
name: "empty algorithm",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "",
|
||||
wantErr: "node selector algorithm option cannot be blank",
|
||||
},
|
||||
{
|
||||
name: "unknown sortBy",
|
||||
sortBy: "invalid",
|
||||
algorithm: "twochoice",
|
||||
wantErr: "unknown sort by option",
|
||||
},
|
||||
{
|
||||
name: "unknown algorithm",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "invalid",
|
||||
wantErr: "unknown node selector algorithm option",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector := &AnySelector{
|
||||
SortBy: tt.sortBy,
|
||||
Algorithm: tt.algorithm,
|
||||
}
|
||||
|
||||
node, err := selector.SelectNode(nodes)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
require.Nil(t, node)
|
||||
})
|
||||
}
|
||||
}
|
||||
55
livekit/pkg/routing/selector/cpuload.go
Normal file
55
livekit/pkg/routing/selector/cpuload.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// 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
|
||||
Algorithm 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, s.Algorithm)
|
||||
}
|
||||
51
livekit/pkg/routing/selector/cpuload_test.go
Normal file
51
livekit/pkg/routing/selector/cpuload_test.go
Normal file
|
|
@ -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", Algorithm: "lowest"}
|
||||
|
||||
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 range 5 {
|
||||
node, err := sel.SelectNode(nodes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if node != nodeLoadLow {
|
||||
t.Error("selected the wrong node")
|
||||
}
|
||||
}
|
||||
}
|
||||
27
livekit/pkg/routing/selector/errors.go
Normal file
27
livekit/pkg/routing/selector/errors.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// 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")
|
||||
ErrAlgorithmNotSet = errors.New("node selector algorithm option cannot be blank")
|
||||
ErrSortByUnknown = errors.New("unknown sort by option")
|
||||
ErrAlgorithmUnknown = errors.New("unknown node selector algorithm option")
|
||||
)
|
||||
66
livekit/pkg/routing/selector/interfaces.go
Normal file
66
livekit/pkg/routing/selector/interfaces.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// 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, conf.NodeSelector.Algorithm}, nil
|
||||
case "cpuload":
|
||||
return &CPULoadSelector{
|
||||
CPULoadLimit: conf.NodeSelector.CPULoadLimit,
|
||||
SortBy: conf.NodeSelector.SortBy,
|
||||
Algorithm: conf.NodeSelector.Algorithm,
|
||||
}, nil
|
||||
case "sysload":
|
||||
return &SystemLoadSelector{
|
||||
SysloadLimit: conf.NodeSelector.SysloadLimit,
|
||||
SortBy: conf.NodeSelector.SortBy,
|
||||
Algorithm: conf.NodeSelector.Algorithm,
|
||||
}, nil
|
||||
case "regionaware":
|
||||
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm)
|
||||
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, conf.NodeSelector.Algorithm}, nil
|
||||
default:
|
||||
return nil, ErrUnsupportedSelector
|
||||
}
|
||||
}
|
||||
127
livekit/pkg/routing/selector/regionaware.go
Normal file
127
livekit/pkg/routing/selector/regionaware.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// 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
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string, algorithm 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,
|
||||
Algorithm: algorithm,
|
||||
}
|
||||
|
||||
var currentRC *config.RegionConfig
|
||||
|
||||
for _, region := range regions {
|
||||
if region.Name == currentRegion {
|
||||
currentRC = ®ion
|
||||
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, s.Algorithm)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
166
livekit/pkg/routing/selector/regionaware_test.go
Normal file
166
livekit/pkg/routing/selector/regionaware_test.go
Normal 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 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"
|
||||
algorithm = "lowest"
|
||||
)
|
||||
|
||||
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, algorithm)
|
||||
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, algorithm)
|
||||
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, algorithm)
|
||||
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, algorithm)
|
||||
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, algorithm)
|
||||
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, algorithm)
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
64
livekit/pkg/routing/selector/sortby_test.go
Normal file
64
livekit/pkg/routing/selector/sortby_test.go
Normal file
|
|
@ -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, Algorithm: "lowest"}
|
||||
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
|
||||
|
||||
for range 5 {
|
||||
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{Algorithm: "lowest"}
|
||||
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)
|
||||
}
|
||||
}
|
||||
54
livekit/pkg/routing/selector/sysload.go
Normal file
54
livekit/pkg/routing/selector/sysload.go
Normal file
|
|
@ -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"
|
||||
)
|
||||
|
||||
// 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
|
||||
Algorithm 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, s.Algorithm)
|
||||
}
|
||||
114
livekit/pkg/routing/selector/sysload_test.go
Normal file
114
livekit/pkg/routing/selector/sysload_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// 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,
|
||||
Rates: []*livekit.NodeStatsRate{
|
||||
{
|
||||
BytesIn: 1000,
|
||||
BytesOut: 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,
|
||||
Rates: []*livekit.NodeStatsRate{
|
||||
{
|
||||
BytesIn: 5000,
|
||||
BytesOut: 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,
|
||||
Rates: []*livekit.NodeStatsRate{
|
||||
{
|
||||
BytesIn: 10000,
|
||||
BytesOut: 40000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestSystemLoadSelector_SelectNode(t *testing.T) {
|
||||
sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random", Algorithm: "lowest"}
|
||||
|
||||
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 range 5 {
|
||||
node, err := sel.SelectNode(nodes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if node != nodeLoadLow {
|
||||
t.Error("selected the wrong node")
|
||||
}
|
||||
}
|
||||
}
|
||||
178
livekit/pkg/routing/selector/utils.go
Normal file
178
livekit/pkg/routing/selector/utils.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// 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/rand/v2"
|
||||
"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
|
||||
}
|
||||
|
||||
rate := &livekit.NodeStatsRate{}
|
||||
if len(nodeStats.Rates) > 0 {
|
||||
rate = nodeStats.Rates[0]
|
||||
}
|
||||
if limitConfig.BytesPerSec > 0 && limitConfig.BytesPerSec <= rate.BytesIn+rate.BytesOut {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func SelectSortedNode(nodes []*livekit.Node, sortBy string, algorithm string) (*livekit.Node, error) {
|
||||
if sortBy == "" {
|
||||
return nil, ErrSortByNotSet
|
||||
}
|
||||
if algorithm == "" {
|
||||
return nil, ErrAlgorithmNotSet
|
||||
}
|
||||
|
||||
switch algorithm {
|
||||
case "lowest": // examine all nodes and select the lowest based on sort criteria
|
||||
return selectLowestSortedNode(nodes, sortBy)
|
||||
case "twochoice": // randomly select two nodes and return the lowest based on sort criteria "Power of Two Random Choices"
|
||||
return selectTwoChoiceSortedNode(nodes, sortBy)
|
||||
default:
|
||||
return nil, ErrAlgorithmUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func selectTwoChoiceSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
|
||||
if len(nodes) <= 2 {
|
||||
return selectLowestSortedNode(nodes, sortBy)
|
||||
}
|
||||
|
||||
// randomly select two nodes
|
||||
node1, node2, err := selectTwoRandomNodes(nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// compare the two nodes based on the sort criteria
|
||||
if node1 == nil || node2 == nil {
|
||||
return nil, ErrNoAvailableNodes
|
||||
}
|
||||
|
||||
selectedNode, err := selectLowestSortedNode([]*livekit.Node{node1, node2}, sortBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return selectedNode, nil
|
||||
}
|
||||
|
||||
func selectLowestSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
|
||||
// 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 {
|
||||
ratei := &livekit.NodeStatsRate{}
|
||||
if len(nodes[i].Stats.Rates) > 0 {
|
||||
ratei = nodes[i].Stats.Rates[0]
|
||||
}
|
||||
|
||||
ratej := &livekit.NodeStatsRate{}
|
||||
if len(nodes[j].Stats.Rates) > 0 {
|
||||
ratej = nodes[j].Stats.Rates[0]
|
||||
}
|
||||
return ratei.BytesIn+ratei.BytesOut < ratej.BytesIn+ratej.BytesOut
|
||||
})
|
||||
return nodes[0], nil
|
||||
default:
|
||||
return nil, ErrSortByUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func selectTwoRandomNodes(nodes []*livekit.Node) (*livekit.Node, *livekit.Node, error) {
|
||||
if len(nodes) < 2 {
|
||||
return nil, nil, ErrNoAvailableNodes
|
||||
}
|
||||
|
||||
shuffledIndices := rand.Perm(len(nodes))
|
||||
|
||||
return nodes[shuffledIndices[0]], nodes[shuffledIndices[1]], nil
|
||||
}
|
||||
46
livekit/pkg/routing/selector/utils_test.go
Normal file
46
livekit/pkg/routing/selector/utils_test.go
Normal file
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
385
livekit/pkg/routing/signal.go
Normal file
385
livekit/pkg/routing/signal.go
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
// 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/livekit-server/pkg/utils"
|
||||
"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) {
|
||||
client, 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: client,
|
||||
}, 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 := utils.GetLogger(ctx).WithValues(
|
||||
"room", roomName,
|
||||
"reqNodeID", nodeID,
|
||||
"participant", pi.Identity,
|
||||
"connID", connectionID,
|
||||
"participantInit", pi,
|
||||
"startSession", logger.Proto(ss),
|
||||
)
|
||||
|
||||
l.Debugw("starting signal connection")
|
||||
|
||||
stream, err := r.client.RelaySignal(ctx, nodeID)
|
||||
if err != nil {
|
||||
prometheus.RecordSignalRequestFailure()
|
||||
return
|
||||
}
|
||||
|
||||
err = stream.Send(&rpc.RelaySignalRequest{StartSession: ss})
|
||||
if err != nil {
|
||||
stream.Close(err)
|
||||
prometheus.RecordSignalRequestFailure()
|
||||
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,
|
||||
prometheus.RecordSignalResponseSuccess,
|
||||
prometheus.RecordSignalResponseFailure,
|
||||
)
|
||||
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,
|
||||
promSignalSuccess func(),
|
||||
promSignalFailure func(),
|
||||
) error {
|
||||
r := &signalMessageReader[SendType, RecvType]{
|
||||
reader: reader,
|
||||
config: config,
|
||||
}
|
||||
for msg := range stream.Channel() {
|
||||
res, err := r.Read(msg)
|
||||
if err != nil {
|
||||
promSignalFailure()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range res {
|
||||
if err = ch.WriteMessage(r); err != nil {
|
||||
promSignalFailure()
|
||||
return err
|
||||
}
|
||||
promSignalSuccess()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
142
livekit/pkg/rtc/clientinfo.go
Normal file
142
livekit/pkg/rtc/clientinfo.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// 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) isOBS() bool {
|
||||
return c.ClientInfo != nil && strings.Contains(c.ClientInfo.Browser, "OBS")
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportsAudioRED() bool {
|
||||
return !c.isFirefox() && !c.isSafari()
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportsPrflxOverRelay() 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) SupportsTrackSubscribedEvent() bool {
|
||||
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_RUST && c.ClientInfo.GetProtocol() < 10)
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportsRequestResponse() bool {
|
||||
return c.SupportsTrackSubscribedEvent()
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportsSctpZeroChecksum() 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 := range 3 {
|
||||
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
livekit/pkg/rtc/clientinfo_test.go
Normal file
59
livekit/pkg/rtc/clientinfo_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
207
livekit/pkg/rtc/config.go
Normal file
207
livekit/pkg/rtc/config.go
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// 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"
|
||||
act "github.com/livekit/livekit-server/pkg/sfu/rtpextension/abscapturetime"
|
||||
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
|
||||
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
frameMarkingURI = "urn:ietf:params:rtp-hdrext:framemarking"
|
||||
repairedRTPStreamIDURI = "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
|
||||
}
|
||||
|
||||
return &WebRTCConfig{
|
||||
WebRTCConfig: *webRTCConfig,
|
||||
Receiver: ReceiverConfig{
|
||||
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
|
||||
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
|
||||
},
|
||||
Publisher: getPublisherConfig(false),
|
||||
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdatePublisherConfig(consolidated bool) {
|
||||
c.Publisher = getPublisherConfig(consolidated)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdateSubscriberConfig(ccConf config.CongestionControlConfig) {
|
||||
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
|
||||
c.BufferFactory = factory
|
||||
c.SettingEngine.BufferFactory = factory.GetOrNew
|
||||
}
|
||||
|
||||
func getPublisherConfig(consolidated bool) DirectionConfig {
|
||||
if consolidated {
|
||||
return DirectionConfig{
|
||||
RTPHeaderExtension: RTPHeaderExtensionConfig{
|
||||
Audio: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.AudioLevelURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
Video: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.TransportCCURI,
|
||||
sdp.ABSSendTimeURI,
|
||||
frameMarkingURI,
|
||||
dd.ExtensionURI,
|
||||
repairedRTPStreamIDURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
},
|
||||
RTCPFeedback: RTCPFeedbackConfig{
|
||||
Audio: []webrtc.RTCPFeedback{
|
||||
{Type: webrtc.TypeRTCPFBNACK},
|
||||
},
|
||||
Video: []webrtc.RTCPFeedback{
|
||||
{Type: webrtc.TypeRTCPFBTransportCC},
|
||||
{Type: webrtc.TypeRTCPFBGoogREMB},
|
||||
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
|
||||
{Type: webrtc.TypeRTCPFBNACK},
|
||||
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return DirectionConfig{
|
||||
RTPHeaderExtension: RTPHeaderExtensionConfig{
|
||||
Audio: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.AudioLevelURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
Video: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.TransportCCURI,
|
||||
frameMarkingURI,
|
||||
dd.ExtensionURI,
|
||||
repairedRTPStreamIDURI,
|
||||
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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
99
livekit/pkg/rtc/datadowntrack.go
Normal file
99
livekit/pkg/rtc/datadowntrack.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// 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/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var _ types.DataDownTrack = (*DataDownTrack)(nil)
|
||||
var _ types.DataTrackSender = (*DataDownTrack)(nil)
|
||||
|
||||
type DataDownTrackParams struct {
|
||||
Logger logger.Logger
|
||||
SubscriberID livekit.ParticipantID
|
||||
PublishDataTrack types.DataTrack
|
||||
Handle uint16
|
||||
Transport types.DataTrackTransport
|
||||
}
|
||||
|
||||
type DataDownTrack struct {
|
||||
params DataDownTrackParams
|
||||
createdAt int64
|
||||
}
|
||||
|
||||
func NewDataDownTrack(params DataDownTrackParams) (*DataDownTrack, error) {
|
||||
d := &DataDownTrack{
|
||||
params: params,
|
||||
createdAt: time.Now().UnixNano(),
|
||||
}
|
||||
|
||||
if err := d.params.PublishDataTrack.AddDataDownTrack(d); err != nil {
|
||||
d.params.Logger.Warnw("could not add data down track", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.params.Logger.Infow("created data down track", "name", d.Name())
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) Close() {
|
||||
d.params.Logger.Infow("closing data down track", "name", d.Name())
|
||||
d.params.PublishDataTrack.DeleteDataDownTrack(d.SubscriberID())
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) Handle() uint16 {
|
||||
return d.params.Handle
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) PublishDataTrack() types.DataTrack {
|
||||
return d.params.PublishDataTrack
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) ID() livekit.TrackID {
|
||||
return d.params.PublishDataTrack.ID()
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) Name() string {
|
||||
return d.params.PublishDataTrack.Name()
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) SubscriberID() livekit.ParticipantID {
|
||||
// add `createdAt` to ensure repeated subscriptions from same subscriber to same publisher does not collide
|
||||
return livekit.ParticipantID(fmt.Sprintf("%s:%d", d.params.SubscriberID, d.createdAt))
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) WritePacket(data []byte, packet *datatrack.Packet, _arrivalTime int64) {
|
||||
forwardedPacket := *packet
|
||||
forwardedPacket.Handle = d.params.Handle
|
||||
buf, err := forwardedPacket.Marshal()
|
||||
if err != nil {
|
||||
d.params.Logger.Warnw("could not marshal data track message", err)
|
||||
return
|
||||
}
|
||||
if err := d.params.Transport.SendDataTrackMessage(buf); err != nil {
|
||||
d.params.Logger.Warnw("could not send data track message", err, "handle", d.params.Handle)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) UpdateSubscriptionOptions(subscriptionOptions *livekit.DataTrackSubscriptionOptions) {
|
||||
// DT-TODO
|
||||
}
|
||||
162
livekit/pkg/rtc/datatrack.go
Normal file
162
livekit/pkg/rtc/datatrack.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// 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/frostbyte73/core"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
sfuutils "github.com/livekit/livekit-server/pkg/sfu/utils"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
errReceiverClosed = errors.New("datatrack is closed")
|
||||
)
|
||||
|
||||
var _ types.DataTrack = (*DataTrack)(nil)
|
||||
|
||||
type DataTrackParams struct {
|
||||
Logger logger.Logger
|
||||
ParticipantID func() livekit.ParticipantID
|
||||
ParticipantIdentity livekit.ParticipantIdentity
|
||||
}
|
||||
|
||||
type DataTrack struct {
|
||||
params DataTrackParams
|
||||
|
||||
lock sync.Mutex
|
||||
dti *livekit.DataTrackInfo
|
||||
subscribedTracks map[livekit.ParticipantID]types.DataDownTrack
|
||||
|
||||
downTrackSpreader *sfuutils.DownTrackSpreader[types.DataTrackSender]
|
||||
|
||||
closed core.Fuse
|
||||
}
|
||||
|
||||
func NewDataTrack(params DataTrackParams, dti *livekit.DataTrackInfo) *DataTrack {
|
||||
d := &DataTrack{
|
||||
params: params,
|
||||
dti: dti,
|
||||
subscribedTracks: make(map[livekit.ParticipantID]types.DataDownTrack),
|
||||
downTrackSpreader: sfuutils.NewDownTrackSpreader[types.DataTrackSender](sfuutils.DownTrackSpreaderParams{
|
||||
Threshold: 20,
|
||||
Logger: params.Logger,
|
||||
}),
|
||||
}
|
||||
d.params.Logger.Infow("created data track", "name", d.Name())
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DataTrack) Close() {
|
||||
d.params.Logger.Infow("closing data track", "name", d.Name())
|
||||
d.closed.Break()
|
||||
}
|
||||
|
||||
func (d *DataTrack) PublisherID() livekit.ParticipantID {
|
||||
return d.params.ParticipantID()
|
||||
}
|
||||
|
||||
func (d *DataTrack) PublisherIdentity() livekit.ParticipantIdentity {
|
||||
return d.params.ParticipantIdentity
|
||||
}
|
||||
|
||||
func (d *DataTrack) ToProto() *livekit.DataTrackInfo {
|
||||
return utils.CloneProto(d.dti)
|
||||
}
|
||||
|
||||
func (d *DataTrack) PubHandle() uint16 {
|
||||
return uint16(d.dti.PubHandle)
|
||||
}
|
||||
|
||||
func (d *DataTrack) ID() livekit.TrackID {
|
||||
return livekit.TrackID(d.dti.Sid)
|
||||
}
|
||||
|
||||
func (d *DataTrack) Name() string {
|
||||
return d.dti.Name
|
||||
}
|
||||
|
||||
func (d *DataTrack) AddSubscriber(sub types.LocalParticipant) (types.DataDownTrack, error) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if _, ok := d.subscribedTracks[sub.ID()]; ok {
|
||||
return nil, errAlreadySubscribed
|
||||
}
|
||||
|
||||
dataDownTrack, err := NewDataDownTrack(DataDownTrackParams{
|
||||
Logger: sub.GetLogger().WithValues("trackID", d.ID()),
|
||||
SubscriberID: sub.ID(),
|
||||
PublishDataTrack: d,
|
||||
Handle: sub.GetNextSubscribedDataTrackHandle(),
|
||||
Transport: sub.GetDataTrackTransport(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.subscribedTracks[sub.ID()] = dataDownTrack
|
||||
return dataDownTrack, nil
|
||||
}
|
||||
|
||||
func (d *DataTrack) RemoveSubscriber(subID livekit.ParticipantID) {
|
||||
d.lock.Lock()
|
||||
dataDownTrack, ok := d.subscribedTracks[subID]
|
||||
delete(d.subscribedTracks, subID)
|
||||
d.lock.Unlock()
|
||||
|
||||
if ok {
|
||||
dataDownTrack.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataTrack) IsSubscriber(subID livekit.ParticipantID) bool {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
_, ok := d.subscribedTracks[subID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *DataTrack) AddDataDownTrack(dts types.DataTrackSender) error {
|
||||
if d.closed.IsBroken() {
|
||||
return errReceiverClosed
|
||||
}
|
||||
|
||||
if d.downTrackSpreader.HasDownTrack(dts.SubscriberID()) {
|
||||
d.params.Logger.Infow("subscriberID already exists, replacing data downtrack", "subscriberID", dts.SubscriberID())
|
||||
}
|
||||
|
||||
d.downTrackSpreader.Store(dts)
|
||||
d.params.Logger.Infow("data downtrack added", "subscriberID", dts.SubscriberID())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataTrack) DeleteDataDownTrack(subscriberID livekit.ParticipantID) {
|
||||
d.downTrackSpreader.Free(subscriberID)
|
||||
d.params.Logger.Infow("data downtrack deleted", "subscriberID", subscriberID)
|
||||
}
|
||||
|
||||
func (d *DataTrack) HandlePacket(data []byte, packet *datatrack.Packet, arrivalTime int64) {
|
||||
d.downTrackSpreader.Broadcast(func(dts types.DataTrackSender) {
|
||||
dts.WritePacket(data, packet, arrivalTime)
|
||||
})
|
||||
}
|
||||
59
livekit/pkg/rtc/datatrack/extension_participant_sid.go
Normal file
59
livekit/pkg/rtc/datatrack/extension_participant_sid.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// 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 datatrack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type ExtensionParticipantSid struct {
|
||||
participantID livekit.ParticipantID
|
||||
}
|
||||
|
||||
func NewExtensionParticipantSid(participantID livekit.ParticipantID) (*ExtensionParticipantSid, error) {
|
||||
if len(participantID) >= 65536 {
|
||||
return nil, errors.New("participantID too long")
|
||||
}
|
||||
|
||||
return &ExtensionParticipantSid{participantID}, nil
|
||||
}
|
||||
|
||||
func (e *ExtensionParticipantSid) ParticipantID() livekit.ParticipantID {
|
||||
return e.participantID
|
||||
}
|
||||
|
||||
func (e *ExtensionParticipantSid) Marshal() (Extension, error) {
|
||||
data := make([]byte, len(e.participantID))
|
||||
copy(data, e.participantID)
|
||||
return Extension{
|
||||
id: uint16(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID),
|
||||
data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *ExtensionParticipantSid) Unmarshal(ext Extension) error {
|
||||
if ext.id != uint16(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID) {
|
||||
return errors.New("invalid extension ID")
|
||||
}
|
||||
|
||||
if len(ext.data) == 0 {
|
||||
return errors.New("empty extension data")
|
||||
}
|
||||
|
||||
e.participantID = livekit.ParticipantID(ext.data)
|
||||
return nil
|
||||
}
|
||||
46
livekit/pkg/rtc/datatrack/extension_participant_sid_test.go
Normal file
46
livekit/pkg/rtc/datatrack/extension_participant_sid_test.go
Normal file
|
|
@ -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 datatrack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtensionParticipantSid(t *testing.T) {
|
||||
longTestParticipantID := livekit.ParticipantID(make([]byte, 65536))
|
||||
extParticipantSid, err := NewExtensionParticipantSid(longTestParticipantID)
|
||||
require.Error(t, err)
|
||||
|
||||
testParticipantID := livekit.ParticipantID("test")
|
||||
extParticipantSid, err = NewExtensionParticipantSid(testParticipantID)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedExt := Extension{
|
||||
id: uint16(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID),
|
||||
data: []byte{'t', 'e', 's', 't'},
|
||||
}
|
||||
ext, err := extParticipantSid.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedExt, ext)
|
||||
|
||||
var unmarshaled ExtensionParticipantSid
|
||||
err = unmarshaled.Unmarshal(ext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testParticipantID, unmarshaled.ParticipantID())
|
||||
}
|
||||
269
livekit/pkg/rtc/datatrack/packet.go
Normal file
269
livekit/pkg/rtc/datatrack/packet.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// 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 datatrack
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
errHeaderSizeInsufficient = errors.New("data track packet header size insufficient")
|
||||
errBufferSizeInsufficient = errors.New("data track packet buffer size insufficient")
|
||||
errExtensionSizeInsufficient = errors.New("data track packet extension size insufficient")
|
||||
errExtensionNotFound = errors.New("data track packet extension not found")
|
||||
)
|
||||
|
||||
const (
|
||||
headerLength = 12
|
||||
|
||||
versionShift = 5
|
||||
versionMask = (1 << 3) - 1
|
||||
|
||||
startOfFrameShift = 4
|
||||
startOfFrameMask = (1 << 1) - 1
|
||||
|
||||
finalOfFrameShift = 3
|
||||
finalOfFrameMask = (1 << 1) - 1
|
||||
|
||||
extensionsShift = 2
|
||||
extensionsMask = (1 << 1) - 1
|
||||
|
||||
handleOffset = 2
|
||||
handleLength = 2
|
||||
|
||||
seqNumOffset = 4
|
||||
seqNumLength = 2
|
||||
|
||||
frameNumOffset = 6
|
||||
frameNumLength = 2
|
||||
|
||||
timestampOffset = 8
|
||||
timestampLength = 4
|
||||
|
||||
extensionsSizeOffset = headerLength
|
||||
extensionsSizeLength = 2
|
||||
|
||||
extensionIDLength = 2
|
||||
extensionSizeLength = 2
|
||||
)
|
||||
|
||||
type Extension struct {
|
||||
id uint16
|
||||
data []byte
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Version uint8
|
||||
IsStartOfFrame bool
|
||||
IsFinalOfFrame bool
|
||||
HasExtensions bool
|
||||
Handle uint16
|
||||
SequenceNumber uint16
|
||||
FrameNumber uint16
|
||||
Timestamp uint32
|
||||
ExtensionsSize uint16
|
||||
Extensions []Extension
|
||||
}
|
||||
|
||||
/*
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* 0 1 2 3
|
||||
┆* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* |V |F|L|X| reserved | handle |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* | sequence number | frame number |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* | timestamp |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|* Extensions Size if X=1 | Extensions... |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Each extension
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* 0 1 2 3
|
||||
┆* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* | Extension ID | Extension size |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|* Extension payload (padded to 4 byte boundary) |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
|
||||
func (h *Header) Unmarshal(buf []byte) (int, error) {
|
||||
if len(buf) < headerLength {
|
||||
return 0, fmt.Errorf("%w: %d < %d", errHeaderSizeInsufficient, len(buf), headerLength)
|
||||
}
|
||||
|
||||
hdrSize := headerLength
|
||||
h.Version = buf[0] >> versionShift & versionMask
|
||||
h.IsStartOfFrame = (buf[0] >> startOfFrameShift & startOfFrameMask) > 0
|
||||
h.IsFinalOfFrame = (buf[0] >> finalOfFrameShift & finalOfFrameMask) > 0
|
||||
h.HasExtensions = (buf[0] >> extensionsShift & extensionsMask) > 0
|
||||
|
||||
h.Handle = binary.BigEndian.Uint16(buf[handleOffset : handleOffset+handleLength])
|
||||
h.SequenceNumber = binary.BigEndian.Uint16(buf[seqNumOffset : seqNumOffset+seqNumLength])
|
||||
h.FrameNumber = binary.BigEndian.Uint16(buf[frameNumOffset : frameNumOffset+frameNumLength])
|
||||
h.Timestamp = binary.BigEndian.Uint32(buf[timestampOffset : timestampOffset+timestampLength])
|
||||
|
||||
if h.HasExtensions {
|
||||
h.ExtensionsSize = (binary.BigEndian.Uint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength]) + 1) * 4
|
||||
hdrSize += extensionsSizeLength
|
||||
|
||||
remainingSize := int(h.ExtensionsSize)
|
||||
idx := extensionsSizeOffset + extensionsSizeLength
|
||||
for remainingSize != 0 {
|
||||
if len(buf[idx:]) < 4 || remainingSize < 4 {
|
||||
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), 4)
|
||||
}
|
||||
|
||||
id := binary.BigEndian.Uint16(buf[idx : idx+2])
|
||||
size := int(binary.BigEndian.Uint16(buf[idx+2 : idx+4]))
|
||||
remainingSize -= 4
|
||||
idx += 4
|
||||
hdrSize += 4
|
||||
|
||||
if len(buf[idx:]) < size || remainingSize < size {
|
||||
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), size)
|
||||
}
|
||||
h.Extensions = append(h.Extensions, Extension{id: id, data: buf[idx : idx+size]})
|
||||
|
||||
size = ((size + 3) / 4) * 4
|
||||
remainingSize -= size
|
||||
idx += size
|
||||
hdrSize += size
|
||||
}
|
||||
}
|
||||
|
||||
return hdrSize, nil
|
||||
}
|
||||
|
||||
func (h *Header) MarshalSize() int {
|
||||
size := headerLength
|
||||
if h.HasExtensions {
|
||||
size += 2 // extensions size field
|
||||
for _, ext := range h.Extensions {
|
||||
size += ((len(ext.data)+3)/4)*4 + 2 /* extension ID field */ + 2 /* extension length field */
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (h *Header) MarshalTo(buf []byte) (int, error) {
|
||||
if len(buf) < headerLength {
|
||||
return 0, fmt.Errorf("%w: %d < %d", errHeaderSizeInsufficient, len(buf), headerLength)
|
||||
}
|
||||
|
||||
hdrSize := headerLength
|
||||
buf[0] = h.Version << versionShift
|
||||
if h.IsStartOfFrame {
|
||||
buf[0] |= (1 << startOfFrameShift)
|
||||
}
|
||||
if h.IsFinalOfFrame {
|
||||
buf[0] |= (1 << finalOfFrameShift)
|
||||
}
|
||||
if h.HasExtensions {
|
||||
buf[0] |= (1 << extensionsShift)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(buf[handleOffset:handleOffset+handleLength], h.Handle)
|
||||
binary.BigEndian.PutUint16(buf[seqNumOffset:seqNumOffset+seqNumLength], h.SequenceNumber)
|
||||
binary.BigEndian.PutUint16(buf[frameNumOffset:frameNumOffset+frameNumLength], h.FrameNumber)
|
||||
binary.BigEndian.PutUint32(buf[timestampOffset:timestampOffset+timestampLength], h.Timestamp)
|
||||
|
||||
if h.HasExtensions {
|
||||
binary.BigEndian.PutUint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength], (h.ExtensionsSize/4)-1)
|
||||
hdrSize += extensionsSizeLength
|
||||
|
||||
idx := extensionsSizeOffset + extensionsSizeLength
|
||||
for _, ext := range h.Extensions {
|
||||
binary.BigEndian.PutUint16(buf[idx:idx+extensionIDLength], ext.id)
|
||||
binary.BigEndian.PutUint16(buf[idx+extensionIDLength:idx+extensionIDLength+extensionSizeLength], uint16(len(ext.data)))
|
||||
copy(buf[idx+extensionIDLength+extensionSizeLength:], ext.data)
|
||||
|
||||
idx += ((len(ext.data)+3)/4)*4 + 2 /* extension ID field */ + 2 /* extension length field */
|
||||
hdrSize += ((len(ext.data)+3)/4)*4 + 2 /* extension ID field */ + 2 /* extension length field */
|
||||
}
|
||||
}
|
||||
|
||||
return hdrSize, nil
|
||||
}
|
||||
|
||||
func (h *Header) AddExtension(ext Extension) {
|
||||
for i, existingExt := range h.Extensions {
|
||||
if existingExt.id == ext.id {
|
||||
h.ExtensionsSize -= uint16((len(existingExt.data)+3)/4*4 + 2 /* extension ID field */ + 2 /* extension length field */)
|
||||
h.Extensions[i].data = ext.data
|
||||
h.ExtensionsSize += uint16((len(h.Extensions[i].data)+3)/4*4 + 2 /* extension ID field */ + 2 /* extension length field */)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.Extensions = append(h.Extensions, ext)
|
||||
h.ExtensionsSize += uint16((len(ext.data)+3)/4*4 + 2 /* extension ID field */ + 2 /* extension length field */)
|
||||
h.HasExtensions = true
|
||||
}
|
||||
|
||||
func (h *Header) GetExtension(id uint16) (Extension, error) {
|
||||
for _, ext := range h.Extensions {
|
||||
if ext.id == id {
|
||||
return ext, nil
|
||||
}
|
||||
}
|
||||
return Extension{}, fmt.Errorf("%w, id: %d", errExtensionNotFound, id)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
type Packet struct {
|
||||
Header
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func (p *Packet) Unmarshal(buf []byte) error {
|
||||
hdrSize, err := p.Header.Unmarshal(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Payload = buf[hdrSize:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) Marshal() ([]byte, error) {
|
||||
buf := make([]byte, p.Header.MarshalSize()+len(p.Payload))
|
||||
if err := p.MarshalTo(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (p *Packet) MarshalTo(buf []byte) error {
|
||||
size := p.Header.MarshalSize() + len(p.Payload)
|
||||
if len(buf) < size {
|
||||
return fmt.Errorf("%w: %d < %d", errBufferSizeInsufficient, len(buf), size)
|
||||
}
|
||||
|
||||
hdrSize, err := p.Header.MarshalTo(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copy(buf[hdrSize:], p.Payload)
|
||||
return nil
|
||||
}
|
||||
265
livekit/pkg/rtc/datatrack/packet_test.go
Normal file
265
livekit/pkg/rtc/datatrack/packet_test.go
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
// 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 datatrack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPacket(t *testing.T) {
|
||||
t.Run("without extension", func(t *testing.T) {
|
||||
payload := make([]byte, 6)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: true,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x18, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
0xfb, 0xfa,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
})
|
||||
|
||||
t.Run("with extension", func(t *testing.T) {
|
||||
payload := make([]byte, 4)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: false,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x04, 0x00, 0x01,
|
||||
0x00, 0x10, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70,
|
||||
0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61,
|
||||
0x6e, 0x74, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
|
||||
ext, err := unmarshaled.GetExtension(uint16(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var extParticipantSid ExtensionParticipantSid
|
||||
require.NoError(t, extParticipantSid.Unmarshal(ext))
|
||||
require.Equal(t, livekit.ParticipantID("test_participant"), extParticipantSid.ParticipantID())
|
||||
})
|
||||
|
||||
t.Run("with extension padding", func(t *testing.T) {
|
||||
payload := make([]byte, 4)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: false,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x00, 0x01,
|
||||
0x00, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63,
|
||||
0x69, 0x70, 0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe,
|
||||
0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
|
||||
ext, err := unmarshaled.GetExtension(uint16(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var extParticipantSid ExtensionParticipantSid
|
||||
require.NoError(t, extParticipantSid.Unmarshal(ext))
|
||||
require.Equal(t, livekit.ParticipantID("participant"), extParticipantSid.ParticipantID())
|
||||
})
|
||||
|
||||
t.Run("replace extension", func(t *testing.T) {
|
||||
payload := make([]byte, 4)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: false,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x00, 0x01,
|
||||
0x00, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63,
|
||||
0x69, 0x70, 0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe,
|
||||
0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
// replace existing extension ID and ensure that marshalled packet is updated
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err = packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x04, 0x00, 0x01,
|
||||
0x00, 0x10, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70,
|
||||
0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61,
|
||||
0x6e, 0x74, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
|
||||
ext, err := unmarshaled.GetExtension(uint16(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var extParticipantSid ExtensionParticipantSid
|
||||
require.NoError(t, extParticipantSid.Unmarshal(ext))
|
||||
require.Equal(t, livekit.ParticipantID("test_participant"), extParticipantSid.ParticipantID())
|
||||
})
|
||||
|
||||
t.Run("bad pcaket", func(t *testing.T) {
|
||||
var unmarshaled Packet
|
||||
// extensions size too small
|
||||
badPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x02, 0x00, 0x01,
|
||||
0x00, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63,
|
||||
0x69, 0x70, 0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe,
|
||||
0xfd, 0xfc,
|
||||
}
|
||||
err := unmarshaled.Unmarshal(badPacket)
|
||||
require.Error(t, err)
|
||||
|
||||
// get an invalid extension id
|
||||
badPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x00, 0x02,
|
||||
0x00, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63,
|
||||
0x69, 0x70, 0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe,
|
||||
0xfd, 0xfc,
|
||||
}
|
||||
err = unmarshaled.Unmarshal(badPacket)
|
||||
require.NoError(t, err)
|
||||
_, err = unmarshaled.GetExtension(uint16(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.Error(t, err)
|
||||
|
||||
// extension payload size bigger than payload
|
||||
badPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x00, 0x01,
|
||||
0x00, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63,
|
||||
0x69, 0x70, 0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe,
|
||||
0xfd, 0xfc,
|
||||
}
|
||||
err = unmarshaled.Unmarshal(badPacket)
|
||||
require.Error(t, err)
|
||||
|
||||
// extension payload size smaller than payload
|
||||
badPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x00, 0x01,
|
||||
0x00, 0x07, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63,
|
||||
0x69, 0x70, 0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe,
|
||||
0xfd, 0xfc,
|
||||
}
|
||||
err = unmarshaled.Unmarshal(badPacket)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
74
livekit/pkg/rtc/datatrack/testutils.go
Normal file
74
livekit/pkg/rtc/datatrack/testutils.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// 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 datatrack
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateRawDataPackets(handle uint16, seqNum uint16, frameNum uint16, numFrames int, frameSize int, frameDuration time.Duration) [][]byte {
|
||||
if seqNum == 0 {
|
||||
seqNum = uint16(rand.Intn(256) + 1)
|
||||
}
|
||||
if frameNum == 0 {
|
||||
frameNum = uint16(rand.Intn(256) + 1)
|
||||
}
|
||||
timestamp := uint32(rand.Intn(1024))
|
||||
|
||||
packetsPerFrame := (frameSize + 255) / 256 // using 256 bytes of payload per packet
|
||||
if packetsPerFrame == 0 {
|
||||
return nil
|
||||
}
|
||||
numPackets := packetsPerFrame * numFrames
|
||||
rawPackets := make([][]byte, 0, numPackets)
|
||||
for range numFrames {
|
||||
remainingSize := frameSize
|
||||
for packetIdx := range packetsPerFrame {
|
||||
payloadSize := min(remainingSize, 256)
|
||||
payload := make([]byte, payloadSize)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: packetIdx == 0,
|
||||
IsFinalOfFrame: packetIdx == packetsPerFrame-1,
|
||||
Handle: handle,
|
||||
SequenceNumber: seqNum,
|
||||
FrameNumber: frameNum,
|
||||
Timestamp: timestamp,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
if err == nil {
|
||||
rawPackets = append(rawPackets, rawPacket)
|
||||
}
|
||||
seqNum++
|
||||
remainingSize -= payloadSize
|
||||
}
|
||||
frameNum++
|
||||
timestamp += uint32(90000 * frameDuration.Seconds())
|
||||
}
|
||||
|
||||
return rawPackets
|
||||
}
|
||||
516
livekit/pkg/rtc/dynacast/dynacastmanager_test.go
Normal file
516
livekit/pkg/rtc/dynacast/dynacastmanager_test.go
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
type testDynacastManagerListener struct {
|
||||
onSubscribedMaxQualityChange func(subscribedQualties []*livekit.SubscribedCodec)
|
||||
onSubscribedAudioCodecChange func(subscribedCodecs []*livekit.SubscribedAudioCodec)
|
||||
}
|
||||
|
||||
func (t *testDynacastManagerListener) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
_maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
t.onSubscribedMaxQualityChange(subscribedQualities)
|
||||
}
|
||||
|
||||
func (t *testDynacastManagerListener) OnDynacastSubscribedAudioCodecChange(
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
t.onSubscribedAudioCodecChange(codecs)
|
||||
}
|
||||
|
||||
func TestSubscribedMaxQuality(t *testing.T) {
|
||||
t.Run("subscribers muted", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
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) {
|
||||
lock := sync.RWMutex{}
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.(*dynacastManagerVideo).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) {
|
||||
t.Run("codec regression video", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("codec regression audio", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerAudio(DynacastManagerAudioParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedAudioCodecChange: func(subscribedCodecs []*livekit.SubscribedAudioCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedCodecs = subscribedCodecs
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.NotifySubscription("s1", mime.MimeTypeRED, true)
|
||||
|
||||
expectedSubscribedCodecs := []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
dm.HandleCodecRegression(mime.MimeTypeRED, mime.MimeTypeOpus)
|
||||
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// RED disable as subscriber or subscriber node should be ignored as it has been regressed
|
||||
dm.NotifySubscription("s1", mime.MimeTypeRED, false)
|
||||
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
|
||||
{Codec: mime.MimeTypeRED.String(), Enabled: false},
|
||||
})
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// `s1` unsubscription should turn off `opus`
|
||||
dm.NotifySubscription("s1", mime.MimeTypeOpus, false)
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// a node subscription should turn `opus` back on
|
||||
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
})
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 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
|
||||
}
|
||||
|
||||
func subscribedAudioCodecsAsString(c1 []*livekit.SubscribedAudioCodec) 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
|
||||
}
|
||||
198
livekit/pkg/rtc/dynacast/dynacastmanageraudio.go
Normal file
198
livekit/pkg/rtc/dynacast/dynacastmanageraudio.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// 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 (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
)
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerAudio)(nil)
|
||||
var _ dynacastQualityListener = (*dynacastManagerAudio)(nil)
|
||||
|
||||
type DynacastManagerAudioParams struct {
|
||||
Listener DynacastManagerListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type dynacastManagerAudio struct {
|
||||
params DynacastManagerAudioParams
|
||||
|
||||
subscribedCodecs map[mime.MimeType]bool
|
||||
committedSubscribedCodecs map[mime.MimeType]bool
|
||||
|
||||
isClosed bool
|
||||
|
||||
*dynacastManagerBase
|
||||
}
|
||||
|
||||
func NewDynacastManagerAudio(params DynacastManagerAudioParams) DynacastManager {
|
||||
if params.Logger == nil {
|
||||
params.Logger = logger.GetLogger()
|
||||
}
|
||||
d := &dynacastManagerAudio{
|
||||
params: params,
|
||||
subscribedCodecs: make(map[mime.MimeType]bool),
|
||||
committedSubscribedCodecs: make(map[mime.MimeType]bool),
|
||||
}
|
||||
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
|
||||
Logger: params.Logger,
|
||||
OpsQueueDepth: 4,
|
||||
OnRestart: func() {
|
||||
d.committedSubscribedCodecs = make(map[mime.MimeType]bool)
|
||||
},
|
||||
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
|
||||
dq := newDynacastQualityAudio(dynacastQualityAudioParams{
|
||||
MimeType: mimeType,
|
||||
Listener: d,
|
||||
Logger: d.params.Logger,
|
||||
})
|
||||
return dq
|
||||
},
|
||||
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
|
||||
d.subscribedCodecs[fromMime] = false
|
||||
|
||||
// if the new codec is not added, notify the publisher to start publishing
|
||||
if _, ok := d.subscribedCodecs[toMime]; !ok {
|
||||
d.subscribedCodecs[toMime] = true
|
||||
}
|
||||
},
|
||||
OnUpdateNeeded: d.update,
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
// 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 codec is disabled,
|
||||
// i.e. indicating not pulling track any more.
|
||||
func (d *dynacastManagerAudio) ForceEnable(enabled bool) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
for mime := range d.committedSubscribedCodecs {
|
||||
d.committedSubscribedCodecs[mime] = enabled
|
||||
}
|
||||
|
||||
d.enqueueSubscribedChange()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
dq := d.getOrCreateDynacastQuality(mime)
|
||||
if dq != nil {
|
||||
dq.NotifySubscription(subscriberID, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
for _, codec := range codecs {
|
||||
dq := d.getOrCreateDynacastQuality(mime.NormalizeMimeType(codec.Codec))
|
||||
if dq != nil {
|
||||
dq.NotifySubscriptionNode(nodeID, codec.Enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) OnUpdateAudioCodecForMime(mime mime.MimeType, enabled bool) {
|
||||
d.lock.Lock()
|
||||
if _, ok := d.regressedCodec[mime]; !ok {
|
||||
d.subscribedCodecs[mime] = enabled
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.update(false)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) update(force bool) {
|
||||
d.lock.Lock()
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"processing subscribed codec change",
|
||||
"force", force,
|
||||
"committedSubscribedCodecs", d.committedSubscribedCodecs,
|
||||
"subscribedCodecs", d.subscribedCodecs,
|
||||
)
|
||||
|
||||
if len(d.subscribedCodecs) == 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.subscribedCodecs) != len(d.committedSubscribedCodecs)
|
||||
if !changed {
|
||||
for mime, enabled := range d.subscribedCodecs {
|
||||
if ce, ok := d.committedSubscribedCodecs[mime]; ok {
|
||||
if ce != enabled {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !force && !changed {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"committing subscribed codec change",
|
||||
"force", force,
|
||||
"committedSubscribedCoecs", d.committedSubscribedCodecs,
|
||||
"subscribedcodecs", d.subscribedCodecs,
|
||||
)
|
||||
|
||||
// commit change
|
||||
d.committedSubscribedCodecs = make(map[mime.MimeType]bool, len(d.subscribedCodecs))
|
||||
for mime, enabled := range d.subscribedCodecs {
|
||||
d.committedSubscribedCodecs[mime] = enabled
|
||||
}
|
||||
|
||||
d.enqueueSubscribedChange()
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) enqueueSubscribedChange() {
|
||||
if d.isClosed || d.params.Listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
subscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0, len(d.committedSubscribedCodecs))
|
||||
for mime, enabled := range d.committedSubscribedCodecs {
|
||||
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedAudioCodec{
|
||||
Codec: mime.String(),
|
||||
Enabled: enabled,
|
||||
})
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"subscribedAudioCodecChange",
|
||||
"subscribedCodecs", logger.ProtoSlice(subscribedCodecs),
|
||||
)
|
||||
d.notifyOpsQueue.Enqueue(func() {
|
||||
d.params.Listener.OnDynacastSubscribedAudioCodecChange(subscribedCodecs)
|
||||
})
|
||||
}
|
||||
165
livekit/pkg/rtc/dynacast/dynacastmanagerbase.go
Normal file
165
livekit/pkg/rtc/dynacast/dynacastmanagerbase.go
Normal file
|
|
@ -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 dynacast
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
)
|
||||
|
||||
type dynacastManagerBaseParams struct {
|
||||
Logger logger.Logger
|
||||
OpsQueueDepth uint
|
||||
OnRestart func()
|
||||
OnDynacastQualityCreate func(mimeType mime.MimeType) dynacastQuality
|
||||
OnRegressCodec func(fromMime, toMime mime.MimeType)
|
||||
OnUpdateNeeded func(force bool)
|
||||
}
|
||||
|
||||
type dynacastManagerBase struct {
|
||||
params dynacastManagerBaseParams
|
||||
|
||||
lock sync.RWMutex
|
||||
regressedCodec map[mime.MimeType]struct{}
|
||||
dynacastQuality map[mime.MimeType]dynacastQuality
|
||||
|
||||
notifyOpsQueue *utils.OpsQueue
|
||||
|
||||
isClosed bool
|
||||
|
||||
dynacastManagerNull
|
||||
dynacastQualityListenerNull
|
||||
}
|
||||
|
||||
func newDynacastManagerBase(params dynacastManagerBaseParams) *dynacastManagerBase {
|
||||
if params.OpsQueueDepth == 0 {
|
||||
params.OpsQueueDepth = 4
|
||||
}
|
||||
d := &dynacastManagerBase{
|
||||
params: params,
|
||||
regressedCodec: make(map[mime.MimeType]struct{}),
|
||||
dynacastQuality: make(map[mime.MimeType]dynacastQuality),
|
||||
notifyOpsQueue: utils.NewOpsQueue(utils.OpsQueueParams{
|
||||
Name: "dynacast-notify",
|
||||
MinSize: params.OpsQueueDepth,
|
||||
FlushOnStop: true,
|
||||
Logger: params.Logger,
|
||||
}),
|
||||
}
|
||||
d.notifyOpsQueue.Start()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) AddCodec(mime mime.MimeType) {
|
||||
d.getOrCreateDynacastQuality(mime)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) 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, "toMime", toMime)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
d.regressedCodec[fromMime] = struct{}{}
|
||||
d.params.OnRegressCodec(fromMime, toMime)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.OnUpdateNeeded(false)
|
||||
|
||||
fromDq.Stop()
|
||||
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) Restart() {
|
||||
d.lock.Lock()
|
||||
d.params.OnRestart()
|
||||
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.lock.Unlock()
|
||||
|
||||
for _, dq := range dqs {
|
||||
dq.Restart()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) Close() {
|
||||
d.notifyOpsQueue.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 subscription changes needs to sent to the provider immediately.
|
||||
// This bypasses any debouncing and forces a subscription change update
|
||||
// with immediate effect.
|
||||
func (d *dynacastManagerBase) ForceUpdate() {
|
||||
d.params.OnUpdateNeeded(true)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.lock.Unlock()
|
||||
for _, dq := range dqs {
|
||||
dq.ClearSubscriberNodes()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) 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 := d.params.OnDynacastQualityCreate(mimeType)
|
||||
dq.Start()
|
||||
|
||||
d.dynacastQuality[mimeType] = dq
|
||||
return dq
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) getDynacastQualitiesLocked() []dynacastQuality {
|
||||
return maps.Values(d.dynacastQuality)
|
||||
}
|
||||
272
livekit/pkg/rtc/dynacast/dynacastmanagervideo.go
Normal file
272
livekit/pkg/rtc/dynacast/dynacastmanagervideo.go
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerVideo)(nil)
|
||||
var _ dynacastQualityListener = (*dynacastManagerVideo)(nil)
|
||||
|
||||
type DynacastManagerVideoParams struct {
|
||||
DynacastPauseDelay time.Duration
|
||||
Listener DynacastManagerListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type dynacastManagerVideo struct {
|
||||
params DynacastManagerVideoParams
|
||||
|
||||
maxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
|
||||
committedMaxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
|
||||
|
||||
maxSubscribedQualityDebounce func(func())
|
||||
maxSubscribedQualityDebouncePending bool
|
||||
|
||||
isClosed bool
|
||||
|
||||
*dynacastManagerBase
|
||||
}
|
||||
|
||||
func NewDynacastManagerVideo(params DynacastManagerVideoParams) DynacastManager {
|
||||
if params.Logger == nil {
|
||||
params.Logger = logger.GetLogger()
|
||||
}
|
||||
d := &dynacastManagerVideo{
|
||||
params: params,
|
||||
maxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
|
||||
committedMaxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
|
||||
}
|
||||
if params.DynacastPauseDelay > 0 {
|
||||
d.maxSubscribedQualityDebounce = debounce.New(params.DynacastPauseDelay)
|
||||
}
|
||||
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
|
||||
Logger: params.Logger,
|
||||
OpsQueueDepth: 64,
|
||||
OnRestart: func() {
|
||||
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
|
||||
},
|
||||
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
|
||||
dq := newDynacastQualityVideo(dynacastQualityVideoParams{
|
||||
MimeType: mimeType,
|
||||
Listener: d,
|
||||
Logger: d.params.Logger,
|
||||
})
|
||||
return dq
|
||||
},
|
||||
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
|
||||
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
|
||||
}
|
||||
},
|
||||
OnUpdateNeeded: d.update,
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
// 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 *dynacastManagerVideo) ForceQuality(quality livekit.VideoQuality) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
for mime := range d.committedMaxSubscribedQuality {
|
||||
d.committedMaxSubscribedQuality[mime] = quality
|
||||
}
|
||||
|
||||
d.enqueueSubscribedQualityChange()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerVideo) NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
) {
|
||||
dq := d.getOrCreateDynacastQuality(mime)
|
||||
if dq != nil {
|
||||
dq.NotifySubscriberMaxQuality(subscriberID, quality)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerVideo) 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 *dynacastManagerVideo) OnUpdateMaxQualityForMime(
|
||||
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 *dynacastManagerVideo) 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 *dynacastManagerVideo) enqueueSubscribedQualityChange() {
|
||||
if d.isClosed || d.params.Listener == 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.notifyOpsQueue.Enqueue(func() {
|
||||
d.params.Listener.OnDynacastSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
|
||||
})
|
||||
}
|
||||
168
livekit/pkg/rtc/dynacast/dynacastqualityaudio.go
Normal file
168
livekit/pkg/rtc/dynacast/dynacastqualityaudio.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// 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"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityAudio)(nil)
|
||||
|
||||
type dynacastQualityAudioParams struct {
|
||||
MimeType mime.MimeType
|
||||
Listener dynacastQualityListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
// dynacastQualityAudio manages enable a single receiver of a media track
|
||||
type dynacastQualityAudio struct {
|
||||
params dynacastQualityAudioParams
|
||||
|
||||
// quality level enable/disable
|
||||
lock sync.RWMutex
|
||||
initialized bool
|
||||
subscriberEnables map[livekit.ParticipantID]bool
|
||||
subscriberNodeEnables map[livekit.NodeID]bool
|
||||
enabled bool
|
||||
regressTo dynacastQuality
|
||||
|
||||
dynacastQualityNull
|
||||
}
|
||||
|
||||
func newDynacastQualityAudio(params dynacastQualityAudioParams) dynacastQuality {
|
||||
return &dynacastQualityAudio{
|
||||
params: params,
|
||||
subscriberEnables: make(map[livekit.ParticipantID]bool),
|
||||
subscriberNodeEnables: make(map[livekit.NodeID]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Start() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Restart() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Stop() {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber codec enable",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberID", subscriberID,
|
||||
"enabled", enabled,
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
delete(d.subscriberEnables, subscriberID)
|
||||
} else {
|
||||
d.subscriberEnables[subscriberID] = true
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber node codec enabled",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberNodeID", nodeID,
|
||||
"enabled", enabled,
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
// the downstream node will synthesize correct enable (its dynacast manager has codec regression), just ignore it
|
||||
d.params.Logger.Debugw(
|
||||
"ignoring node codec change, regressed to another dynacast quality",
|
||||
"mime", d.params.MimeType,
|
||||
"regressedMime", d.regressTo.Mime(),
|
||||
)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
delete(d.subscriberNodeEnables, nodeID)
|
||||
} else {
|
||||
d.subscriberNodeEnables[nodeID] = true
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
d.subscriberNodeEnables = make(map[livekit.NodeID]bool)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Mime() mime.MimeType {
|
||||
return d.params.MimeType
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) RegressTo(other dynacastQuality) {
|
||||
d.lock.Lock()
|
||||
d.regressTo = other
|
||||
d.lock.Unlock()
|
||||
|
||||
other.Restart()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) reset() {
|
||||
d.lock.Lock()
|
||||
d.initialized = false
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) updateQualityChange(force bool) {
|
||||
d.lock.Lock()
|
||||
enabled := len(d.subscriberEnables) != 0 || len(d.subscriberNodeEnables) != 0
|
||||
if enabled == d.enabled && d.initialized && !force {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
d.initialized = true
|
||||
d.enabled = enabled
|
||||
d.params.Logger.Debugw(
|
||||
"notifying enabled change",
|
||||
"mime", d.params.MimeType,
|
||||
"enabled", d.enabled,
|
||||
"subscriberNodeEnables", d.subscriberNodeEnables,
|
||||
"subscribedEnables", d.subscriberEnables,
|
||||
"force", force,
|
||||
)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.Listener.OnUpdateAudioCodecForMime(d.params.MimeType, enabled)
|
||||
}
|
||||
249
livekit/pkg/rtc/dynacast/dynacastqualityvideo.go
Normal file
249
livekit/pkg/rtc/dynacast/dynacastqualityvideo.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityVideo)(nil)
|
||||
|
||||
const (
|
||||
initialQualityUpdateWait = 10 * time.Second
|
||||
)
|
||||
|
||||
type dynacastQualityVideoParams struct {
|
||||
MimeType mime.MimeType
|
||||
Listener dynacastQualityListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
// dynacastQualityVideo manages max subscribed quality of a single receiver of a media track
|
||||
type dynacastQualityVideo struct {
|
||||
params dynacastQualityVideoParams
|
||||
|
||||
// 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
|
||||
|
||||
dynacastQualityNull
|
||||
}
|
||||
|
||||
func newDynacastQualityVideo(params dynacastQualityVideoParams) dynacastQuality {
|
||||
return &dynacastQualityVideo{
|
||||
params: params,
|
||||
maxSubscriberQuality: make(map[livekit.ParticipantID]livekit.VideoQuality),
|
||||
maxSubscriberNodeQuality: make(map[livekit.NodeID]livekit.VideoQuality),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Start() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Restart() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Stop() {
|
||||
d.stopMaxQualityTimer()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) 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 *dynacastQualityVideo) 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.params.Logger.Debugw(
|
||||
"ignoring node quality change, regressed to another dynacast quality",
|
||||
"mime", d.params.MimeType,
|
||||
"regressedMime", d.regressTo.Mime(),
|
||||
)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if quality == livekit.VideoQuality_OFF {
|
||||
delete(d.maxSubscriberNodeQuality, nodeID)
|
||||
} else {
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Mime() mime.MimeType {
|
||||
return d.params.MimeType
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) 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.Replace(maxSubscriberQuality, maxSubscriberNodeQuality)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
) {
|
||||
d.lock.Lock()
|
||||
for subID, quality := range maxSubscriberQuality {
|
||||
if oldQuality, ok := d.maxSubscriberQuality[subID]; ok {
|
||||
// no QUALITY_OFF in the map
|
||||
if quality > oldQuality {
|
||||
d.maxSubscriberQuality[subID] = quality
|
||||
}
|
||||
} else {
|
||||
d.maxSubscriberQuality[subID] = quality
|
||||
}
|
||||
}
|
||||
|
||||
for nodeID, quality := range maxSubscriberNodeQuality {
|
||||
if oldQuality, ok := d.maxSubscriberNodeQuality[nodeID]; ok {
|
||||
// no QUALITY_OFF in the map
|
||||
if quality > oldQuality {
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
} else {
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.Restart()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) reset() {
|
||||
d.lock.Lock()
|
||||
d.initialized = false
|
||||
d.lock.Unlock()
|
||||
|
||||
d.startMaxQualityTimer()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) 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,
|
||||
)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.Listener.OnUpdateMaxQualityForMime(d.params.MimeType, maxSubscribedQuality)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) 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 *dynacastQualityVideo) stopMaxQualityTimer() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if d.maxQualityTimer != nil {
|
||||
d.maxQualityTimer.Stop()
|
||||
d.maxQualityTimer = nil
|
||||
}
|
||||
}
|
||||
185
livekit/pkg/rtc/dynacast/interfaces.go
Normal file
185
livekit/pkg/rtc/dynacast/interfaces.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// 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 (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
)
|
||||
|
||||
type DynacastManagerListener interface {
|
||||
OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
)
|
||||
|
||||
OnDynacastSubscribedAudioCodecChange(codecs []*livekit.SubscribedAudioCodec)
|
||||
}
|
||||
|
||||
var _ DynacastManagerListener = (*DynacastManagerListenerNull)(nil)
|
||||
|
||||
type DynacastManagerListenerNull struct {
|
||||
}
|
||||
|
||||
func (d *DynacastManagerListenerNull) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
}
|
||||
func (d *DynacastManagerListenerNull) OnDynacastSubscribedAudioCodecChange(
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
type DynacastManager interface {
|
||||
AddCodec(mime mime.MimeType)
|
||||
HandleCodecRegression(fromMime, toMime mime.MimeType)
|
||||
Restart()
|
||||
Close()
|
||||
ForceUpdate()
|
||||
ForceQuality(quality livekit.VideoQuality)
|
||||
ForceEnable(enabled bool)
|
||||
|
||||
NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
)
|
||||
NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
)
|
||||
|
||||
NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
)
|
||||
NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
)
|
||||
ClearSubscriberNodes()
|
||||
}
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerNull)(nil)
|
||||
|
||||
type dynacastManagerNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastManagerNull) AddCodec(mime mime.MimeType) {}
|
||||
func (d *dynacastManagerNull) HandleCodecRegression(fromMime, toMime mime.MimeType) {}
|
||||
func (d *dynacastManagerNull) Restart() {}
|
||||
func (d *dynacastManagerNull) Close() {}
|
||||
func (d *dynacastManagerNull) ForceUpdate() {}
|
||||
func (d *dynacastManagerNull) ForceQuality(quality livekit.VideoQuality) {}
|
||||
func (d *dynacastManagerNull) ForceEnable(enabled bool) {}
|
||||
func (d *dynacastManagerNull) NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) ClearSubscriberNodes() {}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type dynacastQualityListener interface {
|
||||
OnUpdateMaxQualityForMime(mimeType mime.MimeType, maxQuality livekit.VideoQuality)
|
||||
OnUpdateAudioCodecForMime(mimeType mime.MimeType, enabled bool)
|
||||
}
|
||||
|
||||
var _ dynacastQualityListener = (*dynacastQualityListenerNull)(nil)
|
||||
|
||||
type dynacastQualityListenerNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityListenerNull) OnUpdateMaxQualityForMime(
|
||||
mimeType mime.MimeType,
|
||||
maxQuality livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityListenerNull) OnUpdateAudioCodecForMime(
|
||||
mimeType mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type dynacastQuality interface {
|
||||
Start()
|
||||
Restart()
|
||||
Stop()
|
||||
|
||||
NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality)
|
||||
NotifySubscription(subscriberID livekit.ParticipantID, enabled bool)
|
||||
|
||||
NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality)
|
||||
NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool)
|
||||
ClearSubscriberNodes()
|
||||
|
||||
Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
)
|
||||
|
||||
Mime() mime.MimeType
|
||||
RegressTo(other dynacastQuality)
|
||||
}
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityNull)(nil)
|
||||
|
||||
type dynacastQualityNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityNull) Start() {}
|
||||
func (d *dynacastQualityNull) Restart() {}
|
||||
func (d *dynacastQualityNull) Stop() {}
|
||||
func (d *dynacastQualityNull) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
|
||||
}
|
||||
func (d *dynacastQualityNull) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {}
|
||||
func (d *dynacastQualityNull) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
|
||||
}
|
||||
func (d *dynacastQualityNull) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {}
|
||||
func (d *dynacastQualityNull) ClearSubscriberNodes() {}
|
||||
func (d *dynacastQualityNull) Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastQualityNull) Mime() mime.MimeType { return mime.MimeTypeUnknown }
|
||||
func (d *dynacastQualityNull) RegressTo(other dynacastQuality) {}
|
||||
175
livekit/pkg/rtc/egress.go
Normal file
175
livekit/pkg/rtc/egress.go
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// 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
|
||||
|
||||
info := &livekit.EgressInfo{
|
||||
RoomId: string(roomID),
|
||||
RoomName: string(roomName),
|
||||
Status: livekit.EgressStatus_EGRESS_FAILED,
|
||||
Error: err.Error(),
|
||||
Request: &livekit.EgressInfo_Participant{Participant: req},
|
||||
}
|
||||
|
||||
ts.NotifyEgressEvent(ctx, webhook.EventEgressEnded, info)
|
||||
|
||||
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
|
||||
|
||||
info := &livekit.EgressInfo{
|
||||
RoomId: string(roomID),
|
||||
RoomName: string(roomName),
|
||||
Status: livekit.EgressStatus_EGRESS_FAILED,
|
||||
Error: err.Error(),
|
||||
Request: &livekit.EgressInfo_Track{Track: req},
|
||||
}
|
||||
ts.NotifyEgressEvent(ctx, webhook.EventEgressEnded, info)
|
||||
|
||||
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:])
|
||||
}
|
||||
}
|
||||
44
livekit/pkg/rtc/errors.go
Normal file
44
livekit/pkg/rtc/errors.go
Normal 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.
|
||||
|
||||
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")
|
||||
|
||||
// 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")
|
||||
)
|
||||
318
livekit/pkg/rtc/mediaengine.go
Normal file
318
livekit/pkg/rtc/mediaengine.go
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
// 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 (
|
||||
OpusCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeOpus.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "minptime=10;useinbandfec=1",
|
||||
},
|
||||
PayloadType: 111,
|
||||
}
|
||||
|
||||
RedCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRED.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "111/111",
|
||||
},
|
||||
PayloadType: 63,
|
||||
}
|
||||
|
||||
PCMUCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypePCMU.String(),
|
||||
ClockRate: 8000,
|
||||
},
|
||||
PayloadType: 0,
|
||||
}
|
||||
|
||||
PCMACodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypePCMA.String(),
|
||||
ClockRate: 8000,
|
||||
},
|
||||
PayloadType: 8,
|
||||
}
|
||||
|
||||
videoRTXCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRTX.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
}
|
||||
|
||||
vp8CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
PayloadType: 96,
|
||||
}
|
||||
|
||||
vp9ProfileId0CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=0",
|
||||
},
|
||||
PayloadType: 98,
|
||||
}
|
||||
|
||||
vp9ProfileId1CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=1",
|
||||
},
|
||||
PayloadType: 100,
|
||||
}
|
||||
|
||||
h264ProfileLevelId42e01fPacketizationMode0CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||
},
|
||||
PayloadType: 125,
|
||||
}
|
||||
|
||||
h264ProfileLevelId42e01fPacketizationMode1CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
|
||||
},
|
||||
PayloadType: 108,
|
||||
}
|
||||
|
||||
h264HighProfileFmtp = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
|
||||
h264HighProfileCodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: h264HighProfileFmtp,
|
||||
},
|
||||
PayloadType: 123,
|
||||
}
|
||||
|
||||
av1CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeAV1.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
PayloadType: 35,
|
||||
}
|
||||
|
||||
h265CodecParameters = webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH265.String(),
|
||||
ClockRate: 90000,
|
||||
},
|
||||
PayloadType: 116,
|
||||
}
|
||||
|
||||
videoCodecsParameters = []webrtc.RTPCodecParameters{
|
||||
vp8CodecParameters,
|
||||
vp9ProfileId0CodecParameters,
|
||||
vp9ProfileId1CodecParameters,
|
||||
h264ProfileLevelId42e01fPacketizationMode0CodecParameters,
|
||||
h264ProfileLevelId42e01fPacketizationMode1CodecParameters,
|
||||
h264HighProfileCodecParameters,
|
||||
av1CodecParameters,
|
||||
h265CodecParameters,
|
||||
}
|
||||
)
|
||||
|
||||
func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedback RTCPFeedbackConfig, filterOutH264HighProfile bool) error {
|
||||
// audio codecs
|
||||
if IsCodecEnabled(codecs, OpusCodecParameters.RTPCodecCapability) {
|
||||
cp := OpusCodecParameters
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if IsCodecEnabled(codecs, RedCodecParameters.RTPCodecCapability) {
|
||||
if err := me.RegisterCodec(RedCodecParameters, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, codec := range []webrtc.RTPCodecParameters{PCMUCodecParameters, PCMACodecParameters} {
|
||||
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
continue
|
||||
}
|
||||
|
||||
cp := codec
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// video codecs
|
||||
rtxEnabled := IsCodecEnabled(codecs, videoRTXCodecParameters.RTPCodecCapability)
|
||||
for _, codec := range videoCodecsParameters {
|
||||
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == h264HighProfileFmtp {
|
||||
continue
|
||||
}
|
||||
if mime.IsMimeTypeStringRTX(codec.MimeType) {
|
||||
continue
|
||||
}
|
||||
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
continue
|
||||
}
|
||||
|
||||
cp := codec
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Video
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !rtxEnabled {
|
||||
continue
|
||||
}
|
||||
|
||||
cp = videoRTXCodecParameters
|
||||
cp.RTPCodecCapability.SDPFmtpLine = fmt.Sprintf("apt=%d", codec.PayloadType)
|
||||
cp.PayloadType = codec.PayloadType + 1
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func selectAlternativeAudioCodec(enabledCodecs []*livekit.Codec) string {
|
||||
for _, c := range enabledCodecs {
|
||||
if mime.IsMimeTypeStringAudio(c.Mime) {
|
||||
return c.Mime
|
||||
}
|
||||
}
|
||||
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
|
||||
return mime.MimeTypeOpus.String()
|
||||
}
|
||||
|
||||
func filterCodecs(
|
||||
codecs []webrtc.RTPCodecParameters,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
filterOutH264HighProfile bool,
|
||||
) []webrtc.RTPCodecParameters {
|
||||
filteredCodecs := make([]webrtc.RTPCodecParameters, 0, len(codecs))
|
||||
for _, c := range codecs {
|
||||
if filterOutH264HighProfile && isH264HighProfile(c.RTPCodecCapability.SDPFmtpLine) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, enabledCodec := range enabledCodecs {
|
||||
if mime.NormalizeMimeType(enabledCodec.Mime) == mime.NormalizeMimeType(c.RTPCodecCapability.MimeType) {
|
||||
if !mime.IsMimeTypeStringEqual(c.RTPCodecCapability.MimeType, mime.MimeTypeRTX.String()) {
|
||||
if mime.IsMimeTypeStringVideo(c.RTPCodecCapability.MimeType) {
|
||||
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Video
|
||||
} else {
|
||||
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Audio
|
||||
}
|
||||
}
|
||||
filteredCodecs = append(filteredCodecs, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredCodecs
|
||||
}
|
||||
|
||||
func isH264HighProfile(fmtp string) bool {
|
||||
params := strings.Split(fmtp, ";")
|
||||
for _, param := range params {
|
||||
parts := strings.Split(param, "=")
|
||||
if len(parts) == 2 {
|
||||
if parts[0] == "profile-level-id" {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6184#section-8.1
|
||||
// hex value 0x64 for profile_idc is high profile
|
||||
return strings.HasPrefix(parts[1], "64")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
41
livekit/pkg/rtc/mediaengine_test.go
Normal file
41
livekit/pkg/rtc/mediaengine_test.go
Normal file
|
|
@ -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
livekit/pkg/rtc/medialossproxy.go
Normal file
105
livekit/pkg/rtc/medialossproxy.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
707
livekit/pkg/rtc/mediatrack.go
Normal file
707
livekit/pkg/rtc/mediatrack.go
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
// 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/protocol/observability/roomobs"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
|
||||
"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/interceptor"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
util "github.com/livekit/mediatransportutil"
|
||||
)
|
||||
|
||||
var _ types.LocalMediaTrack = (*MediaTrack)(nil)
|
||||
|
||||
// MediaTrack represents a WebRTC track that needs to be forwarded
|
||||
// Implements MediaTrack and PublishedTrack interface
|
||||
type MediaTrack struct {
|
||||
params MediaTrackParams
|
||||
buffer *buffer.Buffer
|
||||
everSubscribed atomic.Bool
|
||||
|
||||
*MediaTrackReceiver
|
||||
*MediaLossProxy
|
||||
|
||||
dynacastManager dynacast.DynacastManager
|
||||
|
||||
lock sync.RWMutex
|
||||
|
||||
rttFromXR atomic.Bool
|
||||
|
||||
backupCodecPolicy livekit.BackupCodecPolicy
|
||||
regressionTargetCodec mime.MimeType
|
||||
regressionTargetCodecReceived bool
|
||||
|
||||
onSubscribedMaxQualityChange func(
|
||||
trackID livekit.TrackID,
|
||||
trackInfo *livekit.TrackInfo,
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) error
|
||||
onSubscribedAudioCodecChange func(
|
||||
trackID livekit.TrackID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) error
|
||||
}
|
||||
|
||||
type MediaTrackParams struct {
|
||||
ParticipantID func() livekit.ParticipantID
|
||||
ParticipantIdentity livekit.ParticipantIdentity
|
||||
ParticipantVersion uint32
|
||||
ParticipantCountry string
|
||||
BufferFactory *buffer.Factory
|
||||
ReceiverConfig ReceiverConfig
|
||||
SubscriberConfig DirectionConfig
|
||||
PLIThrottleConfig sfu.PLIThrottleConfig
|
||||
AudioConfig sfu.AudioConfig
|
||||
VideoConfig config.VideoConfig
|
||||
Telemetry telemetry.TelemetryService
|
||||
Logger logger.Logger
|
||||
Reporter roomobs.TrackReporter
|
||||
SimTracks map[uint32]interceptor.SimulcastTrackInfo
|
||||
OnRTCP func([]rtcp.Packet)
|
||||
ForwardStats *sfu.ForwardStats
|
||||
OnTrackEverSubscribed func(livekit.TrackID)
|
||||
ShouldRegressCodec func() bool
|
||||
PreferVideoSizeFromMedia bool
|
||||
EnableRTPStreamRestartDetection bool
|
||||
UpdateTrackInfoByVideoSizeChange bool
|
||||
ForceBackupCodecPolicySimulcast bool
|
||||
}
|
||||
|
||||
func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
|
||||
t := &MediaTrack{
|
||||
params: params,
|
||||
backupCodecPolicy: ti.BackupCodecPolicy,
|
||||
}
|
||||
|
||||
if t.params.ForceBackupCodecPolicySimulcast {
|
||||
t.backupCodecPolicy = livekit.BackupCodecPolicy_SIMULCAST
|
||||
}
|
||||
|
||||
if t.backupCodecPolicy != livekit.BackupCodecPolicy_SIMULCAST && len(ti.Codecs) > 1 {
|
||||
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,
|
||||
PreferVideoSizeFromMedia: params.PreferVideoSizeFromMedia,
|
||||
}, 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)
|
||||
}
|
||||
|
||||
switch ti.Type {
|
||||
case livekit.TrackType_VIDEO:
|
||||
t.dynacastManager = dynacast.NewDynacastManagerVideo(dynacast.DynacastManagerVideoParams{
|
||||
DynacastPauseDelay: params.VideoConfig.DynacastPauseDelay,
|
||||
Listener: t,
|
||||
Logger: params.Logger,
|
||||
})
|
||||
|
||||
case livekit.TrackType_AUDIO:
|
||||
if len(ti.Codecs) > 1 {
|
||||
t.dynacastManager = dynacast.NewDynacastManagerAudio(dynacast.DynacastManagerAudioParams{
|
||||
Listener: t,
|
||||
Logger: params.Logger,
|
||||
})
|
||||
}
|
||||
}
|
||||
t.MediaTrackReceiver.OnSetupReceiver(func(mime mime.MimeType) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.AddCodec(mime)
|
||||
}
|
||||
})
|
||||
t.MediaTrackReceiver.OnSubscriberMaxQualityChange(
|
||||
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, layer int32) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.NotifySubscriberMaxQuality(
|
||||
subscriberID,
|
||||
mimeType,
|
||||
buffer.GetVideoQualityForSpatialLayer(
|
||||
mimeType,
|
||||
layer,
|
||||
t.MediaTrackReceiver.TrackInfo(),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
t.MediaTrackReceiver.OnSubscriberAudioCodecChange(
|
||||
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, enabled bool) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.NotifySubscription(subscriberID, mimeType, enabled)
|
||||
}
|
||||
},
|
||||
)
|
||||
t.MediaTrackReceiver.OnCodecRegression(func(old, new webrtc.RTPCodecParameters) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.HandleCodecRegression(
|
||||
mime.NormalizeMimeType(old.MimeType),
|
||||
mime.NormalizeMimeType(new.MimeType),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
t.SetMuted(ti.Muted)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *MediaTrack) OnSubscribedMaxQualityChange(
|
||||
f func(
|
||||
trackID livekit.TrackID,
|
||||
trackInfo *livekit.TrackInfo,
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) error,
|
||||
) {
|
||||
t.lock.Lock()
|
||||
t.onSubscribedMaxQualityChange = f
|
||||
t.lock.Unlock()
|
||||
}
|
||||
|
||||
func (t *MediaTrack) OnSubscribedAudioCodecChange(
|
||||
f func(
|
||||
trackID livekit.TrackID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) error,
|
||||
) {
|
||||
t.lock.Lock()
|
||||
t.onSubscribedAudioCodecChange = f
|
||||
t.lock.Unlock()
|
||||
}
|
||||
|
||||
func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.NotifySubscriberNodeMaxQuality(nodeID, qualities)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) NotifySubscriptionNode(nodeID livekit.NodeID, codecs []*livekit.SubscribedAudioCodec) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.NotifySubscriptionNode(nodeID, codecs)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) ClearSubscriberNodes() {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.ClearSubscriberNodes()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) HasSignalCid(cid string) bool {
|
||||
if cid != "" {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == cid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *MediaTrack) HasSdpCid(cid string) bool {
|
||||
if cid != "" {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == cid || c.SdpCid == cid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *MediaTrack) GetMimeTypeForSdpCid(cid string) mime.MimeType {
|
||||
if cid != "" {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == cid || c.SdpCid == cid {
|
||||
return mime.NormalizeMimeType(c.MimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mime.MimeTypeUnknown
|
||||
}
|
||||
|
||||
func (t *MediaTrack) GetCidsForMimeType(mimeType mime.MimeType) (string, string) {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if mime.NormalizeMimeType(c.MimeType) == mimeType {
|
||||
return c.Cid, c.SdpCid
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func (t *MediaTrack) ToProto() *livekit.TrackInfo {
|
||||
return t.MediaTrackReceiver.TrackInfoClone()
|
||||
}
|
||||
|
||||
// AddReceiver adds a new RTP receiver to the track, returns true when receiver represents a new codec
|
||||
// and if a receiver was added successfully
|
||||
func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRemote, mid string) (bool, 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, false
|
||||
}
|
||||
|
||||
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(&livekit.RTCPSenderReportState{
|
||||
RtpTimestamp: pkt.RTPTime,
|
||||
NtpTimestamp: pkt.NTPTime,
|
||||
Packets: pkt.PacketCount,
|
||||
Octets: uint64(pkt.OctetCount),
|
||||
At: mono.UnixNano(),
|
||||
})
|
||||
}
|
||||
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.GetSpatialLayerForRid(mimeType, track.RID(), ti)
|
||||
if layer < 0 {
|
||||
t.params.Logger.Warnw(
|
||||
"AddReceiver failed due to negative layer", nil,
|
||||
"rid", track.RID(),
|
||||
"layer", layer,
|
||||
"ssrc", track.SSRC(),
|
||||
"codec", track.Codec(),
|
||||
"trackInfo", logger.Proto(ti),
|
||||
)
|
||||
t.lock.Unlock()
|
||||
return newCodec, false
|
||||
}
|
||||
|
||||
t.params.Logger.Debugw(
|
||||
"AddReceiver",
|
||||
"rid", track.RID(),
|
||||
"layer", layer,
|
||||
"ssrc", track.SSRC(),
|
||||
"codec", track.Codec(),
|
||||
"trackInfo", logger.Proto(ti),
|
||||
)
|
||||
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
|
||||
t.params.Logger.Warnw(
|
||||
"unexpected 0 codecs in track info", nil,
|
||||
"mime", mimeType,
|
||||
"track", logger.Proto(ti),
|
||||
)
|
||||
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,
|
||||
"mime", mimeType,
|
||||
"track", logger.Proto(ti),
|
||||
)
|
||||
t.lock.Unlock()
|
||||
return newCodec, 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.WithForwardStats(t.params.ForwardStats),
|
||||
sfu.WithEnableRTPStreamRestartDetection(t.params.EnableRTPStreamRestartDetection),
|
||||
)
|
||||
newWR.OnCloseHandler(func() {
|
||||
t.MediaTrackReceiver.SetClosing(false)
|
||||
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
|
||||
statsKey := telemetry.StatsKeyForTrack(
|
||||
t.params.ParticipantCountry,
|
||||
livekit.StreamType_UPSTREAM,
|
||||
t.PublisherID(),
|
||||
t.ID(),
|
||||
ti.Source,
|
||||
ti.Type,
|
||||
)
|
||||
newWR.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
|
||||
// send for only one codec, either primary (priority == 0) OR regressed codec
|
||||
t.lock.RLock()
|
||||
regressionTargetCodecReceived := t.regressionTargetCodecReceived
|
||||
t.lock.RUnlock()
|
||||
if priority == 0 || regressionTargetCodecReceived {
|
||||
t.params.Telemetry.TrackStats(statsKey, stat)
|
||||
|
||||
if cs, ok := telemetry.CondenseStat(stat); ok {
|
||||
t.params.Reporter.Tx(func(tx roomobs.TrackTx) {
|
||||
tx.ReportName(ti.Name)
|
||||
tx.ReportKind(roomobs.TrackKindPub)
|
||||
tx.ReportType(roomobs.TrackTypeFromProto(ti.Type))
|
||||
tx.ReportSource(roomobs.TrackSourceFromProto(ti.Source))
|
||||
tx.ReportMime(mime.NormalizeMimeType(ti.MimeType).ReporterType())
|
||||
tx.ReportLayer(roomobs.PackTrackLayer(ti.Height, ti.Width))
|
||||
tx.ReportDuration(uint16(cs.EndTime.Sub(cs.StartTime).Milliseconds()))
|
||||
tx.ReportFrames(uint16(cs.Frames))
|
||||
tx.ReportRecvBytes(uint32(cs.Bytes))
|
||||
tx.ReportRecvPackets(cs.Packets)
|
||||
tx.ReportPacketsLost(cs.PacketsLost)
|
||||
tx.ReportScore(stat.Score)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
newWR.OnMaxLayerChange(func(mimeType mime.MimeType, maxLayer int32) {
|
||||
// send for only one codec, either primary (priority == 0) OR regressed codec
|
||||
t.lock.RLock()
|
||||
regressionTargetCodecReceived := t.regressionTargetCodecReceived
|
||||
t.lock.RUnlock()
|
||||
if priority == 0 || regressionTargetCodecReceived {
|
||||
t.MediaTrackReceiver.NotifyMaxLayerChange(mimeType, maxLayer)
|
||||
}
|
||||
})
|
||||
// SIMULCAST-CODEC-TODO END: these need to be receiver/mime aware, setting it up only for primary now
|
||||
|
||||
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 && !info.IsRepairStream {
|
||||
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, info.StreamID, ssrc, info.RepairSSRC)
|
||||
}
|
||||
}
|
||||
wr = newWR
|
||||
newCodec = true
|
||||
|
||||
newWR.AddOnCodecStateChange(func(codec webrtc.RTPCodecParameters, state sfu.ReceiverCodecState) {
|
||||
t.MediaTrackReceiver.HandleReceiverCodecChange(newWR, codec, state)
|
||||
})
|
||||
|
||||
// update subscriber video layers when video size changes
|
||||
newWR.OnVideoSizeChanged(func() {
|
||||
if t.params.UpdateTrackInfoByVideoSizeChange {
|
||||
t.MediaTrackReceiver.UpdateVideoSize(mimeType, newWR.VideoSizes())
|
||||
}
|
||||
|
||||
t.MediaTrackSubscriptions.UpdateVideoLayers()
|
||||
})
|
||||
}
|
||||
|
||||
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 newCodec, false
|
||||
}
|
||||
|
||||
var expectedBitrate int
|
||||
layers := buffer.GetVideoLayersForMimeType(mimeType, ti)
|
||||
if layer >= 0 && len(layers) > int(layer) {
|
||||
expectedBitrate = int(layers[layer].GetBitrate())
|
||||
}
|
||||
if err := buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, expectedBitrate); err != nil {
|
||||
t.params.Logger.Warnw(
|
||||
"binding buffer failed", err,
|
||||
"rid", track.RID(),
|
||||
"layer", layer,
|
||||
"ssrc", track.SSRC(),
|
||||
"newCodec", newCodec,
|
||||
)
|
||||
buff.Close()
|
||||
return newCodec, false
|
||||
}
|
||||
|
||||
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, track.RID(), uint32(track.SSRC()), 0)
|
||||
|
||||
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.OnNotifyRTX(t.MediaTrackReceiver.setLayerRtxInfo)
|
||||
|
||||
// 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, true
|
||||
}
|
||||
|
||||
func (t *MediaTrack) GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality) {
|
||||
receiver := t.ActiveReceiver()
|
||||
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) Restart() {
|
||||
t.MediaTrackReceiver.Restart()
|
||||
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.Restart()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) Close(isExpectedToResume bool) {
|
||||
t.MediaTrackReceiver.SetClosing(isExpectedToResume)
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.Close()
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) enableRegression() bool {
|
||||
return t.backupCodecPolicy == livekit.BackupCodecPolicy_REGRESSION ||
|
||||
(t.backupCodecPolicy == livekit.BackupCodecPolicy_PREFER_REGRESSION && t.params.ShouldRegressCodec())
|
||||
}
|
||||
|
||||
func (t *MediaTrack) Logger() logger.Logger {
|
||||
return t.params.Logger
|
||||
}
|
||||
|
||||
// dynacast.DynacastManagerListtener implementation
|
||||
var _ dynacast.DynacastManagerListener = (*MediaTrack)(nil)
|
||||
|
||||
func (t *MediaTrack) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
t.lock.RLock()
|
||||
onSubscribedMaxQualityChange := t.onSubscribedMaxQualityChange
|
||||
t.lock.RUnlock()
|
||||
|
||||
if onSubscribedMaxQualityChange != nil && !t.IsMuted() {
|
||||
_ = onSubscribedMaxQualityChange(
|
||||
t.ID(),
|
||||
t.ToProto(),
|
||||
subscribedQualities,
|
||||
maxSubscribedQualities,
|
||||
)
|
||||
}
|
||||
|
||||
for _, q := range maxSubscribedQualities {
|
||||
receiver := t.Receiver(q.CodecMime)
|
||||
if receiver != nil {
|
||||
receiver.SetMaxExpectedSpatialLayer(
|
||||
buffer.GetSpatialLayerForVideoQuality(
|
||||
q.CodecMime,
|
||||
q.Quality,
|
||||
t.MediaTrackReceiver.TrackInfo(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) OnDynacastSubscribedAudioCodecChange(codecs []*livekit.SubscribedAudioCodec) {
|
||||
t.lock.RLock()
|
||||
onSubscribedAudioCodecChange := t.onSubscribedAudioCodecChange
|
||||
t.lock.RUnlock()
|
||||
|
||||
if onSubscribedAudioCodecChange != nil {
|
||||
_ = onSubscribedAudioCodecChange(t.ID(), codecs)
|
||||
}
|
||||
}
|
||||
197
livekit/pkg/rtc/mediatrack_test.go
Normal file
197
livekit/pkg/rtc/mediatrack_test.go
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
// 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/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestGetQualityForDimension(t *testing.T) {
|
||||
t.Run("landscape source", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
})
|
||||
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 300, 200))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 200, 250))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 700, 480))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 500, 1000))
|
||||
})
|
||||
|
||||
t.Run("portrait source", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 540,
|
||||
Height: 960,
|
||||
})
|
||||
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 200, 400))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 400, 400))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 400, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 600, 900))
|
||||
})
|
||||
|
||||
t.Run("layers provided", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
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(mime.MimeTypeH264, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
|
||||
})
|
||||
|
||||
t.Run("highest layer with smallest dimensions", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
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(mime.MimeTypeH264, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1200, 800))
|
||||
|
||||
mt = NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
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(mime.MimeTypeH264, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1200, 800))
|
||||
})
|
||||
|
||||
}
|
||||
1241
livekit/pkg/rtc/mediatrackreceiver.go
Normal file
1241
livekit/pkg/rtc/mediatrackreceiver.go
Normal file
File diff suppressed because it is too large
Load diff
367
livekit/pkg/rtc/mediatracksubscriptions.go
Normal file
367
livekit/pkg/rtc/mediatracksubscriptions.go
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
// 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"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"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)
|
||||
onSubscriberAudioCodecChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)
|
||||
}
|
||||
|
||||
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) OnSubscriberAudioCodecChange(f func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)) {
|
||||
t.onSubscriberAudioCodecChange = 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()
|
||||
|
||||
subTrack, err := NewSubscribedTrack(SubscribedTrackParams{
|
||||
ReceiverConfig: t.params.ReceiverConfig,
|
||||
SubscriberConfig: t.params.SubscriberConfig,
|
||||
Subscriber: sub,
|
||||
MediaTrack: t.params.MediaTrack,
|
||||
AdaptiveStream: sub.GetAdaptiveStream(),
|
||||
Telemetry: t.params.Telemetry,
|
||||
WrappedReceiver: wr,
|
||||
IsRelayed: t.params.IsRelayed,
|
||||
OnDownTrackCreated: t.onDownTrackCreated,
|
||||
OnDownTrackClosed: func(subscriberID livekit.ParticipantID) {
|
||||
t.subscribedTracksMu.Lock()
|
||||
delete(t.subscribedTracks, subscriberID)
|
||||
t.subscribedTracksMu.Unlock()
|
||||
},
|
||||
OnSubscriberMaxQualityChange: t.onSubscriberMaxQualityChange,
|
||||
OnSubscriberAudioCodecChange: t.onSubscriberAudioCodecChange,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Bind callback can happen from replaceTrack, so set it up early
|
||||
var reusingTransceiver atomic.Bool
|
||||
var dtState sfu.DownTrackState
|
||||
downTrack := subTrack.DownTrack()
|
||||
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())
|
||||
})
|
||||
|
||||
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: slices.Contains(info.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO),
|
||||
Red: IsRedEnabled(info),
|
||||
}
|
||||
codecs := wr.Codecs()
|
||||
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)
|
||||
|
||||
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, false)
|
||||
} else {
|
||||
// flushing blocks, avoid blocking when publisher removes all its subscribers
|
||||
go dt.CloseWithFlush(true, 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]any {
|
||||
subscribedTrackInfo := make([]map[string]any, 0)
|
||||
for _, val := range t.getAllSubscribedTracks() {
|
||||
if st, ok := val.(*SubscribedTrack); ok {
|
||||
subscribedTrackInfo = append(subscribedTrackInfo, st.DownTrack().DebugInfo())
|
||||
}
|
||||
}
|
||||
|
||||
return subscribedTrackInfo
|
||||
}
|
||||
59
livekit/pkg/rtc/migrationdatacache.go
Normal file
59
livekit/pkg/rtc/migrationdatacache.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package rtc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type MigrationDataCacheState int
|
||||
|
||||
const (
|
||||
MigrationDataCacheStateWaiting MigrationDataCacheState = iota
|
||||
MigrationDataCacheStateTimeout
|
||||
MigrationDataCacheStateDone
|
||||
)
|
||||
|
||||
type MigrationDataCache struct {
|
||||
lastSeq uint32
|
||||
pkts []*livekit.DataPacket
|
||||
state MigrationDataCacheState
|
||||
expiredAt time.Time
|
||||
}
|
||||
|
||||
func NewMigrationDataCache(lastSeq uint32, expiredAt time.Time) *MigrationDataCache {
|
||||
return &MigrationDataCache{
|
||||
lastSeq: lastSeq,
|
||||
expiredAt: expiredAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a message to the cache if there is a gap between the last sequence number and cached messages then return the cache State:
|
||||
// - MigrationDataCacheStateWaiting: waiting for the next packet (lastSeq + 1) of last sequence from old node
|
||||
// - MigrationDataCacheStateTimeout: the next packet is not received before the expiredAt, participant will
|
||||
// continue to process the reliable messages, subscribers will see the gap after the publisher migration
|
||||
// - MigrationDataCacheStateDone: the next packet is received, participant can continue to process the reliable messages
|
||||
func (c *MigrationDataCache) Add(pkt *livekit.DataPacket) MigrationDataCacheState {
|
||||
if c.state == MigrationDataCacheStateDone || c.state == MigrationDataCacheStateTimeout {
|
||||
return c.state
|
||||
}
|
||||
|
||||
if pkt.Sequence <= c.lastSeq {
|
||||
return c.state
|
||||
}
|
||||
|
||||
if pkt.Sequence == c.lastSeq+1 {
|
||||
c.state = MigrationDataCacheStateDone
|
||||
return c.state
|
||||
}
|
||||
|
||||
c.pkts = append(c.pkts, pkt)
|
||||
if time.Now().After(c.expiredAt) {
|
||||
c.state = MigrationDataCacheStateTimeout
|
||||
}
|
||||
return c.state
|
||||
}
|
||||
|
||||
func (c *MigrationDataCache) Get() []*livekit.DataPacket {
|
||||
return c.pkts
|
||||
}
|
||||
38
livekit/pkg/rtc/migrationdatacache_test.go
Normal file
38
livekit/pkg/rtc/migrationdatacache_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package rtc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
func TestMigrationDataCache_Add(t *testing.T) {
|
||||
expiredAt := time.Now().Add(100 * time.Millisecond)
|
||||
cache := NewMigrationDataCache(10, expiredAt)
|
||||
|
||||
pkt1 := &livekit.DataPacket{Sequence: 9}
|
||||
state := cache.Add(pkt1)
|
||||
require.Equal(t, MigrationDataCacheStateWaiting, state)
|
||||
require.Empty(t, cache.Get())
|
||||
|
||||
pkt2 := &livekit.DataPacket{Sequence: 11}
|
||||
state = cache.Add(pkt2)
|
||||
require.Equal(t, MigrationDataCacheStateDone, state)
|
||||
require.Empty(t, cache.Get())
|
||||
|
||||
pkt3 := &livekit.DataPacket{Sequence: 12}
|
||||
state = cache.Add(pkt3)
|
||||
require.Equal(t, MigrationDataCacheStateDone, state)
|
||||
require.Empty(t, cache.Get())
|
||||
|
||||
cache2 := NewMigrationDataCache(20, time.Now().Add(10*time.Millisecond))
|
||||
pkt4 := &livekit.DataPacket{Sequence: 22}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
state = cache2.Add(pkt4)
|
||||
require.Equal(t, MigrationDataCacheStateTimeout, state)
|
||||
require.Len(t, cache2.Get(), 1)
|
||||
require.Equal(t, uint32(22), cache2.Get()[0].Sequence)
|
||||
}
|
||||
4221
livekit/pkg/rtc/participant.go
Normal file
4221
livekit/pkg/rtc/participant.go
Normal file
File diff suppressed because it is too large
Load diff
160
livekit/pkg/rtc/participant_data_track.go
Normal file
160
livekit/pkg/rtc/participant_data_track.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// 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/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
)
|
||||
|
||||
func (p *ParticipantImpl) HandlePublishDataTrackRequest(req *livekit.PublishDataTrackRequest) {
|
||||
if !p.CanPublishData() || !p.params.EnableDataTracks {
|
||||
p.pubLogger.Warnw("no permission to publish data track", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_NOT_ALLOWED,
|
||||
Message: "does not have permission to publish data",
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.PubHandle == 0 || req.PubHandle > 65535 {
|
||||
p.pubLogger.Warnw("invalid data track handle", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_INVALID_HANDLE,
|
||||
Message: "handle should be > 0 AND < 65536",
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Name) == 0 || len(req.Name) > 256 {
|
||||
p.pubLogger.Warnw("invalid data track name", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_INVALID_NAME,
|
||||
Message: "name should not be empty and should not exceed 256 characters",
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
publishedDataTracks := p.UpDataTrackManager.GetPublishedDataTracks()
|
||||
for _, dt := range publishedDataTracks {
|
||||
message := ""
|
||||
reason := livekit.RequestResponse_OK
|
||||
switch {
|
||||
case dt.PubHandle() == uint16(req.PubHandle):
|
||||
message = "a data track with same handle already exists"
|
||||
reason = livekit.RequestResponse_DUPLICATE_HANDLE
|
||||
case dt.Name() == req.Name:
|
||||
message = "a data track with same name already exists"
|
||||
reason = livekit.RequestResponse_DUPLICATE_NAME
|
||||
}
|
||||
if message != "" {
|
||||
p.pubLogger.Warnw(
|
||||
"cannot publish duplicate data track", nil,
|
||||
"req", logger.Proto(req),
|
||||
"existing", logger.Proto(dt.ToProto()),
|
||||
)
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dti := &livekit.DataTrackInfo{
|
||||
PubHandle: req.PubHandle,
|
||||
Sid: guid.New(utils.DataTrackPrefix),
|
||||
Name: req.Name,
|
||||
Encryption: req.Encryption,
|
||||
}
|
||||
dt := NewDataTrack(
|
||||
DataTrackParams{
|
||||
Logger: p.params.Logger.WithValues("trackID", dti.Sid),
|
||||
ParticipantID: p.ID,
|
||||
ParticipantIdentity: p.params.Identity,
|
||||
},
|
||||
dti,
|
||||
)
|
||||
|
||||
p.UpDataTrackManager.AddPublishedDataTrack(dt)
|
||||
|
||||
p.sendPublishDataTrackResponse(dti)
|
||||
|
||||
p.setIsPublisher(true)
|
||||
p.dirty.Store(true)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) HandleUnpublishDataTrackRequest(req *livekit.UnpublishDataTrackRequest) {
|
||||
dt := p.UpDataTrackManager.GetPublishedDataTrack(uint16(req.PubHandle))
|
||||
if dt == nil {
|
||||
p.pubLogger.Warnw("unpublish data track not found", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_NOT_FOUND,
|
||||
Request: &livekit.RequestResponse_UnpublishDataTrack{
|
||||
UnpublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.UpDataTrackManager.RemovePublishedDataTrack(dt)
|
||||
|
||||
p.sendUnpublishDataTrackResponse(dt.ToProto())
|
||||
|
||||
p.dirty.Store(true)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) HandleUpdateDataSubscription(req *livekit.UpdateDataSubscription) {
|
||||
p.listener().OnUpdateDataSubscriptions(p, req)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) onReceivedDataTrackMessage(data []byte, arrivalTime int64) {
|
||||
var packet datatrack.Packet
|
||||
if err := packet.Unmarshal(data); err != nil {
|
||||
p.params.Logger.Errorw("could not unmarshal data track message", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.UpDataTrackManager.HandleReceivedDataTrackMessage(data, &packet, arrivalTime)
|
||||
|
||||
p.listener().OnDataTrackMessage(p, data, &packet)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) GetNextSubscribedDataTrackHandle() uint16 {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
p.nextSubscribedDataTrackHandle++
|
||||
if p.nextSubscribedDataTrackHandle == 0 {
|
||||
p.nextSubscribedDataTrackHandle++
|
||||
}
|
||||
|
||||
return p.nextSubscribedDataTrackHandle
|
||||
}
|
||||
828
livekit/pkg/rtc/participant_internal_test.go
Normal file
828
livekit/pkg/rtc/participant_internal_test.go
Normal file
|
|
@ -0,0 +1,828 @@
|
|||
// 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/observability/roomobs"
|
||||
lksdp "github.com/livekit/protocol/sdp"
|
||||
"github.com/livekit/protocol/signalling"
|
||||
"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.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackUpdatedCalls(func(p types.Participant, track types.MediaTrack) {
|
||||
updated = true
|
||||
})
|
||||
p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackPublishedCalls(func(p types.Participant, track types.MediaTrack) {
|
||||
published = true
|
||||
})
|
||||
p.UpTrackManager.AddPublishedTrack(track)
|
||||
p.handleTrackPublished(track, false)
|
||||
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,
|
||||
})
|
||||
|
||||
// error response on duplicate adds a message
|
||||
require.Equal(t, 2, 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.HasSignalCidCalls(func(s string) bool { return s == "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,
|
||||
})
|
||||
// `queued` `RequestResponse` should add a message
|
||||
require.Equal(t, 1, 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,
|
||||
})
|
||||
// `queued` `RequestResponse`s should have been sent for duplicate additions
|
||||
require.Equal(t, 2, 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,
|
||||
})
|
||||
// `queued` `RequestResponse` should add a message
|
||||
require.Equal(t, 1, 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,
|
||||
})
|
||||
// `queued` `RequestResponse`s should have been sent for duplicate additions
|
||||
require.Equal(t, 2, 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,
|
||||
})
|
||||
// an error response for disallowed source should send a `RequestResponse`.
|
||||
require.Equal(t, 2, 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, false)
|
||||
|
||||
// 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.MuteTrackRequest{
|
||||
Sid: ti.Sid,
|
||||
Muted: 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)
|
||||
offerId := uint32(42)
|
||||
|
||||
// negotiated codec should not contain h264
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
var answer webrtc.SessionDescription
|
||||
var answerId uint32
|
||||
var answerReceived atomic.Bool
|
||||
var answerIdReceived atomic.Uint32
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
if res.GetAnswer() != nil {
|
||||
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
|
||||
answerReceived.Store(true)
|
||||
answerIdReceived.Store(answerId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
participant.HandleOffer(&livekit.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer.String(),
|
||||
Sdp: sdp.SDP,
|
||||
Id: offerId,
|
||||
})
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if answerReceived.Load() && answerIdReceived.Load() == offerId {
|
||||
return ""
|
||||
} else {
|
||||
return "answer not received OR answer id mismatch"
|
||||
}
|
||||
})
|
||||
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.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
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 TestPreferMediaCodecForPublisher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mediaKind string
|
||||
trackBaseCid string
|
||||
preferredCodec string
|
||||
addTrack *livekit.AddTrackRequest
|
||||
mimeTypeStringChecker func(string) bool
|
||||
mimeTypeCodecStringChecker func(string) bool
|
||||
transceiverMimeType mime.MimeType
|
||||
}{
|
||||
{
|
||||
name: "video",
|
||||
mediaKind: "video",
|
||||
trackBaseCid: "preferH264Video",
|
||||
preferredCodec: "h264",
|
||||
addTrack: &livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Name: "video",
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Source: livekit.TrackSource_CAMERA,
|
||||
},
|
||||
mimeTypeStringChecker: mime.IsMimeTypeStringH264,
|
||||
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringH264,
|
||||
transceiverMimeType: mime.MimeTypeVP8,
|
||||
},
|
||||
{
|
||||
name: "audio",
|
||||
mediaKind: "audio",
|
||||
trackBaseCid: "preferPCMAAudio",
|
||||
preferredCodec: "pcma",
|
||||
addTrack: &livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
Name: "audio",
|
||||
Source: livekit.TrackSource_MICROPHONE,
|
||||
},
|
||||
mimeTypeStringChecker: mime.IsMimeTypeStringPCMA,
|
||||
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringPCMA,
|
||||
transceiverMimeType: mime.MimeTypeOpus,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(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 preferred track without client using setCodecPreferences()
|
||||
trackCid := fmt.Sprintf("%s-%d", tc.trackBaseCid, i)
|
||||
req := utils.CloneProto(tc.addTrack)
|
||||
req.SimulcastCodecs = []*livekit.SimulcastCodec{
|
||||
{
|
||||
Codec: tc.preferredCodec,
|
||||
Cid: trackCid,
|
||||
},
|
||||
}
|
||||
participant.AddTrack(req)
|
||||
|
||||
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: tc.transceiverMimeType.String()}, 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 tested preferred codec not preferred
|
||||
for tc.mimeTypeStringChecker(codecs[0].MimeType) {
|
||||
codecs = append(codecs[1:], codecs[0])
|
||||
}
|
||||
}
|
||||
// preferred codec should not be preferred in `offer`
|
||||
require.False(t, tc.mimeTypeStringChecker(codecs[0].MimeType), "codecs", codecs)
|
||||
|
||||
sdp, err := pc.CreateOffer(nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pc.SetLocalDescription(sdp))
|
||||
offerId := uint32(23)
|
||||
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
var answer webrtc.SessionDescription
|
||||
var answerId uint32
|
||||
var answerReceived atomic.Bool
|
||||
var answerIdReceived atomic.Uint32
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
if res.GetAnswer() != nil {
|
||||
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
|
||||
pc.SetRemoteDescription(answer)
|
||||
answerReceived.Store(true)
|
||||
answerIdReceived.Store(answerId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
participant.HandleOffer(&livekit.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer.String(),
|
||||
Sdp: sdp.SDP,
|
||||
Id: offerId,
|
||||
})
|
||||
|
||||
require.Eventually(t, func() bool { return answerReceived.Load() && answerIdReceived.Load() == offerId }, 5*time.Second, 10*time.Millisecond)
|
||||
|
||||
var havePreferred bool
|
||||
parsed, err := answer.Unmarshal()
|
||||
require.NoError(t, err)
|
||||
var mediaSectionIndex int
|
||||
for _, m := range parsed.MediaDescriptions {
|
||||
if m.MediaName.Media == tc.mediaKind {
|
||||
if mediaSectionIndex == i {
|
||||
codecs, err := lksdp.CodecsFromMediaDescription(m)
|
||||
require.NoError(t, err)
|
||||
if tc.mimeTypeCodecStringChecker(codecs[0].Name) {
|
||||
havePreferred = true
|
||||
break
|
||||
}
|
||||
}
|
||||
mediaSectionIndex++
|
||||
}
|
||||
}
|
||||
|
||||
require.Truef(t, havePreferred, "%s should be preferred for %s section %d, answer sdp: \n%s", tc.preferredCodec, tc.mediaKind, i, answer.SDP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreferAudioCodecForRed(t *testing.T) {
|
||||
participant := newParticipantForTestWithOpts("123", &participantOpts{
|
||||
publisher: true,
|
||||
})
|
||||
participant.SetMigrateState(types.MigrateStateComplete)
|
||||
|
||||
me := webrtc.MediaEngine{}
|
||||
opusCodecParameters := OpusCodecParameters
|
||||
opusCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
|
||||
require.NoError(t, me.RegisterCodec(opusCodecParameters, webrtc.RTPCodecTypeAudio))
|
||||
redCodecParameters := RedCodecParameters
|
||||
redCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
|
||||
require.NoError(t, me.RegisterCodec(redCodecParameters, webrtc.RTPCodecTypeAudio))
|
||||
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(&me))
|
||||
pc, err := api.NewPeerConnection(webrtc.Configuration{})
|
||||
require.NoError(t, err)
|
||||
defer pc.Close()
|
||||
|
||||
for idx, disableRed := range []bool{false, true, false, true} {
|
||||
t.Run(fmt.Sprintf("disableRed=%v", disableRed), func(t *testing.T) {
|
||||
trackCid := fmt.Sprintf("audiotrack%d", idx)
|
||||
req := &livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
Cid: trackCid,
|
||||
}
|
||||
if idx < 2 {
|
||||
req.DisableRed = disableRed
|
||||
} else {
|
||||
codec := "red"
|
||||
if disableRed {
|
||||
codec = "opus"
|
||||
}
|
||||
req.SimulcastCodecs = []*livekit.SimulcastCodec{
|
||||
{
|
||||
Codec: codec,
|
||||
Cid: trackCid,
|
||||
},
|
||||
}
|
||||
}
|
||||
participant.AddTrack(req)
|
||||
|
||||
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" {
|
||||
if 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)
|
||||
offerId := uint32(0xffffff)
|
||||
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
var answer webrtc.SessionDescription
|
||||
var answerId uint32
|
||||
var answerReceived atomic.Bool
|
||||
var answerIdReceived atomic.Uint32
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
if res.GetAnswer() != nil {
|
||||
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
|
||||
pc.SetRemoteDescription(answer)
|
||||
answerReceived.Store(true)
|
||||
answerIdReceived.Store(answerId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
participant.HandleOffer(&livekit.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer.String(),
|
||||
Sdp: sdp.SDP,
|
||||
Id: offerId,
|
||||
})
|
||||
require.Eventually(
|
||||
t,
|
||||
func() bool {
|
||||
return answerReceived.Load() && answerIdReceived.Load() == offerId
|
||||
},
|
||||
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 == idx {
|
||||
codecs, err := lksdp.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),
|
||||
Reporter: roomobs.NewNoopParticipantSessionReporter(),
|
||||
Telemetry: &telemetryfakes.FakeTelemetryService{},
|
||||
VersionGenerator: utils.NewDefaultTimedVersionGenerator(),
|
||||
ParticipantListener: &typesfakes.FakeLocalParticipantListener{},
|
||||
ParticipantHelper: &typesfakes.FakeLocalParticipantHelper{},
|
||||
})
|
||||
p.isPublisher.Store(opts.publisher)
|
||||
p.updateState(livekit.ParticipantInfo_ACTIVE)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func newParticipantForTest(identity livekit.ParticipantIdentity) *ParticipantImpl {
|
||||
return newParticipantForTestWithOpts(identity, nil)
|
||||
}
|
||||
408
livekit/pkg/rtc/participant_sdp.go
Normal file
408
livekit/pkg/rtc/participant_sdp.go
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
// 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"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
lksdp "github.com/livekit/protocol/sdp"
|
||||
"github.com/livekit/protocol/utils"
|
||||
)
|
||||
|
||||
func (p *ParticipantImpl) populateSdpCid(parsedOffer *sdp.SessionDescription) ([]*sdp.MediaDescription, []*sdp.MediaDescription) {
|
||||
processUnmatch := func(unmatches []*sdp.MediaDescription, trackType livekit.TrackType) {
|
||||
for _, unmatch := range unmatches {
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatch)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
sdpCodecs, err := lksdp.CodecsFromMediaDescription(unmatch)
|
||||
if err != nil || len(sdpCodecs) == 0 {
|
||||
p.pubLogger.Errorw(
|
||||
"extract codecs from media section failed", err,
|
||||
"media", unmatch,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
p.pendingTracksLock.Lock()
|
||||
signalCid, info, _, migrated, _ := p.getPendingTrack(streamID, trackType, false)
|
||||
if migrated {
|
||||
p.pendingTracksLock.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
p.pendingTracksLock.Unlock()
|
||||
|
||||
// could be already published track and the unmatch could be a back up codec publish
|
||||
numUnmatchedTracks := 0
|
||||
var unmatchedTrack types.MediaTrack
|
||||
var unmatchedSdpMimeType mime.MimeType
|
||||
|
||||
found := false
|
||||
for _, sdpCodec := range sdpCodecs {
|
||||
sdpMimeType := mime.NormalizeMimeTypeCodec(sdpCodec.Name).ToMimeType()
|
||||
for _, publishedTrack := range p.GetPublishedTracks() {
|
||||
if sigCid, sdpCid := publishedTrack.(*MediaTrack).GetCidsForMimeType(sdpMimeType); sigCid != "" && sdpCid == "" {
|
||||
// a back up codec has a SDP cid match
|
||||
if sigCid == streamID {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
numUnmatchedTracks++
|
||||
unmatchedTrack = publishedTrack
|
||||
unmatchedSdpMimeType = sdpMimeType
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && unmatchedTrack != nil {
|
||||
if numUnmatchedTracks != 1 {
|
||||
p.pubLogger.Warnw(
|
||||
"too many unmatched tracks", nil,
|
||||
"media", unmatch,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
}
|
||||
unmatchedTrack.(*MediaTrack).UpdateCodecSdpCid(unmatchedSdpMimeType, streamID)
|
||||
p.pubLogger.Debugw(
|
||||
"published track SDP cid updated",
|
||||
"trackID", unmatchedTrack.ID(),
|
||||
"track", logger.Proto(unmatchedTrack.ToProto()),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(info.Codecs) == 0 {
|
||||
p.pendingTracksLock.Unlock()
|
||||
p.pubLogger.Warnw(
|
||||
"track without codecs", nil,
|
||||
"trackID", info.Sid,
|
||||
"pendingTrack", p.pendingTracks[signalCid],
|
||||
"media", unmatch,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
found := false
|
||||
updated := false
|
||||
for _, sdpCodec := range sdpCodecs {
|
||||
if mime.NormalizeMimeTypeCodec(sdpCodec.Name) == mime.GetMimeTypeCodec(info.Codecs[0].MimeType) {
|
||||
// set SdpCid only if different from SignalCid
|
||||
if streamID != info.Codecs[0].Cid {
|
||||
info.Codecs[0].SdpCid = streamID
|
||||
updated = true
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// not using SimulcastCodec, i. e. mime type not available till track publish
|
||||
if len(info.Codecs) == 1 {
|
||||
// set SdpCid only if different from SignalCid
|
||||
if streamID != info.Codecs[0].Cid {
|
||||
info.Codecs[0].SdpCid = streamID
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
p.pendingTracks[signalCid].trackInfos[0] = utils.CloneProto(info)
|
||||
p.pubLogger.Debugw(
|
||||
"pending track SDP cid updated",
|
||||
"signalCid", signalCid,
|
||||
"trackID", info.Sid,
|
||||
"pendingTrack", p.pendingTracks[signalCid],
|
||||
)
|
||||
}
|
||||
p.pendingTracksLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
unmatchAudios, err := p.TransportManager.GetUnmatchMediaForOffer(parsedOffer, "audio")
|
||||
if err != nil {
|
||||
p.pubLogger.Warnw("could not get unmatch audios", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
unmatchVideos, err := p.TransportManager.GetUnmatchMediaForOffer(parsedOffer, "video")
|
||||
if err != nil {
|
||||
p.pubLogger.Warnw("could not get unmatch videos", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
processUnmatch(unmatchAudios, livekit.TrackType_AUDIO)
|
||||
processUnmatch(unmatchVideos, livekit.TrackType_VIDEO)
|
||||
return unmatchAudios, unmatchVideos
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesForPublisher(
|
||||
parsedOffer *sdp.SessionDescription,
|
||||
unmatchAudios []*sdp.MediaDescription,
|
||||
unmatchVideos []*sdp.MediaDescription,
|
||||
) {
|
||||
unprocessedUnmatchAudios := p.setCodecPreferencesForPublisherMedia(
|
||||
parsedOffer,
|
||||
unmatchAudios,
|
||||
livekit.TrackType_AUDIO,
|
||||
)
|
||||
p.setCodecPreferencesOpusRedForPublisher(parsedOffer, unprocessedUnmatchAudios)
|
||||
_ = p.setCodecPreferencesForPublisherMedia(
|
||||
parsedOffer,
|
||||
unmatchVideos,
|
||||
livekit.TrackType_VIDEO,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesForPublisherMedia(
|
||||
parsedOffer *sdp.SessionDescription,
|
||||
unmatches []*sdp.MediaDescription,
|
||||
trackType livekit.TrackType,
|
||||
) []*sdp.MediaDescription {
|
||||
unprocessed := make([]*sdp.MediaDescription, 0, len(unmatches))
|
||||
for _, unmatch := range unmatches {
|
||||
var ti *livekit.TrackInfo
|
||||
var mimeType string
|
||||
|
||||
mid := lksdp.GetMidValue(unmatch)
|
||||
if mid == "" {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
transceiver := p.TransportManager.GetPublisherRTPTransceiver(mid)
|
||||
if transceiver == nil {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatch)
|
||||
if !ok {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
p.pendingTracksLock.RLock()
|
||||
mt := p.getPublishedTrackBySdpCid(streamID)
|
||||
if mt != nil {
|
||||
ti = mt.ToProto()
|
||||
} else {
|
||||
_, ti, _, _, _ = p.getPendingTrack(streamID, trackType, false)
|
||||
}
|
||||
p.pendingTracksLock.RUnlock()
|
||||
|
||||
if ti == nil {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == streamID || c.SdpCid == streamID {
|
||||
mimeType = c.MimeType
|
||||
break
|
||||
}
|
||||
}
|
||||
if mimeType == "" && len(ti.Codecs) > 0 {
|
||||
mimeType = ti.Codecs[0].MimeType
|
||||
}
|
||||
|
||||
if mimeType == "" {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
configureReceiverCodecs(
|
||||
transceiver,
|
||||
mimeType,
|
||||
p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer(),
|
||||
)
|
||||
}
|
||||
|
||||
return unprocessed
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(
|
||||
parsedOffer *sdp.SessionDescription,
|
||||
unmatchAudios []*sdp.MediaDescription,
|
||||
) {
|
||||
for _, unmatchAudio := range unmatchAudios {
|
||||
mid := lksdp.GetMidValue(unmatchAudio)
|
||||
if mid == "" {
|
||||
continue
|
||||
}
|
||||
transceiver := p.TransportManager.GetPublisherRTPTransceiver(mid)
|
||||
if transceiver == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatchAudio)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
p.pendingTracksLock.RLock()
|
||||
_, ti, _, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
|
||||
p.pendingTracksLock.RUnlock()
|
||||
if ti == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
codecs, err := lksdp.CodecsFromMediaDescription(unmatchAudio)
|
||||
if err != nil {
|
||||
p.pubLogger.Errorw(
|
||||
"extract codecs from media section failed", err,
|
||||
"media", unmatchAudio,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
var opusPayload uint8
|
||||
for _, codec := range codecs {
|
||||
if mime.IsMimeTypeCodecStringOpus(codec.Name) {
|
||||
opusPayload = codec.PayloadType
|
||||
break
|
||||
}
|
||||
}
|
||||
if opusPayload == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
preferRED := IsRedEnabled(ti)
|
||||
// if RED is enabled for this track, prefer RED codec in offer
|
||||
for _, codec := range codecs {
|
||||
// codec contain opus/red
|
||||
if preferRED &&
|
||||
mime.IsMimeTypeCodecStringRED(codec.Name) &&
|
||||
strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
|
||||
configureReceiverCodecs(transceiver, "audio/red", true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
parsedAnswer, err := answer.Unmarshal()
|
||||
if err != nil {
|
||||
return answer
|
||||
}
|
||||
|
||||
for _, m := range parsedAnswer.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 {
|
||||
// no need to configure
|
||||
continue
|
||||
}
|
||||
|
||||
opusPT, err := parsedAnswer.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 !slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_NO_DTX) {
|
||||
attr.Value += ";usedtx=1"
|
||||
} else {
|
||||
attr.Value = strings.ReplaceAll(attr.Value, ";usedtx=1", "")
|
||||
}
|
||||
if slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO) {
|
||||
attr.Value += ";stereo=1;maxaveragebitrate=510000"
|
||||
} else {
|
||||
attr.Value = strings.ReplaceAll(attr.Value, ";stereo=1", "")
|
||||
}
|
||||
m.Attributes[i] = attr
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := parsedAnswer.Marshal()
|
||||
if err != nil {
|
||||
p.pubLogger.Infow("failed to marshal answer", "error", err)
|
||||
return answer
|
||||
}
|
||||
answer.SDP = string(bytes)
|
||||
return answer
|
||||
}
|
||||
370
livekit/pkg/rtc/participant_signal.go
Normal file
370
livekit/pkg/rtc/participant_signal.go
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
protosignalling "github.com/livekit/protocol/signalling"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
func (p *ParticipantImpl) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
|
||||
p.signaller.SwapResponseSink(sink, reason)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) GetResponseSink() routing.MessageSink {
|
||||
return p.signaller.GetResponseSink()
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) CloseSignalConnection(reason types.SignallingCloseReason) {
|
||||
p.signaller.CloseSignalConnection(reason)
|
||||
}
|
||||
|
||||
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.signaller.WriteMessage(p.signalling.SignalJoinResponse(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.ID()) {
|
||||
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()
|
||||
|
||||
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSpeakerUpdate(scopedSpeakers))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendRoomUpdate(room *livekit.Room) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRoomUpdate(room))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalConnectionQualityUpdate(update))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendRefreshToken(token string) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRefreshToken(token))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendRequestResponse(requestResponse *livekit.RequestResponse) error {
|
||||
if !p.params.ClientInfo.SupportsRequestResponse() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if requestResponse.Reason == livekit.RequestResponse_OK && !p.ProtocolVersion().SupportsNonErrorSignalResponse() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRequestResponse(requestResponse))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendRoomMovedResponse(roomMovedResponse *livekit.RoomMovedResponse) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRoomMovedResponse(roomMovedResponse))
|
||||
}
|
||||
|
||||
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.signaller.WriteMessage(p.signalling.SignalReconnectResponse(reconnectResponse)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.params.ProtocolVersion.SupportsDisconnectedUpdate() {
|
||||
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()
|
||||
|
||||
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(disconnectedParticipants))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livekit.SignalTarget) error {
|
||||
prevIC := p.icQueue[target].Swap(ic)
|
||||
if prevIC == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
trickle := protosignalling.ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
|
||||
p.params.Logger.Debugw("sending ICE candidate", "transport", target, "trickle", logger.Proto(trickle))
|
||||
|
||||
return p.signaller.WriteMessage(p.signalling.SignalICECandidate(trickle))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackMuted(trackID livekit.TrackID, muted bool) {
|
||||
_ = p.signaller.WriteMessage(p.signalling.SignalTrackMuted(&livekit.MuteTrackRequest{
|
||||
Sid: string(trackID),
|
||||
Muted: muted,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackPublished(cid string, ti *livekit.TrackInfo) error {
|
||||
p.pubLogger.Debugw("sending track published", "cid", cid, "trackInfo", logger.Proto(ti))
|
||||
return p.signaller.WriteMessage(p.signalling.SignalTrackPublished(&livekit.TrackPublishedResponse{
|
||||
Cid: cid,
|
||||
Track: ti,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackUnpublished(trackID livekit.TrackID) {
|
||||
_ = p.signaller.WriteMessage(p.signalling.SignalTrackUnpublished(&livekit.TrackUnpublishedResponse{
|
||||
TrackSid: string(trackID),
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackHasBeenSubscribed(trackID livekit.TrackID) {
|
||||
if !p.params.ClientInfo.SupportsTrackSubscribedEvent() {
|
||||
return
|
||||
}
|
||||
_ = p.signaller.WriteMessage(p.signalling.SignalTrackSubscribed(&livekit.TrackSubscribed{
|
||||
TrackSid: string(trackID),
|
||||
}))
|
||||
p.params.Logger.Debugw("track has been subscribed", "trackID", trackID)
|
||||
}
|
||||
|
||||
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 {
|
||||
// sending region settings even for RESUME just in case client wants to a full reconnect despite server saying RESUME
|
||||
leave.Regions = p.helper().GetRegionSettings(p.params.ClientInfo.Address)
|
||||
}
|
||||
} else {
|
||||
if !sendOnlyIfSupportingLeaveRequestWithAction {
|
||||
leave = &livekit.LeaveRequest{
|
||||
CanReconnect: isExpectedToReconnect,
|
||||
Reason: reason.ToDisconnectReason(),
|
||||
}
|
||||
}
|
||||
}
|
||||
if leave != nil {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalLeaveRequest(leave))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSdpAnswer(answer webrtc.SessionDescription, answerId uint32, midToTrackID map[string]string) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSdpAnswer(protosignalling.ToProtoSessionDescription(answer, answerId, midToTrackID)))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSdpOffer(offer webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSdpOffer(protosignalling.ToProtoSessionDescription(offer, offerId, midToTrackID)))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalStreamStateUpdate(streamStateUpdate))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSubscribedQualityUpdate(subscribedQualityUpdate))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSubscriptionResponse(trackID livekit.TrackID, subErr livekit.SubscriptionError) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSubscriptionResponse(&livekit.SubscriptionResponse{
|
||||
TrackSid: string(trackID),
|
||||
Err: subErr,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendSubscriptionPermissionUpdate(publisherID livekit.ParticipantID, trackID livekit.TrackID, allowed bool) error {
|
||||
p.subLogger.Debugw("sending subscription permission update", "publisherID", publisherID, "trackID", trackID, "allowed", allowed)
|
||||
err := p.signaller.WriteMessage(p.signalling.SignalSubscriptionPermissionUpdate(&livekit.SubscriptionPermissionUpdate{
|
||||
ParticipantSid: string(publisherID),
|
||||
TrackSid: string(trackID),
|
||||
Allowed: allowed,
|
||||
}))
|
||||
if err != nil {
|
||||
p.subLogger.Errorw("could not send subscription permission update", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendMediaSectionsRequirement(numAudios uint32, numVideos uint32) error {
|
||||
p.pubLogger.Debugw(
|
||||
"sending media sections requirement",
|
||||
"numAudios", numAudios,
|
||||
"numVideos", numVideos,
|
||||
)
|
||||
err := p.signaller.WriteMessage(p.signalling.SignalMediaSectionsRequirement(&livekit.MediaSectionsRequirement{
|
||||
NumAudios: numAudios,
|
||||
NumVideos: numVideos,
|
||||
}))
|
||||
if err != nil {
|
||||
p.subLogger.Errorw("could not send media sections requirement", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendPublishDataTrackResponse(dti *livekit.DataTrackInfo) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalPublishDataTrackResponse(&livekit.PublishDataTrackResponse{
|
||||
Info: dti,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendUnpublishDataTrackResponse(dti *livekit.DataTrackInfo) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalUnpublishDataTrackResponse(&livekit.UnpublishDataTrackResponse{
|
||||
Info: dti,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendDataTrackSubscriberHandles(handles map[uint32]*livekit.DataTrackSubscriberHandles_PublishedDataTrack) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalDataTrackSubscriberHandles(&livekit.DataTrackSubscriberHandles{
|
||||
SubHandles: handles,
|
||||
}))
|
||||
}
|
||||
2296
livekit/pkg/rtc/room.go
Normal file
2296
livekit/pkg/rtc/room.go
Normal file
File diff suppressed because it is too large
Load diff
860
livekit/pkg/rtc/room_test.go
Normal file
860
livekit/pkg/rtc/room_test.go
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
// 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/auth/authfakes"
|
||||
"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.LocalParticipantListener())
|
||||
|
||||
_ = 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})
|
||||
lpl := rm.LocalParticipantListener()
|
||||
p := NewMockParticipant("new", types.CurrentProtocol, false, false, lpl)
|
||||
|
||||
err := rm.Join(p, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
|
||||
require.NoError(t, err)
|
||||
|
||||
p.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
||||
lpl.OnStateChange(p)
|
||||
|
||||
// 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.Participant) {
|
||||
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, rm.LocalParticipantListener())
|
||||
|
||||
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(&livekit.MuteTrackRequest{Muted: 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{{ParticipantInfo: publisher1v1}},
|
||||
},
|
||||
{
|
||||
name: "subscriber updates are queued",
|
||||
pi: subscriber1v1,
|
||||
},
|
||||
{
|
||||
name: "last version is enqueued",
|
||||
pi: subscriber1v2,
|
||||
existing: &ParticipantUpdate{ParticipantInfo: 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.ParticipantInfo)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "latest version when immediate",
|
||||
pi: subscriber1v2,
|
||||
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
|
||||
immediate: true,
|
||||
expected: []*ParticipantUpdate{{ParticipantInfo: 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{ParticipantInfo: utils.CloneProto(subscriber1v2)},
|
||||
validate: func(t *testing.T, rm *Room, updates []*ParticipantUpdate) {
|
||||
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
|
||||
requirePIEquals(t, subscriber1v2, queued.ParticipantInfo)
|
||||
},
|
||||
},
|
||||
{
|
||||
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{ParticipantInfo: utils.CloneProto(subscriber1v2), CloseReason: types.ParticipantCloseReasonStale},
|
||||
expected: []*ParticipantUpdate{
|
||||
{
|
||||
ParticipantInfo: &livekit.ParticipantInfo{
|
||||
Identity: identity,
|
||||
Sid: "1",
|
||||
Version: 2,
|
||||
State: livekit.ParticipantInfo_DISCONNECTED,
|
||||
},
|
||||
IsSynthesizedDisconnect: true,
|
||||
CloseReason: types.ParticipantCloseReasonStale,
|
||||
},
|
||||
{ParticipantInfo: publisher2, CloseReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when switching to publisher, queue is cleared",
|
||||
pi: publisher1v2,
|
||||
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
|
||||
expected: []*ParticipantUpdate{{ParticipantInfo: 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.ParticipantInfo.Identity)] = tc.existing
|
||||
}
|
||||
rm.batchedUpdatesMu.Lock()
|
||||
updates := PushAndDequeueUpdates(
|
||||
tc.pi,
|
||||
tc.closeReason,
|
||||
tc.immediate,
|
||||
rm.GetParticipant(livekit.ParticipantIdentity(tc.pi.Identity)),
|
||||
rm.batchedUpdates,
|
||||
)
|
||||
rm.batchedUpdatesMu.Unlock()
|
||||
require.Equal(t, len(tc.expected), len(updates))
|
||||
for i, item := range tc.expected {
|
||||
requirePIEquals(t, item.ParticipantInfo, updates[i].ParticipantInfo)
|
||||
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})
|
||||
lpl := rm.LocalParticipantListener()
|
||||
|
||||
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")
|
||||
lpl.OnTrackPublished(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 := range numCalls {
|
||||
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)
|
||||
|
||||
lpl := rm.LocalParticipantListener()
|
||||
|
||||
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)
|
||||
lpl.OnDataMessage(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.SendDataMessageCallCount())
|
||||
continue
|
||||
}
|
||||
require.Equal(t, 1, fp.SendDataMessageCallCount())
|
||||
_, got, _, _ := fp.SendDataMessageArgsForCall(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)
|
||||
|
||||
lpl := rm.LocalParticipantListener()
|
||||
|
||||
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)
|
||||
lpl.OnDataMessage(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.SendDataMessageCallCount())
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, p1.SendDataMessageCallCount())
|
||||
_, got, _, _ := p1.SendDataMessageArgsForCall(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() {
|
||||
lpl := rm.LocalParticipantListener()
|
||||
lpl.OnDataMessage(p, packet.Kind, &packet)
|
||||
}
|
||||
|
||||
// no one should've been sent packet
|
||||
for _, op := range participants {
|
||||
fp := op.(*typesfakes.FakeLocalParticipant)
|
||||
require.Zero(t, fp.SendDataMessageCallCount())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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.LocalParticipantListener())
|
||||
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})
|
||||
lpl := rm.LocalParticipantListener()
|
||||
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false, lpl)
|
||||
|
||||
err := rm.Join(hidden, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
|
||||
require.NoError(t, err)
|
||||
|
||||
hidden.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
||||
lpl.OnStateChange(hidden)
|
||||
|
||||
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, rm.LocalParticipantListener())
|
||||
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 {
|
||||
kp := &authfakes.FakeKeyProvider{}
|
||||
kp.GetSecretReturns("testkey")
|
||||
|
||||
n, err := webhook.NewDefaultNotifier(webhook.DefaultWebHookConfig, kp)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(n, &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, rm.LocalParticipantListener())
|
||||
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
|
||||
}
|
||||
263
livekit/pkg/rtc/roomtrackmanager.go
Normal file
263
livekit/pkg/rtc/roomtrackmanager.go
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* 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"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
// RoomTrackManager holds tracks that are published to the room
|
||||
type RoomTrackManager struct {
|
||||
logger logger.Logger
|
||||
|
||||
lock sync.RWMutex
|
||||
changedNotifier *utils.ChangeNotifierManager
|
||||
removedNotifier *utils.ChangeNotifierManager
|
||||
tracks map[livekit.TrackID][]*TrackInfo
|
||||
dataTracks map[livekit.TrackID][]*DataTrackInfo
|
||||
}
|
||||
|
||||
type TrackInfo struct {
|
||||
Track types.MediaTrack
|
||||
PublisherIdentity livekit.ParticipantIdentity
|
||||
PublisherID livekit.ParticipantID
|
||||
}
|
||||
|
||||
type DataTrackInfo struct {
|
||||
DataTrack types.DataTrack
|
||||
PublisherIdentity livekit.ParticipantIdentity
|
||||
PublisherID livekit.ParticipantID
|
||||
}
|
||||
|
||||
func NewRoomTrackManager(logger logger.Logger) *RoomTrackManager {
|
||||
return &RoomTrackManager{
|
||||
logger: logger,
|
||||
tracks: make(map[livekit.TrackID][]*TrackInfo),
|
||||
dataTracks: make(map[livekit.TrackID][]*DataTrackInfo),
|
||||
changedNotifier: utils.NewChangeNotifierManager(),
|
||||
removedNotifier: utils.NewChangeNotifierManager(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) AddTrack(track types.MediaTrack, publisherIdentity livekit.ParticipantIdentity, publisherID livekit.ParticipantID) {
|
||||
trackID := track.ID()
|
||||
r.lock.Lock()
|
||||
infos, ok := r.tracks[trackID]
|
||||
if ok {
|
||||
for _, info := range infos {
|
||||
if info.Track == track {
|
||||
r.lock.Unlock()
|
||||
r.logger.Infow("not adding duplicate track", "trackID", trackID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
r.tracks[trackID] = append(r.tracks[trackID], &TrackInfo{
|
||||
Track: track,
|
||||
PublisherIdentity: publisherIdentity,
|
||||
PublisherID: publisherID,
|
||||
})
|
||||
r.lock.Unlock()
|
||||
|
||||
r.NotifyTrackChanged(trackID)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) RemoveTrack(track types.MediaTrack) {
|
||||
trackID := track.ID()
|
||||
r.lock.Lock()
|
||||
// ensure we are removing the same track as added
|
||||
infos, ok := r.tracks[trackID]
|
||||
if !ok {
|
||||
r.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
numRemoved := 0
|
||||
idx := 0
|
||||
for _, info := range infos {
|
||||
if info.Track == track {
|
||||
numRemoved++
|
||||
} else {
|
||||
r.tracks[trackID][idx] = info
|
||||
idx++
|
||||
}
|
||||
}
|
||||
for j := idx; j < len(infos); j++ {
|
||||
r.tracks[trackID][j] = nil
|
||||
}
|
||||
r.tracks[trackID] = r.tracks[trackID][:idx]
|
||||
if len(r.tracks[trackID]) == 0 {
|
||||
delete(r.tracks, trackID)
|
||||
}
|
||||
r.lock.Unlock()
|
||||
if numRemoved == 0 {
|
||||
return
|
||||
}
|
||||
if numRemoved > 1 {
|
||||
r.logger.Warnw("removed multiple tracks", nil, "trackID", trackID, "numRemoved", numRemoved)
|
||||
}
|
||||
|
||||
n := r.removedNotifier.GetNotifier(string(trackID))
|
||||
if n != nil {
|
||||
n.NotifyChanged()
|
||||
}
|
||||
|
||||
r.changedNotifier.RemoveNotifier(string(trackID), true)
|
||||
r.removedNotifier.RemoveNotifier(string(trackID), true)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) GetTrackInfo(trackID livekit.TrackID) *TrackInfo {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
infos := r.tracks[trackID]
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// earliest added track is used till it is removed
|
||||
info := infos[0]
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) AddDataTrack(dataTrack types.DataTrack, publisherIdentity livekit.ParticipantIdentity, publisherID livekit.ParticipantID) {
|
||||
trackID := dataTrack.ID()
|
||||
r.lock.Lock()
|
||||
infos, ok := r.dataTracks[trackID]
|
||||
if ok {
|
||||
for _, info := range infos {
|
||||
if info.DataTrack == dataTrack {
|
||||
r.lock.Unlock()
|
||||
r.logger.Infow("not adding duplicate data track", "trackID", trackID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
r.dataTracks[trackID] = append(r.dataTracks[trackID], &DataTrackInfo{
|
||||
DataTrack: dataTrack,
|
||||
PublisherIdentity: publisherIdentity,
|
||||
PublisherID: publisherID,
|
||||
})
|
||||
r.lock.Unlock()
|
||||
|
||||
r.NotifyTrackChanged(trackID)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) RemoveDataTrack(dataTrack types.DataTrack) {
|
||||
trackID := dataTrack.ID()
|
||||
r.lock.Lock()
|
||||
// ensure we are removing the same track as added
|
||||
infos, ok := r.dataTracks[trackID]
|
||||
if !ok {
|
||||
r.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
numRemoved := 0
|
||||
idx := 0
|
||||
for _, info := range infos {
|
||||
if info.DataTrack == dataTrack {
|
||||
numRemoved++
|
||||
} else {
|
||||
r.dataTracks[trackID][idx] = info
|
||||
idx++
|
||||
}
|
||||
}
|
||||
for j := idx; j < len(infos); j++ {
|
||||
r.dataTracks[trackID][j] = nil
|
||||
}
|
||||
r.dataTracks[trackID] = r.dataTracks[trackID][:idx]
|
||||
if len(r.dataTracks[trackID]) == 0 {
|
||||
delete(r.dataTracks, trackID)
|
||||
}
|
||||
r.lock.Unlock()
|
||||
if numRemoved == 0 {
|
||||
return
|
||||
}
|
||||
if numRemoved > 1 {
|
||||
r.logger.Warnw("removed multiple data tracks", nil, "trackID", trackID, "numRemoved", numRemoved)
|
||||
}
|
||||
|
||||
n := r.removedNotifier.GetNotifier(string(trackID))
|
||||
if n != nil {
|
||||
n.NotifyChanged()
|
||||
}
|
||||
|
||||
r.changedNotifier.RemoveNotifier(string(trackID), true)
|
||||
r.removedNotifier.RemoveNotifier(string(trackID), true)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) GetDataTrackInfo(trackID livekit.TrackID) *DataTrackInfo {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
infos := r.dataTracks[trackID]
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// earliest added data track is used till it is removed
|
||||
return infos[0]
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) Report() (int, int) {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
return len(r.tracks), len(r.dataTracks)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue