Merge commit '6bd7fac875d9e9009915053d8a590abb372c5679' into feature/livekit-upgrade-1.13.1

This commit is contained in:
2026-06-25 23:54:33 +09:00
291 changed files with 37631 additions and 15381 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

+13 -11
View File
@@ -14,6 +14,9 @@
name: Test
permissions:
contents: read
on:
workflow_dispatch:
push:
@@ -25,17 +28,17 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shogo82148/actions-setup-redis@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: shogo82148/actions-setup-redis@2f3253b148c73d7a0682eae73e862b777a4fa74e # v1
with:
redis-version: "6.x"
auto-start: true
- run: redis-cli ping
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: "go.mod"
go-version: "^1.26"
- name: Set up gotestfmt
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.4.1
@@ -45,22 +48,21 @@ jobs:
go get github.com/sasha-s/go-deadlock
grep -rl sync.Mutex ./pkg | xargs sed -i 's/sync\.Mutex/deadlock\.Mutex/g'
grep -rl sync.RWMutex ./pkg | xargs sed -i 's/sync\.RWMutex/deadlock\.RWMutex/g'
go install golang.org/x/tools/cmd/goimports
go install golang.org/x/tools/cmd/goimports@latest
grep -rl deadlock.Mutex ./pkg | xargs goimports -w
grep -rl deadlock.RWMutex ./pkg | xargs goimports -w
go mod tidy
- name: Mage Build
uses: magefile/mage-action@v3
uses: magefile/mage-action@a662bd8c29d8106879588cfff83b2faf6e6f59db # v4
with:
version: latest
args: build
- name: Static Check
uses: amarpal/staticcheck-action@master
- name: Lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
checks: '["all", "-ST1000", "-ST1003", "-ST1020", "-ST1021", "-ST1022", "-SA1019"]'
install-go: false
version: v2.11.4
- name: Test
run: |
@@ -69,7 +71,7 @@ jobs:
# Upload the original go test log as an artifact for later review.
- name: Upload test log
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: test-log
+10 -7
View File
@@ -14,6 +14,9 @@
name: Release to Docker
permissions:
contents: read
# Controls when the action will run.
on:
workflow_dispatch:
@@ -26,10 +29,10 @@ jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
# list of Docker images to use as base name for tags
images: |
@@ -41,7 +44,7 @@ jobs:
type=semver,pattern=v{{major}}.{{minor}}
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: "go.mod"
@@ -49,24 +52,24 @@ jobs:
run: go mod download
- name: Generate code
uses: magefile/mage-action@v3
uses: magefile/mage-action@a662bd8c29d8106879588cfff83b2faf6e6f59db # v4
with:
version: latest
args: generate
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v6
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
+3 -3
View File
@@ -28,17 +28,17 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Fetch all tags
run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: "go.mod"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
with:
distribution: goreleaser
version: '~> v2'
+25
View File
@@ -0,0 +1,25 @@
name: PR Slack Notifier
on:
pull_request:
types: [review_requested, reopened, closed]
pull_request_review:
types: [submitted]
permissions:
contents: read
pull-requests: write
issues: write
concurrency:
group: pr-slack-${{ github.event.pull_request.number }}-${{ github.workflow }}
cancel-in-progress: false
jobs:
notify-devs:
runs-on: ubuntu-latest
steps:
- uses: livekit/slack-notifier-action@main
with:
config_json: ${{ secrets.SLACK_NOTIFY_CONFIG_JSON }}
slack_token: ${{ secrets.SLACK_PR_NOTIFIER_TOKEN }}
+25
View File
@@ -0,0 +1,25 @@
version: "2"
linters:
default: none
enable:
- depguard
- staticcheck
settings:
depguard:
rules:
protojson:
deny:
- pkg: google.golang.org/protobuf/encoding/protojson
desc: use github.com/livekit/protocol/utils/protojson instead — it sets DiscardUnknown by default to avoid schema-drift footguns
staticcheck:
checks:
- "all"
- "-ST1000"
- "-ST1003"
- "-ST1020"
- "-ST1021"
- "-ST1022"
- "-SA1019"
- "-QF1008"
exclusions:
generated: strict
+4 -1
View File
@@ -53,7 +53,10 @@ changelog:
- '^docs:'
- '^test:'
gomod:
proxy: true
# proxy must be disabled: it re-fetches modules from the Go proxy in a clean
# directory, which does not honor the replace directive in go.mod
# (replace github.com/pion/dtls/v3 => ... pinning v3.1.2 for gst whipsink compat)
proxy: false
mod: mod
checksum:
name_template: 'checksums.txt'
+681
View File
@@ -2,6 +2,687 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.13.1] - 2026-06-08
### Fixed
- ci: disable goreleaser gomod proxy, incompatible with go.mod replace directive
## [1.13.0] - 2026-06-08
## ATTENTION: This release removes backwards compatibility for TURN authentication without TTL. Please refer to change log of [1.12.0](https://github.com/livekit/livekit/releases/tag/v1.12.0) for details on changes to TURN authentication and permissions handling. It was removed in https://github.com/livekit/livekit/pull/4539.
### Added
- rtc: report participant kind code and details (#4534)
- Add IsIntentionalDisconnect (#4537)
- rtc: emit per-data-track bytes via BytesTrackStats (#4540)
- Start tracking Twirp method request latency in prometheus too, not just in logs (#4545)
- Config documentation for advertise_internal_ip and skip_external_ip_validation (#4552, #4554, #4563)
- Metrics for participant active, i. e. fully established. (#4557)
- turn: allow for providing secret via file (#4564)
### Changed
- Always enable rtx codec (#4533)
- Don't require media sections when joining (#4535)
- Use NACKQuueInterface type. (#4538)
- Remove backwards compatibility support for TURN auth. (#4539)
- telemetry: split webhook-processed hook out of NewTelemetryService (#4548)
- rtc: prevent duration reporting for inactive participants (#4550)
- rtc: report participant session end time on room move (#4561)
- Update mediatransportutil to get ICE candidate timeout config (#4572)
### Fixed
- Check Less and LessEq in version compare. (#4532)
- fix sip error categorization (#4528)
- handle nil clientInfo (#4546, #4547)
- Prevent panic from nil(illegal) syncState.Subscriptions message (#4560)
## [1.12.0] - 2026-05-16
## ATTENTION: This release introduces important changes to how TURN authentication and permissions are handled. These changes make the system more secure. This release maintains backwards compatibility. However, backwards compatibility will be removed in the next release. So, please plan accordingly.
### TURN permission handling changes
By default, TURN will not relay traffic to private IPs. If you need to relay traffic to private IPs, please use `allow_restricted_peer_cidrs` which is explained in config-sample.yaml and replicated below.
```
# # list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to.
# # By default (i. e. empty list), all restricted peer CIDRs are denied access.
# # When not empty, only the specified CIDRs are allowed access.
# # Note that this check is applied to restricted peer CIDRs only.
# allow_restricted_peer_cidrs:
# - 10.0.0.0/8
# - 192.168.0.0/16
```
That list can be paired with a deny list which takes precedence if there is an overlap.
```
# # list of peer CIDRs to deny access to.
# # This applies to all peer CIDRs, including restricted ones.
# # Deny list takes precedence over allow list.
# deny_peer_cidrs:
# - 10.0.0.0/8
# - 192.168.0.0/16
```
Relevant PRs: #4505
### TURN authentication handling changes
TURN credentials now have a TTL (Time-To-Live) beyond which they are not valid to join a room. We recommend rotating your TURN authentication secrets to ensure older credentials are rendered stale and not usable.
```
# # TTL of the TURN credentials in seconds - defaults to 300
# ttl_seconds: 300
```
Please note that the TTL is always used in constructing the credentials. So, a value of 0 will create credentials that expire immediately.
Relevant PRs: #4505, #4515, #4524, #4526
### Added
- add support for client capabilities (#4461)
- Additional data tracks logging (#4489)
- Add CloseWithReason to agent SignalConn interface (#4492)
- add agent environment (#4498)
- add duration seconds reporting (#4500)
- add helper to check for agent worker endpoint (#4503)
- test: verify upstream and downstream connection stats end-to-end (#4508)
- Add TURN permission handler. (#4505)
- allow setting agent job assignment url (#4512)
- Add expiry to TURN password. (#4505, #4515)
- add AssignmentHook to AssignJob; propagate websocket write errors (#4516)
- Apply room tags from JWT grant room configuration (#4518)
- feat: auto create rooms for tokens with the RoomCreate grant (#4320)
- Add integration test for TURN auth failures (#4524)
- Support SIP auth realm for inbound. (#4522)
- Apply ttl check only when authenticate allocation creating (#4526)
### Changed
- feat(pion/ice): replace deprecated NAT1To1 with SetAddressRewriteRules (#4466)
- do not log data track stats if not started (#4468)
- Consolidate RTCP packets and do RTCP callback outside lock. (#4469)
- Keep a shadow copy of tracks for use by different stream allocator state (#4470)
- Avoid stream allocator event data cast to interface and back. (#4471)
- Convert sort.Slice -> slices.SortFunc (#4472)
- Turn off transceiver re-use on Safari. (#4474)
- reduce some heap use in packet path by (#4478)
- Close peer connection unconditionally to unblock set local/remote (#4485)
- Misc optimisations. (#4490)
- report all simulcast layers (#4491)
- Use mediatransportutil/codec package, no functional change (#4497)
- rename agent environment to deployment (#4506)
- Update protocol to support SIP media config. (#4509)
- update protocol for protojson (#4510)
- Populate data track loggers with context (#4514)
- Log large packets receive/send. (#4521)
- Log details of RTCP packets. (#4525)
- Create NACK tracker only once. (#4527)
### Fixed
- fix: wrap IPv6 addresses in brackets in UDP TURN URLs (RFC 3986) (#4476)
- Legacy TrackInfo.Simulcast flag. (#4493)
- Fix publish-only limitations being incorrectly applied to receivers (#4495)
- Include reception reoprts in receiver report callback. (#4496)
- Fix sense check in DeltaInfo gathering (#4507)
- Fix SIP media config upgrade. (#4511)
## [1.11.0] - 2026-04-17
NOTE: Minor version bump that enables data tracks (https://docs.livekit.io/transport/data/data-tracks/) by default.
### Added
- Embedded turn test (#4412)
- chore: log API key during worker registration (#4428)
- Add some simple data track stats (#4431)
- Add `Close` method for UpDataTrackManager and call it on participant (#4432)
- Log join duration. (#4433)
- Add subscriber stream start event notification (#4449)
### Changed
- Cleaning up some logs and standardising log frequency. (#4420)
- Keep subscription synchronous when publisher is expected to resume. (#4424, #4425)
- Do not close publisher peer connection to aid migration. (#4426, #4427)
- Enable data tracks by default. (#4429)
- chore: pin GH commits and switch to golangci-lint (#4444)
- Switch to stdlib maps, slices (#4445)
- Store concrete ICE candidate for remote candidates. (#4458)
### Fixed
- clear track notifier observers on subscription teardown (#4413)
- Guard against timestamp inversion in RED -> Opus conversion. (#4414, #4415, #4418)
- ensure participant init is correctly serialized for logging (#4417)
- Clean up data track observers on unsubscribe. (#4421)
- compute agent dispatch affinity from target load (#4442)
- Apply IPFilter when get local ip (#4440)
- Unsubscribe from data track on close (#4443)
- Use Muted in TrackInfo to propagated published track muted. (#4453)
- fix: limit join request and WHIP request body to http.DefaultMaxHeaderBytes (#4450)
- fix publisher frame count reporting for simulcast streams (#4457)
## [1.10.1] - 2026-03-30
### Added
- add packet trailer stripping support (#4361)
- Path check helpers (#4392)
- add deadline to dtls connect context (#4395)
- feat(agent-dispatch): add job restart policy (#4401)
### Changed
- Close both peer connections to aid migration. (#4382)
### Fixed
- Fix TURN server URL (#4389)
- AV1 parser overflow fix. (#4405)
- Address malformed H264/H265 parsing issues (#4407)
## [1.10.0] - 2026-03-23
## PLEASE NOTE: The logging key for participant session ID (a.k.a participant SID) has been changed from `pID` to `participantID` in this release for the sake of clarity. Hence the minor version bump.
### Added
- Add option to require media sections when participant joining (#4347, #4354)
- Support originating calls from custom domains (#4349)
- Add StopEgress function to the EgressLauncher interface (#4353)
- Add option to not re-use transceiver in e2ee. (#4356)
- Add API to restart lite stats. (#4366, #4368)
* handle AGENT_ERROR disconnect reason (#4339)
### Changed
- Mark last run of grow bucket outside goroutine. (#4348)
- Refine ipv6 support (#4352)
- Sample data send error logging. (#4358)
- Switch data track extension to 1-byte ID/length. (#4362)
- Do not kick off migration of closed participant (#4363)
- Do not block all ext ID determination on stream allocator listener (#4364)
- Rename log field pID to participantID (#4365)
- Replace deprecated io/ioutil with io in whipservice (#4375)
- Update grpc to address CVE-2026-33186 (#4381)
### Fixed
- Fix repair stream ID reporting for RTX pairing. (#4369)
## [1.9.12] - 2026-03-05
### Added
- Add silent frame for pcmu/a (#4258)
- adds a test to ensure agent worker errors cause disconnection (#4273)
- Populate client_protocol field in ParticipantInfo (#4293)
- Read client protocol from query param (#4294)
- generate & log egressID for start egress request (#4303)
- ESP32 Client Info (#4267)
- feat: make INSTALL_PATH overridable in install script (#3954)
### Changed
- Defer setting clock rate in RTPStats module till codec is bound. (#4250)
- Wrapping SIP errors for invalid argument and not found (#4253)
- Ignore parse addr error when add remote candidate (#4264)
- Generate config flags (#4268)
- clear reference guard when resetting signal stats (#4279)
- refresh telemetry guard on participant move (#4280)
- use separate allocation for signal stats telemetry guard (#4281)
- Set up audio config in audio level module when config is updated. (#4290)
- Update self-hosting deployment documentation link (#4312)
- Key telemetry stats worker using combination of roomID, participantID (#4323)
- Two phase restart when doing external restart of receiver. (#4329)
- Send participant left event after track unpublished for moved (#4334)
### Fixed
- Fix receiver restart race (#4248)
- require participant broadcast when metadata/attributes are set in token (#4266)
- Create buffer if needed when a PLI is requested. (#4282)
- Do not increase max expected layer on track info update. (#4285)
- Publish is always on publisher peer connection.(#4307)
- Potential fix for code scanning alert no. 35: Workflow does not contain permissions (#4311)
- Potential fix for code scanning alert no. 36: Workflow does not contain permissions (#4310)
- Fix for some CodeQL reported issues (#4314)
- Protect against incorrect temporal layer. (#4327)
- do not discount packets lost on duplicate packets (#4333)
- Use ParticipantTelemetryListener of LocalParticipant. (#4342)
- Fix SIP client timeout. (#4345)
## [1.9.11] - 2026-01-15
## PLEASE NOTE: The previous release tag v1.9.10 hit a panic under some conditions. Sincerely regret the inconvenience caused. Although we do test rigorously, it is not guaranteed to cover all scenarios. We request you to report any issues you encounter. Thank you.
### Added
- Support OpenTelemetry tracing. Add Jaeger support. (#4222)
- Add option to force simuclast codec. (#4226)
- Log timeout in API (#4231, #4232)
- Add participant option for data track auto-subscribe. (#4240)
### Changed
- Remove enable arrival time forwarding method. (#4217)
- sfu/receiver and sfu/buffer refactor (#4221, #4224, #4225)
- Change some logs to debugw (#4229)
- Changing field naming of data track packet (#4235)
- Update Pion transport package. (#4237)
- Wrapping the invalid request errors for CreateSipParticipant (#4239)
### Fixed
- Swap result sink atomically rather than closing and setting (#4216)
- Address crash in v1.9.10 (#4219, #4220)
- Return on SDP fragment read error. (#4228)
## [1.9.10] - 2026-01-01
## WARNING: Please do not use this release. There is a run time issue which causes the server to panic. The issue has been addressed in #4219 and #4220.
### Added
- add explicit room exists servicestore op (#4175)
- Add support for TURN static auth secret credentials (#3796)
- Make new path for signalling v1.5 support. (#4180)
- report video size from media data for whip (#4211)
- Support preserving external supplied time. (#4212)
### Changed
- Use published track for model access in data down track. (#4176)
- Refactor receiver and buffer into Base and higher layer. (#4185, #4186, #4187, #4189, #4196, #4198, #4207)
- Update pion/webrtc to v4.2.1 (#4191)
- Receiver restart related changes. (#4192, #4200, #4202, #4208)
- Do not warn about track not bound if participant is not ready. (#4205, #4206)
### Fixed
- Flush ext packets on restart/close and release packets. (#4179)
- Resolve RTX pair via OnTrack also. (#4190)
- Handle repair SSRC of simulcast tracks during migration. (#4193)
- return iceservers for whip (#4210)
## [1.9.9] - 2025-12-18
### Added
- Add support for RTP stream restart. (#4161)
### Changed
- Avoid duplicate track add to room track manager. (#4152, #4153)
- Consistently undo update to sequence number and timestamp when the incoming packet cannot be sequenced. (#4156)
- deregister observability function when participant is closed (#4157)
- Ensure subscribe data track handles are unique (#4162)
- move delete to oss service store (#4164)
- clean up manual roomservice log redaction (#4165)
- skip lost sequence number ranges in getIntervalStats (#4166, #4169)
### Fixed
- chore: fix a large number of spelling issues (#4147)
- Handle case of sequence number jump just after start. (#4150)
- Drop run away receiver reports. (#4170)
- Publish/Unpublish counter match. (#4173)
## [1.9.8] - 2025-12-10
### Added
- Mark RTCP buffer Write as noinline. (for better heap attribution) (#4138)
- add debug metric for tracking references (#4134)
### Changed
- Use isEnding to indicate if down track could be resumed. (#4132)
- switch participant callbacks to room to listener interface (#4136)
- protocol deps to get inactive file adjusted memory usage. (#4137)
- update webrtc to 4.1.8 to pick up DTLS fingerprint check during handshake (#4140)
### Fixed
- Do not pause rid in SDP to prevent race with adaptive streaming (#4129)
- leak fixes (#4131, #4141, #4142, #4143, #4144)
## [1.9.7] - 2025-12-05
### Added
- Data tracks (experimental and not ready for use) (#4089)
### Changed
- log bucket growth (#4122)
- Update pion/ice to stop gather first on close (#4123)
- move utils.WrapAround to mediatransportutil (#4124)
- Let participant close remove the published tracks. (#4125)
### Fixed
- Fix concurrent map access for https://github.com/livekit/livekit/issues/4126. (#4127)
## [1.9.6] - 2025-12-01
### Added
- Control latency of lossy data channel (#4088)
- logger proto redaction. (#4090)
- Record join/publish/subscribe cancellations (#4102, #4104)
### Fixed
- Fix "address" typo in transport logs (addddress → address) (#4097)
- Clear stereo=1 if stereo is not enabled. (#4101)
- Participant session close deadlock fixes (#4107, #4113, #4116)
### Changed
- Switch forwarding latency log to Debugw (#4098)
- Update mediatransportutil to get OWD estimator relocation (#4115)
## [1.9.5] - 2025-12-01 - scratched
## [1.9.4] - 2025-11-15
### Added
- Log reason for subscriber not being able to determine codec. (#4071)
- Kind details for connector (#4072)
### Fixed
- Prevent invalid track access while peer connection is shutting down. (#4054)
### Changed
- Update PsRPC to get redis pipeliner implementation (#4055)
- Forwarding latency measurement. (#4056. #4057, #4059, #4061, #4062, #4067, #4080)
- Update pion/transport to v3.1.1 (to get batch I/O ping-pong buffer) (#4070)
- Use sync.Pool for objects in packet path. (#4066)
- Bump protocol to pull sip validation changes and error mapping (#4081)
## [1.9.3] - 2025-11-02
### Added
- Opportunistic video layer allocation on setting max spatial layer. (#4003, #4030, #4031, #4033)
- use env var for GOARCH. (#4012)
- Use simulcast codec as default policy for audio track. (#4040)
- Enable AbsCaptureTimeURI in RTC configuration. (#4043)
- Add prom histogram for forwarding latency and jitter. (#4044, #4045)
### Fixed
- Correct direction for request/response for prom counters. (#4027)
- Do not bind buffer if codec is invalid. (#4028)
- Remove ~ from rid which indicates disabled layer to get the actual rid. (#4032)
- Prevent leakage of previous codec after codec regression. (#4035, #4037)
- fix: add missing Unlock() in AddReceiver. (#4036)
### Changed
- Some golang modernisation bits. (#4106)
- Use rtp converter from protocol/utils. (#4019, #4020)
- High forwarding latency. (#4034, #4038)
- if RingingTimeout is provided, deadline should be set to that timeout. (#4018)
- Don't warn 0 payload type for PCMU. (#4039)
## [1.9.2] - 2025-10-17
### Added
- Use gzip reader pool (#3903)
- Rpcs for ingress proxy WHIP (#3911)
- Include agent_name as a participant attribute (#3914)
- Clean code as there is no oss sweeper for ingress (#3918)
- Support simulcasting of audio (#3920)
- Subscrbed audio codecs - update from remote nodes. (#3921)
- Log some information around high forwarding latency. (#3944)
- feat: server rpc apis (#3904)
- short circuit participant broadcast filter in livestream mode (#3955)
- Adjust for hold time when fowarding RTCP report. (#3956)
- Add node_ip to config-sample.yaml (#3960)
- add idempotent reference count to telemetry stats worker (#3964)
- add config for user data recording (#3966)
- Provide the InputVideo/AudioState to Ingress in WHIPRTCConnectionNotify (#3982)
- Add encryption datapacket type (#3869)
- Allow passing inline trunk for outbound calls. (#3987)
- Log RPC details. (#3991)
- "Power of Two Random Choices" option for node selection (#3785)
- Adding ProviderInfo to GetSIPTrunkAuthenticationResponse (#3993)
- Use answer with mid -> trackID mapping when in single peer connection (#4005)
- Include mid -> trackID in both SDP offer and answer. (#4007)
### Fixed
- add incoming request id to request response message (#3912)
- Simulcast audio fixes (#3925)
- Fix dynacast subscriber node clearing on move participant. (#3926)
- mediatransportutil crash fix for logging local address (#3930)
- Do DD restart only if DD structure is present. (#3935)
- Avoid matching on empty track id. (#3937)
- fix stats worker closed condition (#3965)
- Update deps to fix redis issue when 1 cluster address is provided (#3969)
- Revert unintentional change to not handle transport fallback (#3970)
- Do not panic of redis is not configured (#3981)
- Sort codec layers when adding track (#3998)
- Resort to full search for requested quality is not available. (#4000)
- Do not try to read stats from peer connection after close. (#4002)
- Update pion/webrtc to prevent GetStats panic. (#4004)
### Changed
- update protocol for sip api change (#3902)
- Refactor subscribedTrack + mediaTrackSubscriptions. (#3908)
- Set publisher codec preferences after setting remote description (#3913)
- update protocol for psrpc (#3915)
- Wait for `SetRemoteDescription` before configuring senders. (#3924)
- Update mediatransportutil to log external IP found via STUN. (#3929)
- Add debugging from DD frame number wrap around. (#3933)
- More debugging of DD jump (#3934)
- Use difference in key frame counter to stop seeder. (#3936)
- Update protocol for SipCreateParticipant (#3939)
- mediatransportutil to log local address when validating external IP (#3942)
- Use microseconds for forwarding stats. (#3943)
- Tweaks tresholds for logging high forwarding latency/jitter. (#3945)
- Flush stats when there are no packets. (#3947)
- handle terminated job requests (#3948)
- Switch ops queue a singly linked list. (#3949)
- Revert "Switch ops queue a singly linked list." (#3950)
- Adjust stream allocator ping interval based on state. (#3951)
- avoid logging on small values (#3958)
- Update protocol for EventKey helper. (#3963)
- Do not force codec regression between opus and red. (#3980)
- Do not start forawarding on out-of-order packet. (#3985)
- Use padding only packets for dummy start of audio. (#3984)
- Support Opus mixed with RED when encrypted. (#3986)
- Limit check to red + opus when looking for primary codec match. (#3988)
- Increment RTP timestamp on padding when using dummy start. (#3989)
- Revert to using silence packets for audio dummy start. (#3999)
- Count request/response packets on both client and server side. (#4001)
- Do not call receiver methods under settings lock. (#4006)
- counterfeiter needs an older version of x/tools (#4009)
## [1.9.1] - 2025-09-05
### Fixed
- swap pub/sub track metrics (#3717)
- Fix bug with SDP rid, clear only overflow. (#3723)
- Don't check bindState on downtrack.Bind (#3726)
- Return highest available layer if requested quality is higher than max (#3729)
- Fix data packet ParticipantIdentity override logic in participant.go (#3735)
- Fix svc encoding for chrome mobile on iOS (#3751)
- offer could be nil when migrating. (#3752)
- fix(deps): update module github.com/livekit/protocol to v1.39.3 (#3733)
- bounds check layer index (#3768)
- Do not send leave if nil (to older clients) (#3817)
- Fix: RingingTimeout was being skipped for transferParticipant (#3831)
- Handle no codecs in track info (#3859)
- Fix missed unlock (#3861)
- Fix timeout handing in StopEgress (#3876)
- fix: ensure the participant kind is set on refresh tokens (#3881)
- Do not advertise NACK for RED. (#3889)
- Do not send both asb-send-time and twcc. (#3890)
- Prevent race in determining BWE type. (#3891)
### Added
- Adds Devin to readme so it auto updates DeepWiki weekly (#3699)
- Allow passing extra attributes to RTC endpoint. (#3693)
- warn about credentials when used in tokens (#3705)
- protocol dep for webhook stats buckets (#3706)
- for real, pick up protocol change for webhooks queue length buckets (#3707)
- implement observability for room metrics (#3712)
- e2e reliability for data channel (#3716)
- Add simulcast support for WHIP. (#3719)
- Add Id to SDP signalling messages. (#3722)
- Set and use rid/spatial layer in TrackInfo. (#3724)
- Add log for dropping out of order reliable message (#3728)
- chore: set workerid on job creation (#3737)
- return error when moving egress/agent participant (#3741)
- SVC with RID -> spatial layer mapping (#3754)
- feat(cli-flags): add option for cpu profiling (#3765)
- Enable H265 by default (#3773)
- Signalling V2 protocol implementation start (#3794)
- Signal v2: envelope and fragments as wire message format. (#3800)
- Grouping all signal messages into participant_signal. (#3801)
- starting signaller interface (#3802)
- Signal handling interfaces and participant specific HTTP PATCH. (#3804)
- Split signal segmenter and reassembler. (#3805)
- Filling out messages unlikely to change in v2. (#3806)
- Use signalling utils from protocol (#3807)
- Validation end point for v2 signalling. (#3811)
- More v2 signalling changes (#3814)
- Minor tweak to keep RPC type at service level. (#3815)
- Add country label to edge prom stats. (#3816)
- HTTP DELETE of participant session (#3819)
- Get to the point of establishing subscriber peer connection. (#3821)
- Get to the point of connecting publisher PC and using it for async signalling (#3822)
- Support join request as proto + base64 encoded query param (#3836)
- Use wrapped join request to be able to support compressed and uncompressed. (#3838)
- handle SyncState in join request (#3839)
- Support per simulcast codec layers. (#3840)
- Support video layer mode from client and make most of the code mime aware (#3843)
- Send `participant_connection_aborted` when participant session is closed (#3848)
- Support G.711 A-law and U-law (#3849)
- Extract video size from media stream (#3856)
- update mediatransport util for ice port 3478 (#3877)
- Single peer connection mode (#3873)
- handle frame number wrap back in svc (#3885)
- Use departure timeout from room preset. (#3888)
- Use `RequestResponse` to report protocol handling errors (#3895)
### Changed
- Add a trend check before declaring joint queuing region. (#3701)
- Small changes to add/use helper functions for length checks. (#3704)
- remove unused ws signal read loop (#3709)
- Flush stats on close (#3713)
- Do not require create permission for WHIP participant. (#3715)
- Create client config manager in room manager constructor. (#3718)
- Clear rids from default for layers not published. (#3721)
- Clear rids if not present in SDP. (#3731)
- Revert clearing RIDs. (#3732)
- Take ClientInfo from request. (#3738)
- remove unused code (#3740)
- reuse compiled client config scripts (#3743)
- feat(cli): update to urfave/cli/v3 (#3745)
- move egress roomID load to launcher (#3748)
- Log previous allocation to see changes. (#3759)
- Do not need to just clean up receivers. Remove that interface. (#3760)
- ClearAllReceivers interface is used to pause relay tracks. (#3761)
- Temporary change: use pre-defined rids (#3767)
- Revert "Temporary change: use pre-defined rids" (#3769)
- Log SDP rids to understand the mapping better. (#3770)
- Limit taking rids from SDP only in WHIP path. (#3771)
- Set rids for all codecs. (#3772)
- Return default layer for invalid rid + track info combination. (#3778)
- Normalize known rids. (#3779)
- forward agent id to job state (3786)
- Map ErrNoResponse to ErrRequestTimedOut in StopEgress to avoid returning 503 (#3788)
- Set participant active when peerconnection connected (#3790)
- Handle Metadata field from RoomConfig (#3798)
- [🤖 readme-manager] Update README (#3808)
- [🤖 readme-manager] Update README (#3809)
- Rename RTCRest -> WHIP (#3829)
- Delete v2 signalling (#3835)
- Clean up missed v2 pieces (#3837)
- Update go deps (#3849)
- Populate SDP cid in track info when available. (#3845)
- Log signal messages as debug. (#3851)
- Log signal messages on media node. (#3852)
- Log track settings more. (#3853)
- Update pion deps (#3863)
- Update golang Docker tag to v1.25 (#3864)
- Update module github.com/livekit/protocol to v1.40.0 (#3865)
- Remove unnecessary check (#3870)
- chunk room updates (#3880)
- Switch known rids from 012 -> 210, used by OBS. (#3882)
- init ua parser once (#3883)
- Revert to using answer for migration case. (#3884)
- Handle migration better in single peer connection case. (#3886)
## [1.9.0] - 2025-06-02
### Added
- Add pID and connID to log context to make it easier to search using pID. (#3518)
- add server agent load threshold config (#3520)
- Add a key frame seeder in up track. (#3524)
- Implement SIP update API. (#3141)
- Add option to use different pacer with send side bwe. (#3552)
- Allow specifying extra webhooks with egress requests (#3597)
### Fixed
- Fix missing RTCP sender report when forwarding RED as Opus. (#3480)
- Take RTT and jitter from receiver view while reporting track stats for (#3483)
- Fix receiver rtt/jitter. (#3487)
- fix: fix the wrong error return value (#3493)
- load mime type before calling writeBlankFrameRTP (#3502)
- Prevent bind lock deadlock on muted. (#3504)
- Handle subscribe race with track close better. (#3526)
- Do not instantiate 0 sized sequencer. (#3529)
- Fix: Return NotFoundErr instead of Unavailable when the participant does not exist in UpdateParticipant. (#3543)
- skip out of order participant state updates (#3583)
- Exclude RED from enabled codecs for Flutter + 2.4.2 + Android. (#3587)
- protocol update to fix IPv6 SDP fragment parsing (#3603)
- Forward transfer headers to internal request (#3615)
- Do not use Redis pipeline for SIP delete. Fixes Redis clustering support. (#3694)
### Changed
- Use a RED transformer to consolidate both RED -> Opus OR Opus -> RED (#3481)
- refactor: using slices.Contains to simplify the code (#3495)
- Do not bind lock across flush which could take time (#3501)
- Log packet drops/forward. (#3510)
- Clean up published track on participant removal. (#3527)
- Do not accept unsupported track type in AddTrack (#3530)
- Use cgroup for memstats. (#3573)
- Replace Promise with Fuse. (#3580)
- Do not drop audio codecs (#3590)
- map PEER_CONNECTION_DISCONNECTED -> CONNECTION_TIMEOUT (#3591)
- Update mediatransportutil for max sctp message (65K) (#3611)
- Disable vp9 for safari 18.4 due to compatibility (#3631)
- Avoid synthesising duplicate feature. (#3632)
- Take AudioFeatures from AddTrack. (#3635)
- Use unordered for lossy data channel. (#3653)
- Send self participant update immediately. (#3656)
- update mediatransportutil for sctp congestion control (#3673)
## [1.8.4] - 2025-03-01
### Added
+1 -1
View File
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.24-alpine AS builder
FROM golang:1.26-alpine AS builder
ARG TARGETPLATFORM
ARG TARGETARCH
+9 -6
View File
@@ -18,6 +18,7 @@ LiveKit's server is written in Go, using the awesome [Pion WebRTC](https://githu
[![GitHub stars](https://img.shields.io/github/stars/livekit/livekit?style=social&label=Star&maxAge=2592000)](https://github.com/livekit/livekit/stargazers/)
[![Slack community](https://img.shields.io/endpoint?url=https%3A%2F%2Flivekit.io%2Fbadges%2Fslack)](https://livekit.io/join-slack)
[![Twitter Follow](https://img.shields.io/twitter/follow/livekit)](https://twitter.com/livekit)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/livekit/livekit)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/livekit/livekit)](https://github.com/livekit/livekit/releases/latest)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/livekit/livekit/buildtest.yaml?branch=master)](https://github.com/livekit/livekit/actions/workflows/buildtest.yaml)
[![License](https://img.shields.io/github/license/livekit/livekit)](https://github.com/livekit/livekit/blob/master/LICENSE)
@@ -276,7 +277,7 @@ Sign up for [LiveKit Cloud](https://cloud.livekit.io/).
### Self-host
Read our [deployment docs](https://docs.livekit.io/deploy/) for more information.
Read our [deployment docs](https://docs.livekit.io/transport/self-hosting/) for more information.
## Building from source
@@ -307,12 +308,14 @@ LiveKit server is licensed under Apache License v2.0.
<br/><table>
<thead><tr><th colspan="2">LiveKit Ecosystem</th></tr></thead>
<tbody>
<tr><td>LiveKit SDKs</td><td><a href="https://github.com/livekit/client-sdk-js">Browser</a> · <a href="https://github.com/livekit/client-sdk-swift">iOS/macOS/visionOS</a> · <a href="https://github.com/livekit/client-sdk-android">Android</a> · <a href="https://github.com/livekit/client-sdk-flutter">Flutter</a> · <a href="https://github.com/livekit/client-sdk-react-native">React Native</a> · <a href="https://github.com/livekit/rust-sdks">Rust</a> · <a href="https://github.com/livekit/node-sdks">Node.js</a> · <a href="https://github.com/livekit/python-sdks">Python</a> · <a href="https://github.com/livekit/client-sdk-unity">Unity</a> · <a href="https://github.com/livekit/client-sdk-unity-web">Unity (WebGL)</a></td></tr><tr></tr>
<tr><td>Agents SDKs</td><td><a href="https://github.com/livekit/agents">Python</a> · <a href="https://github.com/livekit/agents-js">Node.js</a></td></tr><tr></tr>
<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">Swift</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> · <a href="https://github.com/livekit/client-sdk-cpp">C++</a></td></tr><tr></tr>
<tr><td>Starter Apps</td><td><a href="https://github.com/livekit-examples/agent-starter-python">Python Agent</a> · <a href="https://github.com/livekit-examples/agent-starter-node">TypeScript Agent</a> · <a href="https://github.com/livekit-examples/agent-starter-react">React App</a> · <a href="https://github.com/livekit-examples/agent-starter-swift">SwiftUI App</a> · <a href="https://github.com/livekit-examples/agent-starter-android">Android App</a> · <a href="https://github.com/livekit-examples/agent-starter-flutter">Flutter App</a> · <a href="https://github.com/livekit-examples/agent-starter-react-native">React Native App</a> · <a href="https://github.com/livekit-examples/agent-starter-embed">Web Embed</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>Server APIs</td><td><a href="https://github.com/livekit/node-sdks">Node.js</a> · <a href="https://github.com/livekit/server-sdk-go">Golang</a> · <a href="https://github.com/livekit/server-sdk-ruby">Ruby</a> · <a href="https://github.com/livekit/server-sdk-kotlin">Java/Kotlin</a> · <a href="https://github.com/livekit/python-sdks">Python</a> · <a href="https://github.com/livekit/rust-sdks">Rust</a> · <a href="https://github.com/agence104/livekit-server-sdk-php">PHP (community)</a> · <a href="https://github.com/pabloFuente/livekit-server-sdk-dotnet">.NET (community)</a></td></tr><tr></tr>
<tr><td>UI Components</td><td><a href="https://github.com/livekit/components-js">React</a> · <a href="https://github.com/livekit/components-android">Android Compose</a> · <a href="https://github.com/livekit/components-swift">SwiftUI</a></td></tr><tr></tr>
<tr><td>Agents Frameworks</td><td><a href="https://github.com/livekit/agents">Python</a> · <a href="https://github.com/livekit/agents-js">Node.js</a> · <a href="https://github.com/livekit/agent-playground">Playground</a></td></tr><tr></tr>
<tr><td>Services</td><td><b>LiveKit server</b> · <a href="https://github.com/livekit/egress">Egress</a> · <a href="https://github.com/livekit/ingress">Ingress</a> · <a href="https://github.com/livekit/sip">SIP</a></td></tr><tr></tr>
<tr><td>Resources</td><td><a href="https://docs.livekit.io">Docs</a> · <a href="https://github.com/livekit-examples">Example apps</a> · <a href="https://livekit.io/cloud">Cloud</a> · <a href="https://docs.livekit.io/home/self-hosting/deployment">Self-hosting</a> · <a href="https://github.com/livekit/livekit-cli">CLI</a></td></tr>
<tr><td>Resources</td><td><a href="https://docs.livekit.io">Docs</a> · <a href="https://docs.livekit.io/mcp">Docs MCP Server</a> · <a href="https://github.com/livekit/livekit-cli">CLI</a> · <a href="https://cloud.livekit.io">LiveKit Cloud</a></td></tr><tr></tr>
<tr><td>LiveKit Server OSS</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>Community</td><td><a href="https://community.livekit.io">Developer Community</a> · <a href="https://livekit.io/join-slack">Slack</a> · <a href="https://x.com/livekit">X</a> · <a href="https://www.youtube.com/@livekit_io">YouTube</a></td></tr>
</tbody>
</table>
<!--END_REPO_NAV-->
+52 -29
View File
@@ -15,6 +15,7 @@
package main
import (
"context"
"fmt"
"os"
"strconv"
@@ -23,10 +24,12 @@ import (
"github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v2"
"github.com/olekukonko/tablewriter/tw"
"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"
@@ -35,7 +38,7 @@ import (
"github.com/livekit/livekit-server/pkg/service"
)
func generateKeys(_ *cli.Context) error {
func generateKeys(_ context.Context, _ *cli.Command) error {
apiKey := guid.New(utils.APIKeyPrefix)
secret := utils.RandomSecret()
fmt.Println("API Key: ", apiKey)
@@ -43,7 +46,7 @@ func generateKeys(_ *cli.Context) error {
return nil
}
func printPorts(c *cli.Context) error {
func printPorts(_ context.Context, c *cli.Command) error {
conf, err := getConfig(c)
if err != nil {
return err
@@ -84,17 +87,21 @@ func printPorts(c *cli.Context) error {
return nil
}
func helpVerbose(c *cli.Context) error {
func helpVerbose(_ context.Context, c *cli.Command) error {
generatedFlags, err := config.GenerateCLIFlags(baseFlags, false)
if err != nil {
return err
}
c.App.Flags = append(baseFlags, generatedFlags...)
flags := append([]cli.Flag{}, baseFlags...)
flags = append(flags, generatedFlags...)
root := c.Root()
root.Flags = flags
c.Flags = flags
return cli.ShowAppHelp(c)
}
func createToken(c *cli.Context) error {
func createToken(_ context.Context, c *cli.Command) error {
room := c.String("room")
identity := c.String("identity")
@@ -160,7 +167,7 @@ func createToken(c *cli.Context) error {
return nil
}
func listNodes(c *cli.Context) error {
func listNodes(_ context.Context, c *cli.Command) error {
conf, err := getConfig(c)
if err != nil {
return err
@@ -181,10 +188,27 @@ func listNodes(c *cli.Context) error {
return err
}
table := tablewriter.NewWriter(os.Stdout)
table.SetRowLine(true)
table.SetAutoWrapText(false)
table.SetHeader([]string{
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithHeaderAutoFormat(tw.Off),
tablewriter.WithRowAutoWrap(0),
tablewriter.WithRendition(tw.Rendition{
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.On},
},
}),
tablewriter.WithRowAlignmentConfig(tw.CellAlignment{
PerColumn: []tw.Align{
tw.AlignCenter, tw.AlignCenter, tw.AlignCenter,
tw.AlignRight, tw.AlignRight,
tw.AlignRight,
tw.AlignRight, tw.AlignRight,
tw.AlignRight, tw.AlignRight, tw.AlignRight,
tw.AlignRight, tw.AlignRight,
tw.AlignCenter,
},
}),
)
table.Header(
"ID", "IP Address", "Region",
"CPUs", "CPU Usage\nLoad Avg",
"Memory Used/Total",
@@ -192,19 +216,14 @@ func listNodes(c *cli.Context) error {
"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())
@@ -220,19 +239,23 @@ func listNodes(c *cli.Context) error {
clientsAndTracks := fmt.Sprintf("%d\n%d / %d", stats.NumClients, stats.NumTracksIn, stats.NumTracksOut)
// Packet stats
bytes := fmt.Sprintf("%sps / %sps\n%s / %s", humanize.Bytes(uint64(stats.BytesInPerSec)), humanize.Bytes(uint64(stats.BytesOutPerSec)),
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(stats.PacketsInPerSec)), humanize.Comma(int64(stats.PacketsOutPerSec)),
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, "")))
sysPackets := fmt.Sprintf("%.2f %%\n%v / %v", stats.SysPacketsDroppedPctPerSec*100, float64(stats.SysPacketsOutPerSec), float64(stats.SysPacketsDroppedPerSec))
nacks := fmt.Sprintf("%.2f\n%s", stats.NackPerSec, strings.TrimSpace(humanize.SIWithDigits(float64(stats.NackTotal), 2, "")))
retransmit := fmt.Sprintf("%.2f\n%s", stats.RetransmitPacketsOutPerSec, strings.TrimSpace(humanize.SIWithDigits(float64(stats.RetransmitPacketsOut), 2, "")))
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{
table.Append(
idAndState, node.Ip, node.Region,
cpus, cpuUsageAndLoadAvg,
memUsage,
@@ -240,9 +263,9 @@ func listNodes(c *cli.Context) error {
bytes, packets, sysPackets,
nacks, retransmit,
startedAndUpdated,
})
)
}
table.Render()
table.Render() //nolint:errcheck
return nil
}
+46 -22
View File
@@ -15,6 +15,7 @@
package main
import (
"context"
"fmt"
"math/rand"
"net"
@@ -25,15 +26,16 @@ import (
"syscall"
"time"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"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"
)
@@ -49,7 +51,7 @@ var baseFlags = []cli.Flag{
&cli.StringFlag{
Name: "config-body",
Usage: "LiveKit config in YAML, typically passed in as an environment var in a container",
EnvVars: []string{"LIVEKIT_CONFIG"},
Sources: cli.EnvVars("LIVEKIT_CONFIG"),
},
&cli.StringFlag{
Name: "key-file",
@@ -58,44 +60,47 @@ var baseFlags = []cli.Flag{
&cli.StringFlag{
Name: "keys",
Usage: "api keys (key: secret\\n)",
EnvVars: []string{"LIVEKIT_KEYS"},
Sources: cli.EnvVars("LIVEKIT_KEYS"),
},
&cli.StringFlag{
Name: "region",
Usage: "region of the current node. Used by regionaware node selector",
EnvVars: []string{"LIVEKIT_REGION"},
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",
EnvVars: []string{"NODE_IP"},
Sources: cli.EnvVars("NODE_IP"),
},
&cli.StringFlag{
Name: "udp-port",
Usage: "UDP port(s) to use for WebRTC traffic",
EnvVars: []string{"UDP_PORT"},
Sources: cli.EnvVars("UDP_PORT"),
},
&cli.StringFlag{
Name: "redis-host",
Usage: "host (incl. port) to redis server",
EnvVars: []string{"REDIS_HOST"},
Sources: cli.EnvVars("REDIS_HOST"),
},
&cli.StringFlag{
Name: "redis-password",
Usage: "password to redis",
EnvVars: []string{"REDIS_PASSWORD"},
Sources: cli.EnvVars("REDIS_PASSWORD"),
},
&cli.StringFlag{
Name: "turn-cert",
Usage: "tls cert file for TURN server",
EnvVars: []string{"LIVEKIT_TURN_CERT"},
Sources: cli.EnvVars("LIVEKIT_TURN_CERT"),
},
&cli.StringFlag{
Name: "turn-key",
Usage: "tls key file for TURN server",
EnvVars: []string{"LIVEKIT_TURN_KEY"},
Sources: cli.EnvVars("LIVEKIT_TURN_KEY"),
},
&cli.StringFlag{
Name: "cpuprofile",
Usage: "write CPU profile to `file`",
},
// debugging flags
&cli.StringFlag{
Name: "memprofile",
Usage: "write memory profile to `file`",
@@ -127,7 +132,7 @@ func main() {
fmt.Println(err)
}
app := &cli.App{
cmd := &cli.Command{
Name: "livekit-server",
Usage: "High performance WebRTC server",
Description: "run without subcommands to start the server",
@@ -182,21 +187,18 @@ func main() {
Version: version.Version,
}
if err := app.Run(os.Args); err != nil {
if err := cmd.Run(context.Background(), os.Args); err != nil {
fmt.Println(err)
}
}
func getConfig(c *cli.Context) (*config.Config, error) {
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
}
strictMode := !c.Bool("disable-strict-config")
conf, err := config.NewConfig(confString, strictMode, c, baseFlags)
if err != nil {
@@ -242,11 +244,14 @@ func getConfig(c *cli.Context) (*config.Config, error) {
return conf, nil
}
func startServer(c *cli.Context) error {
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()
@@ -254,6 +259,25 @@ func startServer(c *cli.Context) error {
return err
}
if err = conf.LoadTURNSecrets(); 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
@@ -285,7 +309,7 @@ func startServer(c *cli.Context) error {
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
for i := 0; i < 2; i++ {
for i := range 2 {
sig := <-sigChan
force := i > 0
logger.Infow("exit requested, shutting down", "signal", sig, "force", force)
+44
View File
@@ -65,6 +65,19 @@ rtc:
# this is useful for cloud environments such as AWS & Google where hosts have an internal IP
# that maps to an external one
use_external_ip: true
# # when set to true, advertises both mapped external and internal IPs to clients as server candidates.
# # useful when clients connect from both private and public networks.
# # works only when `use_external_ip` is set to true.
# # when both this and `external_ip_only` are set, SFU advertises all private IPs with their mapped external IPs and skips
# # private IPs that do not have a mapped external IP.
# advertise_internal_ip: true
# # when set to true, SFU skips external IP accessibility checks via self-ping.
# # enable this when SFU is behind NAT that does not allow self-ping. only works when `use_external_ip` is set to true.
# skip_external_ip_validation: 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.
@@ -83,6 +96,13 @@ rtc:
# port: 443
# # tls, tcp, or udp
# protocol: tls
# # Shared secret for TURN server authentication
# secret: ""
# # Path for file containing shared secret for TURN server authentication
# # When both secret and secret_file are set, secret takes precedence
# secret_file: "/var/lib/coturn/secret"
# ttl: 14400 # seconds
# # Insecure username/password authentication
# username: ""
# credential: ""
# # allows LiveKit to monitor congestion when sending streams and automatically
@@ -94,6 +114,12 @@ rtc:
# 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
# # signaling RTT (in milliseconds) below which ICE/TCP is attempted on a UDP failure; at or above it,
# # supporting clients fall back directly to TURN/TLS. 0 (default) disables the check (ICE/TCP always tried).
# # a positive value also enables allow_udp_unstable_fallback.
# tcp_fallback_rtt_threshold: 0
# # migrate an established-but-lossy UDP connection to ICE/TCP or TURN/TLS. requires tcp_fallback_rtt_threshold > 0, default false
# allow_udp_unstable_fallback: false
# # 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
@@ -269,6 +295,21 @@ keys:
# # optional (set only if not using external TLS termination)
# # cert_file: /path/to/cert.pem
# # key_file: /path/to/key.pem
# # TTL of the TURN credentials in seconds - defaults to 300
# ttl_seconds: 300
# # list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to.
# # By default (i. e. empty list), all restricted peer CIDRs are denied access.
# # When not empty, only the specified CIDRs are allowed access.
# # Note that this check is applied to restricted peer CIDRs only.
# allow_restricted_peer_cidrs:
# - 10.0.0.0/8
# - 192.168.0.0/16
# # list of peer CIDRs to deny access to.
# # This applies to all peer CIDRs, including restricted ones.
# # Deny list takes precedence over allow list.
# deny_peer_cidrs:
# - 10.0.0.0/8
# - 192.168.0.0/16
# ingress server
# ingress:
@@ -287,6 +328,9 @@ keys:
# # 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
+119 -103
View File
@@ -1,144 +1,160 @@
module github.com/livekit/livekit-server
go 1.23
toolchain go1.23.6
go 1.26
require (
github.com/bep/debounce v1.2.1
github.com/d5/tengo/v2 v2.17.0
github.com/dennwc/iters v1.0.1
github.com/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.4
github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/florianl/go-tc v0.4.8
github.com/frostbyte73/core v0.1.1
github.com/gammazero/deque v1.0.0
github.com/gammazero/workerpool v1.1.3
github.com/gammazero/deque v1.2.1
github.com/gammazero/workerpool v1.2.1
github.com/google/uuid v1.6.0
github.com/google/wire v0.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/go-version v1.9.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jellydator/ttlcache/v3 v3.3.0
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/jxskiss/base62 v1.1.0
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1
github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564
github.com/livekit/protocol v1.34.0
github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126
github.com/mackerelio/go-osstat v0.2.5
github.com/magefile/mage v1.15.0
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0
github.com/livekit/protocol v1.46.7-0.20260605212020-c0615a2f6f84
github.com/livekit/psrpc v0.7.2
github.com/mackerelio/go-osstat v0.2.7
github.com/magefile/mage v1.17.2
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.2
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/ory/dockertest/v3 v3.11.0
github.com/pion/datachannel v1.5.10
github.com/pion/dtls/v3 v3.0.4
github.com/pion/ice/v4 v4.0.7
github.com/pion/interceptor v0.1.37
github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.11
github.com/pion/sctp v1.8.36
github.com/pion/sdp/v3 v3.0.10
github.com/pion/transport/v3 v3.0.7
github.com/pion/turn/v4 v4.0.0
github.com/pion/webrtc/v4 v4.0.11
github.com/moby/moby/client v0.4.1
github.com/olekukonko/tablewriter v1.1.4
github.com/ory/dockertest/v4 v4.0.0
github.com/pion/datachannel v1.6.0
github.com/pion/dtls/v3 v3.1.3
github.com/pion/ice/v4 v4.2.7
github.com/pion/interceptor v0.1.45
github.com/pion/rtcp v1.2.16
github.com/pion/rtp v1.10.2
github.com/pion/sctp v1.9.5
github.com/pion/sdp/v3 v3.0.18
github.com/pion/transport/v4 v4.0.2
github.com/pion/turn/v5 v5.0.8
github.com/pion/webrtc/v4 v4.2.11
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.20.5
github.com/redis/go-redis/v9 v9.7.0
github.com/prometheus/client_golang v1.23.2
github.com/redis/go-redis/v9 v9.20.0
github.com/rs/cors v1.11.1
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/thoas/go-funk v0.9.3
github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e
github.com/twitchtv/twirp v8.1.3+incompatible
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff
github.com/urfave/cli/v2 v2.27.5
github.com/ua-parser/uap-go v0.0.0-20260529044130-17c35e68e58c
github.com/urfave/negroni/v3 v3.1.1
go.uber.org/atomic v1.11.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/sync v0.11.0
google.golang.org/protobuf v1.36.5
go.uber.org/zap v1.28.0
golang.org/x/mod v0.36.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 // indirect
buf.build/go/protoyaml v0.3.1 // indirect
cel.dev/expr v0.19.0 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cilium/ebpf v0.16.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/nyaruka/phonenumbers v1.8.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.3.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
golang.org/x/time v0.15.0 // indirect
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
buf.build/go/protovalidate v1.2.0 // indirect
buf.build/go/protoyaml v0.7.0 // indirect
cel.dev/expr v0.25.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/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/bufbuild/protovalidate-go v0.8.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v26.1.4+incompatible // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/cel-go v0.22.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/google/cel-go v0.28.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/subcommands v1.2.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/compress v1.18.6 // 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/mitchellh/mapstructure v1.5.0 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mdlayher/netlink v1.11.2 // indirect
github.com/mdlayher/socket v0.6.1 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.39.0 // indirect
github.com/nats-io/nkeys v0.4.10 // indirect
github.com/nats-io/nats.go v1.52.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runc v1.1.14 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/opencontainers/image-spec v1.1.1 // 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.4 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/pion/srtp/v3 v3.0.11 // indirect
github.com/pion/stun/v3 v3.1.4
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.68.1 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/urfave/cli/v3 v3.9.0
github.com/wlynxg/anet v0.0.5 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect
google.golang.org/grpc v1.70.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
)
// for gst whipsink compatibility
// it is unclear what in pion/dtls v3.1.3 breaks compatibility,
// but both sides sends their first flights and do not make further progress
replace github.com/pion/dtls/v3 => github.com/pion/dtls/v3 v3.1.2
+248 -305
View File
@@ -1,99 +1,92 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 h1:ntAj16eF7AtUyzOOAFk5gvbAO52QmUKPKk7GmsIEORo=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1/go.mod h1:AxRT+qTj5PJCz2nyQzsR/qxAcveW5USRhJTt/edTO5w=
buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4=
buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg=
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0=
buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38=
buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/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/bufbuild/protovalidate-go v0.8.0 h1:Xs3kCLCJ4tQiogJ0iOXm+ClKw/KviW3nLAryCGW2I3Y=
github.com/bufbuild/protovalidate-go v0.8.0/go.mod h1:JPWZInGm2y2NBg3vKDKdDIkvDjyLv31J3hLH5GIFc/Q=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/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 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao=
github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/d5/tengo/v2 v2.17.0 h1:BWUN9NoJzw48jZKiYDXDIF3QrIVZRm1uV1gTzeZ2lqM=
github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dennwc/iters v1.0.1 h1:XwMudE6xtS0ugEdum4HQ+iRi+5HSvaeKxJPM/VI3pJs=
github.com/dennwc/iters v1.0.1/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elliotchance/orderedmap/v2 v2.7.0 h1:WHuf0DRo63uLnldCPp9ojm3gskYwEdIIfAUVG5KhoOc=
github.com/elliotchance/orderedmap/v2 v2.7.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/florianl/go-tc v0.4.4 h1:q6lhEWEfyhGffRzdl3eIcNqX/yVIw0IJwXqa9Rdcctw=
github.com/florianl/go-tc v0.4.4/go.mod h1:uvp6pIlOw7Z8hhfnT5M4+V1hHVgZWRZwwMS8Z0JsRxc=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/florianl/go-tc v0.4.8 h1:hgmakUX1Nm0Ba1I0ZkbUl9CH6HbRwqSiwipnpmYp3Es=
github.com/florianl/go-tc v0.4.8/go.mod h1:B8GeOEnmrbOnxZtaCvsYJcgIzzmM8c/AIhtfCZsDj3Q=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/frostbyte73/core v0.1.1 h1:ChhJOR7bAKOCPbA+lqDLE2cGKlCG5JXsDvvQr4YaJIA=
github.com/frostbyte73/core v0.1.1/go.mod h1:mhfOtR+xWAvwXiwor7jnqPMnu4fxbv1F2MwZ0BEpzZo=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
github.com/gammazero/workerpool v1.1.3 h1:WixN4xzukFoN0XSeXF6puqEqFTl2mECI9S6W44HWy9Q=
github.com/gammazero/workerpool v1.1.3/go.mod h1:wPjyBLDbyKnUn2XwwyD3EEwo9dHutia9/fwNmSHWACc=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ=
github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g=
github.com/gammazero/workerpool v1.2.1 h1:MEDvUJsNYGuCvl1RwIXNKu2YtQtHqCSF9XWF04N7lqs=
github.com/gammazero/workerpool v1.2.1/go.mod h1:E32GVRUanF4d6QtRmdss3AScgaDkIyrvPtgRQUWgmx4=
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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40=
github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
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=
@@ -103,32 +96,33 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
github.com/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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.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.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
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=
@@ -145,12 +139,10 @@ github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa79
github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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=
@@ -162,30 +154,28 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58=
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564 h1:GX7KF/V9ExmcfT/2Bdia8aROjkxrgx7WpyH7w9MB4J4=
github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564/go.mod h1:36s+wwmU3O40IAhE+MjBWP3W71QRiEE9SfooSBvtBqY=
github.com/livekit/protocol v1.34.0 h1:hbIXgNW+JPiTcGjzNg1XgQg3Wqa2R5dBhzuy+LLEIS4=
github.com/livekit/protocol v1.34.0/go.mod h1:yXuQ7ucrLj91nbxL6/AHgtxdha1DGzLj1LkgvnT90So=
github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126 h1:fzuYpAQbCid7ySPpQWWePfQOWUrs8x6dJ0T3Wl07n+Y=
github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126/go.mod h1:X5WtEZ7OnEs72Fi5/J+i0on3964F1aynQpCalcgMqRo=
github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o=
github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU=
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ=
github.com/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-20260608063931-a3417d38cda0 h1:XHNNzebIKZRkLimla/hFGrAIX5EMWHctrgt3hLw7s+I=
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU=
github.com/livekit/protocol v1.46.7-0.20260605212020-c0615a2f6f84 h1:dkHHthyor9dwxxdBmbeG1ZUI4bPHpTEk9DjYJSSSIl4=
github.com/livekit/protocol v1.46.7-0.20260605212020-c0615a2f6f84/go.mod h1:jO+y05AU9Ec4JswDyuzKCZ4bhziOS0CzMqgnbj60Dzs=
github.com/livekit/psrpc v0.7.2 h1:6oZ+NODJ2pLyaT6VqDq1F4Qc/3TpDUSpyphj/P9MhQc=
github.com/livekit/psrpc v0.7.2/go.mod h1:rAI+m2+/cb4x9RXhLRtUx5ZwdfjjXOl4zi46IjEetaw=
github.com/mackerelio/go-osstat v0.2.7 h1:TCavZi10wF49bT6iQZ9eT2keGZQpC69MTDfdJej5e94=
github.com/mackerelio/go-osstat v0.2.7/go.mod h1:dwpYh5pIPmvk+IEwBKNIWRFMB92mrC08CmXOhDC7nQk=
github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.2 h1:V23nK2R2B63g2GhygF9zVGpnigmhvoZoH8d0hrZwMGY=
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.2/go.mod h1:Mr897yU9FmyKaQDPtRlVKibrjz40XXyOHUfyZBPSyZU=
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=
@@ -198,216 +188,195 @@ github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnN
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/netlink v1.6.2/go.mod h1:O1HXX2sIWSMJ3Qn1BYZk1yZM+7iMki/uYGGiwGyq/iU=
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
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/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.39.0 h1:2/yg2JQjiYYKLwDuBzV0FbB2sIV+eFNkEevlRi4n9lI=
github.com/nats-io/nats.go v1.39.0/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM=
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/nyaruka/phonenumbers v1.8.0 h1:TrXNJmbwcAHajzDqin3mLWw57vqLUA6ZjVdeNds0heQ=
github.com/nyaruka/phonenumbers v1.8.0/go.mod h1:fsKPJ70O9JetEA4ggnJadYTFWwtGPvu/lETTXNXq6Cs=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w=
github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA=
github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
github.com/pion/ice/v4 v4.0.7 h1:mnwuT3n3RE/9va41/9QJqN5+Bhc0H/x/ZyiVlWMw35M=
github.com/pion/ice/v4 v4.0.7/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/ory/dockertest/v4 v4.0.0 h1:i19aFsO/VXE0VrMk4ifnKW4G/KIJ93PCjLOslxXoPME=
github.com/ory/dockertest/v4 v4.0.0/go.mod h1:b5Ofu8VIxWNhXFvQcLu17pRNQdoUBKtXBW74G4Ygzx8=
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
github.com/pion/ice/v4 v4.2.7 h1:zDEbC6MiEdhQpF8TxBOTws+NU6ZgGpveHrQq4Lc1kao=
github.com/pion/ice/v4 v4.2.7/go.mod h1:9SNPaq0c7El/ki8leJzyCkK10zsskprR3zTNbO3monY=
github.com/pion/interceptor v0.1.45 h1:6PUo/5829bIfRFIPPJQzuDn8EjxRTSB/CSD7QVCOaqo=
github.com/pion/interceptor v0.1.45/go.mod h1:gNDYM/uFKcLe/B3gS2/7+aw6z+RDiMy2qKTnF1LO31w=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.11 h1:0i7BNFH2n8LVp08q/dqM5iyZBXW4TITbD1+RwNqk/iY=
github.com/pion/webrtc/v4 v4.0.11/go.mod h1:C+5JA7KiyLyoKyGh7hVFD/HCAon3IB/tfniocpZ9JoU=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo=
github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
github.com/pion/sctp v1.9.5 h1:QoSFB/drmAsmSeSFNQNI3xx010nW4HsycCZckRVWWag=
github.com/pion/sctp v1.9.5/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
github.com/pion/srtp/v3 v3.0.11 h1:GiESUr54/K4UuPigfq/CvWUed80JenQAHXn0C2MQQIQ=
github.com/pion/srtp/v3 v3.0.11/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns=
github.com/pion/stun/v3 v3.1.4 h1:/7ZL0j0dmLroKOq4GfkyKQ6asByYqntwyHSp5sYLcGY=
github.com/pion/stun/v3 v3.1.4/go.mod h1:ET7PFiXo1nrD2ZNVpbEHDuT0kCPVXhKmyWdiePNMw/U=
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.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
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/turn/v5 v5.0.8 h1:pZUCtmwWCMkrRKqh/8pL3WoGADXBe0/lOPkN7oqFjK8=
github.com/pion/turn/v5 v5.0.8/go.mod h1:1VwvxElZaOdJU0liJ/WUSm/Tsh+n2OxS5ISSDxgOWxU=
github.com/pion/webrtc/v4 v4.2.11 h1:QUX1QZKlNIn4O7U5JxLPGP0sV5RTncZkzu9SPR3jVNU=
github.com/pion/webrtc/v4 v4.2.11/go.mod h1:s/rAiyy77GyRFrZMx+Ls6aua26dIBPudH8/ZHYbIRWY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
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.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/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-20250811210735-e5fe3b51442e h1:tD38/4xg4nuQCASJ/JxcvCHNb46w0cdAaJfkzQOO1bA=
github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e/go.mod h1:krvJ5AY/MjdPkTeRgMYbIDhbbbVvnPQPzsIsDJO8xrY=
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff h1:NwMEGwb7JJ8wPjT8OPKP5hO1Xz6AQ7Z00+GLSJfW21s=
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/ua-parser/uap-go v0.0.0-20260529044130-17c35e68e58c h1:XbG4n3OWA1PcRTpbBA22E2ChPLvJCuwYRXO12tIyVL0=
github.com/ua-parser/uap-go v0.0.0-20260529044130-17c35e68e58c/go.mod h1:gwANdYmo9R8LLwGnyDFWK2PMsaXXX2HhAvCnb/UhZsM=
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw=
github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
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/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
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/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
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-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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=
@@ -428,71 +397,45 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
+1 -1
View File
@@ -21,7 +21,7 @@ set -o errexit
set -o pipefail
REPO="livekit"
INSTALL_PATH="/usr/local/bin"
INSTALL_PATH="${INSTALL_PATH:-/usr/local/bin}"
log() { printf "%b\n" "$*"; }
abort() {
+15 -3
View File
@@ -76,7 +76,7 @@ func Build() error {
return nil
}
// builds binary that runs on linux amd64
// builds binary that runs on linux
func BuildLinux() error {
mg.Deps(generateWire)
if !checksummer.IsChanged() {
@@ -88,10 +88,14 @@ func BuildLinux() error {
if err := os.MkdirAll("bin", 0755); err != nil {
return err
}
cmd := mageutil.CommandDir(context.Background(), "cmd/server", "go build -buildvcs=false -o ../../bin/livekit-server-amd64")
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=amd64",
"GOARCH=" + buildArch,
"HOME=" + os.Getenv("HOME"),
"GOPATH=" + os.Getenv("GOPATH"),
}
@@ -173,6 +177,14 @@ func TestAll() error {
return mageutil.Run(context.Background(), "go test ./... -count=1 -timeout=4m -v")
}
// runs golangci-lint
func Lint() error {
if _, err := exec.LookPath("golangci-lint"); err != nil {
return errors.New("golangci-lint is not installed, install instructions: https://golangci-lint.run/docs/welcome/install/")
}
return mageutil.Run(context.Background(), "golangci-lint run ./...")
}
// cleans up builds
func Clean() {
fmt.Println("cleaning...")
+44 -7
View File
@@ -12,6 +12,7 @@ import (
"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"
@@ -20,6 +21,7 @@ import (
)
func TestAgent(t *testing.T) {
testAgentName := "test_agent"
t.Run("dispatched jobs are assigned to a worker", func(t *testing.T) {
bus := psrpc.NewLocalMessageBus()
@@ -28,7 +30,7 @@ func TestAgent(t *testing.T) {
t.Cleanup(server.Close)
worker := server.SimulateAgentWorker()
worker.Register("test", livekit.JobType_JT_ROOM)
worker.Register(testAgentName, livekit.JobType_JT_ROOM)
jobAssignments := worker.JobAssignments.Observe()
job := &livekit.Job{
@@ -36,14 +38,19 @@ func TestAgent(t *testing.T) {
DispatchId: guid.New(guid.AgentDispatchPrefix),
Type: livekit.JobType_JT_ROOM,
Room: &livekit.Room{},
AgentName: "test",
AgentName: testAgentName,
}
_, err := client.JobRequest(context.Background(), "test", agent.RoomAgentTopic, job)
_, 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")
}
@@ -108,7 +115,7 @@ func TestAgentLoadBalancing(t *testing.T) {
t.Cleanup(server.Close)
agents := make([]*testutils.AgentWorker, totalWorkers)
for i := 0; i < totalWorkers; i++ {
for i := range totalWorkers {
agents[i] = server.SimulateAgentWorker(
testutils.WithLabel(fmt.Sprintf("agent-%d", i)),
testutils.WithJobLoad(testutils.NewStableJobLoad(0.01)),
@@ -128,7 +135,7 @@ func TestAgentLoadBalancing(t *testing.T) {
}
// check that jobs are distributed normally
for i := 0; i < totalWorkers; i++ {
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
@@ -147,7 +154,7 @@ func TestAgentLoadBalancing(t *testing.T) {
t.Cleanup(server.Close)
agents := make([]*testutils.AgentWorker, totalWorkers)
for i := 0; i < totalWorkers; i++ {
for i := range totalWorkers {
label := fmt.Sprintf("agent-%d", i)
if i%2 == 0 {
// make sure we have some workers that can accept jobs
@@ -169,7 +176,7 @@ func TestAgentLoadBalancing(t *testing.T) {
jobCount[w.Label] = len(w.Jobs())
}
for i := 0; i < totalWorkers; i++ {
for i := range totalWorkers {
label := fmt.Sprintf("agent-%d", i)
if i%2 == 0 {
@@ -181,3 +188,33 @@ func TestAgentLoadBalancing(t *testing.T) {
}
})
}
func TestConnectionClosedOnDispatchError(t *testing.T) {
t.Run("connection closed when unknown message type received", func(t *testing.T) {
bus := psrpc.NewLocalMessageBus()
server := testutils.NewTestServer(bus)
t.Cleanup(server.Close)
// register agent
worker := server.SimulateAgentWorker()
worker.Register("test_agent", livekit.JobType_JT_ROOM)
responses := worker.RegisterWorkerResponses.Observe()
select {
case <-responses.Events():
// registered
case <-time.After(time.Second):
require.Fail(t, "registration timeout")
}
responses.Stop()
// send invalid message (nil Message field triggers ErrUnknownWorkerSignal)
worker.SendMessage(&livekit.WorkerMessage{Message: nil})
select {
case <-worker.Closed():
// connection closed
case <-time.After(time.Second):
require.Fail(t, "connection should have been closed after dispatch error")
}
})
}
+25 -15
View File
@@ -62,11 +62,13 @@ type JobRequest struct {
// only set for participant jobs
Participant *livekit.ParticipantInfo
Metadata string
AgentName string
AgentName string
Deployment string
}
type agentClient struct {
client rpc.AgentInternalClient
config Config
mu sync.RWMutex
@@ -87,7 +89,7 @@ type agentClient struct {
subDone chan struct{}
}
func NewAgentClient(bus psrpc.MessageBus) (Client, error) {
func NewAgentClient(bus psrpc.MessageBus, config Config) (Client, error) {
client, err := rpc.NewAgentInternalClient(bus)
if err != nil {
return nil, err
@@ -95,6 +97,7 @@ func NewAgentClient(bus psrpc.MessageBus) (Client, error) {
c := &agentClient{
client: client,
config: config,
workers: workerpool.New(50),
subDone: make(chan struct{}),
}
@@ -152,21 +155,23 @@ func (c *agentClient) LaunchJob(ctx context.Context, desc *JobRequest) *serverut
}
dispatcher.ForEach(func(curNs string) {
topic := GetAgentTopic(desc.AgentName, curNs)
topic := GetAgentTopic(desc.AgentName, curNs, desc.Deployment)
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,
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,
Deployment: desc.Deployment,
}
resp, err := c.client.JobRequest(context.Background(), topic, jobTypeTopic, job)
if err != nil {
@@ -323,14 +328,19 @@ func (c *agentClient) Stop() error {
return nil
}
func GetAgentTopic(agentName, namespace string) string {
func GetAgentTopic(agentName, namespace, deployment string) string {
var topic string
if agentName == "" {
// Backward compatibility
return namespace
topic = namespace
} else if namespace == "" {
// Forward compatibility once the namespace field is removed from the worker SDK
return agentName
topic = agentName
} else {
return fmt.Sprintf("%s_%s", agentName, namespace)
topic = fmt.Sprintf("%s_%s", agentName, namespace)
}
if deployment != "" {
topic += "_" + deployment
}
return topic
}
+8
View File
@@ -0,0 +1,8 @@
package agent
const DefaultTargetLoad = 0.7
type Config struct {
EnableUserDataRecording bool `yaml:"enable_user_data_recording"`
TargetLoad float32 `yaml:"target_load,omitempty"`
}
+31 -14
View File
@@ -4,14 +4,15 @@ import (
"context"
"errors"
"io"
"maps"
"math"
"math/rand/v2"
"slices"
"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"
@@ -27,18 +28,25 @@ import (
)
type AgentService interface {
HandleConnection(context.Context, agent.SignalConn, agent.WorkerProtocolVersion)
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"},
&config.Config{
Region: "test",
Agents: agent.Config{
TargetLoad: agent.DefaultTargetLoad,
},
},
localNode,
bus,
auth.NewSimpleKeyProvider("test", "verysecretsecret"),
@@ -46,7 +54,7 @@ func NewTestServer(bus psrpc.MessageBus) *TestServer {
}
func NewTestServerWithService(s AgentService) *TestServer {
return &TestServer{s}
return &TestServer{s, "test", "verysecretsecret"}
}
type SimulatedWorkerOptions struct {
@@ -126,7 +134,7 @@ func (h *TestServer) SimulateAgentWorker(opts ...SimulatedWorkerOption) *AgentWo
}
ctx := service.WithAPIKey(o.Context, &auth.ClaimGrants{}, "test")
go h.HandleConnection(ctx, w, agent.CurrentProtocol)
go h.HandleConnection(ctx, w, agent.MakeWorkerRegistration())
return w
}
@@ -204,6 +212,15 @@ func (w *AgentWorker) Close() error {
return nil
}
func (w *AgentWorker) CloseWithReason(_ string) error {
return w.Close()
}
// Closed returns a channel that is closed when the connection is closed by the server
func (w *AgentWorker) Closed() <-chan struct{} {
return w.fuse.Watch()
}
func (w *AgentWorker) SetReadDeadline(t time.Time) error {
w.mu.Lock()
defer w.mu.Unlock()
@@ -315,7 +332,7 @@ func (w *AgentWorker) handlePong(m *livekit.WorkerPong) {
w.WorkerPongs.Emit(m)
}
func (w *AgentWorker) sendMessage(m *livekit.WorkerMessage) {
func (w *AgentWorker) SendMessage(m *livekit.WorkerMessage) {
select {
case <-w.fuse.Watch():
case w.workerMessages <- m:
@@ -323,43 +340,43 @@ func (w *AgentWorker) sendMessage(m *livekit.WorkerMessage) {
}
func (w *AgentWorker) SendRegister(m *livekit.RegisterWorkerRequest) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Register{
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{
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{
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{
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{
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{
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{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_MigrateJob{
MigrateJob: m,
}})
}
@@ -411,7 +428,7 @@ func (w *AgentWorker) SimulateRoomJob(roomName string) {
func (w *AgentWorker) Jobs() []*AgentJob {
w.mu.Lock()
defer w.mu.Unlock()
return maps.Values(w.jobs)
return slices.Collect(maps.Values(w.jobs))
}
type stableJobLoad struct {
+72 -34
View File
@@ -18,12 +18,14 @@ import (
"context"
"errors"
"fmt"
"maps"
"sync"
"time"
"go.uber.org/multierr"
"google.golang.org/protobuf/proto"
pagent "github.com/livekit/protocol/agent"
protoagent "github.com/livekit/protocol/agent"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
@@ -43,6 +45,8 @@ var (
ErrDuplicateJobAssignment = errors.New("duplicate job assignment")
)
const AgentNameAttributeKey = "lk.agent_name"
type WorkerProtocolVersion int
const CurrentProtocol = 1
@@ -57,12 +61,15 @@ type SignalConn interface {
ReadWorkerMessage() (*livekit.WorkerMessage, int, error)
SetReadDeadline(time.Time) error
Close() error
CloseWithReason(reason string) error
}
func JobStatusIsEnded(s livekit.JobStatus) bool {
return s == livekit.JobStatus_JS_SUCCESS || s == livekit.JobStatus_JS_FAILED
}
type AssignmentHook func(next func(*livekit.JobAssignment) error) func(*livekit.JobAssignment) error
type WorkerSignalHandler interface {
HandleRegister(*livekit.RegisterWorkerRequest) error
HandleAvailability(*livekit.AvailabilityResponse) error
@@ -141,10 +148,20 @@ type WorkerRegistration struct {
Protocol WorkerProtocolVersion
ID string
Version string
AgentID string
AgentName string
Namespace string
JobType livekit.JobType
Permissions *livekit.ParticipantPermission
ClientIP string
Deployment string
}
func MakeWorkerRegistration() WorkerRegistration {
return WorkerRegistration{
ID: guid.New(guid.AgentWorkerPrefix),
Protocol: CurrentProtocol,
}
}
var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
@@ -152,18 +169,17 @@ var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
type WorkerRegisterer struct {
WorkerPingHandler
serverInfo *livekit.ServerInfo
protocol WorkerProtocolVersion
deadline time.Time
registration WorkerRegistration
registered bool
}
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, protocol WorkerProtocolVersion) *WorkerRegisterer {
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, base WorkerRegistration) *WorkerRegisterer {
return &WorkerRegisterer{
WorkerPingHandler: WorkerPingHandler{conn: conn},
serverInfo: serverInfo,
protocol: protocol,
registration: base,
deadline: time.Now().Add(RegisterTimeout),
}
}
@@ -185,6 +201,10 @@ func (h *WorkerRegisterer) HandleRegister(req *livekit.RegisterWorkerRequest) er
return ErrUnknownJobType
}
if err := protoagent.ValidateDeployment(req.GetDeployment()); err != nil {
return err
}
permissions := req.AllowedPermissions
if permissions == nil {
permissions = &livekit.ParticipantPermission{
@@ -195,15 +215,12 @@ func (h *WorkerRegisterer) HandleRegister(req *livekit.RegisterWorkerRequest) er
}
}
h.registration = WorkerRegistration{
Protocol: h.protocol,
ID: guid.New(guid.AgentWorkerPrefix),
Version: req.Version,
AgentName: req.AgentName,
Namespace: req.GetNamespace(),
JobType: req.GetType(),
Permissions: permissions,
}
h.registration.Version = req.Version
h.registration.AgentName = req.AgentName
h.registration.Namespace = req.GetNamespace()
h.registration.JobType = req.GetType()
h.registration.Permissions = permissions
h.registration.Deployment = req.GetDeployment()
h.registered = true
_, err := h.conn.WriteServerMessage(&livekit.ServerMessage{
@@ -256,6 +273,9 @@ func NewWorker(
logger: logger.WithValues(
"workerID", registration.ID,
"agentName", registration.AgentName,
"deployment", registration.Deployment,
"agentID", registration.AgentID,
"version", registration.Version,
"jobType", registration.JobType.String(),
),
@@ -268,10 +288,8 @@ func NewWorker(
}
}
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) APIKey() string {
return w.apiKey
}
func (w *Worker) Status() livekit.WorkerStatus {
@@ -293,11 +311,7 @@ func (w *Worker) Logger() logger.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
return maps.Clone(w.runningJobs)
}
func (w *Worker) RunningJobCount() int {
@@ -316,7 +330,7 @@ func (w *Worker) GetJobState(jobID livekit.JobID) (*livekit.JobState, error) {
return utils.CloneProto(j.State), nil
}
func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobState, error) {
func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job, hook AssignmentHook) (*livekit.JobState, error) {
availCh := make(chan *livekit.AvailabilityResponse, 1)
job = utils.CloneProto(job)
jobID := livekit.JobID(job.Id)
@@ -340,13 +354,17 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
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{
if _, err := w.conn.WriteServerMessage(&livekit.ServerMessage{Message: &livekit.ServerMessage_Availability{
Availability: &livekit.AvailabilityRequest{Job: job},
}})
}}); err != nil {
return nil, err
}
timeout := time.NewTimer(AssignJobTimeout)
defer timeout.Stop()
@@ -354,20 +372,31 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
// 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(
token, err := protoagent.BuildAgentToken(
w.apiKey,
w.apiSecret,
job.Room.Name,
res.ParticipantIdentity,
res.ParticipantName,
res.ParticipantMetadata,
res.ParticipantAttributes,
attributes,
w.Permissions,
)
if err != nil {
@@ -375,10 +404,18 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
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},
}})
send := func(a *livekit.JobAssignment) error {
_, err := w.conn.WriteServerMessage(&livekit.ServerMessage{
Message: &livekit.ServerMessage_Assignment{Assignment: a},
})
return err
}
if hook != nil {
send = hook(send)
}
if err := send(&livekit.JobAssignment{Job: job, Token: token}); err != nil {
return nil, err
}
state := utils.CloneProto(job.State)
@@ -407,7 +444,7 @@ func (w *Worker) TerminateJob(jobID livekit.JobID, reason rpc.JobTerminateReason
return nil, ErrJobNotFound
}
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Termination{
_, writeErr := w.conn.WriteServerMessage(&livekit.ServerMessage{Message: &livekit.ServerMessage_Termination{
Termination: &livekit.JobTermination{
JobId: string(jobID),
},
@@ -420,11 +457,12 @@ func (w *Worker) TerminateJob(jobID livekit.JobID, reason rpc.JobTerminateReason
errorStr = "agent worker left the room"
}
return w.UpdateJobStatus(&livekit.UpdateJobStatus{
state, updateErr := w.UpdateJobStatus(&livekit.UpdateJobStatus{
JobId: string(jobID),
Status: status,
Error: errorStr,
})
return state, multierr.Combine(writeErr, updateErr)
}
func (w *Worker) UpdateMetadata(metadata string) {
@@ -530,7 +568,7 @@ func (w *Worker) HandleSimulateJob(simulate *livekit.SimulateJobRequest) error {
}
go func() {
_, err := w.AssignJob(w.ctx, job)
_, err := w.AssignJob(w.ctx, job, nil)
if err != nil {
w.logger.Errorw("unable to simulate job", err, "jobID", job.Id)
}
+30 -12
View File
@@ -15,30 +15,48 @@
package clientconfiguration
import (
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/codecs/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: &ScriptMatch{Expr: `c.protocol <= 5 || c.browser == "firefox"`},
// Match: must.Get(NewScriptMatch(`c.protocol <= 5 || c.browser == "firefox"`)),
// Configuration: &livekit.ClientConfiguration{ResumeConnection: livekit.ClientConfigSetting_DISABLED},
// Merge: false,
// },
{
Match: &ScriptMatch{Expr: `c.browser == "safari"`},
Configuration: &livekit.ClientConfiguration{DisabledCodecs: &livekit.DisabledCodecs{Codecs: []*livekit.Codec{
{Mime: mime.MimeTypeAV1.String()},
}}},
Merge: false,
},
{
Match: &ScriptMatch{Expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android") ||
((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`},
Match: must.Get(NewScriptMatch(`c.browser == "safari"`)),
Configuration: &livekit.ClientConfiguration{
DisabledCodecs: &livekit.DisabledCodecs{
Publish: []*livekit.Codec{{Mime: mime.MimeTypeH264.String()}},
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,
@@ -20,13 +20,14 @@ import (
"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: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
Match: must.Get(NewScriptMatch(`c.protocol > 5 && c.browser != "firefox"`)),
Configuration: &livekit.ClientConfiguration{
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
},
@@ -48,14 +49,14 @@ func TestScriptMatchConfiguration(t *testing.T) {
t.Run("merge", func(t *testing.T) {
confs := []ConfigurationItem{
{
Match: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
Match: must.Get(NewScriptMatch(`c.protocol > 5 && c.browser != "firefox"`)),
Configuration: &livekit.ClientConfiguration{
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
},
Merge: true,
},
{
Match: &ScriptMatch{Expr: `c.sdk == "android"`},
Match: must.Get(NewScriptMatch(`c.sdk == "android"`)),
Configuration: &livekit.ClientConfiguration{
Video: &livekit.VideoConfiguration{
HardwareEncoder: livekit.ClientConfigSetting_DISABLED,
@@ -81,10 +82,12 @@ func TestScriptMatchConfiguration(t *testing.T) {
func TestScriptMatch(t *testing.T) {
client := &livekit.ClientInfo{
Protocol: 6,
Browser: "chrome",
Sdk: 3, // android
DeviceModel: "12345",
Protocol: 6,
Browser: "chrome",
Sdk: 3, // android
DeviceModel: "12345",
BrowserVersion: "13.2",
Version: "2.17.1",
}
type testcase struct {
@@ -99,12 +102,26 @@ func TestScriptMatch(t *testing.T) {
{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},
{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},
{name: "string lesser", expr: `c.browser_version < "11.3"`, result: false},
{name: "string lesser eq", expr: `c.browser_version <= "13.2"`, result: true},
{name: "string greater", expr: `c.browser_version > "11.3"`, result: true},
{name: "string greater eq", expr: `c.browser_version >= "13.2"`, result: true},
{name: "semantic lesser", expr: `c.version < "2.16.10"`, result: false},
{name: "semantic lesser eq", expr: `c.version <= "2.17.1"`, result: true},
{name: "semantic greater", expr: `c.version > "2.16.10"`, result: true},
{name: "semantic greater eq", expr: `c.version >= "2.17.1"`, result: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
match := &ScriptMatch{Expr: c.expr}
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)
@@ -15,11 +15,13 @@
package clientconfiguration
import (
"context"
"errors"
"fmt"
"strings"
"github.com/d5/tengo/v2"
"github.com/d5/tengo/v2/token"
"golang.org/x/mod/semver"
"github.com/livekit/protocol/livekit"
)
@@ -29,7 +31,19 @@ type Match interface {
}
type ScriptMatch struct {
Expr string
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.
@@ -38,17 +52,23 @@ type ScriptMatch struct {
// browser if firefox: c.browser == "firefox"
// combined rule : c.protocol > 5 && c.browser == "firefox"
func (m *ScriptMatch) Match(clientInfo *livekit.ClientInfo) (bool, error) {
res, err := tengo.Eval(context.TODO(), m.Expr, map[string]interface{}{"c": &clientObject{info: clientInfo}})
if err != nil {
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
@@ -72,7 +92,7 @@ func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error
case "sdk":
return &tengo.String{Value: strings.ToLower(c.info.Sdk.String())}, nil
case "version":
return &tengo.String{Value: c.info.Version}, nil
return &ruleSdkVersion{sdkVersion: c.info.Version}, nil
case "protocol":
return &tengo.Int{Value: int64(c.info.Protocol)}, nil
case "os":
@@ -84,9 +104,74 @@ func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error
case "browser":
return &tengo.String{Value: strings.ToLower(c.info.Browser)}, nil
case "browser_version":
return &tengo.String{Value: c.info.BrowserVersion}, nil
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
case token.Less:
isMatch = cmp < 0
case token.LessEq:
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
}
+242 -104
View File
@@ -18,25 +18,30 @@ import (
"fmt"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"gopkg.in/yaml.v3"
"github.com/livekit/livekit-server/pkg/metric"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe"
"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
"github.com/livekit/protocol/codecs/mime"
"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/pacer"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
)
const (
@@ -44,8 +49,9 @@ const (
)
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")
ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0")
ErrTURNSecretFileIncorrectPermission = errors.New("turn secret file others permissions must be set to 0")
ErrKeysNotSet = errors.New("one of key-file or keys must be provided")
)
type Config struct {
@@ -62,7 +68,7 @@ type Config struct {
TURN TURNConfig `yaml:"turn,omitempty"`
Ingress IngressConfig `yaml:"ingress,omitempty"`
SIP SIPConfig `yaml:"sip,omitempty"`
WebHook WebHookConfig `yaml:"webhook,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"`
@@ -73,10 +79,18 @@ type Config struct {
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"`
API APIConfig `yaml:"api,omitempty"`
}
type RTCConfig struct {
@@ -102,6 +116,18 @@ type RTCConfig struct {
// allow TCP and TURN/TLS fallback
AllowTCPFallback *bool `yaml:"allow_tcp_fallback,omitempty"`
// Signaling RTT threshold (in milliseconds) governing ICE/TCP fallback. On a UDP
// failure, ICE/TCP is attempted only while the measured signaling RTT is below this
// value; at or above it, supporting clients fall back directly to TURN/TLS. When 0
// (the default), the RTT check is disabled and ICE/TCP is always attempted (for
// clients that support it). A positive value also gates allow_udp_unstable_fallback.
TCPFallbackRTTThreshold int `yaml:"tcp_fallback_rtt_threshold,omitempty"`
// When enabled, an established UDP connection reporting sustained high packet loss is
// migrated to ICE/TCP or TURN/TLS. Requires tcp_fallback_rtt_threshold to be set
// (> 0). Disabled by default.
AllowUDPUnstableFallback bool `yaml:"allow_udp_unstable_fallback,omitempty"`
// force a reconnect on a publication error
ReconnectOnPublicationError *bool `yaml:"reconnect_on_publication_error,omitempty"`
@@ -118,7 +144,13 @@ type RTCConfig struct {
// 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 {
@@ -127,6 +159,14 @@ type TURNServer struct {
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"`
// File containing the secret
SecretFile string `yaml:"secret_file,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 {
@@ -139,8 +179,9 @@ type CongestionControlConfig struct {
UseSendSideBWEInterceptor bool `yaml:"use_send_side_bwe_interceptor,omitempty"`
UseSendSideBWE bool `yaml:"use_send_side_bwe,omitempty"`
SendSideBWE sendsidebwe.SendSideBWEConfig `yaml:"send_side_bwe,omitempty"`
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 {
@@ -152,6 +193,8 @@ type PlayoutDelayConfig struct {
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 {
@@ -167,6 +210,8 @@ type RoomConfig struct {
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
@@ -187,26 +232,33 @@ type LoggingConfig struct {
}
type TURNConfig struct {
Enabled bool `yaml:"enabled,omitempty"`
Domain string `yaml:"domain,omitempty"`
CertFile string `yaml:"cert_file,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
TLSPort int `yaml:"tls_port,omitempty"`
UDPPort int `yaml:"udp_port,omitempty"`
RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"`
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
ExternalTLS bool `yaml:"external_tls,omitempty"`
}
type WebHookConfig struct {
URLs []string `yaml:"urls,omitempty"`
// key to use for webhook
APIKey string `yaml:"api_key,omitempty"`
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"`
BindAddresses []string `yaml:"bind_addresses,omitempty"`
// TTL of the TURN credentials in seconds - defaults to 300
TTLSeconds int `yaml:"ttl_seconds,omitempty"`
// list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to.
// By default (i. e. empty list), all restricted peer CIDRs are denied access.
// When not empty, only the specified CIDRs are allowed access.
// Note that this check is applied to restricted peer CIDRs only.
AllowRestrictedPeerCIDRs []string `yaml:"allow_restricted_peer_cidrs,omitempty"`
// list of peer CIDRs to deny access to
// This applies to all peer CIDRs, including restricted ones.
// Deny list takes precedence over allow list.
DenyPeerCIDRs []string `yaml:"deny_peer_cidrs,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"`
@@ -245,6 +297,10 @@ 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
}
@@ -281,6 +337,9 @@ type APIConfig struct {
// max amount of time to wait before checking for operation complete
MaxCheckInterval time.Duration `yaml:"max_check_interval,omitempty"`
// Backwards compatibility for room service api calls, will enable by default and remove in a future release
EnablePsrpcForGetListParticpants bool `yaml:"enable_psrpc_for_get_list_participants,omitempty"`
}
type PrometheusConfig struct {
@@ -295,6 +354,13 @@ type ForwardStatsConfig struct {
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,
@@ -303,6 +369,18 @@ func DefaultAPIConfig() APIConfig {
}
}
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{
@@ -324,31 +402,37 @@ var DefaultConfig = Config{
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,
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,
EmptyTimeout: 5 * 60,
DepartureTimeout: 20,
CreateRoomEnabled: true,
CreateRoomTimeout: 10 * time.Second,
CreateRoomAttempts: 3,
UpdateBatchTargetSize: 128 * 1024,
},
Limit: LimitConfig{
MaxMetadataSize: 64000,
@@ -361,13 +445,16 @@ var DefaultConfig = Config{
PionLevel: "error",
},
TURN: TURNConfig{
Enabled: false,
Enabled: false,
BindAddresses: []string{"0.0.0.0"},
TTLSeconds: 300,
},
NodeSelector: NodeSelectorConfig{
Kind: "any",
SortBy: "random",
SysloadLimit: 0.9,
CPULoadLimit: 0.9,
Algorithm: "lowest",
},
SignalRelay: SignalRelayConfig{
RetryTimeout: 7500 * time.Millisecond,
@@ -376,12 +463,19 @@ var DefaultConfig = Config{
StreamBufferSize: 1000,
ConnectAttempts: 3,
},
PSRPC: rpc.DefaultPSRPCConfig,
Keys: map[string]string{},
Metric: metric.DefaultMetricConfig,
Agents: agent.Config{
TargetLoad: agent.DefaultTargetLoad,
},
PSRPC: rpc.DefaultPSRPCConfig,
Keys: map[string]string{},
Metric: metric.DefaultMetricConfig,
WebHook: webhook.DefaultWebHookConfig,
NodeStats: DefaultNodeStatsConfig,
API: DefaultAPIConfig(),
EnableDataTracks: true,
}
func NewConfig(confString string, strictMode bool, c *cli.Context, baseFlags []cli.Flag) (*Config, error) {
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 {
@@ -563,74 +657,110 @@ func (conf *Config) ValidateKeys() error {
return nil
}
func (conf *Config) LoadTURNSecrets() error {
var otherFilter os.FileMode = 0o007
for i, s := range conf.RTC.TURNServers {
if s.SecretFile == "" {
continue
}
if s.Secret != "" {
logger.Warnw("both secret and secret_file are set for TURN server, the hardcoded secret will be used", nil,
"host", s.Host, "port", s.Port)
continue
}
st, err := os.Stat(s.SecretFile)
if err != nil {
return err
}
if st.Mode().Perm()&otherFilter != 0o000 {
return ErrTURNSecretFileIncorrectPermission
}
data, err := os.ReadFile(s.SecretFile)
if err != nil {
return fmt.Errorf("reading turn secret file %q: %w", s.SecretFile, err)
}
conf.RTC.TURNServers[i].Secret = strings.TrimSpace(string(data))
}
return nil
}
func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error) {
blankConfig := &Config{}
defaultConfig := &DefaultConfig
flags := make([]cli.Flag, 0)
for name, value := range blankConfig.ToCLIFlagNames(existingFlags) {
for name, value := range (defaultConfig).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)))
envVar := fmt.Sprintf("LIVEKIT_%s", strings.ToUpper(strings.ReplaceAll(name, ".", "_")))
defaultText := cliDefaultText(value)
switch kind {
case reflect.Bool:
flag = &cli.BoolFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.String:
flag = &cli.StringFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Int, reflect.Int32:
flag = &cli.IntFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Int64:
flag = &cli.Int64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
flag = &cli.UintFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Uint64:
flag = &cli.Uint64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Float32:
flag = &cli.Float64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Float64:
flag = &cli.Float64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Slice:
// TODO
@@ -651,13 +781,39 @@ func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error)
return flags, nil
}
func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
func cliDefaultText(value reflect.Value) string {
if value.Kind() == reflect.Ptr {
if value.IsNil() {
return ""
}
value = value.Elem()
}
switch value.Kind() {
case reflect.Bool:
return strconv.FormatBool(value.Bool())
case reflect.String:
return value.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if value.Type() == reflect.TypeOf(time.Duration(0)) {
return value.Interface().(time.Duration).String()
}
return strconv.FormatInt(value.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(value.Uint(), 10)
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(value.Float(), 'f', -1, 64)
default:
return ""
}
}
func (conf *Config) updateFromCLI(c *cli.Command, baseFlags []cli.Flag) error {
generatedFlagNames := conf.ToCLIFlagNames(baseFlags)
for _, flag := range c.App.Flags {
for _, flag := range c.Flags {
flagName := flag.Names()[0]
// the `c.App.Name != "test"` check is needed because `c.IsSet(...)` is always false in unit tests
if !c.IsSet(flagName) && c.App.Name != "test" {
if !c.IsSet(flagName) {
continue
}
@@ -666,34 +822,16 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
continue
}
kind := configValue.Kind()
if kind == reflect.Ptr {
// instantiate value to be set
if configValue.Kind() == reflect.Ptr {
configValue.Set(reflect.New(configValue.Type().Elem()))
kind = configValue.Type().Elem().Kind()
configValue = configValue.Elem()
}
switch kind {
case reflect.Bool:
configValue.SetBool(c.Bool(flagName))
case reflect.String:
configValue.SetString(c.String(flagName))
case reflect.Int, reflect.Int32, reflect.Int64:
configValue.SetInt(c.Int64(flagName))
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
configValue.SetUint(c.Uint64(flagName))
case reflect.Float32:
configValue.SetFloat(c.Float64(flagName))
case reflect.Float64:
configValue.SetFloat(c.Float64(flagName))
// case reflect.Slice:
// // TODO
// case reflect.Map:
// // TODO
default:
return fmt.Errorf("unsupported generated cli flag type for config: %s is a %s", flagName, kind.String())
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())
}
}
@@ -724,7 +862,7 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
conf.TURN.KeyFile = c.String("turn-key")
}
if c.IsSet("node-ip") {
conf.RTC.NodeIP = c.String("node-ip")
conf.RTC.NodeIP.UnmarshalString(c.String("node-ip"))
}
if c.IsSet("udp-port") {
conf.RTC.UDPPort.UnmarshalString(c.String("udp-port"))
@@ -736,7 +874,7 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
}
func (conf *Config) unmarshalKeys(keys string) error {
temp := make(map[string]interface{})
temp := make(map[string]any)
if err := yaml.Unmarshal([]byte(keys), temp); err != nil {
return err
}
+10 -13
View File
@@ -15,11 +15,10 @@
package config
import (
"flag"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"github.com/livekit/livekit-server/pkg/config/configtest"
)
@@ -53,19 +52,17 @@ func TestGeneratedFlags(t *testing.T) {
generatedFlags, err := GenerateCLIFlags(nil, false)
require.NoError(t, err)
app := cli.NewApp()
app.Name = "test"
app.Flags = append(app.Flags, generatedFlags...)
c := &cli.Command{}
c.Name = "test"
c.Flags = append(c.Flags, generatedFlags...)
set := flag.NewFlagSet("test", 0)
set.Bool("rtc.use_ice_lite", true, "") // bool
set.String("redis.address", "localhost:6379", "") // string
set.Uint("prometheus.port", 9999, "") // uint32
set.Bool("rtc.allow_tcp_fallback", true, "") // pointer
set.Bool("rtc.reconnect_on_publication_error", true, "") // pointer
set.Bool("rtc.reconnect_on_subscription_error", false, "") // pointer
c.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")
c := cli.NewContext(app, set, nil)
conf, err := NewConfig("", true, c, nil)
require.NoError(t, err)
@@ -18,7 +18,7 @@ import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/sfu/utils"
"github.com/livekit/mediatransportutil/pkg/latency"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
@@ -49,7 +49,7 @@ type MetricTimestamperParams struct {
type MetricTimestamper struct {
params MetricTimestamperParams
lock sync.Mutex
owdEstimator *utils.OWDEstimator
owdEstimator *latency.OWDEstimator
lastOWDEstimatorRunAt time.Time
batchesSinceLastOWDEstimatorRun int
}
@@ -57,7 +57,7 @@ type MetricTimestamper struct {
func NewMetricTimestamper(params MetricTimestamperParams) *MetricTimestamper {
return &MetricTimestamper{
params: params,
owdEstimator: utils.NewOWDEstimator(utils.OWDEstimatorParamsDefault),
owdEstimator: latency.NewOWDEstimator(latency.OWDEstimatorParamsDefault),
lastOWDEstimatorRunAt: time.Now().Add(-params.Config.OneWayDelayEstimatorMinInterval),
}
}
+74 -42
View File
@@ -23,6 +23,7 @@ import (
"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"
@@ -94,7 +95,7 @@ type NullMessageSource struct {
func NewNullMessageSource(connID livekit.ConnectionID) *NullMessageSource {
return &NullMessageSource{
connID: connID,
msgChan: make(chan proto.Message, 0),
msgChan: make(chan proto.Message),
}
}
@@ -152,8 +153,13 @@ type StartParticipantSignalResults struct {
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)
StartParticipantSignal(
ctx context.Context,
roomName livekit.RoomName,
pi ParticipantInit,
) (res StartParticipantSignalResults, err error)
}
func CreateRouter(
@@ -162,8 +168,9 @@ func CreateRouter(
signalClient SignalClient,
roomManagerClient RoomManagerClient,
kps rpc.KeepalivePubSub,
nodeStatsConfig config.NodeStatsConfig,
) Router {
lr := NewLocalRouter(node, signalClient, roomManagerClient)
lr := NewLocalRouter(node, signalClient, roomManagerClient, nodeStatsConfig)
if rc != nil {
return NewRedisRouter(lr, rc, kps)
@@ -177,19 +184,24 @@ func CreateRouter(
// ------------------------------------------------
type ParticipantInit struct {
Identity livekit.ParticipantIdentity
Name livekit.ParticipantName
Reconnect bool
ReconnectReason livekit.ReconnectReason
AutoSubscribe bool
Client *livekit.ClientInfo
Grants *auth.ClaimGrants
Region string
AdaptiveStream bool
ID livekit.ParticipantID
SubscriberAllowPause *bool
DisableICELite bool
CreateRoom *livekit.CreateRoomRequest
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 {
@@ -209,6 +221,7 @@ func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
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)
@@ -217,6 +230,10 @@ func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
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
}
@@ -227,20 +244,27 @@ func (pi *ParticipantInit) ToStartSession(roomName livekit.RoomName, connectionI
}
ss := &livekit.StartSession{
RoomName: string(roomName),
Identity: string(pi.Identity),
Name: string(pi.Name),
// connection id is to allow the RTC node to identify where to route the message back to
ConnectionId: string(connectionID),
Reconnect: pi.Reconnect,
ReconnectReason: pi.ReconnectReason,
AutoSubscribe: pi.AutoSubscribe,
Client: pi.Client,
GrantsJson: string(claims),
AdaptiveStream: pi.AdaptiveStream,
ParticipantId: string(pi.ID),
DisableIceLite: pi.DisableICELite,
CreateRoom: pi.CreateRoom,
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
@@ -257,18 +281,26 @@ func ParticipantInitFromStartSession(ss *livekit.StartSession, region string) (*
}
pi := &ParticipantInit{
Identity: livekit.ParticipantIdentity(ss.Identity),
Name: livekit.ParticipantName(ss.Name),
Reconnect: ss.Reconnect,
ReconnectReason: ss.ReconnectReason,
Client: ss.Client,
AutoSubscribe: ss.AutoSubscribe,
Grants: claims,
Region: region,
AdaptiveStream: ss.AdaptiveStream,
ID: livekit.ParticipantID(ss.ParticipantId),
DisableICELite: ss.DisableIceLite,
CreateRoom: ss.CreateRoom,
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
+7 -5
View File
@@ -16,11 +16,11 @@ package routing
import (
"context"
"sync"
"time"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
@@ -32,8 +32,8 @@ type LocalRouter struct {
currentNode LocalNode
signalClient SignalClient
roomManagerClient RoomManagerClient
nodeStatsConfig config.NodeStatsConfig
lock sync.RWMutex
// channels for each participant
requestChannels map[string]*MessageChannel
responseChannels map[string]*MessageChannel
@@ -44,11 +44,13 @@ 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),
}
@@ -106,7 +108,8 @@ func (r *LocalRouter) StartParticipantSignal(ctx context.Context, roomName livek
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,
logger.Errorw(
"could not handle new participant", err,
"room", roomName,
"participant", pi.Identity,
"connID", connectionID,
@@ -146,8 +149,7 @@ func (r *LocalRouter) statsWorker() {
if !r.isStarted.Load() {
return
}
// update every 10 seconds
<-time.After(statsUpdateInterval)
<-time.After(r.nodeStatsConfig.StatsUpdateInterval)
r.currentNode.UpdateNodeStats()
}
}
@@ -38,7 +38,7 @@ func TestMessageChannel_WriteMessageClosed(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
for range 100 {
_ = m.WriteMessage(&livekit.SignalRequest{})
}
}()
+14 -17
View File
@@ -20,12 +20,10 @@ import (
"time"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
)
type LocalNode interface {
@@ -45,30 +43,35 @@ type LocalNodeImpl struct {
lock sync.RWMutex
node *livekit.Node
// previous stats for computing averages
prevStats *livekit.NodeStats
nodeStats *NodeStats
}
func NewLocalNode(conf *config.Config) (*LocalNodeImpl, error) {
nodeID := guid.New(utils.NodePrefix)
if conf != nil && conf.RTC.NodeIP == "" {
if conf != nil && conf.RTC.NodeIP.IsEmpty() {
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: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
StartedAt: nowUnix,
UpdatedAt: nowUnix,
},
},
}
var nsc *config.NodeStatsConfig
if conf != nil {
l.node.Ip = conf.RTC.NodeIP
l.node.Ip = conf.RTC.NodeIP.PrimaryIP()
l.node.Region = conf.Region
nsc = &conf.NodeStats
}
l.nodeStats = NewNodeStats(nsc, nowUnix)
return l, nil
}
@@ -138,18 +141,12 @@ func (l *LocalNodeImpl) UpdateNodeStats() bool {
l.lock.Lock()
defer l.lock.Unlock()
if l.prevStats == nil {
l.prevStats = l.node.Stats
}
updated, computedAvg, err := prometheus.GetUpdatedNodeStats(l.node.Stats, l.prevStats)
stats, err := l.nodeStats.UpdateAndGetNodeStats()
if err != nil {
logger.Errorw("could not update node stats", err)
return false
}
l.node.Stats = updated
if computedAvg {
l.prevStats = updated
}
l.node.Stats = stats
return true
}
+82
View 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
}
+3 -8
View File
@@ -33,11 +33,6 @@ import (
)
const (
// expire participant mappings after a day
participantMappingTTL = 24 * time.Hour
statsUpdateInterval = 2 * time.Second
statsMaxDelaySeconds = float64(30)
// hash of node_id => Node proto
NodesKey = "nodes"
@@ -209,11 +204,11 @@ func (r *RedisRouter) statsWorker() {
for r.ctx.Err() == nil {
// update periodically
select {
case <-time.After(statsUpdateInterval):
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 > statsMaxDelaySeconds {
if delaySeconds > r.nodeStatsConfig.StatsMaxDelay.Seconds() {
if !goroutineDumped {
goroutineDumped = true
buf := bytes.NewBuffer(nil)
@@ -240,7 +235,7 @@ func (r *RedisRouter) keepaliveWorker(startedChan chan error) {
close(startedChan)
for ping := range pings.Channel() {
if time.Since(time.Unix(ping.Timestamp, 0)) > statsUpdateInterval {
if time.Since(time.Unix(ping.Timestamp, 0)) > r.nodeStatsConfig.StatsUpdateInterval {
logger.Infow("keep alive too old, skipping", "timestamp", ping.Timestamp)
continue
}
@@ -243,14 +243,6 @@ func (fake *FakeMessageSink) WriteMessageReturnsOnCall(i int, result1 error) {
func (fake *FakeMessageSink) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
fake.writeMessageMutex.RLock()
defer fake.writeMessageMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -234,14 +234,6 @@ func (fake *FakeMessageSource) ReadChanReturnsOnCall(i int, result1 <-chan proto
func (fake *FakeMessageSource) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
fake.readChanMutex.RLock()
defer fake.readChanMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -129,10 +129,6 @@ func (fake *FakeRoomManagerClient) CreateRoomReturnsOnCall(i int, result1 *livek
func (fake *FakeRoomManagerClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -845,32 +845,6 @@ func (fake *FakeRouter) UnregisterNodeReturnsOnCall(i int, result1 error) {
func (fake *FakeRouter) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.clearRoomStateMutex.RLock()
defer fake.clearRoomStateMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
fake.drainMutex.RLock()
defer fake.drainMutex.RUnlock()
fake.getNodeForRoomMutex.RLock()
defer fake.getNodeForRoomMutex.RUnlock()
fake.getRegionMutex.RLock()
defer fake.getRegionMutex.RUnlock()
fake.listNodesMutex.RLock()
defer fake.listNodesMutex.RUnlock()
fake.registerNodeMutex.RLock()
defer fake.registerNodeMutex.RUnlock()
fake.removeDeadNodesMutex.RLock()
defer fake.removeDeadNodesMutex.RUnlock()
fake.setNodeForRoomMutex.RLock()
defer fake.setNodeForRoomMutex.RUnlock()
fake.startMutex.RLock()
defer fake.startMutex.RUnlock()
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
fake.stopMutex.RLock()
defer fake.stopMutex.RUnlock()
fake.unregisterNodeMutex.RLock()
defer fake.unregisterNodeMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -173,10 +173,6 @@ func (fake *FakeSignalClient) StartParticipantSignalReturnsOnCall(i int, result1
func (fake *FakeSignalClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.activeCountMutex.RLock()
defer fake.activeCountMutex.RUnlock()
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
+3 -2
View File
@@ -20,7 +20,8 @@ import (
// AnySelector selects any available node with no limitations
type AnySelector struct {
SortBy string
SortBy string
Algorithm string
}
func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
@@ -29,5 +30,5 @@ func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
return nil, ErrNoAvailableNodes
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
@@ -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)
})
}
}
@@ -23,6 +23,7 @@ import (
type CPULoadSelector struct {
CPULoadLimit float32
SortBy string
Algorithm string
}
func (s *CPULoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
@@ -50,5 +51,5 @@ func (s *CPULoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, erro
return nil, err
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
@@ -25,7 +25,7 @@ import (
)
func TestCPULoadSelector_SelectNode(t *testing.T) {
sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random"}
sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random", Algorithm: "lowest"}
var nodes []*livekit.Node
_, err := sel.SelectNode(nodes)
@@ -39,7 +39,7 @@ func TestCPULoadSelector_SelectNode(t *testing.T) {
// Select a node with low load when available
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
for i := 0; i < 5; i++ {
for range 5 {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
@@ -21,5 +21,7 @@ var (
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")
)
@@ -37,19 +37,21 @@ func CreateNodeSelector(conf *config.Config) (NodeSelector, error) {
}
switch kind {
case "any":
return &AnySelector{conf.NodeSelector.SortBy}, nil
return &AnySelector{conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm}, nil
case "cpuload":
return &CPULoadSelector{
CPULoadLimit: conf.NodeSelector.CPULoadLimit,
SortBy: conf.NodeSelector.SortBy,
SortBy: conf.NodeSelector.SortBy,
Algorithm: conf.NodeSelector.Algorithm,
}, nil
case "sysload":
return &SystemLoadSelector{
SysloadLimit: conf.NodeSelector.SysloadLimit,
SortBy: conf.NodeSelector.SortBy,
SortBy: conf.NodeSelector.SortBy,
Algorithm: conf.NodeSelector.Algorithm,
}, nil
case "regionaware":
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy)
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm)
if err != nil {
return nil, err
}
@@ -57,7 +59,7 @@ func CreateNodeSelector(conf *config.Config) (NodeSelector, error) {
return s, nil
case "random":
logger.Warnw("random node selector is deprecated, please switch to \"any\" or another selector", nil)
return &AnySelector{conf.NodeSelector.SortBy}, nil
return &AnySelector{conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm}, nil
default:
return nil, ErrUnsupportedSelector
}
@@ -29,9 +29,10 @@ type RegionAwareSelector struct {
regionDistances map[string]float64
regions []config.RegionConfig
SortBy string
Algorithm string
}
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string) (*RegionAwareSelector, error) {
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string, algorithm string) (*RegionAwareSelector, error) {
if currentRegion == "" {
return nil, ErrCurrentRegionNotSet
}
@@ -41,6 +42,7 @@ func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig,
regionDistances: make(map[string]float64),
regions: regions,
SortBy: sortBy,
Algorithm: algorithm,
}
var currentRC *config.RegionConfig
@@ -94,7 +96,7 @@ func (s *RegionAwareSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node,
nodes = nearestNodes
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
// haversine(θ) function
@@ -34,6 +34,7 @@ const (
regionEast = "us-east"
regionSeattle = "seattle"
sortBy = "random"
algorithm = "lowest"
)
func TestRegionAwareRouting(t *testing.T) {
@@ -58,7 +59,7 @@ func TestRegionAwareRouting(t *testing.T) {
nodes := []*livekit.Node{
newTestNodeInRegion("", false),
}
s, err := selector.NewRegionAwareSelector(regionEast, nil, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, nil, sortBy, algorithm)
require.NoError(t, err)
node, err := s.SelectNode(nodes)
@@ -74,7 +75,7 @@ func TestRegionAwareRouting(t *testing.T) {
expectedNode,
newTestNodeInRegion(regionEast, false),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -91,7 +92,7 @@ func TestRegionAwareRouting(t *testing.T) {
newTestNodeInRegion(regionWest, true),
newTestNodeInRegion(regionEast, false),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -107,7 +108,7 @@ func TestRegionAwareRouting(t *testing.T) {
expectedNode,
newTestNodeInRegion(regionEast, true),
}
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -125,7 +126,7 @@ func TestRegionAwareRouting(t *testing.T) {
expectedNode,
expectedNode,
}
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -138,7 +139,7 @@ func TestRegionAwareRouting(t *testing.T) {
nodes := []*livekit.Node{
newTestNodeInRegion(regionWest, true),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
require.NoError(t, err)
node, err := s.SelectNode(nodes)
@@ -23,10 +23,10 @@ import (
)
func SortByTest(t *testing.T, sortBy string) {
sel := selector.SystemLoadSelector{SortBy: sortBy}
sel := selector.SystemLoadSelector{SortBy: sortBy, Algorithm: "lowest"}
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
for i := 0; i < 5; i++ {
for range 5 {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
@@ -38,7 +38,7 @@ func SortByTest(t *testing.T, sortBy string) {
}
func TestSortByErrors(t *testing.T) {
sel := selector.SystemLoadSelector{}
sel := selector.SystemLoadSelector{Algorithm: "lowest"}
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
// Test unset sort by option error
@@ -23,6 +23,7 @@ import (
type SystemLoadSelector struct {
SysloadLimit float32
SortBy string
Algorithm string
}
func (s *SystemLoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
@@ -49,5 +50,5 @@ func (s *SystemLoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, e
return nil, err
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
@@ -33,12 +33,16 @@ var (
NumCpus: 1,
CpuLoad: 0.1,
LoadAvgLast1Min: 0.0,
NumRooms: 1,
NumClients: 2,
NumTracksIn: 4,
NumTracksOut: 8,
BytesInPerSec: 1000,
BytesOutPerSec: 2000,
NumRooms: 1,
NumClients: 2,
NumTracksIn: 4,
NumTracksOut: 8,
Rates: []*livekit.NodeStatsRate{
{
BytesIn: 1000,
BytesOut: 2000,
},
},
},
}
@@ -49,12 +53,16 @@ var (
NumCpus: 1,
CpuLoad: 0.5,
LoadAvgLast1Min: 0.5,
NumRooms: 5,
NumClients: 10,
NumTracksIn: 20,
NumTracksOut: 200,
BytesInPerSec: 5000,
BytesOutPerSec: 10000,
NumRooms: 5,
NumClients: 10,
NumTracksIn: 20,
NumTracksOut: 200,
Rates: []*livekit.NodeStatsRate{
{
BytesIn: 5000,
BytesOut: 10000,
},
},
},
}
@@ -65,18 +73,22 @@ var (
NumCpus: 1,
CpuLoad: 0.99,
LoadAvgLast1Min: 2.0,
NumRooms: 10,
NumClients: 20,
NumTracksIn: 40,
NumTracksOut: 800,
BytesInPerSec: 10000,
BytesOutPerSec: 40000,
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"}
sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random", Algorithm: "lowest"}
var nodes []*livekit.Node
_, err := sel.SelectNode(nodes)
@@ -90,7 +102,7 @@ func TestSystemLoadSelector_SelectNode(t *testing.T) {
// Select a node with low load when available
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
for i := 0; i < 5; i++ {
for range 5 {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
+79 -15
View File
@@ -15,7 +15,8 @@
package selector
import (
"sort"
"math/rand/v2"
"slices"
"time"
"github.com/thoas/go-funk"
@@ -23,6 +24,7 @@ import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/utils"
)
const AvailableSeconds = 5
@@ -62,54 +64,116 @@ func LimitsReached(limitConfig config.LimitConfig, nodeStats *livekit.NodeStats)
if limitConfig.NumTracks > 0 && limitConfig.NumTracks <= nodeStats.NumTracksIn+nodeStats.NumTracksOut {
return true
}
if limitConfig.BytesPerSec > 0 && limitConfig.BytesPerSec <= nodeStats.BytesInPerSec+nodeStats.BytesOutPerSec {
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) (*livekit.Node, error) {
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])
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(GetNodeSysload(a) - GetNodeSysload(b))
})
return nodes[0], nil
case "cpuload":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.CpuLoad < nodes[j].Stats.CpuLoad
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(a.Stats.CpuLoad - b.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
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(a.Stats.NumRooms - b.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
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(a.Stats.NumClients - b.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
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum((a.Stats.NumTracksIn + a.Stats.NumTracksOut) - (b.Stats.NumTracksIn + b.Stats.NumTracksOut))
})
return nodes[0], nil
case "bytespersec":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.BytesInPerSec+nodes[i].Stats.BytesOutPerSec < nodes[j].Stats.BytesInPerSec+nodes[j].Stats.BytesOutPerSec
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
ratea := &livekit.NodeStatsRate{}
if len(a.Stats.Rates) > 0 {
ratea = a.Stats.Rates[0]
}
rateb := &livekit.NodeStatsRate{}
if len(b.Stats.Rates) > 0 {
rateb = b.Stats.Rates[0]
}
return utils.Signum((ratea.BytesIn + ratea.BytesOut) - (rateb.BytesIn + rateb.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
}
+25 -8
View File
@@ -25,6 +25,7 @@ import (
"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"
@@ -50,7 +51,7 @@ type signalClient struct {
}
func NewSignalClient(nodeID livekit.NodeID, bus psrpc.MessageBus, config config.SignalRelayConfig) (SignalClient, error) {
c, err := rpc.NewTypedSignalClient(
client, err := rpc.NewTypedSignalClient(
nodeID,
bus,
middleware.WithClientMetrics(rpc.PSRPCMetricsObserver{}),
@@ -63,7 +64,7 @@ func NewSignalClient(nodeID livekit.NodeID, bus psrpc.MessageBus, config config.
return &signalClient{
nodeID: nodeID,
config: config,
client: c,
client: client,
}, nil
}
@@ -88,25 +89,27 @@ func (r *signalClient) StartParticipantSignal(
return
}
l := logger.GetLogger().WithValues(
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.MessageCounter.WithLabelValues("signal", "failure").Add(1)
prometheus.RecordSignalRequestFailure()
return
}
err = stream.Send(&rpc.RelaySignalRequest{StartSession: ss})
if err != nil {
stream.Close(err)
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
prometheus.RecordSignalRequestFailure()
return
}
@@ -130,6 +133,8 @@ func (r *signalClient) StartParticipantSignal(
resChan,
signalResponseMessageReader{},
r.config,
prometheus.RecordSignalResponseSuccess,
prometheus.RecordSignalResponseFailure,
)
l.Debugw("signal stream closed", "error", err)
@@ -139,6 +144,8 @@ func (r *signalClient) StartParticipantSignal(
return connectionID, sink, resChan, nil
}
// ------------------------------
type signalRequestMessageWriter struct{}
func (e signalRequestMessageWriter) Write(seq uint64, close bool, msgs []proto.Message) *rpc.RelaySignalRequest {
@@ -153,6 +160,8 @@ func (e signalRequestMessageWriter) Write(seq uint64, close bool, msgs []proto.M
return r
}
// -------------------------------
type signalResponseMessageReader struct{}
func (e signalResponseMessageReader) Read(rm *rpc.RelaySignalResponse) ([]proto.Message, error) {
@@ -163,6 +172,8 @@ func (e signalResponseMessageReader) Read(rm *rpc.RelaySignalResponse) ([]proto.
return msgs, nil
}
// -----------------------------------------
type RelaySignalMessage interface {
proto.Message
GetSeq() uint64
@@ -182,6 +193,8 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
ch *MessageChannel,
reader SignalMessageReader[RecvType],
config config.SignalRelayConfig,
promSignalSuccess func(),
promSignalFailure func(),
) error {
r := &signalMessageReader[SendType, RecvType]{
reader: reader,
@@ -190,16 +203,16 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
for msg := range stream.Channel() {
res, err := r.Read(msg)
if err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
promSignalFailure()
return err
}
for _, r := range res {
if err = ch.WriteMessage(r); err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
promSignalFailure()
return err
}
prometheus.MessageCounter.WithLabelValues("signal", "success").Add(1)
promSignalSuccess()
}
if msg.GetClose() {
@@ -209,6 +222,8 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
return stream.Err()
}
// ----------------------------------------
type signalMessageReader[SendType, RecvType RelaySignalMessage] struct {
seq uint64
reader SignalMessageReader[RecvType]
@@ -236,6 +251,8 @@ func (r *signalMessageReader[SendType, RecvType]) Read(msg RecvType) ([]proto.Me
return res, nil
}
// ----------------------------------------
type SignalSinkParams[SendType, RecvType RelaySignalMessage] struct {
Stream psrpc.Stream[SendType, RecvType]
Logger logger.Logger
+30 -10
View File
@@ -15,6 +15,7 @@
package rtc
import (
"slices"
"strconv"
"strings"
@@ -45,11 +46,15 @@ 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) SupportPrflxOverRelay() bool {
func (c ClientInfo) SupportsPrflxOverRelay() bool {
return !c.isFirefox()
}
@@ -93,21 +98,36 @@ func (c ClientInfo) SupportsChangeRTPSenderEncodingActive() bool {
}
func (c ClientInfo) ComplyWithCodecOrderInSDPAnswer() bool {
return !((c.isLinux() || c.isAndroid()) && c.isFirefox())
return (!c.isLinux() && !c.isAndroid()) || !c.isFirefox()
}
// Rust SDK can't decode unknown signal message (TrackSubscribed and ErrorResponse)
func (c ClientInfo) SupportTrackSubscribedEvent() bool {
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_RUST && c.ClientInfo.GetProtocol() < 10)
func (c ClientInfo) SupportsTrackSubscribedEvent() bool {
return c.ClientInfo.GetSdk() != livekit.ClientInfo_RUST || c.ClientInfo.GetProtocol() >= 10
}
func (c ClientInfo) SupportErrorResponse() bool {
return c.SupportTrackSubscribedEvent()
func (c ClientInfo) SupportsRequestResponse() bool {
return c.SupportsTrackSubscribedEvent()
}
func (c ClientInfo) SupportSctpZeroChecksum() bool {
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_UNKNOWN ||
(c.isGo() && c.compareVersion("2.4.0") < 0))
func (c ClientInfo) SupportsSctpZeroChecksum() bool {
return c.ClientInfo.GetSdk() != livekit.ClientInfo_UNKNOWN &&
(!c.isGo() || c.compareVersion("2.4.0") >= 0)
}
func (c ClientInfo) SupportsTransceiverReuse() bool {
return !c.isSafari()
}
func (c ClientInfo) HasCapability(cap livekit.ClientInfo_Capability) bool {
if c.ClientInfo == nil {
return false
}
return slices.Contains(c.ClientInfo.Capabilities, cap)
}
func (c ClientInfo) SupportsPacketTrailer() bool {
return c.HasCapability(livekit.ClientInfo_CAP_PACKET_TRAILER)
}
// compareVersion compares a semver against the current client SDK version
@@ -121,7 +141,7 @@ func (c ClientInfo) compareVersion(version string) int {
parts1 := strings.Split(version, ".")
ints0 := make([]int, 3)
ints1 := make([]int, 3)
for i := 0; i < 3; i++ {
for i := range 3 {
if len(parts0) > i {
ints0[i], _ = strconv.Atoi(parts0[i])
}
+70 -29
View File
@@ -20,13 +20,14 @@ import (
"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 (
frameMarking = "urn:ietf:params:rtp-hdrext:framemarking"
repairedRTPStreamID = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
frameMarkingURI = "urn:ietf:params:rtp-hdrext:framemarking"
repairedRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
)
type WebRTCConfig struct {
@@ -79,23 +80,82 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
rtcConf.PacketBufferSizeAudio = rtcConf.PacketBufferSize
}
// publisher configuration
publisherConfig := DirectionConfig{
return &WebRTCConfig{
WebRTCConfig: *webRTCConfig,
Receiver: ReceiverConfig{
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
},
Publisher: getPublisherConfig(false),
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
}, nil
}
func (c *WebRTCConfig) UpdatePublisherConfig(consolidated bool) {
c.Publisher = getPublisherConfig(consolidated)
}
func (c *WebRTCConfig) UpdateSubscriberConfig(ccConf config.CongestionControlConfig) {
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
}
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
c.BufferFactory = factory
c.SettingEngine.BufferFactory = factory.GetOrNew
}
func getPublisherConfig(consolidated bool) DirectionConfig {
if consolidated {
return DirectionConfig{
RTPHeaderExtension: RTPHeaderExtensionConfig{
Audio: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.AudioLevelURI,
act.AbsCaptureTimeURI,
},
Video: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.TransportCCURI,
sdp.ABSSendTimeURI,
frameMarkingURI,
dd.ExtensionURI,
repairedRTPStreamIDURI,
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
Audio: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBNACK},
},
Video: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBTransportCC},
{Type: webrtc.TypeRTCPFBGoogREMB},
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
{Type: webrtc.TypeRTCPFBNACK},
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
},
},
}
}
return DirectionConfig{
RTPHeaderExtension: RTPHeaderExtensionConfig{
Audio: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.AudioLevelURI,
//act.AbsCaptureTimeURI,
act.AbsCaptureTimeURI,
},
Video: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.TransportCCURI,
frameMarking,
frameMarkingURI,
dd.ExtensionURI,
repairedRTPStreamID,
//act.AbsCaptureTimeURI,
repairedRTPStreamIDURI,
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
@@ -110,25 +170,6 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
},
},
}
return &WebRTCConfig{
WebRTCConfig: *webRTCConfig,
Receiver: ReceiverConfig{
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
},
Publisher: publisherConfig,
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
}, nil
}
func (c *WebRTCConfig) UpdateCongestionControl(ccConf config.CongestionControlConfig) {
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
}
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
c.BufferFactory = factory
c.SettingEngine.BufferFactory = factory.GetOrNew
}
func getSubscriberConfig(enableTWCC bool) DirectionConfig {
@@ -136,10 +177,10 @@ func getSubscriberConfig(enableTWCC bool) DirectionConfig {
RTPHeaderExtension: RTPHeaderExtensionConfig{
Video: []string{
dd.ExtensionURI,
//act.AbsCaptureTimeURI,
act.AbsCaptureTimeURI,
},
Audio: []string{
//act.AbsCaptureTimeURI,
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
+109
View File
@@ -0,0 +1,109 @@
// 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
BytesTrackStats *BytesTrackStats
}
type DataDownTrack struct {
params DataDownTrackParams
logger logger.Logger
createdAt int64
}
func NewDataDownTrack(params DataDownTrackParams) (*DataDownTrack, error) {
d := &DataDownTrack{
params: params,
createdAt: time.Now().UnixNano(),
}
d.logger = params.Logger.WithValues("name", d.Name(), "handle", d.Handle())
if err := d.params.PublishDataTrack.AddDataDownTrack(d); err != nil {
d.logger.Warnw("could not add data down track", err)
return nil, err
}
d.logger.Infow("created data down track")
return d, nil
}
func (d *DataDownTrack) Close() {
d.logger.Infow("closing data down track")
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.Stop()
}
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.logger.Warnw("could not marshal data track message", err)
return
}
if err := d.params.Transport.SendDataTrackMessage(buf); err != nil {
d.logger.Warnw("could not send data track message", err)
return
}
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.AddBytes(uint64(len(buf)), true)
}
}
func (d *DataDownTrack) UpdateSubscriptionOptions(subscriptionOptions *livekit.DataTrackSubscriptionOptions) {
// DT-TODO
}
+190
View File
@@ -0,0 +1,190 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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
BytesTrackStats *BytesTrackStats
}
type DataTrack struct {
params DataTrackParams
logger logger.Logger
lock sync.Mutex
dti *livekit.DataTrackInfo
subscribedTracks map[livekit.ParticipantID]types.DataDownTrack
downTrackSpreader *sfuutils.DownTrackSpreader[types.DataTrackSender]
stats *dataTrackStats
closed core.Fuse
}
func NewDataTrack(params DataTrackParams, dti *livekit.DataTrackInfo) *DataTrack {
d := &DataTrack{
params: params,
dti: dti,
subscribedTracks: make(map[livekit.ParticipantID]types.DataDownTrack),
}
d.logger = params.Logger.WithValues("name", d.Name(), "handle", dti.PubHandle)
d.downTrackSpreader = sfuutils.NewDownTrackSpreader[types.DataTrackSender](sfuutils.DownTrackSpreaderParams{
Threshold: 20,
Logger: d.logger,
})
d.stats = newDataTrackStats(dataTrackStatsParams{Logger: d.logger})
d.logger.Infow("created data track", "dataTrackInfo", logger.Proto(d.dti))
return d
}
func (d *DataTrack) Close() {
d.logger.Infow("closing data track")
d.closed.Break()
d.stats.Close()
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.Stop()
}
}
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
}
bytesStats := NewBytesTrackStats(
sub.GetCountry(),
d.ID(),
sub.ID(),
sub.Kind(),
sub.KindDetails(),
sub.GetTelemetryListener(),
sub.GetReporter(),
)
dataDownTrack, err := NewDataDownTrack(DataDownTrackParams{
Logger: sub.GetLogger().WithValues("trackID", d.ID()),
SubscriberID: sub.ID(),
PublishDataTrack: d,
Handle: sub.GetNextSubscribedDataTrackHandle(),
Transport: sub.GetDataTrackTransport(),
BytesTrackStats: bytesStats,
})
if err != nil {
bytesStats.Stop()
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.logger.Infow("subscriberID already exists, replacing data downtrack", "subscriberID", dts.SubscriberID())
}
d.downTrackSpreader.Store(dts)
d.logger.Infow("data downtrack added", "subscriberID", dts.SubscriberID())
return nil
}
func (d *DataTrack) DeleteDataDownTrack(subscriberID livekit.ParticipantID) {
d.downTrackSpreader.Free(subscriberID)
d.logger.Infow("data downtrack deleted", "subscriberID", subscriberID)
}
func (d *DataTrack) HandlePacket(data []byte, packet *datatrack.Packet, arrivalTime int64) {
d.stats.Update(packet, arrivalTime, len(data))
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.AddBytes(uint64(len(data)), false)
}
d.downTrackSpreader.Broadcast(func(dts types.DataTrackSender) {
dts.WritePacket(data, packet, arrivalTime)
})
}
@@ -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) >= 256 {
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: uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID),
data: data,
}, nil
}
func (e *ExtensionParticipantSid) Unmarshal(ext Extension) error {
if ext.id != uint8(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
}
@@ -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, 256))
_, err := NewExtensionParticipantSid(longTestParticipantID)
require.Error(t, err)
testParticipantID := livekit.ParticipantID("test")
extParticipantSid, err := NewExtensionParticipantSid(testParticipantID)
require.NoError(t, err)
expectedExt := Extension{
id: uint8(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())
}
+301
View File
@@ -0,0 +1,301 @@
// 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")
errExtensionSizeTooBig = errors.New("extension size is too big")
)
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 = 1
extensionSizeLength = 1
)
type Extension struct {
id uint8
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 |S|F|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 |
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
End of all extensions
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|* padded to 4 byte boundary if aggregate of `Extensions Size` |
|* field and all extensions do not end on a 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 {
extensionsSize := (binary.BigEndian.Uint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength])+1)*4 - extensionsSizeLength
hdrSize += extensionsSizeLength
extensionHeaderSize := extensionIDLength + extensionSizeLength
remainingSize := int(extensionsSize)
idx := extensionsSizeOffset + extensionsSizeLength
for remainingSize != 0 {
// read extension header
if len(buf[idx:]) < extensionIDLength || remainingSize < extensionIDLength {
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), extensionIDLength)
}
id := buf[idx]
if id == 0 {
// end of extensions, padding has started
hdrSize += remainingSize
break
}
if len(buf[idx+1:]) < extensionSizeLength || remainingSize < extensionSizeLength {
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), extensionSizeLength)
}
size := int(buf[idx+1])
remainingSize -= extensionHeaderSize
idx += extensionHeaderSize
hdrSize += extensionHeaderSize
// read extension data
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]})
remainingSize -= size
idx += size
hdrSize += size
}
h.ExtensionsSize = extensionsSize - uint16(remainingSize)
}
return hdrSize, nil
}
func (h *Header) MarshalSize() int {
extensionsSize := 0
if h.HasExtensions {
extensionsSize += extensionsSizeLength
for _, ext := range h.Extensions {
extensionsSize += len(ext.data) + extensionIDLength + extensionSizeLength
}
}
return headerLength + (extensionsSize+3)/4*4
}
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 {
extensionsSize := (extensionsSizeLength + h.ExtensionsSize + 3) / 4 * 4
binary.BigEndian.PutUint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength], (extensionsSize/4)-1)
hdrSize += extensionsSizeLength
addedSize := 0
idx := extensionsSizeOffset + extensionsSizeLength
for _, ext := range h.Extensions {
buf[idx] = ext.id
if len(ext.data) > 255 {
return 0, fmt.Errorf("%w: %d > 255", errExtensionSizeTooBig, len(ext.data))
}
buf[idx+extensionIDLength] = byte(len(ext.data))
copy(buf[idx+extensionIDLength+extensionSizeLength:], ext.data)
extSize := len(ext.data) + extensionIDLength + extensionSizeLength
idx += extSize
hdrSize += extSize
addedSize += extSize
}
paddingSize := extensionsSize - extensionsSizeLength - uint16(addedSize)
for i := range paddingSize {
buf[idx+int(i)] = 0
}
idx += int(paddingSize)
hdrSize += int(paddingSize)
}
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) + extensionIDLength + extensionSizeLength)
h.Extensions[i].data = ext.data
h.ExtensionsSize += uint16(len(h.Extensions[i].data) + extensionIDLength + extensionSizeLength)
return
}
}
h.Extensions = append(h.Extensions, ext)
h.ExtensionsSize += uint16(len(ext.data) + extensionIDLength + extensionSizeLength)
h.HasExtensions = true
}
func (h *Header) GetExtension(id uint8) (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
}
@@ -0,0 +1,259 @@
// 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, 0x01, 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(uint8(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, 0x01, 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(uint8(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, 0x01, 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, 0x01, 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(uint8(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 packet", 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, 0x01, 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, 0x02, 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(uint8(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, 0x01, 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, 0x01, 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)
})
}
@@ -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
}
+110
View File
@@ -0,0 +1,110 @@
// 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/livekit/livekit-server/pkg/rtc/datatrack"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
)
type dataTrackStatsParams struct {
Logger logger.Logger
}
type dataTrackStats struct {
params dataTrackStatsParams
lock sync.Mutex
startTime int64
endTime int64
highestSequenceNumber uint16
numPackets int
numPacketsLost int
numPacketsOutOfOrder int
numFrames int // count of `F` tagged packets, i. e. packets with final packet of frame marker
numBytes int
}
func newDataTrackStats(params dataTrackStatsParams) *dataTrackStats {
return &dataTrackStats{
params: params,
}
}
func (d *dataTrackStats) Update(packet *datatrack.Packet, arrivalTime int64, payloadLength int) {
d.lock.Lock()
defer d.lock.Unlock()
d.numBytes += payloadLength
if d.endTime != 0 {
return
}
if d.startTime == 0 {
d.startTime = arrivalTime
d.highestSequenceNumber = packet.SequenceNumber
d.numPackets = 1
} else {
diff := packet.SequenceNumber - d.highestSequenceNumber
switch {
case diff == 0: // duplicate
return
case diff > (1 << 15): // out of order
d.numPackets++
d.numPacketsOutOfOrder++
if d.numPacketsLost > 0 {
d.numPacketsLost--
}
default: // in order
d.numPackets++
d.numPacketsLost += int(diff) - 1
d.highestSequenceNumber = packet.SequenceNumber
}
}
if packet.IsFinalOfFrame {
d.numFrames++
}
}
func (d *dataTrackStats) Close() {
d.lock.Lock()
defer d.lock.Unlock()
d.endTime = mono.UnixNano()
if d.startTime != 0 {
duration := time.Duration(d.endTime - d.startTime).Seconds()
fps := float64(d.numFrames) / duration
d.params.Logger.Infow(
"data track stats",
"duration", duration,
"numPackets", d.numPackets,
"numPacketsLost", d.numPacketsLost,
"numPacketsOutOfOrder", d.numPacketsOutOfOrder,
"numFrames", d.numFrames,
"fps", fps,
"numBytes", d.numBytes,
)
}
}
@@ -15,7 +15,8 @@
package dynacast
import (
"sort"
"slices"
"strings"
"sync"
"testing"
"time"
@@ -23,20 +24,41 @@ import (
"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/codecs/mime"
"github.com/livekit/protocol/livekit"
)
func TestSubscribedMaxQuality(t *testing.T) {
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) {
dm := NewDynacastManager(DynacastManagerParams{})
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
Listener: &testDynacastManagerListener{
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
},
},
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
@@ -72,21 +94,20 @@ func TestSubscribedMaxQuality(t *testing.T) {
})
t.Run("subscribers max quality", func(t *testing.T) {
dm := NewDynacastManager(DynacastManagerParams{
DynacastPauseDelay: 100 * time.Millisecond,
})
lock := sync.RWMutex{}
lock.Lock()
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
lock.Unlock()
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
Listener: &testDynacastManagerListener{
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
},
},
})
dm.maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
dm.(*dynacastManagerVideo).maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
mime.MimeTypeVP8: livekit.VideoQuality_LOW,
mime.MimeTypeAV1: livekit.VideoQuality_LOW,
}
@@ -279,98 +300,224 @@ func TestSubscribedMaxQuality(t *testing.T) {
}
func TestCodecRegression(t *testing.T) {
dm := NewDynacastManager(DynacastManagerParams{})
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
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)
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
t.Run("codec regression audio", func(t *testing.T) {
var lock sync.Mutex
actualSubscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0)
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},
dm := NewDynacastManagerAudio(DynacastManagerAudioParams{
Listener: &testDynacastManagerListener{
onSubscribedAudioCodecChange: func(subscribedCodecs []*livekit.SubscribedAudioCodec) {
lock.Lock()
actualSubscribedCodecs = subscribedCodecs
lock.Unlock()
},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
})
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
dm.NotifySubscription("s1", mime.MimeTypeRED, true)
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},
expectedSubscribedCodecs := []*livekit.SubscribedAudioCodec{
{
Codec: mime.MimeTypeRED.String(),
Enabled: true,
},
},
{
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 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,
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
{
Codec: mime.MimeTypeOpus.String(),
Enabled: true,
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
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)
// av1 quality change should be forwarded to vp8
// av1 quality change of node should be ignored
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_HIGH},
})
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
}
func subscribedCodecsAsString(c1 []*livekit.SubscribedCodec) string {
sort.Slice(c1, func(i, j int) bool { return c1[i].Codec < c1[j].Codec })
var s1 string
slices.SortFunc(c1, func(a, b *livekit.SubscribedCodec) int {
return strings.Compare(a.Codec, b.Codec)
})
var s1 strings.Builder
for _, c := range c1 {
s1 += c.String()
s1.WriteString(c.String())
}
return s1
return s1.String()
}
func subscribedAudioCodecsAsString(c1 []*livekit.SubscribedAudioCodec) string {
slices.SortFunc(c1, func(a, b *livekit.SubscribedAudioCodec) int {
return strings.Compare(a.Codec, b.Codec)
})
var s1 strings.Builder
for _, c := range c1 {
s1.WriteString(c.String())
}
return s1.String()
}
@@ -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/protocol/codecs/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)
})
}
@@ -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 (
"maps"
"slices"
"sync"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/logger"
"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 slices.Collect(maps.Values(d.dynacastQuality))
}
@@ -15,148 +15,85 @@
package dynacast
import (
"sync"
"maps"
"time"
"github.com/bep/debounce"
"golang.org/x/exp/maps"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/utils"
)
type DynacastManagerParams struct {
var _ DynacastManager = (*dynacastManagerVideo)(nil)
var _ dynacastQualityListener = (*dynacastManagerVideo)(nil)
type DynacastManagerVideoParams struct {
DynacastPauseDelay time.Duration
Listener DynacastManagerListener
Logger logger.Logger
}
type DynacastManager struct {
params DynacastManagerParams
type dynacastManagerVideo struct {
params DynacastManagerVideoParams
lock sync.RWMutex
regressedCodec map[mime.MimeType]struct{}
dynacastQuality map[mime.MimeType]*DynacastQuality
maxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
committedMaxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
maxSubscribedQualityDebounce func(func())
maxSubscribedQualityDebouncePending bool
qualityNotifyOpQueue *utils.OpsQueue
isClosed bool
onSubscribedMaxQualityChange func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)
*dynacastManagerBase
}
func NewDynacastManager(params DynacastManagerParams) *DynacastManager {
func NewDynacastManagerVideo(params DynacastManagerVideoParams) DynacastManager {
if params.Logger == nil {
params.Logger = logger.GetLogger()
}
d := &DynacastManager{
d := &dynacastManagerVideo{
params: params,
regressedCodec: make(map[mime.MimeType]struct{}),
dynacastQuality: make(map[mime.MimeType]*DynacastQuality),
maxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
committedMaxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
qualityNotifyOpQueue: utils.NewOpsQueue(utils.OpsQueueParams{
Name: "quality-notify",
MinSize: 64,
FlushOnStop: true,
Logger: params.Logger,
}),
}
if params.DynacastPauseDelay > 0 {
d.maxSubscribedQualityDebounce = debounce.New(params.DynacastPauseDelay)
}
d.qualityNotifyOpQueue.Start()
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
}
func (d *DynacastManager) OnSubscribedMaxQualityChange(f func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)) {
d.lock.Lock()
d.onSubscribedMaxQualityChange = f
d.lock.Unlock()
}
func (d *DynacastManager) AddCodec(mime mime.MimeType) {
d.getOrCreateDynacastQuality(mime)
}
func (d *DynacastManager) HandleCodecRegression(fromMime, toMime mime.MimeType) {
fromDq := d.getOrCreateDynacastQuality(fromMime)
d.lock.Lock()
if d.isClosed {
d.lock.Unlock()
return
}
if fromDq == nil {
// should not happen as we have added the codec on setup receiver
d.params.Logger.Warnw("regression from codec not found", nil, "mime", fromMime)
d.lock.Unlock()
return
}
d.regressedCodec[fromMime] = struct{}{}
d.maxSubscribedQuality[fromMime] = livekit.VideoQuality_OFF
// if the new codec is not added, notify the publisher to start publishing
if _, ok := d.maxSubscribedQuality[toMime]; !ok {
d.maxSubscribedQuality[toMime] = livekit.VideoQuality_HIGH
}
d.lock.Unlock()
d.update(false)
fromDq.Stop()
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
}
func (d *DynacastManager) Restart() {
d.lock.Lock()
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
dqs := d.getDynacastQualitiesLocked()
d.lock.Unlock()
for _, dq := range dqs {
dq.Restart()
}
}
func (d *DynacastManager) Close() {
d.qualityNotifyOpQueue.Stop()
d.lock.Lock()
dqs := d.getDynacastQualitiesLocked()
d.dynacastQuality = make(map[mime.MimeType]*DynacastQuality)
d.isClosed = true
d.lock.Unlock()
for _, dq := range dqs {
dq.Stop()
}
}
// THere are situations like track unmute or streaming from a different node
// where subscribed quality needs to sent to the provider immediately.
// This bypasses any debouncing and forces a subscribed quality update
// with immediate effect.
func (d *DynacastManager) ForceUpdate() {
d.update(true)
}
// It is possible for tracks to be in pending close state. When track
// is waiting to be closed, a node is not streaming a track. This can
// be used to force an update announcing that subscribed quality is OFF,
// i.e. indicating not pulling track any more.
func (d *DynacastManager) ForceQuality(quality livekit.VideoQuality) {
func (d *dynacastManagerVideo) ForceQuality(quality livekit.VideoQuality) {
d.lock.Lock()
defer d.lock.Unlock()
@@ -167,14 +104,21 @@ func (d *DynacastManager) ForceQuality(quality livekit.VideoQuality) {
d.enqueueSubscribedQualityChange()
}
func (d *DynacastManager) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, mime mime.MimeType, quality livekit.VideoQuality) {
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 *DynacastManager) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
func (d *dynacastManagerVideo) NotifySubscriberNodeMaxQuality(
nodeID livekit.NodeID,
qualities []types.SubscribedCodecQuality,
) {
for _, quality := range qualities {
dq := d.getOrCreateDynacastQuality(quality.CodecMime)
if dq != nil {
@@ -183,34 +127,10 @@ func (d *DynacastManager) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
}
}
func (d *DynacastManager) getOrCreateDynacastQuality(mimeType mime.MimeType) *DynacastQuality {
d.lock.Lock()
defer d.lock.Unlock()
if d.isClosed || mimeType == mime.MimeTypeUnknown {
return nil
}
if dq := d.dynacastQuality[mimeType]; dq != nil {
return dq
}
dq := NewDynacastQuality(DynacastQualityParams{
MimeType: mimeType,
Logger: d.params.Logger,
})
dq.OnSubscribedMaxQualityChange(d.updateMaxQualityForMime)
dq.Start()
d.dynacastQuality[mimeType] = dq
return dq
}
func (d *DynacastManager) getDynacastQualitiesLocked() []*DynacastQuality {
return maps.Values(d.dynacastQuality)
}
func (d *DynacastManager) updateMaxQualityForMime(mime mime.MimeType, maxQuality livekit.VideoQuality) {
func (d *dynacastManagerVideo) OnUpdateMaxQualityForMime(
mime mime.MimeType,
maxQuality livekit.VideoQuality,
) {
d.lock.Lock()
if _, ok := d.regressedCodec[mime]; !ok {
d.maxSubscribedQuality[mime] = maxQuality
@@ -220,10 +140,11 @@ func (d *DynacastManager) updateMaxQualityForMime(mime mime.MimeType, maxQuality
d.update(false)
}
func (d *DynacastManager) update(force bool) {
func (d *dynacastManagerVideo) update(force bool) {
d.lock.Lock()
d.params.Logger.Debugw("processing quality change",
d.params.Logger.Debugw(
"processing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
@@ -260,7 +181,8 @@ func (d *DynacastManager) update(force bool) {
if downgradesOnly && d.maxSubscribedQualityDebounce != nil {
if !d.maxSubscribedQualityDebouncePending {
d.params.Logger.Debugw("debouncing quality downgrade",
d.params.Logger.Debugw(
"debouncing quality downgrade",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
@@ -269,7 +191,8 @@ func (d *DynacastManager) update(force bool) {
})
d.maxSubscribedQualityDebouncePending = true
} else {
d.params.Logger.Debugw("quality downgrade waiting for debounce",
d.params.Logger.Debugw(
"quality downgrade waiting for debounce",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
@@ -285,7 +208,8 @@ func (d *DynacastManager) update(force bool) {
d.maxSubscribedQualityDebouncePending = false
}
d.params.Logger.Debugw("committing quality change",
d.params.Logger.Debugw(
"committing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
@@ -293,16 +217,14 @@ func (d *DynacastManager) update(force bool) {
// commit change
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality, len(d.maxSubscribedQuality))
for mime, quality := range d.maxSubscribedQuality {
d.committedMaxSubscribedQuality[mime] = quality
}
maps.Copy(d.committedMaxSubscribedQuality, d.maxSubscribedQuality)
d.enqueueSubscribedQualityChange()
d.lock.Unlock()
}
func (d *DynacastManager) enqueueSubscribedQualityChange() {
if d.isClosed || d.onSubscribedMaxQualityChange == nil {
func (d *dynacastManagerVideo) enqueueSubscribedQualityChange() {
if d.isClosed || d.params.Listener == nil {
return
}
@@ -343,7 +265,7 @@ func (d *DynacastManager) enqueueSubscribedQualityChange() {
"subscribedCodecs", subscribedCodecs,
"maxSubscribedQualities", maxSubscribedQualities,
)
d.qualityNotifyOpQueue.Enqueue(func() {
d.onSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
d.notifyOpsQueue.Enqueue(func() {
d.params.Listener.OnDynacastSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
})
}
@@ -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/protocol/codecs/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)
}
@@ -18,23 +18,26 @@ import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
var _ dynacastQuality = (*dynacastQualityVideo)(nil)
const (
initialQualityUpdateWait = 10 * time.Second
)
type DynacastQualityParams struct {
type dynacastQualityVideoParams struct {
MimeType mime.MimeType
Listener dynacastQualityListener
Logger logger.Logger
}
// DynacastQuality manages max subscribed quality of a single receiver of a media track
type DynacastQuality struct {
params DynacastQualityParams
// 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
@@ -43,38 +46,32 @@ type DynacastQuality struct {
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality
maxSubscribedQuality livekit.VideoQuality
maxQualityTimer *time.Timer
regressTo *DynacastQuality
regressTo dynacastQuality
onSubscribedMaxQualityChange func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)
dynacastQualityNull
}
func NewDynacastQuality(params DynacastQualityParams) *DynacastQuality {
return &DynacastQuality{
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 *DynacastQuality) Start() {
func (d *dynacastQualityVideo) Start() {
d.reset()
}
func (d *DynacastQuality) Restart() {
func (d *dynacastQualityVideo) Restart() {
d.reset()
}
func (d *DynacastQuality) Stop() {
func (d *dynacastQualityVideo) Stop() {
d.stopMaxQualityTimer()
}
func (d *DynacastQuality) OnSubscribedMaxQualityChange(f func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)) {
d.lock.Lock()
defer d.lock.Unlock()
d.onSubscribedMaxQualityChange = f
}
func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
func (d *dynacastQualityVideo) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber max quality",
"mime", d.params.MimeType,
@@ -99,7 +96,7 @@ func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.Partic
d.updateQualityChange(false)
}
func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
func (d *dynacastQualityVideo) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber node max quality",
"mime", d.params.MimeType,
@@ -110,8 +107,12 @@ func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
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()
r.params.Logger.Debugw("ignoring node quality change, regressed to another dynacast quality", "mime", d.params.MimeType)
return
}
@@ -125,7 +126,19 @@ func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
d.updateQualityChange(false)
}
func (d *DynacastQuality) RegressTo(other *DynacastQuality) {
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
@@ -134,33 +147,41 @@ func (d *DynacastQuality) RegressTo(other *DynacastQuality) {
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
d.lock.Unlock()
other.lock.Lock()
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 otherQuality, ok := other.maxSubscriberQuality[subID]; ok {
if oldQuality, ok := d.maxSubscriberQuality[subID]; ok {
// no QUALITY_OFF in the map
if quality > otherQuality {
other.maxSubscriberQuality[subID] = quality
if quality > oldQuality {
d.maxSubscriberQuality[subID] = quality
}
} else {
other.maxSubscriberQuality[subID] = quality
d.maxSubscriberQuality[subID] = quality
}
}
for nodeID, quality := range maxSubscriberNodeQuality {
if otherQuality, ok := other.maxSubscriberNodeQuality[nodeID]; ok {
if oldQuality, ok := d.maxSubscriberNodeQuality[nodeID]; ok {
// no QUALITY_OFF in the map
if quality > otherQuality {
other.maxSubscriberNodeQuality[nodeID] = quality
if quality > oldQuality {
d.maxSubscriberNodeQuality[nodeID] = quality
}
} else {
other.maxSubscriberNodeQuality[nodeID] = quality
d.maxSubscriberNodeQuality[nodeID] = quality
}
}
other.lock.Unlock()
other.Restart()
d.lock.Unlock()
d.Restart()
}
func (d *DynacastQuality) reset() {
func (d *dynacastQualityVideo) reset() {
d.lock.Lock()
d.initialized = false
d.lock.Unlock()
@@ -168,7 +189,7 @@ func (d *DynacastQuality) reset() {
d.startMaxQualityTimer()
}
func (d *DynacastQuality) updateQualityChange(force bool) {
func (d *dynacastQualityVideo) updateQualityChange(force bool) {
d.lock.Lock()
maxSubscribedQuality := livekit.VideoQuality_OFF
for _, subQuality := range d.maxSubscriberQuality {
@@ -189,22 +210,20 @@ func (d *DynacastQuality) updateQualityChange(force bool) {
d.initialized = true
d.maxSubscribedQuality = maxSubscribedQuality
d.params.Logger.Debugw("notifying quality change",
d.params.Logger.Debugw(
"notifying quality change",
"mime", d.params.MimeType,
"maxSubscriberQuality", d.maxSubscriberQuality,
"maxSubscriberNodeQuality", d.maxSubscriberNodeQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
"force", force,
)
onSubscribedMaxQualityChange := d.onSubscribedMaxQualityChange
d.lock.Unlock()
if onSubscribedMaxQualityChange != nil {
onSubscribedMaxQualityChange(d.params.MimeType, maxSubscribedQuality)
}
d.params.Listener.OnUpdateMaxQualityForMime(d.params.MimeType, maxSubscribedQuality)
}
func (d *DynacastQuality) startMaxQualityTimer() {
func (d *dynacastQualityVideo) startMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
@@ -219,7 +238,7 @@ func (d *DynacastQuality) startMaxQualityTimer() {
})
}
func (d *DynacastQuality) stopMaxQualityTimer() {
func (d *dynacastQualityVideo) stopMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
@@ -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/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
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) {}
+22 -20
View File
@@ -29,6 +29,7 @@ import (
type EgressLauncher interface {
StartEgress(context.Context, *rpc.StartEgressRequest) (*livekit.EgressInfo, error)
StopEgress(context.Context, *livekit.StopEgressRequest) (*livekit.EgressInfo, error)
}
func StartParticipantEgress(
@@ -42,16 +43,17 @@ func StartParticipantEgress(
) error {
if req, err := startParticipantEgress(ctx, launcher, opts, identity, roomName, roomID); err != nil {
// send egress failed webhook
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
Event: webhook.EventEgressEnded,
EgressInfo: &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Participant{Participant: req},
},
})
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
@@ -103,16 +105,16 @@ func StartTrackEgress(
) error {
if req, err := startTrackEgress(ctx, launcher, opts, track, roomName, roomID); err != nil {
// send egress failed webhook
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
Event: webhook.EventEgressEnded,
EgressInfo: &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Track{Track: req},
},
})
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
-3
View File
@@ -32,9 +32,6 @@ var (
ErrEmptyParticipantID = errors.New("participant ID cannot be empty")
ErrMissingGrants = errors.New("VideoGrant is missing")
ErrInternalError = errors.New("internal error")
ErrNameExceedsLimits = errors.New("name length exceeds limits")
ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits")
ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits")
// Track subscription related
ErrNoTrackPermission = errors.New("participant is not allowed to subscribe to this track")
+117 -122
View File
@@ -20,146 +20,67 @@ import (
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/sfu/mime"
protoCodecs "github.com/livekit/protocol/codecs"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
var OpusCodecCapability = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeOpus.String(),
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
}
var RedCodecCapability = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRED.String(),
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "111/111",
}
var videoRTX = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRTX.String(),
ClockRate: 90000,
}
func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedback RTCPFeedbackConfig, filterOutH264HighProfile bool) error {
opusCodec := OpusCodecCapability
opusCodec.RTCPFeedback = rtcpFeedback.Audio
var opusPayload webrtc.PayloadType
if IsCodecEnabled(codecs, opusCodec) {
opusPayload = 111
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: opusCodec,
PayloadType: opusPayload,
}, webrtc.RTPCodecTypeAudio); err != nil {
// audio codecs
if IsCodecEnabled(codecs, protoCodecs.OpusCodecParameters.RTPCodecCapability) {
cp := protoCodecs.OpusCodecParameters
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
if IsCodecEnabled(codecs, RedCodecCapability) {
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: RedCodecCapability,
PayloadType: 63,
}, webrtc.RTPCodecTypeAudio); err != nil {
if IsCodecEnabled(codecs, protoCodecs.RedCodecParameters.RTPCodecCapability) {
if err := me.RegisterCodec(protoCodecs.RedCodecParameters, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
}
rtxEnabled := IsCodecEnabled(codecs, videoRTX)
for _, codec := range []webrtc.RTPCodecParameters{protoCodecs.PCMUCodecParameters, protoCodecs.PCMACodecParameters} {
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
continue
}
h264HighProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP8.String(),
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP9.String(),
ClockRate: 90000,
SDPFmtpLine: "profile-id=0",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 98,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP9.String(),
ClockRate: 90000,
SDPFmtpLine: "profile-id=1",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 100,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 125,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 108,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: h264HighProfileFmtp,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 123,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeAV1.String(),
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 35,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 116,
},
} {
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == h264HighProfileFmtp {
cp := codec
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
// video codecs
rtxEnabled := IsCodecEnabled(codecs, protoCodecs.VideoRTXCodecParameters.RTPCodecCapability)
for _, codec := range protoCodecs.VideoCodecsParameters {
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == protoCodecs.H264HighProfileFmtp {
continue
}
if mime.IsMimeTypeStringRTX(codec.MimeType) {
continue
}
if IsCodecEnabled(codecs, codec.RTPCodecCapability) {
if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
if rtxEnabled {
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRTX.String(),
ClockRate: 90000,
SDPFmtpLine: fmt.Sprintf("apt=%d", codec.PayloadType),
},
PayloadType: codec.PayloadType + 1,
}, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
continue
}
cp := codec
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Video
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
if !rtxEnabled {
continue
}
cp = protoCodecs.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
@@ -215,3 +136,77 @@ func selectAlternativeVideoCodec(enabledCodecs []*livekit.Codec) string {
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
return mime.MimeTypeVP8.String()
}
func selectAlternativeAudioCodec(enabledCodecs []*livekit.Codec) string {
for _, c := range enabledCodecs {
if mime.IsMimeTypeStringAudio(c.Mime) {
return c.Mime
}
}
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
return mime.MimeTypeOpus.String()
}
// mergeCodecsByMime returns a union b, deduplicated by mime type.
func mergeCodecsByMime(a, b []*livekit.Codec) []*livekit.Codec {
merged := make([]*livekit.Codec, 0, len(a)+len(b))
merged = append(merged, a...)
for _, c := range b {
seen := false
for _, existing := range a {
if mime.IsMimeTypeStringEqual(c.Mime, existing.Mime) {
seen = true
break
}
}
if !seen {
merged = append(merged, c)
}
}
return merged
}
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
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
+348 -122
View File
@@ -15,7 +15,6 @@
package rtc
import (
"context"
"math"
"sync"
"time"
@@ -24,8 +23,11 @@ import (
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"github.com/livekit/protocol/codecs/mime"
"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"
@@ -33,77 +35,100 @@ import (
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/connectionquality"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/sfu/interceptor"
"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
numUpTracks atomic.Uint32
buffer *buffer.Buffer
everSubscribed atomic.Bool
*MediaTrackReceiver
*MediaLossProxy
dynacastManager *dynacast.DynacastManager
dynacastManager dynacast.DynacastManager
lock sync.RWMutex
rttFromXR atomic.Bool
enableRegression 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 {
SignalCid string
SdpCid string
ParticipantID livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
BufferFactory *buffer.Factory
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
PLIThrottleConfig sfu.PLIThrottleConfig
AudioConfig sfu.AudioConfig
VideoConfig config.VideoConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
SimTracks map[uint32]SimulcastTrackInfo
OnRTCP func([]rtcp.Packet)
ForwardStats *sfu.ForwardStats
OnTrackEverSubscribed func(livekit.TrackID)
ParticipantID func() livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
ParticipantCountry string
ParticipantKind livekit.ParticipantInfo_Kind
ParticipantKindDetails []livekit.ParticipantInfo_KindDetail
BufferFactory *buffer.Factory
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
PLIThrottleConfig sfu.PLIThrottleConfig
AudioConfig sfu.AudioConfig
VideoConfig config.VideoConfig
TelemetryListener types.ParticipantTelemetryListener
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,
params: params,
backupCodecPolicy: ti.BackupCodecPolicy,
}
// TODO: disable codec regression until simulcast-codec clients knows that
if ti.BackupCodecPolicy == livekit.BackupCodecPolicy_REGRESSION && len(ti.Codecs) > 1 {
t.enableRegression = true
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,
MediaTrack: t,
IsRelayed: false,
ParticipantID: params.ParticipantID,
ParticipantIdentity: params.ParticipantIdentity,
ParticipantVersion: params.ParticipantVersion,
ReceiverConfig: params.ReceiverConfig,
SubscriberConfig: params.SubscriberConfig,
AudioConfig: params.AudioConfig,
TelemetryListener: params.TelemetryListener,
Logger: params.Logger,
RegressionTargetCodec: t.regressionTargetCodec,
PreferVideoSizeFromMedia: params.PreferVideoSizeFromMedia,
}, ti)
if ti.Type == livekit.TrackType_AUDIO {
@@ -118,27 +143,57 @@ func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
t.MediaTrackReceiver.OnMediaLossFeedback(t.MediaLossProxy.HandleMaxLossFeedback)
}
if ti.Type == livekit.TrackType_VIDEO {
t.dynacastManager = dynacast.NewDynacastManager(dynacast.DynacastManagerParams{
switch ti.Type {
case livekit.TrackType_VIDEO:
t.dynacastManager = dynacast.NewDynacastManagerVideo(dynacast.DynacastManagerVideoParams{
DynacastPauseDelay: params.VideoConfig.DynacastPauseDelay,
Listener: t,
Logger: params.Logger,
})
t.MediaTrackReceiver.OnSetupReceiver(func(mime mime.MimeType) {
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) {
}
})
t.MediaTrackReceiver.OnSubscriberMaxQualityChange(
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, layer int32) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriberMaxQuality(
subscriberID,
mimeType,
buffer.SpatialLayerToVideoQuality(layer, t.MediaTrackReceiver.TrackInfo()),
buffer.GetVideoQualityForSpatialLayer(
mimeType,
layer,
t.MediaTrackReceiver.TrackInfo(),
),
)
},
)
t.MediaTrackReceiver.OnCodecRegression(func(old, new webrtc.RTPCodecParameters) {
t.dynacastManager.HandleCodecRegression(mime.NormalizeMimeType(old.MimeType), mime.NormalizeMimeType(new.MimeType))
})
}
}
},
)
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),
)
}
})
return t
}
@@ -151,24 +206,20 @@ func (t *MediaTrack) OnSubscribedMaxQualityChange(
maxSubscribedQualities []types.SubscribedCodecQuality,
) error,
) {
if t.dynacastManager == nil {
return
}
t.lock.Lock()
t.onSubscribedMaxQualityChange = f
t.lock.Unlock()
}
handler := func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality) {
if f != nil && !t.IsMuted() {
_ = f(t.ID(), t.ToProto(), subscribedQualities, maxSubscribedQualities)
}
for _, q := range maxSubscribedQualities {
receiver := t.Receiver(q.CodecMime)
if receiver != nil {
receiver.SetMaxExpectedSpatialLayer(buffer.VideoQualityToSpatialLayer(q.Quality, t.MediaTrackReceiver.TrackInfo()))
}
}
}
t.dynacastManager.OnSubscribedMaxQualityChange(handler)
func (t *MediaTrack) 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) {
@@ -177,47 +228,84 @@ func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quali
}
}
func (t *MediaTrack) SignalCid() string {
return t.params.SignalCid
func (t *MediaTrack) NotifySubscriptionNode(nodeID livekit.NodeID, codecs []*livekit.SubscribedAudioCodec) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriptionNode(nodeID, codecs)
}
}
func (t *MediaTrack) HasSdpCid(cid string) bool {
if t.params.SdpCid == cid {
return true
func (t *MediaTrack) ClearSubscriberNodes() {
if t.dynacastManager != nil {
t.dynacastManager.ClearSubscriberNodes()
}
}
ti := t.MediaTrackReceiver.TrackInfoClone()
for _, c := range ti.Codecs {
if c.Cid == cid {
return true
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()
}
func (t *MediaTrack) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
t.MediaTrackReceiver.UpdateCodecCid(codecs)
}
// AddReceiver adds a new RTP receiver to the track, returns true when receiver represents a new codec
func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRemote, mid string) bool {
// 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
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)
t.params.Logger.Errorw("could not unmarshal RTCP", err, "size", len(bytes))
return
}
@@ -226,7 +314,13 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
case *rtcp.SourceDescription:
case *rtcp.SenderReport:
if pkt.SSRC == uint32(track.SSRC()) {
buff.SetSenderReportData(pkt.RTPTime, pkt.NTPTime, pkt.PacketCount, pkt.OctetCount)
buff.SetSenderReportData(&livekit.RTCPSenderReportState{
RtpTimestamp: pkt.RTPTime,
NtpTimestamp: pkt.NTPTime,
Packets: pkt.PacketCount,
Octets: uint64(pkt.OctetCount),
At: mono.UnixNano(),
})
}
case *rtcp.ExtendedReport:
rttFromXR:
@@ -255,13 +349,27 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
t.lock.Lock()
var regressCodec bool
mimeType := mime.NormalizeMimeType(track.Codec().MimeType)
layer := buffer.RidToSpatialLayer(track.RID(), ti)
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 {
@@ -276,6 +384,11 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
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
@@ -285,9 +398,13 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
}
}
if priority < 0 {
t.params.Logger.Warnw("could not find codec for webrtc receiver", nil, "webrtcCodec", mimeType, "track", logger.Proto(ti))
t.params.Logger.Warnw(
"could not find codec for webrtc receiver", nil,
"mime", mimeType,
"track", logger.Proto(ti),
)
t.lock.Unlock()
return false
return newCodec, false
}
newWR := sfu.NewWebRTCReceiver(
@@ -300,11 +417,11 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
sfu.WithPliThrottleConfig(t.params.PLIThrottleConfig),
sfu.WithAudioConfig(t.params.AudioConfig),
sfu.WithLoadBalanceThreshold(20),
sfu.WithStreamTrackers(),
sfu.WithForwardStats(t.params.ForwardStats),
sfu.WithEnableRTPStreamRestartDetection(t.params.EnableRTPStreamRestartDetection),
)
newWR.OnCloseHandler(func() {
t.MediaTrackReceiver.SetClosing()
t.MediaTrackReceiver.SetClosing(false)
t.MediaTrackReceiver.ClearReceiver(mimeType, false)
if t.MediaTrackReceiver.TryClose() {
if t.dynacastManager != nil {
@@ -312,15 +429,61 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
}
}
})
// SIMULCAST-CODEC-TODO: these need to be receiver/mime aware, setting it up only for primary now
if priority == 0 {
newWR.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
key := telemetry.StatsKeyForTrack(livekit.StreamType_UPSTREAM, t.PublisherID(), t.ID(), ti.Source, ti.Type)
t.params.Telemetry.TrackStats(key, stat)
})
newWR.OnMaxLayerChange(t.onMaxLayerChange)
// 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,
)
for _, c := range ti.Codecs {
for _, l := range c.Layers {
t.params.Reporter.ReportLayer(roomobs.PackTrackLayer(l.Height, l.Width))
}
}
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.TelemetryListener.OnTrackStats(statsKey, stat)
if cs, ok := telemetry.CondenseStat(stat); ok {
t.params.Reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKind(t.params.ParticipantKind.String())
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(t.params.ParticipantKind))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(t.params.ParticipantKindDetails))
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.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))
@@ -345,8 +508,8 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
t.MediaTrackReceiver.SetupReceiver(newWR, priority, mid)
for ssrc, info := range t.params.SimTracks {
if info.Mid == mid {
t.MediaTrackReceiver.SetLayerSsrc(mimeType, info.Rid, ssrc)
if info.Mid == mid && !info.IsRepairStream {
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, info.StreamID, ssrc, info.RepairSSRC)
}
}
wr = newWR
@@ -355,9 +518,18 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
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 newCodec && t.enableRegression() {
if mimeType == t.regressionTargetCodec {
t.params.Logger.Infow("regression target codec received", "codec", mimeType)
t.regressionTargetCodecReceived = true
@@ -377,21 +549,28 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
"newCodec", newCodec,
)
buff.Close()
return false
return newCodec, false
}
// LK-TODO: can remove this completely when VideoLayers protocol becomes the default as it has info from client or if we decide to use TrackInfo.Simulcast
if t.numUpTracks.Inc() > 1 || track.RID() != "" {
// cannot only rely on numUpTracks since we fire metadata events immediately after the first layer
t.SetSimulcast(true)
var 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
}
var bitrates int
if len(ti.Layers) > int(layer) {
bitrates = int(ti.Layers[layer].GetBitrate())
}
t.MediaTrackReceiver.SetLayerSsrc(mimeType, track.RID(), uint32(track.SSRC()))
t.MediaTrackReceiver.MaybeSetSimulcast()
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, track.RID(), uint32(track.SSRC()), 0)
if regressCodec {
for _, c := range ti.Codecs {
@@ -408,7 +587,7 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
}
}
buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, bitrates)
buff.OnNotifyRTX(t.MediaTrackReceiver.setLayerRtxInfo)
// if subscriber request fps before fps calculated, update them after fps updated.
buff.OnFpsChanged(func() {
@@ -416,20 +595,19 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
})
buff.OnFinalRtpStats(func(stats *livekit.RTPStats) {
t.params.Telemetry.TrackPublishRTPStats(
context.Background(),
t.params.ParticipantID,
t.params.TelemetryListener.OnTrackPublishRTPStats(
t.params.ParticipantID(),
t.ID(),
mimeType,
int(layer),
stats,
)
})
return newCodec
return newCodec, true
}
func (t *MediaTrack) GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality) {
receiver := t.PrimaryReceiver()
receiver := t.ActiveReceiver()
if rtcReceiver, ok := receiver.(*sfu.WebRTCReceiver); ok {
return rtcReceiver.GetConnectionScoreAndQuality()
}
@@ -447,10 +625,6 @@ func (t *MediaTrack) HasPendingCodec() bool {
return t.MediaTrackReceiver.PrimaryReceiver() == nil
}
func (t *MediaTrack) onMaxLayerChange(maxLayer int32) {
t.MediaTrackReceiver.NotifyMaxLayerChange(maxLayer)
}
func (t *MediaTrack) Restart() {
t.MediaTrackReceiver.Restart()
@@ -460,11 +634,10 @@ func (t *MediaTrack) Restart() {
}
func (t *MediaTrack) Close(isExpectedToResume bool) {
t.MediaTrackReceiver.SetClosing()
t.MediaTrackReceiver.SetClosing(isExpectedToResume)
if t.dynacastManager != nil {
t.dynacastManager.Close()
}
t.MediaTrackReceiver.ClearAllReceivers(isExpectedToResume)
t.MediaTrackReceiver.Close(isExpectedToResume)
}
@@ -486,3 +659,56 @@ func (t *MediaTrack) OnTrackSubscribed() {
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)
}
}
+97 -74
View File
@@ -19,7 +19,9 @@ import (
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func TestTrackInfo(t *testing.T) {
@@ -47,128 +49,149 @@ func TestTrackInfo(t *testing.T) {
require.Equal(t, ti.Width, outInfo.Width)
require.Equal(t, ti.Height, outInfo.Height)
require.Equal(t, ti.Simulcast, outInfo.Simulcast)
// make it simulcasted
mt.SetSimulcast(true)
require.True(t, mt.ToProto().Simulcast)
}
func TestGetQualityForDimension(t *testing.T) {
t.Run("landscape source", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 200))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(200, 250))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(700, 480))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(500, 1000))
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{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 540,
Height: 960,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(200, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(600, 900))
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{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
Codecs: []*livekit.SimulcastCodecInfo{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 960,
Height: 540,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
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(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
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{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
Codecs: []*livekit.SimulcastCodecInfo{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 1080,
Height: 720,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
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(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
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{}, &livekit.TrackInfo{
mt = NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
Codecs: []*livekit.SimulcastCodecInfo{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
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(120, 120))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
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))
})
}
+327 -94
View File
@@ -15,19 +15,19 @@
package rtc
import (
"context"
"errors"
"fmt"
"sort"
"slices"
"strings"
"sync"
"github.com/pion/rtcp"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"golang.org/x/exp/slices"
"google.golang.org/protobuf/proto"
"github.com/livekit/mediatransportutil/pkg/codec"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
@@ -35,9 +35,8 @@ import (
"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/mime"
"github.com/livekit/livekit-server/pkg/sfu/rtpstats"
"github.com/livekit/livekit-server/pkg/telemetry"
sutils "github.com/livekit/livekit-server/pkg/utils"
)
const (
@@ -118,17 +117,18 @@ func (r *simulcastReceiver) IsRegressed() bool {
// -----------------------------------------------------
type MediaTrackReceiverParams struct {
MediaTrack types.MediaTrack
IsRelayed bool
ParticipantID livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
AudioConfig sfu.AudioConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
RegressionTargetCodec mime.MimeType
MediaTrack types.MediaTrack
IsRelayed bool
ParticipantID func() livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
AudioConfig sfu.AudioConfig
TelemetryListener types.ParticipantTelemetryListener
Logger logger.Logger
RegressionTargetCodec mime.MimeType
PreferVideoSizeFromMedia bool
}
type MediaTrackReceiver struct {
@@ -161,21 +161,15 @@ func NewMediaTrackReceiver(params MediaTrackReceiverParams, ti *livekit.TrackInf
IsRelayed: params.IsRelayed,
ReceiverConfig: params.ReceiverConfig,
SubscriberConfig: params.SubscriberConfig,
Telemetry: params.Telemetry,
Logger: params.Logger,
})
t.MediaTrackSubscriptions.OnDownTrackCreated(t.onDownTrackCreated)
if ti.Muted {
t.SetMuted(true)
}
return t
}
func (t *MediaTrackReceiver) Restart() {
hq := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.TrackInfo())
for _, receiver := range t.loadReceivers() {
hq := buffer.GetSpatialLayerForVideoQuality(receiver.Mime(), livekit.VideoQuality_HIGH, t.TrackInfo())
receiver.SetMaxExpectedSpatialLayer(hq)
}
}
@@ -213,8 +207,8 @@ func (t *MediaTrackReceiver) SetupReceiver(receiver sfu.TrackReceiver, priority
receivers = append(receivers, &simulcastReceiver{TrackReceiver: receiver, priority: priority})
}
sort.Slice(receivers, func(i, j int) bool {
return receivers[i].Priority() < receivers[j].Priority()
slices.SortFunc(receivers, func(a, b *simulcastReceiver) int {
return sutils.Signum(a.Priority() - b.Priority())
})
if mid != "" {
@@ -308,12 +302,16 @@ func (t *MediaTrackReceiver) HandleReceiverCodecChange(r sfu.TrackReceiver, code
return
}
t.params.Logger.Infow("regressing codec", "from", codec.MimeType, "to", backupCodecReceiver.Codec().MimeType)
t.params.Logger.Infow(
"regressing codec",
"from", codec.MimeType,
"to", backupCodecReceiver.Codec().MimeType,
)
// remove old codec from potential codecs
for i, c := range t.potentialCodecs {
if strings.EqualFold(c.MimeType, codec.MimeType) {
slices.Delete(t.potentialCodecs, i, i+1)
t.potentialCodecs = slices.Delete(t.potentialCodecs, i, i+1)
break
}
}
@@ -351,8 +349,8 @@ func (t *MediaTrackReceiver) SetPotentialCodecs(codecs []webrtc.RTPCodecParamete
})
}
}
sort.Slice(receivers, func(i, j int) bool {
return receivers[i].Priority() < receivers[j].Priority()
slices.SortFunc(receivers, func(a, b *simulcastReceiver) int {
return sutils.Signum(a.Priority() - b.Priority())
})
t.receivers = receivers
t.lock.Unlock()
@@ -376,7 +374,7 @@ func (t *MediaTrackReceiver) ClearReceiver(mime mime.MimeType, isExpectedToResum
}
func (t *MediaTrackReceiver) ClearAllReceivers(isExpectedToResume bool) {
t.params.Logger.Debugw("clearing all receivers")
t.params.Logger.Debugw("clearing all receivers", "isExpectedToResume", isExpectedToResume)
t.lock.Lock()
receivers := t.receivers
t.receivers = nil
@@ -408,12 +406,15 @@ func (t *MediaTrackReceiver) IsOpen() bool {
return true
}
func (t *MediaTrackReceiver) SetClosing() {
func (t *MediaTrackReceiver) SetClosing(isExpectedToResume bool) {
t.lock.Lock()
defer t.lock.Unlock()
if t.state == mediaTrackReceiverStateOpen {
t.state = mediaTrackReceiverStateClosing
}
t.isExpectedToResume = isExpectedToResume
}
func (t *MediaTrackReceiver) TryClose() bool {
@@ -444,6 +445,8 @@ func (t *MediaTrackReceiver) TryClose() bool {
}
func (t *MediaTrackReceiver) Close(isExpectedToResume bool) {
t.ClearAllReceivers(isExpectedToResume)
t.lock.Lock()
if t.state == mediaTrackReceiverStateClosed {
t.lock.Unlock()
@@ -476,7 +479,7 @@ func (t *MediaTrackReceiver) Stream() string {
}
func (t *MediaTrackReceiver) PublisherID() livekit.ParticipantID {
return t.params.ParticipantID
return t.params.ParticipantID()
}
func (t *MediaTrackReceiver) PublisherIdentity() livekit.ParticipantIdentity {
@@ -487,19 +490,6 @@ func (t *MediaTrackReceiver) PublisherVersion() uint32 {
return t.params.ParticipantVersion
}
func (t *MediaTrackReceiver) IsSimulcast() bool {
return t.TrackInfo().Simulcast
}
func (t *MediaTrackReceiver) SetSimulcast(simulcast bool) {
t.lock.Lock()
defer t.lock.Unlock()
trackInfo := t.TrackInfoClone()
trackInfo.Simulcast = simulcast
t.trackInfo.Store(trackInfo)
}
func (t *MediaTrackReceiver) Name() string {
return t.TrackInfo().Name
}
@@ -513,21 +503,19 @@ func (t *MediaTrackReceiver) SetMuted(muted bool) {
trackInfo := t.TrackInfoClone()
trackInfo.Muted = muted
t.trackInfo.Store(trackInfo)
receivers := t.receivers
t.lock.Unlock()
for _, receiver := range receivers {
receiver.SetUpTrackPaused(muted)
}
t.MediaTrackSubscriptions.SetMuted(muted)
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) IsEncrypted() bool {
return t.TrackInfo().Encryption != livekit.Encryption_NONE
}
func (t *MediaTrackReceiver) HasPacketTrailer() bool {
return len(t.TrackInfo().GetPacketTrailerFeatures()) > 0
}
func (t *MediaTrackReceiver) AddOnClose(f func(isExpectedToResume bool)) {
if f == nil {
return
@@ -588,8 +576,10 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su
StreamId: streamId,
UpstreamCodecs: potentialCodecs,
Logger: tLogger,
DisableRed: t.TrackInfo().GetDisableRed() || !t.params.AudioConfig.ActiveREDEncoding,
DisableRed: !IsRedEnabled(t.TrackInfo()) || !t.params.AudioConfig.ActiveREDEncoding,
IsEncrypted: t.IsEncrypted(),
})
subID := sub.ID()
subTrack, err := t.MediaTrackSubscriptions.AddSubscriber(sub, wr)
// media track could have been closed while adding subscription
@@ -603,7 +593,12 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su
t.lock.RUnlock()
if remove {
_ = t.MediaTrackSubscriptions.RemoveSubscriber(sub.ID(), isExpectedToResume)
t.params.Logger.Debugw(
"removing subscriber on a not-open track",
"subscriberID", subID,
"isExpectedToResume", isExpectedToResume,
)
_ = t.MediaTrackSubscriptions.RemoveSubscriber(subID, isExpectedToResume)
return nil, ErrNotOpen
}
@@ -617,7 +612,7 @@ func (t *MediaTrackReceiver) RemoveSubscriber(subscriberID livekit.ParticipantID
}
func (t *MediaTrackReceiver) removeAllSubscribersForMime(mime mime.MimeType, isExpectedToResume bool) {
t.params.Logger.Debugw("removing all subscribers for mime", "mime", mime)
t.params.Logger.Debugw("removing all subscribers for mime", "mime", mime, "isExpectedToResume", isExpectedToResume)
for _, subscriberID := range t.MediaTrackSubscriptions.GetAllSubscribersForMime(mime) {
t.RemoveSubscriber(subscriberID, isExpectedToResume)
}
@@ -632,14 +627,7 @@ func (t *MediaTrackReceiver) RevokeDisallowedSubscribers(allowedSubscriberIdenti
continue
}
found := false
for _, allowedIdentity := range allowedSubscriberIdentities {
if subTrack.SubscriberIdentity() == allowedIdentity {
found = true
break
}
}
found := slices.Contains(allowedSubscriberIdentities, subTrack.SubscriberIdentity())
if !found {
t.params.Logger.Infow("revoking subscription",
"subscriber", subTrack.SubscriberIdentity(),
@@ -658,17 +646,42 @@ func (t *MediaTrackReceiver) updateTrackInfoOfReceivers() {
for _, r := range t.loadReceivers() {
r.UpdateTrackInfo(ti)
}
t.MediaTrackSubscriptions.SetMuted(ti.GetMuted())
}
func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ssrc uint32) {
func (t *MediaTrackReceiver) MaybeSetSimulcast() {
// only primary receiver (i.e. receiver at index 0) for legacy use case
primaryReceiver := t.PrimaryReceiver()
if primaryReceiver == nil {
return
}
if wr, ok := primaryReceiver.(*sfu.WebRTCReceiver); !ok || wr.NumUpTracks() < 2 {
return
}
t.lock.Lock()
trackInfo := t.TrackInfoClone()
layer := buffer.RidToSpatialLayer(rid, trackInfo)
if trackInfo.Simulcast {
t.lock.Unlock()
return
}
trackInfo.Simulcast = true
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) SetLayerSsrcsForRid(mimeType mime.MimeType, rid string, ssrc uint32, repairSSRC uint32) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
layer := buffer.GetSpatialLayerForRid(mimeType, rid, trackInfo)
if layer == buffer.InvalidLayerSpatial {
// non-simulcast case will not have `rid`
layer = 0
}
quality := buffer.SpatialLayerToVideoQuality(layer, trackInfo)
quality := buffer.GetVideoQualityForSpatialLayer(mimeType, layer, trackInfo)
// set video layer ssrc info
for i, ci := range trackInfo.Codecs {
if mime.NormalizeMimeType(ci.MimeType) != mimeType {
@@ -689,6 +702,20 @@ func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ss
}
if !ssrcFound && matchingLayer != nil {
matchingLayer.Ssrc = ssrc
if repairSSRC != 0 {
matchingLayer.RepairSsrc = repairSSRC
}
}
if ssrcFound && (matchingLayer.Ssrc != ssrc || matchingLayer.RepairSsrc != repairSSRC) {
t.params.Logger.Warnw(
"not overriding ssrc", nil,
"rid", rid,
"ssrc", ssrc,
"existingSSRC", matchingLayer.Ssrc,
"repairSSRC", repairSSRC,
"existingRepairSSRC", matchingLayer.RepairSsrc,
"trackInfo", logger.Proto(trackInfo),
)
}
// for client don't use simulcast codecs (old client version or single codec)
@@ -703,13 +730,99 @@ func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ss
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
func (t *MediaTrackReceiver) setLayerRtxInfo(ssrc uint32, repairSSRC uint32, rsid string) {
t.params.Logger.Debugw("rtx notification", "ssrc", ssrc, "repairSSRC", repairSSRC, "rsid", rsid)
if ssrc == 0 || repairSSRC == 0 || rsid == "" {
return
}
t.lock.Lock()
trackInfo := t.TrackInfoClone()
done:
for _, ci := range trackInfo.Codecs {
for _, l := range ci.Layers {
if l.Ssrc == ssrc {
if (l.RepairSsrc != 0 && l.RepairSsrc != repairSSRC) || (l.Rid != "" && l.Rid != rsid) {
t.params.Logger.Warnw(
"not overriding rtx info", nil,
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"existingRepairSSRC", l.RepairSsrc,
"rsid", rsid,
"existingRid", l.Rid,
"trackInfo", logger.Proto(trackInfo),
)
} else {
l.RepairSsrc = repairSSRC
t.params.Logger.Debugw(
"set rtx info",
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"rsid", rsid,
"trackInfo", logger.Proto(trackInfo),
)
}
break done
}
}
}
// backwards compatibility
for _, l := range trackInfo.Layers {
if l.Ssrc == ssrc {
if (l.RepairSsrc != 0 && l.RepairSsrc != repairSSRC) || (l.Rid != "" && l.Rid != rsid) {
t.params.Logger.Warnw(
"not overriding rtx info", nil,
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"existingRepairSSRC", l.RepairSsrc,
"rsid", rsid,
"existingRid", l.Rid,
"trackInfo", logger.Proto(trackInfo),
)
} else {
l.RepairSsrc = repairSSRC
t.params.Logger.Debugw(
"set rtx info",
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"rsid", rsid,
"trackInfo", logger.Proto(trackInfo),
)
}
break
}
}
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
// change not propagated as it is internal
}
func (t *MediaTrackReceiver) UpdateCodecInfo(codecs []*livekit.SimulcastCodec) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
for _, c := range codecs {
for _, origin := range trackInfo.Codecs {
if mime.GetMimeTypeCodec(origin.MimeType) == mime.NormalizeMimeTypeCodec(c.Codec) {
origin.Cid = c.Cid
if len(c.Layers) != 0 {
clonedLayers := make([]*livekit.VideoLayer, 0, len(c.Layers))
for _, l := range c.Layers {
clonedLayers = append(clonedLayers, utils.CloneProto(l))
}
origin.Layers = clonedLayers
mimeType := mime.NormalizeMimeType(origin.MimeType)
for _, layer := range origin.Layers {
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(mimeType, layer.Quality, trackInfo)
layer.Rid = buffer.VideoQualityToRid(mimeType, layer.Quality, trackInfo, buffer.DefaultVideoLayersRid)
}
}
break
}
}
@@ -720,13 +833,49 @@ func (t *MediaTrackReceiver) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateCodecSdpCid(mimeType mime.MimeType, sdpCid string) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
for _, origin := range trackInfo.Codecs {
if mime.NormalizeMimeType(origin.MimeType) == mimeType {
if sdpCid != origin.Cid {
origin.SdpCid = sdpCid
}
}
}
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateCodecRids(mimeType mime.MimeType, rids buffer.VideoLayersRid) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
for _, origin := range trackInfo.Codecs {
originMimeType := mime.NormalizeMimeType(origin.MimeType)
if originMimeType != mimeType {
continue
}
for _, layer := range origin.Layers {
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(mimeType, layer.Quality, trackInfo)
layer.Rid = buffer.VideoQualityToRid(mimeType, layer.Quality, trackInfo, rids)
}
break
}
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
updateMute := false
clonedInfo := utils.CloneProto(ti)
t.lock.Lock()
trackInfo := t.TrackInfo()
// patch Mid and SSRC of codecs/layers by keeping original if available
// patch Mid/Rid and Ssrc/RtxSsrc of codecs/layers by keeping original if available
for i, ci := range clonedInfo.Codecs {
for _, originCi := range trackInfo.Codecs {
if !mime.IsMimeTypeStringEqual(ci.MimeType, originCi.MimeType) {
@@ -743,6 +892,13 @@ func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
if originLayer.Ssrc != 0 {
layer.Ssrc = originLayer.Ssrc
}
if originLayer.Rid != "" {
layer.Rid = originLayer.Rid
}
if originLayer.RepairSsrc != 0 {
layer.RepairSsrc = originLayer.RepairSsrc
}
break
}
}
@@ -755,16 +911,9 @@ func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
clonedInfo.Layers = ci.Layers
}
}
if trackInfo.Muted != clonedInfo.Muted {
updateMute = true
}
t.trackInfo.Store(clonedInfo)
t.lock.Unlock()
if updateMute {
t.SetMuted(clonedInfo.Muted)
}
t.updateTrackInfoOfReceivers()
}
@@ -776,7 +925,9 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
t.lock.Lock()
trackInfo := t.TrackInfo()
clonedInfo := utils.CloneProto(trackInfo)
clonedInfo.AudioFeatures = update.Features
clonedInfo.AudioFeatures = sutils.DedupeSlice(update.Features)
clonedInfo.Stereo = false
clonedInfo.DisableDtx = false
for _, feature := range update.Features {
@@ -787,6 +938,7 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
clonedInfo.DisableDtx = true
}
}
if proto.Equal(trackInfo, clonedInfo) {
t.lock.Unlock()
return
@@ -797,7 +949,7 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
t.updateTrackInfoOfReceivers()
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), clonedInfo)
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
t.params.Logger.Debugw("updated audio track", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
}
@@ -821,10 +973,56 @@ func (t *MediaTrackReceiver) UpdateVideoTrack(update *livekit.UpdateLocalVideoTr
t.updateTrackInfoOfReceivers()
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), clonedInfo)
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
t.params.Logger.Debugw("updated video track", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
}
func (t *MediaTrackReceiver) UpdateVideoSize(mimeType mime.MimeType, sizes []codec.VideoSize) {
var changed bool
t.lock.Lock()
trackInfo := t.TrackInfo()
clonedInfo := utils.CloneProto(trackInfo)
var maxWidth, maxHeight uint32
for _, size := range sizes {
if size.Width > maxWidth {
maxWidth = size.Width
maxHeight = size.Height
}
}
if clonedInfo.Width != maxWidth || clonedInfo.Height != maxHeight {
clonedInfo.Width = maxWidth
clonedInfo.Height = maxHeight
changed = true
}
for _, c := range clonedInfo.Codecs {
if mime.NormalizeMimeType(c.MimeType) == mimeType {
for i, l := range c.Layers {
if i < len(sizes) && (sizes[i].Width != 0 || sizes[i].Height != 0) &&
(l.Width != sizes[i].Width || l.Height != sizes[i].Height) {
l.Width = sizes[i].Width
l.Height = sizes[i].Height
changed = true
}
}
}
}
if !changed {
t.lock.Unlock()
return
}
t.trackInfo.Store(clonedInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
t.params.Logger.Debugw("updated video sizes", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
}
func (t *MediaTrackReceiver) TrackInfo() *livekit.TrackInfo {
return t.trackInfo.Load()
}
@@ -833,16 +1031,17 @@ func (t *MediaTrackReceiver) TrackInfoClone() *livekit.TrackInfo {
return utils.CloneProto(t.TrackInfo())
}
func (t *MediaTrackReceiver) NotifyMaxLayerChange(maxLayer int32) {
func (t *MediaTrackReceiver) NotifyMaxLayerChange(mimeType mime.MimeType, maxLayer int32) {
trackInfo := t.TrackInfo()
quality := buffer.SpatialLayerToVideoQuality(maxLayer, trackInfo)
quality := buffer.GetVideoQualityForSpatialLayer(mimeType, maxLayer, trackInfo)
ti := &livekit.TrackInfo{
Sid: trackInfo.Sid,
Type: trackInfo.Type,
Layers: []*livekit.VideoLayer{{Quality: quality}},
}
if quality != livekit.VideoQuality_OFF {
for _, layer := range trackInfo.Layers {
layers := buffer.GetVideoLayersForMimeType(mimeType, trackInfo)
for _, layer := range layers {
if layer.Quality == quality {
ti.Layers[0].Width = layer.Width
ti.Layers[0].Height = layer.Height
@@ -851,12 +1050,12 @@ func (t *MediaTrackReceiver) NotifyMaxLayerChange(maxLayer int32) {
}
}
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), ti)
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), ti)
}
// GetQualityForDimension finds the closest quality to use for desired dimensions
// affords a 20% tolerance on dimension
func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) livekit.VideoQuality {
func (t *MediaTrackReceiver) GetQualityForDimension(mimeType mime.MimeType, width, height uint32) livekit.VideoQuality {
quality := livekit.VideoQuality_HIGH
if t.Kind() == livekit.TrackType_AUDIO {
return quality
@@ -864,7 +1063,12 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
trackInfo := t.TrackInfo()
if trackInfo.Height == 0 {
var mediaSizes []codec.VideoSize
if receiver := t.Receiver(mimeType); receiver != nil {
mediaSizes = receiver.VideoSizes()
}
if trackInfo.Height == 0 && len(mediaSizes) == 0 {
return quality
}
origSize := trackInfo.Height
@@ -875,19 +1079,38 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
requestedSize = width
}
if origSize == 0 {
for i := len(mediaSizes) - 1; i >= 0; i-- {
if mediaSizes[i].Height > 0 {
origSize = min(mediaSizes[i].Width, mediaSizes[i].Height)
break
}
}
}
// default sizes representing qualities low - high
layerSizes := []uint32{180, 360, origSize}
var providedSizes []uint32
for _, layer := range trackInfo.Layers {
for _, layer := range buffer.GetVideoLayersForMimeType(mimeType, trackInfo) {
providedSizes = append(providedSizes, layer.Height)
}
if len(providedSizes) == 0 || providedSizes[0] == 0 || t.params.PreferVideoSizeFromMedia {
if len(mediaSizes) > 0 {
providedSizes = providedSizes[:0]
for _, size := range mediaSizes {
providedSizes = append(providedSizes, size.Height)
}
} else {
t.params.Logger.Debugw("no video sizes provided by receiver, using track info sizes")
}
}
if len(providedSizes) > 0 {
layerSizes = providedSizes
// comparing height always
requestedSize = height
sort.Slice(layerSizes, func(i, j int) bool {
return layerSizes[i] < layerSizes[j]
})
slices.Sort(layerSizes)
}
// finds the highest layer with smallest dimensions that still satisfy client demands
@@ -905,7 +1128,7 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
}
func (t *MediaTrackReceiver) GetAudioLevel() (float64, bool) {
receiver := t.PrimaryReceiver()
receiver := t.ActiveReceiver()
if receiver == nil {
return 0, false
}
@@ -923,8 +1146,8 @@ func (t *MediaTrackReceiver) onDownTrackCreated(downTrack *sfu.DownTrack) {
}
}
func (t *MediaTrackReceiver) DebugInfo() map[string]interface{} {
info := map[string]interface{}{
func (t *MediaTrackReceiver) DebugInfo() map[string]any {
info := map[string]any{
"ID": t.ID(),
"Kind": t.Kind().String(),
"PubMuted": t.IsMuted(),
@@ -950,6 +1173,16 @@ func (t *MediaTrackReceiver) PrimaryReceiver() sfu.TrackReceiver {
return receivers[0].TrackReceiver
}
func (t *MediaTrackReceiver) ActiveReceiver() sfu.TrackReceiver {
for _, r := range t.loadReceivers() {
if r.IsRegressed() {
return r.TrackReceiver
}
}
return t.PrimaryReceiver()
}
func (t *MediaTrackReceiver) Receiver(mime mime.MimeType) sfu.TrackReceiver {
for _, r := range t.loadReceivers() {
if r.Mime() == mime {
@@ -985,8 +1218,8 @@ func (t *MediaTrackReceiver) SetRTT(rtt uint32) {
}
}
func (t *MediaTrackReceiver) GetTemporalLayerForSpatialFps(spatial int32, fps uint32, mime mime.MimeType) int32 {
receiver := t.Receiver(mime)
func (t *MediaTrackReceiver) GetTemporalLayerForSpatialFps(mimeType mime.MimeType, spatial int32, fps uint32) int32 {
receiver := t.Receiver(mimeType)
if receiver == nil {
return buffer.DefaultMaxLayerTemporal
}
+36 -136
View File
@@ -16,21 +16,17 @@ package rtc
import (
"errors"
"slices"
"sync"
"github.com/pion/rtcp"
"github.com/livekit/protocol/codecs/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/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/mime"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/telemetry"
)
var (
@@ -47,6 +43,7 @@ type MediaTrackSubscriptions struct {
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 {
@@ -56,8 +53,6 @@ type MediaTrackSubscriptionsParams struct {
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
}
@@ -76,6 +71,10 @@ func (t *MediaTrackSubscriptions) OnSubscriberMaxQualityChange(f func(subscriber
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() {
@@ -104,86 +103,32 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
}
t.subscribedTracksMu.Unlock()
var rtcpFeedback []webrtc.RTCPFeedback
var maxTrack int
switch t.params.MediaTrack.Kind() {
case livekit.TrackType_AUDIO:
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Audio
maxTrack = t.params.ReceiverConfig.PacketBufferSizeAudio
case livekit.TrackType_VIDEO:
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Video
maxTrack = t.params.ReceiverConfig.PacketBufferSizeVideo
}
codecs := wr.Codecs()
for _, c := range codecs {
c.RTCPFeedback = rtcpFeedback
}
streamID := wr.StreamID()
if sub.SupportsSyncStreamID() && t.params.MediaTrack.Stream() != "" {
streamID = PackSyncStreamID(t.params.MediaTrack.PublisherID(), t.params.MediaTrack.Stream())
}
var trailer []byte
if t.params.MediaTrack.IsEncrypted() {
trailer = sub.GetTrailer()
}
downTrack, err := sfu.NewDownTrack(sfu.DowntrackParams{
Codecs: codecs,
Source: t.params.MediaTrack.Source(),
Receiver: wr,
BufferFactory: sub.GetBufferFactory(),
SubID: subscriberID,
StreamID: streamID,
MaxTrack: maxTrack,
PlayoutDelayLimit: sub.GetPlayoutDelayConfig(),
Pacer: sub.GetPacer(),
Trailer: trailer,
Logger: LoggerWithTrack(sub.GetLogger().WithComponent(sutils.ComponentSub), trackID, t.params.IsRelayed),
RTCPWriter: sub.WriteSubscriberRTCP,
DisableSenderReportPassThrough: sub.GetDisableSenderReportPassThrough(),
SupportsCodecChange: sub.SupportsCodecChange(),
subTrack, err := NewSubscribedTrack(SubscribedTrackParams{
ReceiverConfig: t.params.ReceiverConfig,
SubscriberConfig: t.params.SubscriberConfig,
Subscriber: sub,
MediaTrack: t.params.MediaTrack,
AdaptiveStream: sub.GetAdaptiveStream(),
TelemetryListener: sub.GetTelemetryListener(),
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
}
if t.onDownTrackCreated != nil {
t.onDownTrackCreated(downTrack)
}
subTrack := NewSubscribedTrack(SubscribedTrackParams{
PublisherID: t.params.MediaTrack.PublisherID(),
PublisherIdentity: t.params.MediaTrack.PublisherIdentity(),
PublisherVersion: t.params.MediaTrack.PublisherVersion(),
Subscriber: sub,
MediaTrack: t.params.MediaTrack,
DownTrack: downTrack,
AdaptiveStream: sub.GetAdaptiveStream(),
})
if !sub.Hidden() {
subTrack.AddOnBind(func(err error) {
if err == nil {
t.params.MediaTrack.OnTrackSubscribed()
}
})
}
// Bind callback can happen from replaceTrack, so set it up early
var reusingTransceiver atomic.Bool
var dtState sfu.DownTrackState
downTrack.OnCodecNegotiated(func(codec webrtc.RTPCodecCapability) {
if !wr.DetermineReceiver(codec) {
if t.onSubscriberMaxQualityChange != nil {
go func() {
spatial := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.params.MediaTrack.ToProto())
t.onSubscriberMaxQualityChange(downTrack.SubscriberID(), mime.NormalizeMimeType(codec.MimeType), spatial)
}()
}
}
})
downTrack := subTrack.DownTrack()
downTrack.OnBinding(func(err error) {
if err != nil {
go subTrack.Bound(err)
@@ -207,25 +152,6 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
subTrack.SetPublisherMuted(t.params.MediaTrack.IsMuted())
})
downTrack.OnStatsUpdate(func(_ *sfu.DownTrack, stat *livekit.AnalyticsStat) {
key := telemetry.StatsKeyForTrack(livekit.StreamType_DOWNSTREAM, subscriberID, trackID, t.params.MediaTrack.Source(), t.params.MediaTrack.Kind())
t.params.Telemetry.TrackStats(key, stat)
})
downTrack.OnMaxLayerChanged(func(dt *sfu.DownTrack, layer int32) {
if t.onSubscriberMaxQualityChange != nil {
t.onSubscriberMaxQualityChange(dt.SubscriberID(), dt.Mime(), layer)
}
})
downTrack.OnRttUpdate(func(_ *sfu.DownTrack, rtt uint32) {
go sub.UpdateMediaRTT(rtt)
})
downTrack.AddReceiverReportListener(func(dt *sfu.DownTrack, report *rtcp.ReceiverReport) {
sub.HandleReceiverReport(dt, report)
})
var transceiver *webrtc.RTPTransceiver
var sender *webrtc.RTPSender
@@ -277,15 +203,16 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
if transceiver == nil {
info := t.params.MediaTrack.ToProto()
addTrackParams := types.AddTrackParams{
Stereo: info.Stereo,
Red: !info.DisableRed,
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() {
if sub.SupportsTransceiverReuse(t.params.MediaTrack) {
//
// AddTrack will create a new transceiver or re-use an unused one
// if the attributes match. This prevents SDP from bloating
@@ -320,6 +247,7 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
// 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.
// Subscription manager will reset this if this subscription proceeds till that point.
subTrack.OnClose(func(isExpectedToResume bool) {
if !isExpectedToResume {
if err := sub.RemoveTrackLocal(sender); err != nil {
@@ -330,10 +258,6 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
downTrack.SetTransceiver(transceiver)
downTrack.OnCloseHandler(func(isExpectedToResume bool) {
t.downTrackClosed(sub, subTrack, isExpectedToResume)
})
t.subscribedTracksMu.Lock()
t.subscribedTracks[subscriberID] = subTrack
t.subscribedTracksMu.Unlock()
@@ -361,10 +285,10 @@ func (t *MediaTrackSubscriptions) closeSubscribedTrack(subTrack types.Subscribed
}
if isExpectedToResume {
dt.CloseWithFlush(false)
dt.CloseWithFlush(false, false)
} else {
// flushing blocks, avoid blocking when publisher removes all its subscribers
go dt.CloseWithFlush(true)
go dt.CloseWithFlush(true, true)
}
}
@@ -429,8 +353,8 @@ func (t *MediaTrackSubscriptions) getAllSubscribedTracksLocked() []types.Subscri
return subTracks
}
func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} {
subscribedTrackInfo := make([]map[string]interface{}, 0)
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())
@@ -439,27 +363,3 @@ func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} {
return subscribedTrackInfo
}
func (t *MediaTrackSubscriptions) downTrackClosed(
sub types.LocalParticipant,
subTrack types.SubscribedTrack,
isExpectedToResume bool,
) {
// Cache transceiver for potential re-use on resume.
// To ensure subscription manager does not re-subscribe before caching,
// delete the subscribed track only after caching.
if isExpectedToResume {
dt := subTrack.DownTrack()
tr := dt.GetTransceiver()
if tr != nil {
sub.CacheDownTrack(subTrack.ID(), tr, dt.GetState())
}
}
go func() {
t.subscribedTracksMu.Lock()
delete(t.subscribedTracks, sub.ID())
t.subscribedTracksMu.Unlock()
subTrack.Close(isExpectedToResume)
}()
}
@@ -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
}
@@ -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)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,169 @@
// 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,
BytesTrackStats: NewBytesTrackStats(
p.params.Country,
livekit.TrackID(dti.Sid),
p.ID(),
p.Kind(),
p.KindDetails(),
p.params.TelemetryListener,
p.params.Reporter,
),
},
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
}
@@ -26,11 +26,14 @@ import (
"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"
protoCodecs "github.com/livekit/protocol/codecs"
"github.com/livekit/protocol/codecs/mime"
"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"
@@ -81,14 +84,14 @@ func TestTrackPublishing(t *testing.T) {
track.IDReturns("id")
published := false
updated := false
p.OnTrackUpdated(func(p types.LocalParticipant, track types.MediaTrack) {
p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackUpdatedCalls(func(p types.Participant, track types.MediaTrack) {
updated = true
})
p.OnTrackPublished(func(p types.LocalParticipant, track types.MediaTrack) {
p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackPublishedCalls(func(p types.Participant, track types.MediaTrack) {
published = true
})
p.UpTrackManager.AddPublishedTrack(track)
p.handleTrackPublished(track)
p.handleTrackPublished(track, false)
require.True(t, published)
require.False(t, updated)
require.Len(t, p.UpTrackManager.publishedTracks, 1)
@@ -129,7 +132,8 @@ func TestTrackPublishing(t *testing.T) {
Type: livekit.TrackType_AUDIO,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
// 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) {
@@ -137,7 +141,7 @@ func TestTrackPublishing(t *testing.T) {
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
track := &typesfakes.FakeLocalMediaTrack{}
track.SignalCidReturns("cid")
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
@@ -147,7 +151,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `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
@@ -156,7 +161,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `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
@@ -178,7 +184,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `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
@@ -187,7 +194,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `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
@@ -217,7 +225,8 @@ func TestTrackPublishing(t *testing.T) {
Type: livekit.TrackType_AUDIO,
Source: livekit.TrackSource_MICROPHONE,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
// an error response for disallowed source should send a `RequestResponse`.
require.Equal(t, 2, sink.WriteMessageCallCount())
})
}
@@ -225,7 +234,7 @@ func TestOutOfOrderUpdates(t *testing.T) {
p := newParticipantForTest("test")
p.updateState(livekit.ParticipantInfo_JOINED)
p.SetMetadata("initial metadata")
sink := p.getResponseSink().(*routingfakes.FakeMessageSink)
sink := p.GetResponseSink().(*routingfakes.FakeMessageSink)
pi1 := p.ToProto()
p.SetMetadata("second update")
pi2 := p.ToProto()
@@ -255,7 +264,7 @@ func TestDisconnectTiming(t *testing.T) {
}()
track := &typesfakes.FakeMediaTrack{}
p.UpTrackManager.AddPublishedTrack(track)
p.handleTrackPublished(track)
p.handleTrackPublished(track, false)
// close channel and then try to Negotiate
msg.Close()
@@ -275,7 +284,10 @@ func TestMuteSetting(t *testing.T) {
ti := &livekit.TrackInfo{Sid: "testTrack"}
p.pendingTracks["cid"] = &pendingTrackInfo{trackInfos: []*livekit.TrackInfo{ti}}
p.SetTrackMuted(livekit.TrackID(ti.Sid), true, false)
p.SetTrackMuted(&livekit.MuteTrackRequest{
Sid: ti.Sid,
Muted: true,
}, false)
require.True(t, p.pendingTracks["cid"].trackInfos[0].Muted)
})
@@ -287,7 +299,7 @@ func TestMuteSetting(t *testing.T) {
Muted: true,
})
_, ti, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
_, ti, _, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
require.NotNil(t, ti)
require.True(t, ti.Muted)
})
@@ -362,28 +374,36 @@ func TestDisableCodecs(t *testing.T) {
}
}
require.True(t, found264)
offerId := uint32(42)
// negotiated codec should not contain h264
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
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 = FromProtoSessionDescription(res.GetAnswer())
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
answerReceived.Store(true)
answerIdReceived.Store(answerId)
}
}
return nil
})
participant.HandleOffer(sdp)
participant.HandleOffer(&livekit.SessionDescription{
Type: webrtc.SDPTypeOffer.String(),
Sdp: sdp.SDP,
Id: offerId,
})
testutils.WithTimeout(t, func() string {
if answerReceived.Load() {
if answerReceived.Load() && answerIdReceived.Load() == offerId {
return ""
} else {
return "answer not received"
return "answer not received OR answer id mismatch"
}
})
require.NoError(t, pc.SetRemoteDescription(answer), answer.SDP, sdp.SDP)
@@ -415,7 +435,7 @@ func TestDisablePublishCodec(t *testing.T) {
}
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
var publishReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
@@ -463,89 +483,138 @@ func TestDisablePublishCodec(t *testing.T) {
require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)
}
func TestPreferVideoCodecForPublisher(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
})
participant.SetMigrateState(types.MigrateStateComplete)
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
for i := 0; i < 2; i++ {
// publish h264 track without client preferred codec
trackCid := fmt.Sprintf("preferh264video%d", i)
participant.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
Name: "video",
Width: 1280,
Height: 720,
Source: livekit.TrackSource_CAMERA,
SimulcastCodecs: []*livekit.SimulcastCodec{
{
Codec: "h264",
Cid: trackCid,
},
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)
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, trackCid, trackCid)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
codecs := transceiver.Receiver().GetParameters().Codecs
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
if i > 0 {
// the negotiated codecs order could be updated by first negotiation, reorder to make h264 not preferred
for mime.IsMimeTypeStringH264(codecs[0].MimeType) {
codecs = append(codecs[1:], codecs[0])
}
}
// h264 should not be preferred
require.False(t, mime.IsMimeTypeStringH264(codecs[0].MimeType), "codecs", codecs)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
require.NoError(t, pc.SetLocalDescription(sdp))
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var answer webrtc.SessionDescription
var answerReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer = FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
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,
},
}
}
return nil
})
participant.HandleOffer(sdp)
participant.AddTrack(req)
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
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
var h264Preferred bool
parsed, err := answer.Unmarshal()
require.NoError(t, err)
var videoSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "video" {
if videoSectionIndex == i {
codecs, err := codecsFromMediaDescription(m)
require.NoError(t, err)
if mime.IsMimeTypeCodecStringH264(codecs[0].Name) {
h264Preferred = true
break
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])
}
}
videoSectionIndex++
}
}
// preferred codec should not be preferred in `offer`
require.False(t, tc.mimeTypeStringChecker(codecs[0].MimeType), "codecs", codecs)
require.Truef(t, h264Preferred, "h264 should be preferred for video section %d, answer sdp: \n%s", i, answer.SDP)
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)
}
})
}
}
@@ -556,33 +625,59 @@ func TestPreferAudioCodecForRed(t *testing.T) {
participant.SetMigrateState(types.MigrateStateComplete)
me := webrtc.MediaEngine{}
me.RegisterDefaultCodecs()
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: RedCodecCapability,
PayloadType: 63,
}, webrtc.RTPCodecTypeAudio))
opusCodecParameters := protoCodecs.OpusCodecParameters
opusCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
require.NoError(t, me.RegisterCodec(opusCodecParameters, webrtc.RTPCodecTypeAudio))
redCodecParameters := protoCodecs.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 i, disableRed := range []bool{false, true} {
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", i)
participant.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_AUDIO,
DisableRed: disableRed,
Cid: trackCid,
})
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, trackCid, trackCid)
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})
transceiver, err := pc.AddTransceiverFromTrack(
track,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv},
)
require.NoError(t, err)
codecs := transceiver.Sender().GetParameters().Codecs
for i, c := range codecs {
if c.MimeType == "audio/opus" && i != 0 {
codecs[0], codecs[i] = codecs[i], codecs[0]
if c.MimeType == "audio/opus" {
if i != 0 {
codecs[0], codecs[i] = codecs[i], codecs[0]
}
break
}
}
@@ -592,24 +687,38 @@ func TestPreferAudioCodecForRed(t *testing.T) {
pc.SetLocalDescription(sdp)
// opus should be preferred
require.Equal(t, codecs[0].MimeType, "audio/opus", sdp)
offerId := uint32(0xffffff)
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
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 = FromProtoSessionDescription(res.GetAnswer())
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
answerIdReceived.Store(answerId)
}
}
return nil
})
participant.HandleOffer(sdp)
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
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()
@@ -617,8 +726,8 @@ func TestPreferAudioCodecForRed(t *testing.T) {
var audioSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "audio" {
if audioSectionIndex == i {
codecs, err := codecsFromMediaDescription(m)
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
@@ -702,8 +811,11 @@ func newParticipantForTestWithOpts(identity livekit.ParticipantIdentity, opts *p
ClientConf: opts.clientConf,
ClientInfo: ClientInfo{ClientInfo: opts.clientInfo},
Logger: LoggerWithParticipant(logger.GetLogger(), identity, sid, false),
Telemetry: &telemetryfakes.FakeTelemetryService{},
Reporter: roomobs.NewNoopParticipantSessionReporter(),
TelemetryListener: &typesfakes.FakeParticipantTelemetryListener{},
VersionGenerator: utils.NewDefaultTimedVersionGenerator(),
ParticipantListener: &typesfakes.FakeLocalParticipantListener{},
ParticipantHelper: &typesfakes.FakeLocalParticipantHelper{},
})
p.isPublisher.Store(opts.publisher)
p.updateState(livekit.ParticipantInfo_ACTIVE)
+268 -134
View File
@@ -16,44 +16,279 @@ package rtc
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/codecs/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) setCodecPreferencesForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
offer = p.setCodecPreferencesOpusRedForPublisher(offer)
offer = p.setCodecPreferencesVideoForPublisher(offer)
return offer
}
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
}
func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
parsed, unmatchAudios, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "audio")
if err != nil || len(unmatchAudios) == 0 {
return offer
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()
_, info, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
// if RED is disabled for this track, don't prefer RED codec in offer
disableRed := info != nil && info.DisableRed
_, ti, _, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
p.pendingTracksLock.RUnlock()
if ti == nil {
continue
}
codecs, err := codecsFromMediaDescription(unmatchAudio)
codecs, err := lksdp.CodecsFromMediaDescription(unmatchAudio)
if err != nil {
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchAudio)
p.pubLogger.Errorw(
"extract codecs from media section failed", err,
"media", unmatchAudio,
"parsedOffer", parsedOffer,
)
continue
}
@@ -68,122 +303,17 @@ func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(offer webrtc.Se
continue
}
var preferredCodecs, leftCodecs []string
preferRED := IsRedEnabled(ti)
// if RED is enabled for this track, prefer RED codec in offer
for _, codec := range codecs {
// codec contain opus/red
if !disableRed && mime.IsMimeTypeCodecStringRED(codec.Name) && strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
}
}
// ensure nack enabled for audio in publisher offer
var nackFound bool
for _, attr := range unmatchAudio.Attributes {
if attr.Key == "rtcp-fb" && strings.Contains(attr.Value, fmt.Sprintf("%d nack", opusPayload)) {
nackFound = true
if preferRED &&
mime.IsMimeTypeCodecStringRED(codec.Name) &&
strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
configureReceiverCodecs(transceiver, "audio/red", true)
break
}
}
if !nackFound {
unmatchAudio.Attributes = append(unmatchAudio.Attributes, sdp.Attribute{
Key: "rtcp-fb",
Value: fmt.Sprintf("%d nack", opusPayload),
})
}
// no opus/red found
if len(preferredCodecs) == 0 {
continue
}
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats[:0], preferredCodecs...)
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats, leftCodecs...)
}
bytes, err := parsed.Marshal()
if err != nil {
p.pubLogger.Errorw("failed to marshal offer", err)
return offer
}
return webrtc.SessionDescription{
Type: offer.Type,
SDP: string(bytes),
}
}
func (p *ParticipantImpl) setCodecPreferencesVideoForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
parsed, unmatchVideos, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "video")
if err != nil || len(unmatchVideos) == 0 {
return offer
}
// unmatched video is pending for publish, set codec preference
for _, unmatchVideo := range unmatchVideos {
streamID, ok := lksdp.ExtractStreamID(unmatchVideo)
if !ok {
continue
}
var info *livekit.TrackInfo
p.pendingTracksLock.RLock()
mt := p.getPublishedTrackBySdpCid(streamID)
if mt != nil {
info = mt.ToProto()
} else {
_, info, _, _ = p.getPendingTrack(streamID, livekit.TrackType_VIDEO, false)
}
if info == nil {
p.pendingTracksLock.RUnlock()
continue
}
var mimeType string
for _, c := range info.Codecs {
if c.Cid == streamID {
mimeType = c.MimeType
break
}
}
if mimeType == "" && len(info.Codecs) > 0 {
mimeType = info.Codecs[0].MimeType
}
p.pendingTracksLock.RUnlock()
if mimeType != "" {
codecs, err := codecsFromMediaDescription(unmatchVideo)
if err != nil {
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchVideo)
continue
}
var preferredCodecs, leftCodecs []string
for _, c := range codecs {
if mime.GetMimeTypeCodec(mimeType) == mime.NormalizeMimeTypeCodec(c.Name) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
}
}
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats[:0], preferredCodecs...)
// if the client don't comply with codec order in SDP answer, only keep preferred codecs to force client to use it
if p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer() {
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats, leftCodecs...)
}
}
}
bytes, err := parsed.Marshal()
if err != nil {
p.pubLogger.Errorw("failed to marshal offer", err)
return offer
}
return webrtc.SessionDescription{
Type: offer.Type,
SDP: string(bytes),
}
}
@@ -195,12 +325,12 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
return answer
}
parsed, err := answer.Unmarshal()
parsedAnswer, err := answer.Unmarshal()
if err != nil {
return answer
}
for _, m := range parsed.MediaDescriptions {
for _, m := range parsedAnswer.MediaDescriptions {
switch m.MediaName.Media {
case "audio":
_, ok := m.Attribute(sdp.AttrKeyInactive)
@@ -227,7 +357,7 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
track, _ := p.getPublishedTrackBySdpCid(streamID).(*MediaTrack)
if track == nil {
p.pendingTracksLock.RLock()
_, ti, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
_, ti, _, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
p.pendingTracksLock.RUnlock()
} else {
ti = track.ToProto()
@@ -236,12 +366,12 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
}
}
if ti == nil || (ti.DisableDtx && !ti.Stereo) {
if ti == nil {
// no need to configure
continue
}
opusPT, err := parsed.GetPayloadTypeForCodec(sdp.Codec{Name: mime.MimeTypeCodecOpus.String()})
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
@@ -249,11 +379,15 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
for i, attr := range m.Attributes {
if strings.HasPrefix(attr.String(), fmt.Sprintf("fmtp:%d", opusPT)) {
if !ti.DisableDtx {
if !slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_NO_DTX) {
attr.Value += ";usedtx=1"
} else {
attr.Value = strings.ReplaceAll(attr.Value, ";usedtx=1", "")
}
if ti.Stereo {
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
}
@@ -264,7 +398,7 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
}
}
bytes, err := parsed.Marshal()
bytes, err := parsedAnswer.Marshal()
if err != nil {
p.pubLogger.Infow("failed to marshal answer", "error", err)
return answer
+124 -155
View File
@@ -15,30 +15,28 @@
package rtc
import (
"fmt"
"time"
"github.com/pion/webrtc/v4"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
protosignalling "github.com/livekit/protocol/signalling"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
func (p *ParticipantImpl) getResponseSink() routing.MessageSink {
p.resSinkMu.Lock()
defer p.resSinkMu.Unlock()
return p.resSink
func (p *ParticipantImpl) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
p.signaller.SwapResponseSink(sink, reason)
}
func (p *ParticipantImpl) SetResponseSink(sink routing.MessageSink) {
p.resSinkMu.Lock()
defer p.resSinkMu.Unlock()
p.resSink = sink
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 {
@@ -55,11 +53,7 @@ func (p *ParticipantImpl) SendJoinResponse(joinResponse *livekit.JoinResponse) e
p.updateLock.Unlock()
// send Join response
err := p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Join{
Join: joinResponse,
},
})
err := p.signaller.WriteMessage(p.signalling.SignalJoinResponse(joinResponse))
if err != nil {
return err
}
@@ -111,7 +105,7 @@ func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.
isValid = false
}
}
if pi.Permission != nil && pi.Permission.Hidden && pi.Sid != string(p.params.SID) {
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
}
@@ -127,17 +121,7 @@ func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.
}
p.updateLock.Unlock()
if len(validUpdates) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: validUpdates,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(validUpdates))
}
// SendSpeakerUpdate notifies participant changes to speakers. only send members that have changed since last update
@@ -158,47 +142,23 @@ func (p *ParticipantImpl) SendSpeakerUpdate(speakers []*livekit.SpeakerInfo, for
}
}
if len(scopedSpeakers) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_SpeakersChanged{
SpeakersChanged: &livekit.SpeakersChanged{
Speakers: scopedSpeakers,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalSpeakerUpdate(scopedSpeakers))
}
func (p *ParticipantImpl) SendRoomUpdate(room *livekit.Room) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RoomUpdate{
RoomUpdate: &livekit.RoomUpdate{
Room: room,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalRoomUpdate(room))
}
func (p *ParticipantImpl) SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_ConnectionQuality{
ConnectionQuality: update,
},
})
return p.signaller.WriteMessage(p.signalling.SignalConnectionQualityUpdate(update))
}
func (p *ParticipantImpl) SendRefreshToken(token string) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RefreshToken{
RefreshToken: token,
},
})
return p.signaller.WriteMessage(p.signalling.SignalRefreshToken(token))
}
func (p *ParticipantImpl) SendRequestResponse(requestResponse *livekit.RequestResponse) error {
if requestResponse.RequestId == 0 || !p.params.ClientInfo.SupportErrorResponse() {
func (p *ParticipantImpl) sendRequestResponse(requestResponse *livekit.RequestResponse) error {
if !p.params.ClientInfo.SupportsRequestResponse() {
return nil
}
@@ -206,11 +166,11 @@ func (p *ParticipantImpl) SendRequestResponse(requestResponse *livekit.RequestRe
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RequestResponse{
RequestResponse: requestResponse,
},
})
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 {
@@ -219,15 +179,11 @@ func (p *ParticipantImpl) HandleReconnectAndSendResponse(reconnectReason livekit
if !p.params.ClientInfo.CanHandleReconnectResponse() {
return nil
}
if err := p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Reconnect{
Reconnect: reconnectResponse,
},
}); err != nil {
if err := p.signaller.WriteMessage(p.signalling.SignalReconnectResponse(reconnectResponse)); err != nil {
return err
}
if p.params.ProtocolVersion.SupportHandlesDisconnectedUpdate() {
if p.params.ProtocolVersion.SupportsDisconnectedUpdate() {
return p.sendDisconnectUpdatesForReconnect()
}
@@ -255,17 +211,7 @@ func (p *ParticipantImpl) sendDisconnectUpdatesForReconnect() error {
}
p.updateLock.Unlock()
if len(disconnectedParticipants) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: disconnectedParticipants,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(disconnectedParticipants))
}
func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livekit.SignalTarget) error {
@@ -274,90 +220,43 @@ func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livek
return nil
}
trickle := ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
trickle := protosignalling.ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
p.params.Logger.Debugw("sending ICE candidate", "transport", target, "trickle", logger.Proto(trickle))
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Trickle{
Trickle: trickle,
},
})
return p.signaller.WriteMessage(p.signalling.SignalICECandidate(trickle))
}
func (p *ParticipantImpl) sendTrackMuted(trackID livekit.TrackID, muted bool) {
_ = p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Mute{
Mute: &livekit.MuteTrackRequest{
Sid: string(trackID),
Muted: muted,
},
},
})
_ = 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.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackUnpublished{
TrackUnpublished: &livekit.TrackUnpublishedResponse{
TrackSid: string(trackID),
},
},
})
_ = p.signaller.WriteMessage(p.signalling.SignalTrackUnpublished(&livekit.TrackUnpublishedResponse{
TrackSid: string(trackID),
}))
}
func (p *ParticipantImpl) sendTrackHasBeenSubscribed(trackID livekit.TrackID) {
if !p.params.ClientInfo.SupportTrackSubscribedEvent() {
if !p.params.ClientInfo.SupportsTrackSubscribedEvent() {
return
}
_ = p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackSubscribed{
TrackSubscribed: &livekit.TrackSubscribed{
TrackSid: string(trackID),
},
},
})
_ = p.signaller.WriteMessage(p.signalling.SignalTrackSubscribed(&livekit.TrackSubscribed{
TrackSid: string(trackID),
}))
p.params.Logger.Debugw("track has been subscribed", "trackID", trackID)
}
func (p *ParticipantImpl) writeMessage(msg *livekit.SignalResponse) error {
if p.IsDisconnected() || (!p.IsReady() && msg.GetJoin() == nil) {
return nil
}
sink := p.getResponseSink()
if sink == nil {
p.params.Logger.Debugw("could not send message to participant", "messageType", fmt.Sprintf("%T", msg.Message))
return nil
}
err := sink.WriteMessage(msg)
if utils.ErrorIsOneOf(err, psrpc.Canceled, routing.ErrChannelClosed) {
p.params.Logger.Debugw(
"could not send message to participant",
"error", err,
"messageType", fmt.Sprintf("%T", msg.Message),
)
return nil
} else if err != nil {
p.params.Logger.Warnw(
"could not send message to participant", err,
"messageType", fmt.Sprintf("%T", msg.Message),
)
return err
}
return nil
}
// closes signal connection to notify client to resume/reconnect
func (p *ParticipantImpl) CloseSignalConnection(reason types.SignallingCloseReason) {
sink := p.getResponseSink()
if sink != nil {
p.params.Logger.Debugw("closing signal connection", "reason", reason, "connID", sink.ConnectionID())
sink.Close()
p.SetResponseSink(nil)
}
}
func (p *ParticipantImpl) sendLeaveRequest(
reason types.ParticipantCloseReason,
isExpectedToResume bool,
@@ -377,9 +276,9 @@ func (p *ParticipantImpl) sendLeaveRequest(
default:
leave.Action = livekit.LeaveRequest_DISCONNECT
}
if leave.Action != livekit.LeaveRequest_DISCONNECT && p.params.GetRegionSettings != nil {
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.params.GetRegionSettings(p.params.ClientInfo.Address)
leave.Regions = p.helper().GetRegionSettings(p.params.ClientInfo.Address)
}
} else {
if !sendOnlyIfSupportingLeaveRequestWithAction {
@@ -390,12 +289,82 @@ func (p *ParticipantImpl) sendLeaveRequest(
}
}
if leave != nil {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Leave{
Leave: leave,
},
})
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,
}))
}
File diff suppressed because it is too large Load Diff
+82 -57
View File
@@ -22,6 +22,7 @@ import (
"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"
@@ -89,7 +90,7 @@ func TestJoinedState(t *testing.T) {
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)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
_ = rm.Join(pNew, nil, nil, iceServersForRoom)
@@ -104,14 +105,14 @@ func TestRoomJoin(t *testing.T) {
t.Run("subscribe to existing channels upon join", func(t *testing.T) {
numExisting := 3
rm := newRoomWithParticipants(t, testRoomOpts{num: numExisting})
p := NewMockParticipant("new", types.CurrentProtocol, false, false)
lpl := rm.LocalParticipantListener()
p := NewMockParticipant("new", types.CurrentProtocol, false, false, lpl)
err := rm.Join(p, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
stateChangeCB := p.OnStateChangeArgsForCall(0)
require.NotNil(t, stateChangeCB)
stateChangeCB(p, livekit.ParticipantInfo_ACTIVE)
p.StateReturns(livekit.ParticipantInfo_ACTIVE)
lpl.OnStateChange(p)
// it should become a subscriber when connectivity changes
numTracks := 0
@@ -128,7 +129,7 @@ func TestRoomJoin(t *testing.T) {
t.Run("participant state change is broadcasted to others", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: numParticipants})
var changedParticipant types.Participant
rm.OnParticipantChanged(func(participant types.LocalParticipant) {
rm.OnParticipantChanged(func(participant types.Participant) {
changedParticipant = participant
})
participants := rm.GetParticipants()
@@ -159,7 +160,7 @@ func TestRoomJoin(t *testing.T) {
rm.lock.Lock()
rm.protoRoom.MaxParticipants = 1
rm.lock.Unlock()
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false)
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false, rm.LocalParticipantListener())
err := rm.Join(p, nil, nil, iceServersForRoom)
require.Equal(t, ErrMaxParticipantsExceeded, err)
@@ -177,7 +178,7 @@ func TestParticipantUpdate(t *testing.T) {
"track mutes are sent to everyone",
true,
func(p types.LocalParticipant) {
p.SetTrackMuted("", true, false)
p.SetTrackMuted(&livekit.MuteTrackRequest{Muted: true}, false)
},
},
{
@@ -270,14 +271,14 @@ func TestPushAndDequeueUpdates(t *testing.T) {
pi *livekit.ParticipantInfo
closeReason types.ParticipantCloseReason
immediate bool
existing *participantUpdate
expected []*participantUpdate
validate func(t *testing.T, rm *Room, updates []*participantUpdate)
existing *ParticipantUpdate
expected []*ParticipantUpdate
validate func(t *testing.T, rm *Room, updates []*ParticipantUpdate)
}{
{
name: "publisher updates are immediate",
pi: publisher1v1,
expected: []*participantUpdate{{pi: publisher1v1}},
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v1}},
},
{
name: "subscriber updates are queued",
@@ -286,20 +287,20 @@ func TestPushAndDequeueUpdates(t *testing.T) {
{
name: "last version is enqueued",
pi: subscriber1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)}, // clone the existing value since it can be modified when setting to disconnected
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
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.pi)
requirePIEquals(t, subscriber1v2, queued.ParticipantInfo)
},
},
{
name: "latest version when immediate",
pi: subscriber1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
immediate: true,
expected: []*participantUpdate{{pi: subscriber1v2}},
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
expected: []*ParticipantUpdate{{ParticipantInfo: subscriber1v2}},
validate: func(t *testing.T, rm *Room, _ []*ParticipantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
require.Nil(t, queued)
},
@@ -307,37 +308,37 @@ func TestPushAndDequeueUpdates(t *testing.T) {
{
name: "out of order updates are rejected",
pi: subscriber1v1,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v2)},
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
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.pi)
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{pi: utils.CloneProto(subscriber1v2), closeReason: types.ParticipantCloseReasonStale},
expected: []*participantUpdate{
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v2), CloseReason: types.ParticipantCloseReasonStale},
expected: []*ParticipantUpdate{
{
pi: &livekit.ParticipantInfo{
ParticipantInfo: &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 2,
State: livekit.ParticipantInfo_DISCONNECTED,
},
isSynthesizedDisconnect: true,
closeReason: types.ParticipantCloseReasonStale,
IsSynthesizedDisconnect: true,
CloseReason: types.ParticipantCloseReasonStale,
},
{pi: publisher2, closeReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
{ParticipantInfo: publisher2, CloseReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
},
},
{
name: "when switching to publisher, queue is cleared",
pi: publisher1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
expected: []*participantUpdate{{pi: publisher1v2}},
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v2}},
validate: func(t *testing.T, rm *Room, updates []*ParticipantUpdate) {
require.Empty(t, rm.batchedUpdates)
},
},
@@ -347,14 +348,22 @@ func TestPushAndDequeueUpdates(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
if tc.existing != nil {
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.pi.Identity)] = tc.existing
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.ParticipantInfo.Identity)] = tc.existing
}
updates := rm.pushAndDequeueUpdates(tc.pi, tc.closeReason, tc.immediate)
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.pi, updates[i].pi)
require.Equal(t, item.isSynthesizedDisconnect, updates[i].isSynthesizedDisconnect)
require.Equal(t, item.closeReason, updates[i].closeReason)
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 {
@@ -417,6 +426,8 @@ func TestRoomClosure(t *testing.T) {
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)
@@ -427,9 +438,8 @@ func TestNewTrack(t *testing.T) {
// pub adds track
track := NewMockTrack(livekit.TrackType_VIDEO, "webcam")
trackCB := pub.OnTrackPublishedArgsForCall(0)
require.NotNil(t, trackCB)
trackCB(pub, track)
lpl.OnTrackPublished(pub, track)
// only p1 should've been subscribed to
require.Equal(t, 0, p0.SubscribeToTrackCallCount())
require.Equal(t, 1, p1.SubscribeToTrackCallCount())
@@ -441,7 +451,7 @@ func TestActiveSpeakers(t *testing.T) {
getActiveSpeakerUpdates := func(p *typesfakes.FakeLocalParticipant) [][]*livekit.SpeakerInfo {
var updates [][]*livekit.SpeakerInfo
numCalls := p.SendSpeakerUpdateCallCount()
for i := 0; i < numCalls; i++ {
for i := range numCalls {
infos, _ := p.SendSpeakerUpdateArgsForCall(i)
updates = append(updates, infos)
}
@@ -518,6 +528,7 @@ func TestActiveSpeakers(t *testing.T) {
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)
@@ -611,6 +622,9 @@ func TestDataChannel(t *testing.T) {
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)
@@ -631,17 +645,17 @@ func TestDataChannel(t *testing.T) {
}
encoded, _ := proto.Marshal(packetExp)
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
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.SendDataPacketCallCount())
require.Zero(t, fp.SendDataMessageCallCount())
continue
}
require.Equal(t, 1, fp.SendDataPacketCallCount())
_, got := fp.SendDataPacketArgsForCall(0)
require.Equal(t, 1, fp.SendDataMessageCallCount())
_, got, _, _ := fp.SendDataMessageArgsForCall(0)
require.Equal(t, encoded, got)
}
})
@@ -654,6 +668,9 @@ func TestDataChannel(t *testing.T) {
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)
@@ -678,17 +695,17 @@ func TestDataChannel(t *testing.T) {
}
encoded, _ := proto.Marshal(packetExp)
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
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.SendDataPacketCallCount())
require.Zero(t, fp.SendDataMessageCallCount())
}
}
require.Equal(t, 1, p1.SendDataPacketCallCount())
_, got := p1.SendDataPacketArgsForCall(0)
require.Equal(t, 1, p1.SendDataMessageCallCount())
_, got, _, _ := p1.SendDataMessageArgsForCall(0)
require.Equal(t, encoded, got)
})
}
@@ -697,6 +714,7 @@ func TestDataChannel(t *testing.T) {
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)
@@ -710,13 +728,14 @@ func TestDataChannel(t *testing.T) {
},
}
if p.CanPublishData() {
p.OnDataPacketArgsForCall(0)(p, packet.Kind, &packet)
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.SendDataPacketCallCount())
require.Zero(t, fp.SendDataMessageCallCount())
}
})
}
@@ -726,7 +745,7 @@ func TestHiddenParticipants(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, numHidden: 1})
defer rm.Close(types.ParticipantCloseReasonNone)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
rm.Join(pNew, nil, nil, iceServersForRoom)
// expect new participant to get a JoinReply
@@ -740,14 +759,14 @@ func TestHiddenParticipants(t *testing.T) {
t.Run("hidden participant subscribes to tracks", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false)
lpl := rm.LocalParticipantListener()
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false, lpl)
err := rm.Join(hidden, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
stateChangeCB := hidden.OnStateChangeArgsForCall(0)
require.NotNil(t, stateChangeCB)
stateChangeCB(hidden, livekit.ParticipantInfo_ACTIVE)
hidden.StateReturns(livekit.ParticipantInfo_ACTIVE)
lpl.OnStateChange(hidden)
require.Eventually(t, func() bool { return hidden.SubscribeToTrackCallCount() == 2 }, 5*time.Second, 10*time.Millisecond)
})
@@ -761,7 +780,7 @@ func TestRoomUpdate(t *testing.T) {
p1 := rm.GetParticipants()[0].(*typesfakes.FakeLocalParticipant)
require.Equal(t, 0, p1.SendRoomUpdateCallCount())
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false)
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
require.NoError(t, rm.Join(p2, nil, nil, iceServersForRoom))
// p1 should have received an update
@@ -795,6 +814,12 @@ type testRoomOpts struct {
}
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,
@@ -816,12 +841,12 @@ func newRoomWithParticipants(t *testing.T, opts testRoomOpts) *Room {
NodeId: "testnode",
Region: "testregion",
},
telemetry.NewTelemetryService(webhook.NewDefaultNotifier("", "", nil), &telemetryfakes.FakeAnalyticsService{}),
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)
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)
+153 -15
View File
@@ -22,14 +22,18 @@ import (
"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
tracks map[livekit.TrackID][]*TrackInfo
dataTracks map[livekit.TrackID][]*DataTrackInfo
}
type TrackInfo struct {
@@ -38,54 +42,101 @@ type TrackInfo struct {
PublisherID livekit.ParticipantID
}
func NewRoomTrackManager() *RoomTrackManager {
type DataTrackInfo struct {
DataTrack types.DataTrack
PublisherIdentity livekit.ParticipantIdentity
PublisherID livekit.ParticipantID
}
func NewRoomTrackManager(logger logger.Logger) *RoomTrackManager {
return &RoomTrackManager{
tracks: make(map[livekit.TrackID]*TrackInfo),
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()
r.tracks[track.ID()] = &TrackInfo{
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(track.ID())
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
info, ok := r.tracks[track.ID()]
if !ok || info.Track != track {
infos, ok := r.tracks[trackID]
if !ok {
r.lock.Unlock()
return
}
delete(r.tracks, track.ID())
r.lock.Unlock()
n := r.removedNotifier.GetNotifier(string(track.ID()))
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(track.ID()), true)
r.removedNotifier.RemoveNotifier(string(track.ID()), true)
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()
info := r.tracks[trackID]
if info == nil {
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
@@ -123,3 +174,90 @@ func (r *RoomTrackManager) GetOrCreateTrackChangeNotifier(trackID livekit.TrackI
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)
}
@@ -0,0 +1,307 @@
// 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"
"fmt"
"sync"
"time"
"github.com/frostbyte73/core"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/utils"
)
type BytesTrackType string
const (
BytesTrackTypeData BytesTrackType = "DT"
BytesTrackTypeSignal BytesTrackType = "SG"
)
// -------------------------------
type TrafficTotals struct {
At time.Time
SendBytes uint64
SendMessages uint32
RecvBytes uint64
RecvMessages uint32
}
// --------------------------------
// stats for signal and data channel
type BytesTrackStats struct {
country string
pID livekit.ParticipantID
kind livekit.ParticipantInfo_Kind
kindDetails []livekit.ParticipantInfo_KindDetail
trackID livekit.TrackID
send, recv atomic.Uint64
sendMessages, recvMessages atomic.Uint32
totalSendBytes, totalRecvBytes atomic.Uint64
totalSendMessages, totalRecvMessages atomic.Uint32
telemetryListener types.ParticipantTelemetryListener
reporter roomobs.TrackReporter
done core.Fuse
}
func NewBytesTrackStats(
country string,
trackID livekit.TrackID,
pID livekit.ParticipantID,
kind livekit.ParticipantInfo_Kind,
kindDetails []livekit.ParticipantInfo_KindDetail,
telemetryListener types.ParticipantTelemetryListener,
participantReporter roomobs.ParticipantSessionReporter,
) *BytesTrackStats {
s := &BytesTrackStats{
country: country,
pID: pID,
kind: kind,
kindDetails: kindDetails,
trackID: trackID,
telemetryListener: telemetryListener,
reporter: participantReporter.WithTrack(trackID.String()),
}
go s.worker()
return s
}
func (s *BytesTrackStats) AddBytes(bytes uint64, isSend bool) {
if isSend {
s.send.Add(bytes)
s.sendMessages.Inc()
s.totalSendBytes.Add(bytes)
s.totalSendMessages.Inc()
s.reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(s.kind))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(s.kindDetails))
tx.ReportType(roomobs.TrackTypeData)
tx.ReportSendBytes(uint32(bytes))
tx.ReportSendPackets(1)
})
} else {
s.recv.Add(bytes)
s.recvMessages.Inc()
s.totalRecvBytes.Add(bytes)
s.totalRecvMessages.Inc()
s.reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(s.kind))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(s.kindDetails))
tx.ReportType(roomobs.TrackTypeData)
tx.ReportRecvBytes(uint32(bytes))
tx.ReportRecvPackets(1)
})
}
}
func (s *BytesTrackStats) GetTrafficTotals() *TrafficTotals {
return &TrafficTotals{
At: time.Now(),
SendBytes: s.totalSendBytes.Load(),
SendMessages: s.totalSendMessages.Load(),
RecvBytes: s.totalRecvBytes.Load(),
RecvMessages: s.totalRecvMessages.Load(),
}
}
func (s *BytesTrackStats) Stop() {
s.done.Break()
}
func (s *BytesTrackStats) report() {
if recv := s.recv.Swap(0); recv > 0 {
packets := s.recvMessages.Swap(0)
s.telemetryListener.OnTrackStats(
telemetry.StatsKeyForData(s.country, livekit.StreamType_UPSTREAM, s.pID, s.trackID),
&livekit.AnalyticsStat{
Streams: []*livekit.AnalyticsStream{
{
PrimaryBytes: recv,
PrimaryPackets: packets,
},
},
},
)
}
if send := s.send.Swap(0); send > 0 {
packets := s.sendMessages.Swap(0)
s.telemetryListener.OnTrackStats(
telemetry.StatsKeyForData(s.country, livekit.StreamType_DOWNSTREAM, s.pID, s.trackID),
&livekit.AnalyticsStat{
Streams: []*livekit.AnalyticsStream{
{
PrimaryBytes: send,
PrimaryPackets: packets,
},
},
},
)
}
}
func (s *BytesTrackStats) worker() {
ticker := time.NewTicker(5 * time.Second)
defer func() {
ticker.Stop()
s.report()
}()
for {
select {
case <-s.done.Watch():
return
case <-ticker.C:
s.report()
}
}
}
// -----------------------------------------------------------------------
var _ types.ParticipantTelemetryListener = (*BytesSignalStats)(nil)
type BytesSignalStats struct {
BytesTrackStats
ctx context.Context
telemetry telemetry.TelemetryService
guard *telemetry.ReferenceGuard
participantResolver roomobs.ParticipantReporterResolver
trackResolver roomobs.KeyResolver
mu sync.Mutex
ri *livekit.Room
pi *livekit.ParticipantInfo
stopped chan struct{}
types.NullParticipantTelemetryListener
}
func NewBytesSignalStats(
ctx context.Context,
telemetryService telemetry.TelemetryService,
) *BytesSignalStats {
projectReporter := telemetryService.RoomProjectReporter(ctx)
participantReporter, participantReporterResolver := roomobs.DeferredParticipantReporter(projectReporter)
trackReporter, trackReporterResolver := participantReporter.WithDeferredTrack()
b := &BytesSignalStats{
ctx: ctx,
telemetry: telemetryService,
guard: &telemetry.ReferenceGuard{},
participantResolver: participantReporterResolver,
trackResolver: trackReporterResolver,
}
b.BytesTrackStats = BytesTrackStats{
telemetryListener: b,
reporter: trackReporter,
}
return b
}
func (s *BytesSignalStats) ResolveRoom(ri *livekit.Room) {
s.mu.Lock()
defer s.mu.Unlock()
if s.ri == nil && ri.GetSid() != "" {
s.ri = &livekit.Room{
Sid: ri.Sid,
Name: ri.Name,
}
s.maybeStart()
}
}
func (s *BytesSignalStats) ResolveParticipant(pi *livekit.ParticipantInfo) {
s.mu.Lock()
defer s.mu.Unlock()
if s.pi == nil && pi != nil {
s.pi = &livekit.ParticipantInfo{
Sid: pi.Sid,
Identity: pi.Identity,
Kind: pi.Kind,
KindDetails: pi.KindDetails,
}
s.kind = pi.Kind
s.kindDetails = pi.KindDetails
s.maybeStart()
}
}
func (s *BytesSignalStats) Reset() {
s.mu.Lock()
defer s.mu.Unlock()
if s.stopped != nil {
s.done.Break()
<-s.stopped
s.stopped = nil
s.done = core.Fuse{}
s.guard = &telemetry.ReferenceGuard{}
}
s.ri = nil
s.pi = nil
s.participantResolver.Reset()
s.trackResolver.Reset()
}
func (s *BytesSignalStats) maybeStart() {
if s.ri == nil || s.pi == nil {
return
}
s.pID = livekit.ParticipantID(s.pi.Sid)
s.trackID = BytesTrackIDForParticipantID(BytesTrackTypeSignal, s.pID)
s.participantResolver.Resolve(
livekit.RoomName(s.ri.Name),
livekit.RoomID(s.ri.Sid),
livekit.ParticipantIdentity(s.pi.Identity),
livekit.ParticipantID(s.pi.Sid),
)
s.trackResolver.Resolve(string(s.trackID))
s.telemetry.ParticipantJoined(s.ctx, s.ri, s.pi, nil, nil, false, s.guard)
s.stopped = make(chan struct{})
go s.worker()
}
func (s *BytesSignalStats) worker() {
s.BytesTrackStats.worker()
s.telemetry.ParticipantLeft(s.ctx, s.ri, s.pi, false, s.guard)
close(s.stopped)
}
func (s *BytesSignalStats) OnTrackStats(key telemetry.StatsKey, stat *livekit.AnalyticsStat) {
stat.RoomId, stat.RoomName = s.ri.Sid, s.ri.Name
s.telemetry.TrackStats(livekit.RoomID(s.ri.Sid), livekit.RoomName(s.ri.Name), key, stat)
}
// -----------------------------------------------------------------------
func BytesTrackIDForParticipantID(typ BytesTrackType, participantID livekit.ParticipantID) livekit.TrackID {
return livekit.TrackID(fmt.Sprintf("%s%s%s", utils.TrackPrefix, typ, participantID))
}
-159
View File
@@ -1,159 +0,0 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
func HandleParticipantSignal(room types.Room, participant types.LocalParticipant, req *livekit.SignalRequest, pLogger logger.Logger) error {
participant.UpdateLastSeenSignal()
switch msg := req.GetMessage().(type) {
case *livekit.SignalRequest_Offer:
participant.HandleOffer(FromProtoSessionDescription(msg.Offer))
case *livekit.SignalRequest_Answer:
participant.HandleAnswer(FromProtoSessionDescription(msg.Answer))
case *livekit.SignalRequest_Trickle:
candidateInit, err := FromProtoTrickle(msg.Trickle)
if err != nil {
pLogger.Warnw("could not decode trickle", err)
return nil
}
participant.AddICECandidate(candidateInit, msg.Trickle.Target)
case *livekit.SignalRequest_AddTrack:
pLogger.Debugw("add track request", "trackID", msg.AddTrack.Cid)
participant.AddTrack(msg.AddTrack)
case *livekit.SignalRequest_Mute:
participant.SetTrackMuted(livekit.TrackID(msg.Mute.Sid), msg.Mute.Muted, false)
case *livekit.SignalRequest_Subscription:
// allow participant to indicate their interest in the subscription
// permission check happens later in SubscriptionManager
room.UpdateSubscriptions(
participant,
livekit.StringsAsIDs[livekit.TrackID](msg.Subscription.TrackSids),
msg.Subscription.ParticipantTracks,
msg.Subscription.Subscribe,
)
case *livekit.SignalRequest_TrackSetting:
for _, sid := range livekit.StringsAsIDs[livekit.TrackID](msg.TrackSetting.TrackSids) {
participant.UpdateSubscribedTrackSettings(sid, msg.TrackSetting)
}
case *livekit.SignalRequest_Leave:
reason := types.ParticipantCloseReasonClientRequestLeave
switch msg.Leave.Reason {
case livekit.DisconnectReason_CLIENT_INITIATED:
reason = types.ParticipantCloseReasonClientRequestLeave
case livekit.DisconnectReason_USER_UNAVAILABLE:
reason = types.ParticipantCloseReasonUserUnavailable
case livekit.DisconnectReason_USER_REJECTED:
reason = types.ParticipantCloseReasonUserRejected
}
pLogger.Debugw("client leaving room", "reason", reason)
room.RemoveParticipant(participant.Identity(), participant.ID(), reason)
case *livekit.SignalRequest_SubscriptionPermission:
err := room.UpdateSubscriptionPermission(participant, msg.SubscriptionPermission)
if err != nil {
pLogger.Warnw("could not update subscription permission", err,
"permissions", msg.SubscriptionPermission)
}
case *livekit.SignalRequest_SyncState:
err := room.SyncState(participant, msg.SyncState)
if err != nil {
pLogger.Warnw("could not sync state", err,
"state", msg.SyncState)
}
case *livekit.SignalRequest_Simulate:
err := room.SimulateScenario(participant, msg.Simulate)
if err != nil {
pLogger.Warnw("could not simulate scenario", err,
"simulate", msg.Simulate)
}
case *livekit.SignalRequest_PingReq:
if msg.PingReq.Rtt > 0 {
participant.UpdateSignalingRTT(uint32(msg.PingReq.Rtt))
}
case *livekit.SignalRequest_UpdateMetadata:
requestResponse := &livekit.RequestResponse{
RequestId: msg.UpdateMetadata.RequestId,
Reason: livekit.RequestResponse_OK,
}
if participant.ClaimGrants().Video.GetCanUpdateOwnMetadata() {
if err := participant.CheckMetadataLimits(
msg.UpdateMetadata.Name,
msg.UpdateMetadata.Metadata,
msg.UpdateMetadata.Attributes,
); err == nil {
if msg.UpdateMetadata.Name != "" {
participant.SetName(msg.UpdateMetadata.Name)
}
if msg.UpdateMetadata.Metadata != "" {
participant.SetMetadata(msg.UpdateMetadata.Metadata)
}
if msg.UpdateMetadata.Attributes != nil {
participant.SetAttributes(msg.UpdateMetadata.Attributes)
}
} else {
pLogger.Warnw("could not update metadata", err)
switch err {
case ErrNameExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds name length limit"
case ErrMetadataExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds metadata size limit"
case ErrAttributesExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds attributes size limit"
}
}
} else {
requestResponse.Reason = livekit.RequestResponse_NOT_ALLOWED
requestResponse.Message = "does not have permission to update own metadata"
}
participant.SendRequestResponse(requestResponse)
case *livekit.SignalRequest_UpdateAudioTrack:
if err := participant.UpdateAudioTrack(msg.UpdateAudioTrack); err != nil {
pLogger.Warnw("could not update audio track", err, "update", msg.UpdateAudioTrack)
}
case *livekit.SignalRequest_UpdateVideoTrack:
if err := participant.UpdateVideoTrack(msg.UpdateVideoTrack); err != nil {
pLogger.Warnw("could not update video track", err, "update", msg.UpdateVideoTrack)
}
}
return nil
}
@@ -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 signalling
import (
"errors"
)
var (
ErrInvalidMessageType = errors.New("invalid message type")
ErrNameExceedsLimits = errors.New("name length exceeds limits")
ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits")
ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits")
ErrUpdateOwnMetadataNotAllowed = errors.New("update own metadata not allowed")
)
@@ -0,0 +1,65 @@
// 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 signalling
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"google.golang.org/protobuf/proto"
)
type ParticipantSignalHandler interface {
HandleMessage(msg proto.Message) error
}
type ParticipantSignaller interface {
SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason)
GetResponseSink() routing.MessageSink
CloseSignalConnection(reason types.SignallingCloseReason)
WriteMessage(msg proto.Message) error
}
type ParticipantSignalling interface {
SignalJoinResponse(join *livekit.JoinResponse) proto.Message
SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message
SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message
SignalRoomUpdate(room *livekit.Room) proto.Message
SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message
SignalRefreshToken(token string) proto.Message
SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message
SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message
SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message
SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message
SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message
SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message
SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message
SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message
SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message
SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message
SignalSdpOffer(offer *livekit.SessionDescription) proto.Message
SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message
SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message
SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message
SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message
SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message
SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message
SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message
SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message
SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message
}
@@ -0,0 +1,157 @@
// 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 signalling
import (
"fmt"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
var _ ParticipantSignalHandler = (*signalhandler)(nil)
type SignalHandlerParams struct {
Logger logger.Logger
Participant types.LocalParticipant
}
type signalhandler struct {
params SignalHandlerParams
}
func NewSignalHandler(params SignalHandlerParams) ParticipantSignalHandler {
return &signalhandler{
params: params,
}
}
func (s *signalhandler) HandleMessage(msg proto.Message) error {
req, ok := msg.(*livekit.SignalRequest)
if !ok {
s.params.Logger.Warnw(
"unknown message type", nil,
"messageType", fmt.Sprintf("%T", msg),
)
return ErrInvalidMessageType
}
s.params.Participant.UpdateLastSeenSignal()
s.params.Logger.Debugw("handling signal request", "request", logger.Proto(req))
switch msg := req.GetMessage().(type) {
case *livekit.SignalRequest_Offer:
s.params.Participant.HandleOffer(msg.Offer)
case *livekit.SignalRequest_Answer:
s.params.Participant.HandleAnswer(msg.Answer)
case *livekit.SignalRequest_Trickle:
s.params.Participant.HandleICETrickle(msg.Trickle)
case *livekit.SignalRequest_AddTrack:
s.params.Participant.AddTrack(msg.AddTrack)
case *livekit.SignalRequest_Mute:
s.params.Participant.SetTrackMuted(msg.Mute, false)
case *livekit.SignalRequest_Subscription:
// allow participant to indicate their interest in the subscription
// permission check happens later in SubscriptionManager
s.params.Participant.HandleUpdateSubscriptions(
livekit.StringsAsIDs[livekit.TrackID](msg.Subscription.TrackSids),
msg.Subscription.ParticipantTracks,
msg.Subscription.Subscribe,
)
case *livekit.SignalRequest_TrackSetting:
for _, sid := range livekit.StringsAsIDs[livekit.TrackID](msg.TrackSetting.TrackSids) {
s.params.Participant.UpdateSubscribedTrackSettings(sid, msg.TrackSetting)
}
case *livekit.SignalRequest_Leave:
reason := types.ParticipantCloseReasonClientRequestLeave
switch msg.Leave.Reason {
case livekit.DisconnectReason_CLIENT_INITIATED:
reason = types.ParticipantCloseReasonClientRequestLeave
case livekit.DisconnectReason_USER_UNAVAILABLE:
reason = types.ParticipantCloseReasonUserUnavailable
case livekit.DisconnectReason_USER_REJECTED:
reason = types.ParticipantCloseReasonUserRejected
case livekit.DisconnectReason_AGENT_ERROR:
reason = types.ParticipantCloseReasonAgentError
}
s.params.Logger.Debugw("client leaving room", "reason", reason)
s.params.Participant.HandleLeaveRequest(reason)
case *livekit.SignalRequest_SubscriptionPermission:
err := s.params.Participant.HandleUpdateSubscriptionPermission(msg.SubscriptionPermission)
if err != nil {
s.params.Logger.Warnw(
"could not update subscription permission", err,
"permissions", msg.SubscriptionPermission,
)
}
case *livekit.SignalRequest_SyncState:
err := s.params.Participant.HandleSyncState(msg.SyncState)
if err != nil {
s.params.Logger.Warnw(
"could not sync state", err,
"state", msg.SyncState,
)
}
case *livekit.SignalRequest_Simulate:
err := s.params.Participant.HandleSimulateScenario(msg.Simulate)
if err != nil {
s.params.Logger.Warnw(
"could not simulate scenario", err,
"simulate", msg.Simulate,
)
}
case *livekit.SignalRequest_PingReq:
if msg.PingReq.Rtt > 0 {
s.params.Participant.UpdateSignalingRTT(uint32(msg.PingReq.Rtt))
}
case *livekit.SignalRequest_UpdateMetadata:
s.params.Participant.UpdateMetadata(msg.UpdateMetadata, false)
case *livekit.SignalRequest_UpdateAudioTrack:
if err := s.params.Participant.UpdateAudioTrack(msg.UpdateAudioTrack); err != nil {
s.params.Logger.Warnw("could not update audio track", err, "update", msg.UpdateAudioTrack)
}
case *livekit.SignalRequest_UpdateVideoTrack:
if err := s.params.Participant.UpdateVideoTrack(msg.UpdateVideoTrack); err != nil {
s.params.Logger.Warnw("could not update video track", err, "update", msg.UpdateVideoTrack)
}
case *livekit.SignalRequest_PublishDataTrackRequest:
s.params.Participant.HandlePublishDataTrackRequest(msg.PublishDataTrackRequest)
case *livekit.SignalRequest_UnpublishDataTrackRequest:
s.params.Participant.HandleUnpublishDataTrackRequest(msg.UnpublishDataTrackRequest)
case *livekit.SignalRequest_UpdateDataSubscription:
s.params.Participant.HandleUpdateDataSubscription(msg.UpdateDataSubscription)
}
return nil
}
@@ -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 signalling
import (
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignalHandler = (*signalhandlerUnimplemented)(nil)
type signalhandlerUnimplemented struct{}
func (u *signalhandlerUnimplemented) HandleMessage(msg proto.Message) error {
return nil
}
@@ -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 signalling
import (
"fmt"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignaller = (*signallerAsync)(nil)
type SignallerAsyncParams struct {
Logger logger.Logger
Participant types.LocalParticipant
}
type signallerAsync struct {
params SignallerAsyncParams
*signallerAsyncBase
}
func NewSignallerAsync(params SignallerAsyncParams) ParticipantSignaller {
return &signallerAsync{
params: params,
signallerAsyncBase: newSignallerAsyncBase(signallerAsyncBaseParams{Logger: params.Logger}),
}
}
func (s *signallerAsync) WriteMessage(msg proto.Message) error {
if msg == nil {
return nil
}
getMessageType := func(msg proto.Message) string {
messageType := "unknown"
if typed, ok := msg.(*livekit.SignalResponse); ok {
messageType = fmt.Sprintf("%T", typed.Message)
}
return messageType
}
if s.params.Participant.IsDisconnected() {
s.params.Logger.Debugw(
"could not send message to participant, participant disconnected",
"messageType", getMessageType(msg),
)
return nil
}
if !s.params.Participant.IsReady() {
if typed, ok := msg.(*livekit.SignalResponse); !ok {
s.params.Logger.Warnw(
"unknown message type", nil,
"messageType", fmt.Sprintf("%T", msg),
)
} else {
if typed.GetJoin() == nil {
return nil
}
}
}
sink := s.GetResponseSink()
if sink == nil {
s.params.Logger.Debugw(
"could not send message to participant, no sink",
"messageType", getMessageType(msg),
)
return nil
}
err := sink.WriteMessage(msg)
if err != nil {
if utils.ErrorIsOneOf(err, psrpc.Canceled, routing.ErrChannelClosed) {
s.params.Logger.Debugw(
"could not send message to participant",
"error", err,
"messageType", getMessageType(msg),
)
return nil
} else {
s.params.Logger.Warnw(
"could not send message to participant", err,
"messageType", getMessageType(msg),
)
return err
}
} else {
s.params.Logger.Debugw("sent signal response", "response", logger.Proto(msg))
}
return nil
}
@@ -0,0 +1,79 @@
// 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 signalling
import (
"sync"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
type signallerAsyncBaseParams struct {
Logger logger.Logger
}
type signallerAsyncBase struct {
signallerUnimplemented
params signallerAsyncBaseParams
resSinkMu sync.Mutex
resSink routing.MessageSink
}
func newSignallerAsyncBase(params signallerAsyncBaseParams) *signallerAsyncBase {
return &signallerAsyncBase{
params: params,
}
}
func (s *signallerAsyncBase) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
s.resSinkMu.Lock()
oldSink := s.resSink
s.resSink = sink
s.resSinkMu.Unlock()
if oldSink != nil {
if sink != nil {
s.params.Logger.Debugw(
"swapping signal connection",
"reason", reason,
"connID", oldSink.ConnectionID(),
"newConnID", sink.ConnectionID(),
)
} else {
s.params.Logger.Debugw(
"closing signal connection",
"reason", reason,
"connID", oldSink.ConnectionID(),
)
}
oldSink.Close()
}
}
func (s *signallerAsyncBase) GetResponseSink() routing.MessageSink {
s.resSinkMu.Lock()
defer s.resSinkMu.Unlock()
return s.resSink
}
// closes signal connection to notify client to resume/reconnect
func (s *signallerAsyncBase) CloseSignalConnection(reason types.SignallingCloseReason) {
s.SwapResponseSink(nil, reason)
}
@@ -0,0 +1,39 @@
// 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 signalling
import (
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignaller = (*signallerUnimplemented)(nil)
type signallerUnimplemented struct{}
func (u *signallerUnimplemented) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
}
func (u *signallerUnimplemented) GetResponseSink() routing.MessageSink {
return nil
}
func (u *signallerUnimplemented) CloseSignalConnection(reason types.SignallingCloseReason) {}
func (u *signallerUnimplemented) WriteMessage(msg proto.Message) error {
return nil
}
@@ -0,0 +1,260 @@
// 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 signalling
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignalling = (*signalling)(nil)
type SignallingParams struct {
Logger logger.Logger
}
type signalling struct {
params SignallingParams
}
func NewSignalling(params SignallingParams) ParticipantSignalling {
return &signalling{
params: params,
}
}
func (s *signalling) SignalJoinResponse(join *livekit.JoinResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Join{
Join: join,
},
}
}
func (s *signalling) SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message {
if len(participants) == 0 {
return nil
}
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: participants,
},
},
}
}
func (s *signalling) SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message {
if len(speakers) == 0 {
return nil
}
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SpeakersChanged{
SpeakersChanged: &livekit.SpeakersChanged{
Speakers: speakers,
},
},
}
}
func (s *signalling) SignalRoomUpdate(room *livekit.Room) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RoomUpdate{
RoomUpdate: &livekit.RoomUpdate{
Room: room,
},
},
}
}
func (s *signalling) SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_ConnectionQuality{
ConnectionQuality: connectionQuality,
},
}
}
func (s *signalling) SignalRefreshToken(token string) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RefreshToken{
RefreshToken: token,
},
}
}
func (s *signalling) SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RequestResponse{
RequestResponse: requestResponse,
},
}
}
func (s *signalling) SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RoomMoved{
RoomMoved: roomMoved,
},
}
}
func (s *signalling) SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Reconnect{
Reconnect: reconnect,
},
}
}
func (s *signalling) SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Trickle{
Trickle: trickle,
},
}
}
func (s *signalling) SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Mute{
Mute: mute,
},
}
}
func (s *signalling) SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackPublished{
TrackPublished: trackPublished,
},
}
}
func (s *signalling) SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackUnpublished{
TrackUnpublished: trackUnpublished,
},
}
}
func (s *signalling) SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackSubscribed{
TrackSubscribed: trackSubscribed,
},
}
}
func (s *signalling) SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Leave{
Leave: leave,
},
}
}
func (s *signalling) SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Answer{
Answer: answer,
},
}
}
func (s *signalling) SignalSdpOffer(offer *livekit.SessionDescription) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Offer{
Offer: offer,
},
}
}
func (s *signalling) SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_StreamStateUpdate{
StreamStateUpdate: streamStateUpdate,
},
}
}
func (s *signalling) SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscribedQualityUpdate{
SubscribedQualityUpdate: subscribedQualityUpdate,
},
}
}
func (s *signalling) SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscriptionResponse{
SubscriptionResponse: subscriptionResponse,
},
}
}
func (s *signalling) SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscriptionPermissionUpdate{
SubscriptionPermissionUpdate: subscriptionPermissionUpdate,
},
}
}
func (s *signalling) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_MediaSectionsRequirement{
MediaSectionsRequirement: mediaSectionsRequirement,
},
}
}
func (s *signalling) SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscribedAudioCodecUpdate{
SubscribedAudioCodecUpdate: subscribedAudioCodecUpdate,
},
}
}
func (s *signalling) SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_PublishDataTrackResponse{
PublishDataTrackResponse: publishDataTrackResponse,
},
}
}
func (s *signalling) SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_UnpublishDataTrackResponse{
UnpublishDataTrackResponse: unpublishDataTrackResponse,
},
}
}
func (s *signalling) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_DataTrackSubscriberHandles{
DataTrackSubscriberHandles: dataTrackSubscriberHandles,
},
}
}
@@ -0,0 +1,129 @@
// 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 signalling
import (
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignalling = (*signallingUnimplemented)(nil)
type signallingUnimplemented struct{}
func (u *signallingUnimplemented) SignalJoinResponse(join *livekit.JoinResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRoomUpdate(room *livekit.Room) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRefreshToken(token string) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSdpOffer(offer *livekit.SessionDescription) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message {
return nil
}
+210 -24
View File
@@ -23,9 +23,12 @@ import (
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/telemetry"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/utils"
"github.com/livekit/livekit-server/pkg/rtc/types"
@@ -37,19 +40,27 @@ const (
subscriptionDebounceInterval = 100 * time.Millisecond
)
var _ types.SubscribedTrack = (*SubscribedTrack)(nil)
type SubscribedTrackParams struct {
PublisherID livekit.ParticipantID
PublisherIdentity livekit.ParticipantIdentity
PublisherVersion uint32
Subscriber types.LocalParticipant
MediaTrack types.MediaTrack
DownTrack *sfu.DownTrack
AdaptiveStream bool
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
Subscriber types.LocalParticipant
MediaTrack types.MediaTrack
AdaptiveStream bool
TelemetryListener types.ParticipantTelemetryListener
WrappedReceiver *WrappedReceiver
IsRelayed bool
OnDownTrackCreated func(downTrack *sfu.DownTrack)
OnDownTrackClosed func(subscriberID livekit.ParticipantID)
OnSubscriberMaxQualityChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)
OnSubscriberAudioCodecChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)
}
type SubscribedTrack struct {
params SubscribedTrackParams
logger logger.Logger
downTrack *sfu.DownTrack
sender atomic.Pointer[webrtc.RTPSender]
needsNegotiation atomic.Bool
@@ -65,21 +76,98 @@ type SubscribedTrack struct {
onClose atomic.Value // func(bool)
debouncer func(func())
statsKey telemetry.StatsKey
reporter roomobs.TrackReporter
}
func NewSubscribedTrack(params SubscribedTrackParams) *SubscribedTrack {
func NewSubscribedTrack(params SubscribedTrackParams) (*SubscribedTrack, error) {
s := &SubscribedTrack{
params: params,
logger: params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub).WithValues(
"trackID", params.DownTrack.ID(),
"publisherID", params.PublisherID,
"publisher", params.PublisherIdentity,
"trackID", params.MediaTrack.ID(),
"publisherID", params.MediaTrack.PublisherID(),
"publisher", params.MediaTrack.PublisherIdentity(),
),
versionGenerator: utils.NewDefaultTimedVersionGenerator(),
debouncer: debounce.New(subscriptionDebounceInterval),
statsKey: telemetry.StatsKeyForTrack(
params.Subscriber.GetCountry(),
livekit.StreamType_DOWNSTREAM,
params.Subscriber.ID(),
params.MediaTrack.ID(),
params.MediaTrack.Source(),
params.MediaTrack.Kind(),
),
reporter: params.Subscriber.GetReporter().WithTrack(params.MediaTrack.ID().String()),
}
return s
var rtcpFeedback []webrtc.RTCPFeedback
var maxTrack int
switch params.MediaTrack.Kind() {
case livekit.TrackType_AUDIO:
rtcpFeedback = params.SubscriberConfig.RTCPFeedback.Audio
maxTrack = params.ReceiverConfig.PacketBufferSizeAudio
case livekit.TrackType_VIDEO:
rtcpFeedback = params.SubscriberConfig.RTCPFeedback.Video
maxTrack = params.ReceiverConfig.PacketBufferSizeVideo
default:
s.logger.Warnw("unexpected track type", nil, "kind", params.MediaTrack.Kind())
}
codecs := params.WrappedReceiver.Codecs()
for _, c := range codecs {
c.RTCPFeedback = rtcpFeedback
}
streamID := params.WrappedReceiver.StreamID()
if params.Subscriber.SupportsSyncStreamID() && params.MediaTrack.Stream() != "" {
streamID = PackSyncStreamID(params.MediaTrack.PublisherID(), params.MediaTrack.Stream())
}
isEncrypted := params.MediaTrack.IsEncrypted()
var trailer []byte
if isEncrypted {
trailer = params.Subscriber.GetTrailer()
}
subClientInfo := ClientInfo{ClientInfo: params.Subscriber.GetClientInfo()}
subSupportsPacketTrailer := subClientInfo.SupportsPacketTrailer()
// Strip packet trailer if track has packet trailer but subscriber does not have cap
stripPacketTrailer := params.MediaTrack.HasPacketTrailer() && !subSupportsPacketTrailer
downTrack, err := sfu.NewDownTrack(sfu.DownTrackParams{
Codecs: codecs,
IsEncrypted: isEncrypted,
Source: params.MediaTrack.Source(),
Receiver: params.WrappedReceiver,
BufferFactory: params.Subscriber.GetBufferFactory(),
SubID: params.Subscriber.ID(),
StreamID: streamID,
MaxTrack: maxTrack,
PlayoutDelayLimit: params.Subscriber.GetPlayoutDelayConfig(),
Pacer: params.Subscriber.GetPacer(),
Trailer: trailer,
StripPacketTrailer: stripPacketTrailer,
Logger: LoggerWithTrack(
params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub),
params.MediaTrack.ID(),
params.IsRelayed,
),
RTCPWriter: params.Subscriber.WriteSubscriberRTCP,
DisableSenderReportPassThrough: params.Subscriber.GetDisableSenderReportPassThrough(),
SupportsCodecChange: params.Subscriber.SupportsCodecChange(),
Listener: s,
})
if err != nil {
return nil, err
}
if params.OnDownTrackCreated != nil {
params.OnDownTrackCreated(downTrack)
}
downTrack.AddReceiverReportListener(params.Subscriber.HandleReceiverReport)
s.downTrack = downTrack
return s, nil
}
func (t *SubscribedTrack) AddOnBind(f func(error)) {
@@ -121,6 +209,7 @@ func (t *SubscribedTrack) Bound(err error) {
if t.params.AdaptiveStream {
// remove `disabled` flag to force a visibility update
t.settings.Disabled = false
t.logger.Debugw("enabling subscriber track settings on bind", "settings", logger.Proto(t.settings))
}
} else {
if t.params.AdaptiveStream {
@@ -128,6 +217,7 @@ func (t *SubscribedTrack) Bound(err error) {
} else {
t.settings = &livekit.UpdateTrackSettings{Quality: livekit.VideoQuality_HIGH}
}
t.logger.Debugw("initializing subscriber track settings on bind", "settings", logger.Proto(t.settings))
}
t.settingsLock.Unlock()
t.applySettings()
@@ -141,7 +231,7 @@ func (t *SubscribedTrack) Bound(err error) {
// for DownTrack callback to notify us that it's closed
func (t *SubscribedTrack) Close(isExpectedToResume bool) {
if onClose := t.onClose.Load(); onClose != nil {
go onClose.(func(bool))(isExpectedToResume)
onClose.(func(bool))(isExpectedToResume)
}
}
@@ -157,19 +247,19 @@ func (t *SubscribedTrack) IsBound() bool {
}
func (t *SubscribedTrack) ID() livekit.TrackID {
return livekit.TrackID(t.params.DownTrack.ID())
return livekit.TrackID(t.downTrack.ID())
}
func (t *SubscribedTrack) PublisherID() livekit.ParticipantID {
return t.params.PublisherID
return t.params.MediaTrack.PublisherID()
}
func (t *SubscribedTrack) PublisherIdentity() livekit.ParticipantIdentity {
return t.params.PublisherIdentity
return t.params.MediaTrack.PublisherIdentity()
}
func (t *SubscribedTrack) PublisherVersion() uint32 {
return t.params.PublisherVersion
return t.params.MediaTrack.PublisherVersion()
}
func (t *SubscribedTrack) SubscriberID() livekit.ParticipantID {
@@ -185,7 +275,7 @@ func (t *SubscribedTrack) Subscriber() types.LocalParticipant {
}
func (t *SubscribedTrack) DownTrack() *sfu.DownTrack {
return t.params.DownTrack
return t.downTrack
}
func (t *SubscribedTrack) MediaTrack() types.MediaTrack {
@@ -209,18 +299,20 @@ func (t *SubscribedTrack) isMutedLocked() bool {
}
func (t *SubscribedTrack) SetPublisherMuted(muted bool) {
t.DownTrack().PubMute(muted)
t.downTrack.PubMute(muted)
}
func (t *SubscribedTrack) UpdateSubscriberSettings(settings *livekit.UpdateTrackSettings, isImmediate bool) {
t.settingsLock.Lock()
if proto.Equal(t.settings, settings) {
t.logger.Debugw("skipping subscriber track settings", "settings", logger.Proto(t.settings))
t.settingsLock.Unlock()
return
}
isImmediate = isImmediate || (!settings.Disabled && settings.Disabled != t.isMutedLocked())
t.settings = utils.CloneProto(settings)
t.logger.Debugw("saving subscriber track settings", "settings", logger.Proto(t.settings))
t.settingsLock.Unlock()
if isImmediate {
@@ -242,7 +334,6 @@ func (t *SubscribedTrack) applySettings() {
return
}
t.logger.Debugw("updating subscriber track settings", "settings", logger.Proto(t.settings))
t.settingsVersion = t.versionGenerator.Next()
settingsVersion := t.settingsVersion
t.settingsLock.Unlock()
@@ -253,23 +344,25 @@ func (t *SubscribedTrack) applySettings() {
if dt.Kind() == webrtc.RTPCodecTypeVideo {
mt := t.MediaTrack()
quality := t.settings.Quality
mimeType := dt.Mime()
if t.settings.Width > 0 {
quality = mt.GetQualityForDimension(t.settings.Width, t.settings.Height)
quality = mt.GetQualityForDimension(mimeType, t.settings.Width, t.settings.Height)
}
spatial = buffer.VideoQualityToSpatialLayer(quality, mt.ToProto())
spatial = buffer.GetSpatialLayerForVideoQuality(mimeType, quality, mt.ToProto())
if t.settings.Fps > 0 {
temporal = mt.GetTemporalLayerForSpatialFps(spatial, t.settings.Fps, dt.Mime())
temporal = mt.GetTemporalLayerForSpatialFps(mimeType, spatial, t.settings.Fps)
}
}
t.settingsLock.Lock()
if settingsVersion != t.settingsVersion {
// a newer settings has superceded this one
// a newer settings has superseded this one
t.settingsLock.Unlock()
return
}
t.logger.Debugw("applying subscriber track settings", "settings", logger.Proto(t.settings))
if t.settings.Disabled {
dt.Mute(true)
t.settingsLock.Unlock()
@@ -302,3 +395,96 @@ func (t *SubscribedTrack) RTPSender() *webrtc.RTPSender {
func (t *SubscribedTrack) SetRTPSender(sender *webrtc.RTPSender) {
t.sender.Store(sender)
}
// DownTrackListener implementation
var _ sfu.DownTrackListener = (*SubscribedTrack)(nil)
func (t *SubscribedTrack) OnBindAndConnected() {
if t.params.Subscriber.Hidden() {
return
}
t.params.MediaTrack.OnTrackSubscribed()
}
func (t *SubscribedTrack) OnStatsUpdate(stat *livekit.AnalyticsStat) {
t.params.TelemetryListener.OnTrackStats(t.statsKey, stat)
if cs, ok := telemetry.CondenseStat(stat); ok {
ti := t.params.WrappedReceiver.TrackInfo()
t.reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(t.params.Subscriber.Kind()))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(t.params.Subscriber.KindDetails()))
tx.ReportName(ti.Name)
tx.ReportKind(roomobs.TrackKindSub)
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.ReportSendBytes(uint32(cs.Bytes))
tx.ReportSendPackets(cs.Packets)
tx.ReportPacketsLost(cs.PacketsLost)
tx.ReportScore(stat.Score)
})
}
}
func (t *SubscribedTrack) OnMaxSubscribedLayerChanged(layer int32) {
if t.params.OnSubscriberMaxQualityChange != nil {
t.params.OnSubscriberMaxQualityChange(t.downTrack.SubscriberID(), t.downTrack.Mime(), layer)
}
}
func (t *SubscribedTrack) OnRttUpdate(rtt uint32) {
go t.params.Subscriber.UpdateMediaRTT(rtt)
}
func (t *SubscribedTrack) OnCodecNegotiated(codec webrtc.RTPCodecCapability) {
if isAvailable, needsPublish := t.params.WrappedReceiver.DetermineReceiver(codec); !isAvailable || !needsPublish {
return
}
if t.params.OnSubscriberMaxQualityChange != nil || t.params.OnSubscriberAudioCodecChange != nil {
go func() {
mimeType := mime.NormalizeMimeType(codec.MimeType)
switch t.params.MediaTrack.Kind() {
case livekit.TrackType_VIDEO:
spatial := buffer.GetSpatialLayerForVideoQuality(
mimeType,
livekit.VideoQuality_HIGH,
t.params.MediaTrack.ToProto(),
)
if t.params.OnSubscriberMaxQualityChange != nil {
t.params.OnSubscriberMaxQualityChange(t.downTrack.SubscriberID(), mimeType, spatial)
}
case livekit.TrackType_AUDIO:
if t.params.OnSubscriberAudioCodecChange != nil {
t.params.OnSubscriberAudioCodecChange(t.downTrack.SubscriberID(), mimeType, true)
}
}
}()
}
}
func (t *SubscribedTrack) OnDownTrackClose(isExpectedToResume bool) {
// Cache transceiver for potential re-use on resume.
// To ensure subscription manager does not re-subscribe before caching,
// delete the subscribed track only after caching.
if isExpectedToResume {
if tr := t.downTrack.GetTransceiver(); tr != nil {
t.params.Subscriber.CacheDownTrack(t.ID(), tr, t.downTrack.GetState())
}
}
if t.params.OnDownTrackClosed != nil {
t.params.OnDownTrackClosed(t.params.Subscriber.ID())
}
t.Close(isExpectedToResume)
}
func (t *SubscribedTrack) OnStreamStarted() {
t.params.TelemetryListener.OnTrackSubscribeStreamStarted(t.params.Subscriber.ID(), t.params.MediaTrack.ToProto())
}

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