mirror of
https://github.com/livekit/livekit.git
synced 2026-07-11 20:39:08 +00:00
Merge branch 'master' into duan/add-video-frame-caching
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
# Builds and publishes the SDK conformance test server (cmd/test-server) to
|
||||
# Docker Hub as livekit/test-server:latest on every push to master. The server
|
||||
# SDK repos boot this image in their CI to run region-failover tests.
|
||||
name: Release Test Server to Docker
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
||||
with:
|
||||
context: .
|
||||
file: cmd/test-server/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# Each build overrides the latest tag so SDK CI always boots the
|
||||
# current mock server.
|
||||
tags: livekit/test-server:latest
|
||||
@@ -2,6 +2,39 @@
|
||||
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.13.2] - 2026-06-27
|
||||
|
||||
### Added
|
||||
|
||||
- Add Prometheus metrics for join latency and peer connection state (#4574, #4616)
|
||||
- Preserve original expiry when refreshing token (#4580)
|
||||
- Add grants expiry to Auth context (#4581)
|
||||
- Add ability to run pprof on dedicated HTTP server (#4584)
|
||||
- Add API to get latest node stats (#4589)
|
||||
- Enforce subscription permission to data track (#4588)
|
||||
- egress v2 api (#4592)
|
||||
- agent: thread attributes map from dispatch to job (#4598)
|
||||
- Log subscription limit breaches (#4603)
|
||||
- Acquire requested video layer directly at HIGH quality by default (#4595)
|
||||
- Report participant capabilities in ParticipantInfo (#4606)
|
||||
- Add option to force drain rtcService/agentService connections (#4618)
|
||||
- Add support for data blob (a. k. a. async participant attributes) (#4619)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update dependencies: pion/sctp, DTLS v3.1.4, protocol (#4587, #4601, #4623)
|
||||
- rtc: add RestartSessionTimer to re-anchor participant session duration (#4566)
|
||||
- Record more RTC cancellation points (#4600)
|
||||
- Cap all metadata at 512 KiB; enforce on join, agent dispatch, and embedded agents (#4602)
|
||||
- Tighten up publish latency stat (#4615)
|
||||
- Echo offered audio payload types in single-PC subscriber answer (#4614)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix skipped packets accounting (#4604)
|
||||
- Do not log due to negative getting interpreted as large unsigned positive (#4605)
|
||||
- Do not call nil callback (#4607)
|
||||
|
||||
## [1.13.1] - 2026-06-08
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
# Mock LiveKit API server for SDK conformance testing. Build from the repo
|
||||
# root: docker build -f cmd/test-server/Dockerfile -t livekit/test-server .
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
RUN go mod download
|
||||
|
||||
COPY cmd/ cmd/
|
||||
COPY pkg/ pkg/
|
||||
COPY version/ version/
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH GO111MODULE=on go build -a -o livekit-test-server ./cmd/test-server
|
||||
|
||||
FROM alpine
|
||||
|
||||
COPY --from=builder /workspace/livekit-test-server /livekit-test-server
|
||||
|
||||
# region-0 (primary, 9999) + 3 fallback regions
|
||||
EXPOSE 9999 10000 10001 10002
|
||||
|
||||
ENTRYPOINT ["/livekit-test-server"]
|
||||
@@ -0,0 +1,84 @@
|
||||
# LiveKit SDK test server
|
||||
|
||||
A stateless, per-request programmable mock of the LiveKit server HTTP API. It
|
||||
exists so the server SDKs (Go, Rust, Python, Node, Kotlin, Ruby) can exercise
|
||||
client-side behavior against one shared implementation, published as a Docker
|
||||
image and booted by each SDK's CI.
|
||||
|
||||
## Why it looks the way it does
|
||||
|
||||
- **Stateless.** All behavior is selected by per-request `X-Lk-Mock-*` headers,
|
||||
so the server holds no mutable state and tests run in parallel.
|
||||
- **Multi-port = multi-region.** The process binds one listener per simulated
|
||||
region (`--ports`). A port's position in the list is its **region index**;
|
||||
index `0` is the primary the SDK is initially pointed at. `GET
|
||||
/settings/regions` advertises all of them in order.
|
||||
- **One header drives every attempt.** The SDK sends the same control header on
|
||||
the initial request *and* every failover retry. Each listener decides what to
|
||||
do from its **own** index, so a single `X-Lk-Mock-Fail-Regions: 0` makes the
|
||||
primary fail while the first fallback succeeds — no coordination needed.
|
||||
- **The whole API is mocked with populated responses.** Every RoomService,
|
||||
Egress, Ingress, SIP, and Connector method returns a type-correct, populated
|
||||
response: scalar fields that share a name with the request are echoed (e.g.
|
||||
`name`, `metadata`, `identity`, timeouts), `id`/`sid` fields get placeholder
|
||||
values, and list endpoints return one element. Both protobuf and JSON Twirp
|
||||
clients are supported. A client can override the response entirely with the
|
||||
`X-Lk-Mock-Response` header (see below). Unregistered/future methods fall back
|
||||
to an empty (all-default) message, which still decodes cleanly.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
go run ./cmd/test-server # primary :9999, regions :10000-10002
|
||||
go run ./cmd/test-server --ports 9999,10000 # primary + one fallback
|
||||
|
||||
# Docker
|
||||
docker build -f cmd/test-server/Dockerfile -t livekit/test-server .
|
||||
docker run -p 9999-10002:9999-10002 livekit/test-server
|
||||
```
|
||||
|
||||
| Flag | Env | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `--ports` | `LK_TEST_SERVER_PORTS` | `9999,10000,10001,10002` | listener ports; index = position |
|
||||
| `--advertise-host` | `LK_TEST_SERVER_ADVERTISE_HOST` | `http://127.0.0.1` | base URL used in `/settings/regions` |
|
||||
| `--bind` | `LK_TEST_SERVER_BIND` | `0.0.0.0` | bind address |
|
||||
| `--twirp-prefix` | `LK_TEST_SERVER_TWIRP_PREFIX` | `/twirp` | Twirp path prefix |
|
||||
|
||||
## Control protocol
|
||||
|
||||
Request headers (sent by the SDK on API calls; the SDK must forward
|
||||
client-configured custom headers onto the `/settings/regions` fetch and every
|
||||
failover retry):
|
||||
|
||||
| Header | Default | Effect |
|
||||
|---|---|---|
|
||||
| `X-Lk-Mock-Fail-Regions` | — | comma list of region indices that fail this request, e.g. `0` or `0,1`. Each listener fails only if its own index is listed. |
|
||||
| `X-Lk-Mock-Fail-Mode` | `status` | how a failing region fails: `status`, `drop` (close connection → transport error), `delay`. |
|
||||
| `X-Lk-Mock-Fail-Status` | `503` | HTTP status when failing with `status`/`delay`. |
|
||||
| `X-Lk-Mock-Fail-Twirp-Code` | derived from status | Twirp error code string in the failure body. |
|
||||
| `X-Lk-Mock-Delay-Ms` | `30000` | delay before a `delay`-mode region responds (for timeout tests). |
|
||||
| `X-Lk-Mock-Regions-Status` | `200` | override the status of `GET /settings/regions`. |
|
||||
| `X-Lk-Mock-Response` | — | protojson of the response message for the called method; replaces the populated default, giving full control over the returned payload. |
|
||||
|
||||
Response headers:
|
||||
|
||||
| Header | Meaning |
|
||||
|---|---|
|
||||
| `X-Lk-Mock-Region` | index of the region that served the response (blank on a failed region). Assert on this to confirm which region a failover landed on. |
|
||||
|
||||
## Common recipes
|
||||
|
||||
| Goal | Headers |
|
||||
|---|---|
|
||||
| Happy path | _(none)_ → 200 from region `0` |
|
||||
| Failover succeeds on region 1 | `X-Lk-Mock-Fail-Regions: 0` |
|
||||
| Exhaust to region 2 | `X-Lk-Mock-Fail-Regions: 0,1` |
|
||||
| All regions down | `X-Lk-Mock-Fail-Regions: 0,1,2,3` |
|
||||
| 4xx, no retry | `X-Lk-Mock-Fail-Regions: 0` + `X-Lk-Mock-Fail-Status: 400` |
|
||||
| Transport-error failover | `X-Lk-Mock-Fail-Regions: 0` + `X-Lk-Mock-Fail-Mode: drop` |
|
||||
| Region discovery unreachable | `X-Lk-Mock-Regions-Status: 500` |
|
||||
| Custom response payload | `X-Lk-Mock-Response: {"sid":"RM_x","name":"my-room"}` |
|
||||
|
||||
Note: SDK region failover normally only engages for `*.livekit.cloud` hosts.
|
||||
Since tests point at `127.0.0.1`, set the SDK's failover-enable option to its
|
||||
forced-on value so failover engages against localhost.
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright 2026 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils/protojson"
|
||||
)
|
||||
|
||||
// apiSpec captures the request and response message types for one Twirp method,
|
||||
// so the mock can decode the incoming request and build a typed response.
|
||||
type apiSpec struct {
|
||||
newReq func() proto.Message
|
||||
newResp func() proto.Message
|
||||
}
|
||||
|
||||
// ptrMsg constrains a pointer type that is also a proto.Message, letting reg
|
||||
// construct fresh request/response values generically.
|
||||
type ptrMsg[T any] interface {
|
||||
*T
|
||||
proto.Message
|
||||
}
|
||||
|
||||
// apiHandlers maps "<package>.<Service>/<Method>" to its message types. It
|
||||
// covers the full LiveKit API surface; see init below.
|
||||
var apiHandlers = map[string]apiSpec{}
|
||||
|
||||
func reg[ReqT, RespT any, Req ptrMsg[ReqT], Resp ptrMsg[RespT]](key string) {
|
||||
apiHandlers[key] = apiSpec{
|
||||
newReq: func() proto.Message { return Req(new(ReqT)) },
|
||||
newResp: func() proto.Message { return Resp(new(RespT)) },
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// RoomService
|
||||
reg[livekit.CreateRoomRequest, livekit.Room]("livekit.RoomService/CreateRoom")
|
||||
reg[livekit.ListRoomsRequest, livekit.ListRoomsResponse]("livekit.RoomService/ListRooms")
|
||||
reg[livekit.DeleteRoomRequest, livekit.DeleteRoomResponse]("livekit.RoomService/DeleteRoom")
|
||||
reg[livekit.ListParticipantsRequest, livekit.ListParticipantsResponse]("livekit.RoomService/ListParticipants")
|
||||
reg[livekit.RoomParticipantIdentity, livekit.ParticipantInfo]("livekit.RoomService/GetParticipant")
|
||||
reg[livekit.RoomParticipantIdentity, livekit.RemoveParticipantResponse]("livekit.RoomService/RemoveParticipant")
|
||||
reg[livekit.MuteRoomTrackRequest, livekit.MuteRoomTrackResponse]("livekit.RoomService/MutePublishedTrack")
|
||||
reg[livekit.UpdateParticipantRequest, livekit.ParticipantInfo]("livekit.RoomService/UpdateParticipant")
|
||||
reg[livekit.UpdateSubscriptionsRequest, livekit.UpdateSubscriptionsResponse]("livekit.RoomService/UpdateSubscriptions")
|
||||
reg[livekit.SendDataRequest, livekit.SendDataResponse]("livekit.RoomService/SendData")
|
||||
reg[livekit.UpdateRoomMetadataRequest, livekit.Room]("livekit.RoomService/UpdateRoomMetadata")
|
||||
reg[livekit.ForwardParticipantRequest, livekit.ForwardParticipantResponse]("livekit.RoomService/ForwardParticipant")
|
||||
reg[livekit.MoveParticipantRequest, livekit.MoveParticipantResponse]("livekit.RoomService/MoveParticipant")
|
||||
reg[livekit.PerformRpcRequest, livekit.PerformRpcResponse]("livekit.RoomService/PerformRpc")
|
||||
|
||||
// Egress
|
||||
reg[livekit.StartEgressRequest, livekit.EgressInfo]("livekit.Egress/StartEgress")
|
||||
reg[livekit.UpdateLayoutRequest, livekit.EgressInfo]("livekit.Egress/UpdateLayout")
|
||||
reg[livekit.UpdateStreamRequest, livekit.EgressInfo]("livekit.Egress/UpdateStream")
|
||||
reg[livekit.ListEgressRequest, livekit.ListEgressResponse]("livekit.Egress/ListEgress")
|
||||
reg[livekit.StopEgressRequest, livekit.EgressInfo]("livekit.Egress/StopEgress")
|
||||
reg[livekit.RoomCompositeEgressRequest, livekit.EgressInfo]("livekit.Egress/StartRoomCompositeEgress")
|
||||
reg[livekit.WebEgressRequest, livekit.EgressInfo]("livekit.Egress/StartWebEgress")
|
||||
reg[livekit.ParticipantEgressRequest, livekit.EgressInfo]("livekit.Egress/StartParticipantEgress")
|
||||
reg[livekit.TrackCompositeEgressRequest, livekit.EgressInfo]("livekit.Egress/StartTrackCompositeEgress")
|
||||
reg[livekit.TrackEgressRequest, livekit.EgressInfo]("livekit.Egress/StartTrackEgress")
|
||||
|
||||
// Ingress
|
||||
reg[livekit.CreateIngressRequest, livekit.IngressInfo]("livekit.Ingress/CreateIngress")
|
||||
reg[livekit.UpdateIngressRequest, livekit.IngressInfo]("livekit.Ingress/UpdateIngress")
|
||||
reg[livekit.ListIngressRequest, livekit.ListIngressResponse]("livekit.Ingress/ListIngress")
|
||||
reg[livekit.DeleteIngressRequest, livekit.IngressInfo]("livekit.Ingress/DeleteIngress")
|
||||
|
||||
// SIP
|
||||
reg[livekit.ListSIPTrunkRequest, livekit.ListSIPTrunkResponse]("livekit.SIP/ListSIPTrunk")
|
||||
reg[livekit.CreateSIPInboundTrunkRequest, livekit.SIPInboundTrunkInfo]("livekit.SIP/CreateSIPInboundTrunk")
|
||||
reg[livekit.CreateSIPOutboundTrunkRequest, livekit.SIPOutboundTrunkInfo]("livekit.SIP/CreateSIPOutboundTrunk")
|
||||
reg[livekit.UpdateSIPInboundTrunkRequest, livekit.SIPInboundTrunkInfo]("livekit.SIP/UpdateSIPInboundTrunk")
|
||||
reg[livekit.UpdateSIPOutboundTrunkRequest, livekit.SIPOutboundTrunkInfo]("livekit.SIP/UpdateSIPOutboundTrunk")
|
||||
reg[livekit.GetSIPInboundTrunkRequest, livekit.GetSIPInboundTrunkResponse]("livekit.SIP/GetSIPInboundTrunk")
|
||||
reg[livekit.GetSIPOutboundTrunkRequest, livekit.GetSIPOutboundTrunkResponse]("livekit.SIP/GetSIPOutboundTrunk")
|
||||
reg[livekit.ListSIPInboundTrunkRequest, livekit.ListSIPInboundTrunkResponse]("livekit.SIP/ListSIPInboundTrunk")
|
||||
reg[livekit.ListSIPOutboundTrunkRequest, livekit.ListSIPOutboundTrunkResponse]("livekit.SIP/ListSIPOutboundTrunk")
|
||||
reg[livekit.DeleteSIPTrunkRequest, livekit.SIPTrunkInfo]("livekit.SIP/DeleteSIPTrunk")
|
||||
reg[livekit.CreateSIPDispatchRuleRequest, livekit.SIPDispatchRuleInfo]("livekit.SIP/CreateSIPDispatchRule")
|
||||
reg[livekit.UpdateSIPDispatchRuleRequest, livekit.SIPDispatchRuleInfo]("livekit.SIP/UpdateSIPDispatchRule")
|
||||
reg[livekit.ListSIPDispatchRuleRequest, livekit.ListSIPDispatchRuleResponse]("livekit.SIP/ListSIPDispatchRule")
|
||||
reg[livekit.DeleteSIPDispatchRuleRequest, livekit.SIPDispatchRuleInfo]("livekit.SIP/DeleteSIPDispatchRule")
|
||||
reg[livekit.CreateSIPParticipantRequest, livekit.SIPParticipantInfo]("livekit.SIP/CreateSIPParticipant")
|
||||
reg[livekit.TransferSIPParticipantRequest, emptypb.Empty]("livekit.SIP/TransferSIPParticipant")
|
||||
|
||||
// Connector
|
||||
reg[livekit.DialWhatsAppCallRequest, livekit.DialWhatsAppCallResponse]("livekit.Connector/DialWhatsAppCall")
|
||||
reg[livekit.DisconnectWhatsAppCallRequest, livekit.DisconnectWhatsAppCallResponse]("livekit.Connector/DisconnectWhatsAppCall")
|
||||
reg[livekit.ConnectWhatsAppCallRequest, livekit.ConnectWhatsAppCallResponse]("livekit.Connector/ConnectWhatsAppCall")
|
||||
reg[livekit.AcceptWhatsAppCallRequest, livekit.AcceptWhatsAppCallResponse]("livekit.Connector/AcceptWhatsAppCall")
|
||||
reg[livekit.ConnectTwilioCallRequest, livekit.ConnectTwilioCallResponse]("livekit.Connector/ConnectTwilioCall")
|
||||
}
|
||||
|
||||
// writeAPIResponse serves a populated, type-correct response for a known API
|
||||
// method. The response is the reflection-populated default unless the request
|
||||
// carries an X-Lk-Mock-Response header (protojson), which overrides it
|
||||
// entirely. Content type (protobuf vs JSON) mirrors the request.
|
||||
func (h *mockHandler) writeAPIResponse(w http.ResponseWriter, r *http.Request) {
|
||||
json := strings.Contains(r.Header.Get("Content-Type"), "json")
|
||||
w.Header().Set(headerRegion, strconv.Itoa(h.regionIndex))
|
||||
|
||||
key := strings.TrimPrefix(r.URL.Path, h.twirpPrefix+"/")
|
||||
spec, ok := apiHandlers[key]
|
||||
if !ok {
|
||||
// Unknown/future method: an empty body still decodes to a valid default
|
||||
// message in every Twirp client.
|
||||
writeEmptySuccess(w, json)
|
||||
return
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req := spec.newReq()
|
||||
if json {
|
||||
_ = protojson.Unmarshal(body, req)
|
||||
} else {
|
||||
_ = proto.Unmarshal(body, req)
|
||||
}
|
||||
|
||||
resp := spec.newResp()
|
||||
if override := r.Header.Get(headerResponse); override != "" {
|
||||
if err := protojson.Unmarshal([]byte(override), resp); err != nil {
|
||||
// Malformed override: fall back to the populated default.
|
||||
resp = spec.newResp()
|
||||
populateMessage(resp.ProtoReflect(), req.ProtoReflect(), 1)
|
||||
}
|
||||
} else {
|
||||
populateMessage(resp.ProtoReflect(), req.ProtoReflect(), 1)
|
||||
}
|
||||
|
||||
if json {
|
||||
out, _ := protojson.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(out)
|
||||
} else {
|
||||
out, _ := proto.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/protobuf")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(out)
|
||||
}
|
||||
}
|
||||
|
||||
func writeEmptySuccess(w http.ResponseWriter, json bool) {
|
||||
if json {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("{}"))
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/protobuf")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// populateMessage fills a response message with plausible values: it echoes
|
||||
// scalar fields that share a name with the request, assigns placeholder values
|
||||
// to id/sid fields, and adds one element to repeated-message (list) fields so
|
||||
// list endpoints return non-empty results. depth bounds list-element nesting.
|
||||
func populateMessage(m protoreflect.Message, req protoreflect.Message, depth int) {
|
||||
fields := m.Descriptor().Fields()
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
fd := fields.Get(i)
|
||||
|
||||
// Echo a same-named scalar field from the request (e.g. name, metadata,
|
||||
// identity, room, timeouts).
|
||||
if req != nil && fd.Cardinality() != protoreflect.Repeated && isScalarKind(fd.Kind()) {
|
||||
if rf := req.Descriptor().Fields().ByName(fd.Name()); rf != nil &&
|
||||
rf.Kind() == fd.Kind() && rf.Cardinality() != protoreflect.Repeated && req.Has(rf) {
|
||||
m.Set(fd, req.Get(rf))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Give id/sid-like string fields a deterministic placeholder.
|
||||
if fd.Kind() == protoreflect.StringKind && fd.Cardinality() != protoreflect.Repeated && !m.Has(fd) {
|
||||
n := string(fd.Name())
|
||||
if n == "id" || n == "sid" || strings.HasSuffix(n, "_id") || strings.HasSuffix(n, "_sid") {
|
||||
m.Set(fd, protoreflect.ValueOfString("MOCK_"+strings.ToUpper(n)))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Populate list endpoints with a single element so clients see results.
|
||||
if depth > 0 && fd.IsList() && fd.Kind() == protoreflect.MessageKind {
|
||||
list := m.Mutable(fd).List()
|
||||
elem := list.NewElement()
|
||||
populateMessage(elem.Message(), nil, depth-1)
|
||||
list.Append(elem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isScalarKind(k protoreflect.Kind) bool {
|
||||
switch k {
|
||||
case protoreflect.BoolKind,
|
||||
protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Uint32Kind,
|
||||
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Uint64Kind,
|
||||
protoreflect.Sfixed32Kind, protoreflect.Fixed32Kind,
|
||||
protoreflect.Sfixed64Kind, protoreflect.Fixed64Kind,
|
||||
protoreflect.FloatKind, protoreflect.DoubleKind,
|
||||
protoreflect.StringKind, protoreflect.BytesKind:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// Copyright 2026 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.
|
||||
|
||||
// Command test-server is a programmable mock of the LiveKit server HTTP API,
|
||||
// used by the server SDKs to test client behavior. See cmd/test-server/README.md.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils/protojson"
|
||||
)
|
||||
|
||||
// X-Lk-Mock-* request headers control the mock's behavior; see the README.
|
||||
const (
|
||||
headerFailRegions = "X-Lk-Mock-Fail-Regions"
|
||||
headerFailMode = "X-Lk-Mock-Fail-Mode"
|
||||
headerFailStatus = "X-Lk-Mock-Fail-Status"
|
||||
headerFailTwirpCode = "X-Lk-Mock-Fail-Twirp-Code"
|
||||
headerDelayMs = "X-Lk-Mock-Delay-Ms"
|
||||
headerRegionsStatus = "X-Lk-Mock-Regions-Status"
|
||||
headerResponse = "X-Lk-Mock-Response"
|
||||
// headerRegion is set on responses to the index of the region that served it.
|
||||
headerRegion = "X-Lk-Mock-Region"
|
||||
)
|
||||
|
||||
const defaultDelayMs = 30_000
|
||||
|
||||
func main() {
|
||||
portsFlag := flagValue("--ports", "LK_TEST_SERVER_PORTS", "9999,10000,10001,10002")
|
||||
advertiseHost := flagValue("--advertise-host", "LK_TEST_SERVER_ADVERTISE_HOST", "http://127.0.0.1")
|
||||
bindAddr := flagValue("--bind", "LK_TEST_SERVER_BIND", "0.0.0.0")
|
||||
twirpPrefix := flagValue("--twirp-prefix", "LK_TEST_SERVER_TWIRP_PREFIX", "/twirp")
|
||||
|
||||
ports, err := parsePorts(portsFlag)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid --ports: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
advertiseHost = strings.TrimRight(advertiseHost, "/")
|
||||
|
||||
regions := &livekit.RegionSettings{}
|
||||
for i, p := range ports {
|
||||
regions.Regions = append(regions.Regions, &livekit.RegionInfo{
|
||||
Region: fmt.Sprintf("region-%d", i),
|
||||
Url: fmt.Sprintf("%s:%d", advertiseHost, p),
|
||||
Distance: int64(i),
|
||||
})
|
||||
}
|
||||
|
||||
errCh := make(chan error, len(ports))
|
||||
for i, p := range ports {
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", bindAddr, p),
|
||||
Handler: &mockHandler{regionIndex: i, regions: regions, twirpPrefix: twirpPrefix},
|
||||
}
|
||||
go func() { errCh <- srv.ListenAndServe() }()
|
||||
fmt.Printf("test-server: region-%d listening on %s:%d (advertised as %s:%d)\n", i, bindAddr, p, advertiseHost, p)
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
fmt.Fprintf(os.Stderr, "listener failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
case <-sigCh:
|
||||
fmt.Println("test-server: shutting down")
|
||||
}
|
||||
}
|
||||
|
||||
type mockHandler struct {
|
||||
regionIndex int
|
||||
regions *livekit.RegionSettings
|
||||
twirpPrefix string
|
||||
}
|
||||
|
||||
func (h *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/settings/regions":
|
||||
h.handleRegions(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, h.twirpPrefix+"/"):
|
||||
h.handleTwirp(w, r)
|
||||
case r.URL.Path == "/" || r.URL.Path == "/_test/health":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *mockHandler) handleRegions(w http.ResponseWriter, r *http.Request) {
|
||||
if status := parseStatus(r.Header.Get(headerRegionsStatus), 0); status != 0 && status != http.StatusOK {
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
body, err := protojson.Marshal(h.regions)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "max-age=0")
|
||||
w.Header().Set(headerRegion, strconv.Itoa(h.regionIndex))
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func (h *mockHandler) handleTwirp(w http.ResponseWriter, r *http.Request) {
|
||||
if h.shouldFail(r) {
|
||||
h.fail(w, r)
|
||||
return
|
||||
}
|
||||
h.writeAPIResponse(w, r)
|
||||
}
|
||||
|
||||
func (h *mockHandler) shouldFail(r *http.Request) bool {
|
||||
for _, part := range strings.Split(r.Header.Get(headerFailRegions), ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if idx, err := strconv.Atoi(part); err == nil && idx == h.regionIndex {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *mockHandler) fail(w http.ResponseWriter, r *http.Request) {
|
||||
switch strings.ToLower(r.Header.Get(headerFailMode)) {
|
||||
case "drop":
|
||||
if hj, ok := w.(http.Hijacker); ok {
|
||||
if conn, _, err := hj.Hijack(); err == nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
case "delay":
|
||||
delay := defaultDelayMs
|
||||
if ms, err := strconv.Atoi(r.Header.Get(headerDelayMs)); err == nil && ms >= 0 {
|
||||
delay = ms
|
||||
}
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
writeTwirpError(w, r, parseStatus(r.Header.Get(headerFailStatus), http.StatusServiceUnavailable))
|
||||
default:
|
||||
writeTwirpError(w, r, parseStatus(r.Header.Get(headerFailStatus), http.StatusServiceUnavailable))
|
||||
}
|
||||
}
|
||||
|
||||
func writeTwirpError(w http.ResponseWriter, r *http.Request, status int) {
|
||||
code := r.Header.Get(headerFailTwirpCode)
|
||||
if code == "" {
|
||||
code = twirpCodeForStatus(status)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set(headerRegion, "")
|
||||
w.WriteHeader(status)
|
||||
_, _ = fmt.Fprintf(w, `{"code":%q,"msg":%q}`, code, fmt.Sprintf("mock failure (status %d)", status))
|
||||
}
|
||||
|
||||
func twirpCodeForStatus(status int) string {
|
||||
switch {
|
||||
case status == http.StatusBadRequest:
|
||||
return "invalid_argument"
|
||||
case status == http.StatusUnauthorized:
|
||||
return "unauthenticated"
|
||||
case status == http.StatusForbidden:
|
||||
return "permission_denied"
|
||||
case status == http.StatusNotFound:
|
||||
return "not_found"
|
||||
case status == http.StatusTooManyRequests:
|
||||
return "resource_exhausted"
|
||||
case status >= 500:
|
||||
return "unavailable"
|
||||
default:
|
||||
return "internal"
|
||||
}
|
||||
}
|
||||
|
||||
func parseStatus(s string, def int) int {
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
if v, err := strconv.Atoi(strings.TrimSpace(s)); err == nil && v >= 100 && v <= 599 {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func parsePorts(s string) ([]int, error) {
|
||||
var ports []int
|
||||
for _, part := range strings.Split(s, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%q is not a port number", part)
|
||||
}
|
||||
ports = append(ports, v)
|
||||
}
|
||||
if len(ports) == 0 {
|
||||
return nil, errors.New("at least one port is required")
|
||||
}
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// flagValue resolves a setting from a --flag, then an environment variable, then a default.
|
||||
func flagValue(flag, env, def string) string {
|
||||
prefix := flag + "="
|
||||
for i, arg := range os.Args[1:] {
|
||||
if arg == flag {
|
||||
if i+2 <= len(os.Args[1:]) {
|
||||
return os.Args[1:][i+1]
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(arg, prefix) {
|
||||
return strings.TrimPrefix(arg, prefix)
|
||||
}
|
||||
}
|
||||
if v := os.Getenv(env); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -21,7 +21,7 @@ require (
|
||||
github.com/jxskiss/base62 v1.1.0
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
|
||||
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0
|
||||
github.com/livekit/protocol v1.47.1-0.20260617164816-2f8e4d6d263b
|
||||
github.com/livekit/protocol v1.48.1-0.20260623210753-2e1bfd81dd63
|
||||
github.com/livekit/psrpc v0.7.2
|
||||
github.com/mackerelio/go-osstat v0.2.7
|
||||
github.com/magefile/mage v1.17.2
|
||||
@@ -30,20 +30,20 @@ require (
|
||||
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/datachannel v1.6.2
|
||||
github.com/pion/dtls/v3 v3.1.4
|
||||
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/sctp v1.10.3
|
||||
github.com/pion/sdp/v3 v3.0.19
|
||||
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/pion/turn/v5 v5.0.10
|
||||
github.com/pion/webrtc/v4 v4.2.16
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/redis/go-redis/v9 v9.20.0
|
||||
github.com/redis/go-redis/v9 v9.21.0
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/thoas/go-funk v0.9.3
|
||||
@@ -54,8 +54,8 @@ require (
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.28.0
|
||||
golang.org/x/mod v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/sync v0.21.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -92,7 +92,7 @@ require (
|
||||
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/exp v0.0.0-20260611194520-c48552f49976 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
@@ -134,22 +134,22 @@ require (
|
||||
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.11 // indirect
|
||||
github.com/pion/stun/v3 v3.1.4
|
||||
github.com/pion/srtp/v3 v3.0.12 // indirect
|
||||
github.com/pion/stun/v3 v3.1.6
|
||||
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/common v0.69.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/urfave/cli/v3 v3.9.0
|
||||
github.com/urfave/cli/v3 v3.10.0
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.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
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/tools v0.46.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
)
|
||||
|
||||
@@ -160,8 +160,8 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT
|
||||
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.47.1-0.20260617164816-2f8e4d6d263b h1:7gH58XkJ0wtU+5d33Jkktmf4I3heKjQzDhwYHDSGmTo=
|
||||
github.com/livekit/protocol v1.47.1-0.20260617164816-2f8e4d6d263b/go.mod h1:jO+y05AU9Ec4JswDyuzKCZ4bhziOS0CzMqgnbj60Dzs=
|
||||
github.com/livekit/protocol v1.48.1-0.20260623210753-2e1bfd81dd63 h1:Rj9/54oztXeioAwUkukXBwcPE/GxL97WylFd1m7V2pQ=
|
||||
github.com/livekit/protocol v1.48.1-0.20260623210753-2e1bfd81dd63/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=
|
||||
@@ -229,8 +229,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
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/datachannel v1.6.2 h1:7EXQ8TH3vTouBUdRWYbcX2edSx9Yj6k5zl5P+qyxEPc=
|
||||
github.com/pion/datachannel v1.6.2/go.mod h1:pzbdAZvyGtXbcHM1hBbsFaOTf40lZizU/dNlvVOak6E=
|
||||
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
|
||||
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
|
||||
github.com/pion/ice/v4 v4.2.7 h1:zDEbC6MiEdhQpF8TxBOTws+NU6ZgGpveHrQq4Lc1kao=
|
||||
@@ -247,24 +247,22 @@ 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/sctp v1.10.3 h1:1gBtLMA9lmwNuJkZSZJCdD5/Hz4yJs+7dAqi6ZY97QI=
|
||||
github.com/pion/sctp v1.10.3/go.mod h1:7KFmTwLcoYgJs/Z+99nJvsWL0qDpuyloSI0RbAqlrz0=
|
||||
github.com/pion/sdp/v3 v3.0.19 h1:1VMKs3gIkTQV5M3hNKfTAPrDXSNrYtOlmOD8+mSZUGQ=
|
||||
github.com/pion/sdp/v3 v3.0.19/go.mod h1:dE5WOSlzXrtiE/iuZqe9n+AcEbOjtAd3k5m5NtlV/qU=
|
||||
github.com/pion/srtp/v3 v3.0.12 h1:U7V17bckl7sI4mb3sepiojByDuBY0wNCqQE+6IlQBbc=
|
||||
github.com/pion/srtp/v3 v3.0.12/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns=
|
||||
github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
|
||||
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.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/pion/turn/v5 v5.0.10 h1:mOMZjudflXpte5OsCnXztpUKwNXcpXIAzMBnq9TXOSQ=
|
||||
github.com/pion/turn/v5 v5.0.10/go.mod h1:u3XjBqy2Z4+NhCUpDoOSsNuQDrPLvKStlCGWk6sTQ1E=
|
||||
github.com/pion/webrtc/v4 v4.2.16 h1:oK1GAg0TWJtZWYB8J/BgTgGWPoV2148gQWocH12vr3Q=
|
||||
github.com/pion/webrtc/v4 v4.2.16/go.mod h1:y4HjLAkX90LH+C/qPqGOUgz8RA8CbDj3Iar3d+2hdKQ=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -274,14 +272,14 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
||||
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/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk=
|
||||
github.com/prometheus/common v0.69.0/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/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||
github.com/redis/go-redis/v9 v9.21.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=
|
||||
@@ -305,8 +303,8 @@ github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJX
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
|
||||
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/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=
|
||||
github.com/urfave/cli/v3 v3.10.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=
|
||||
@@ -351,12 +349,12 @@ 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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
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-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -371,12 +369,12 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
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-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/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.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=
|
||||
@@ -400,29 +398,29 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.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/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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
|
||||
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.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/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d h1:xr2lwHI91bn3UiXcnyzRMQjp2LRiM8wEHzwUaE0YhTs=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d/go.mod h1:O0ZOWSrfWfJ+Z5HbwZ+wNtHsg/vk1k2C/w67eww8PfQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d h1:mpAgMyM9vQHxycBlDq50y1VHpfSfVwzXvrQKtYbXuUY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d/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=
|
||||
|
||||
@@ -177,6 +177,11 @@ func TestAll() error {
|
||||
return mageutil.Run(context.Background(), "go test ./... -count=1 -timeout=4m -v")
|
||||
}
|
||||
|
||||
// runs the SDK test server (cmd/test-server) in the foreground
|
||||
func TestServer() error {
|
||||
return mageutil.Run(context.Background(), "go run ./cmd/test-server")
|
||||
}
|
||||
|
||||
// runs golangci-lint
|
||||
func Lint() error {
|
||||
if _, err := exec.LookPath("golangci-lint"); err != nil {
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
type AgentService interface {
|
||||
HandleConnection(context.Context, agent.SignalConn, agent.WorkerRegistration)
|
||||
DrainConnections(time.Duration)
|
||||
DrainConnections(time.Duration, bool)
|
||||
}
|
||||
|
||||
type TestServer struct {
|
||||
@@ -140,7 +140,7 @@ func (h *TestServer) SimulateAgentWorker(opts ...SimulatedWorkerOption) *AgentWo
|
||||
}
|
||||
|
||||
func (h *TestServer) Close() {
|
||||
h.DrainConnections(1)
|
||||
h.DrainConnections(1, false)
|
||||
}
|
||||
|
||||
var _ agent.SignalConn = (*AgentWorker)(nil)
|
||||
|
||||
@@ -91,6 +91,8 @@ type Config struct {
|
||||
|
||||
EnableDataTracks bool `yaml:"enable_data_tracks,omitempty"`
|
||||
|
||||
EnableParticipantDataBlob bool `yaml:"enable_participant_data_blob,omitempty"`
|
||||
|
||||
API APIConfig `yaml:"api,omitempty"`
|
||||
}
|
||||
|
||||
@@ -280,6 +282,8 @@ type RegionConfig struct {
|
||||
Lon float64 `yaml:"lon,omitempty"`
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
type LimitConfig struct {
|
||||
NumTracks int32 `yaml:"num_tracks,omitempty"`
|
||||
BytesPerSec float32 `yaml:"bytes_per_sec,omitempty"`
|
||||
@@ -291,6 +295,9 @@ type LimitConfig struct {
|
||||
MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"`
|
||||
MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"`
|
||||
MaxParticipantNameLength int `yaml:"max_participant_name_length,omitempty"`
|
||||
|
||||
MaxDataBlobKeyLength int `yaml:"max_data_blob_key_length,omitempty"`
|
||||
MaxDataBlobSize uint32 `yaml:"max_data_blobs_size,omitempty"`
|
||||
}
|
||||
|
||||
func (l LimitConfig) CheckRoomNameLength(name string) bool {
|
||||
@@ -321,6 +328,36 @@ func (l LimitConfig) CheckAttributesSize(attributes map[string]string) bool {
|
||||
return uint32(total) <= l.MaxAttributesSize
|
||||
}
|
||||
|
||||
func (l LimitConfig) CheckDataBlobKeyLength(key string) bool {
|
||||
return l.MaxDataBlobKeyLength == 0 || len(key) <= l.MaxDataBlobKeyLength
|
||||
}
|
||||
|
||||
func (l LimitConfig) CheckDataBlobsSize(dataBlobs []*livekit.DataBlob) bool {
|
||||
if l.MaxDataBlobSize == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
total := 0
|
||||
for _, dataBlob := range dataBlobs {
|
||||
total += len(dataBlob.GetKey().String()) + len(dataBlob.Contents)
|
||||
}
|
||||
return uint32(total) <= l.MaxDataBlobSize
|
||||
}
|
||||
|
||||
func (l LimitConfig) CanAddDataBlob(dataBlobs []*livekit.DataBlob, toAdd *livekit.DataBlob) bool {
|
||||
if l.MaxDataBlobSize == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
total := 0
|
||||
for _, dataBlob := range dataBlobs {
|
||||
total += len(dataBlob.Key.String()) + len(dataBlob.Contents)
|
||||
}
|
||||
return uint32(total+len(toAdd.GetKey().String())+len(toAdd.Contents)) <= l.MaxDataBlobSize
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
type IngressConfig struct {
|
||||
RTMPBaseURL string `yaml:"rtmp_base_url,omitempty"`
|
||||
WHIPBaseURL string `yaml:"whip_base_url,omitempty"`
|
||||
@@ -443,6 +480,8 @@ var DefaultConfig = Config{
|
||||
MaxRoomNameLength: 256,
|
||||
MaxParticipantIdentityLength: 256,
|
||||
MaxParticipantNameLength: 256,
|
||||
MaxDataBlobKeyLength: 256,
|
||||
MaxDataBlobSize: 64000,
|
||||
},
|
||||
Logging: LoggingConfig{
|
||||
PionLevel: "error",
|
||||
|
||||
+19
-10
@@ -225,8 +225,10 @@ type ParticipantParams struct {
|
||||
EnableRTPStreamRestartDetection bool
|
||||
ForceBackupCodecPolicySimulcast bool
|
||||
DisableTransceiverReuseForE2EE bool
|
||||
EnableParticipantDataBlob bool
|
||||
EnableStartAtDesiredQuality bool
|
||||
VideoFrameCachingDuration time.Duration
|
||||
MigrationWaitDuration time.Duration
|
||||
VideoFrameCachingDuration time.Duration
|
||||
}
|
||||
|
||||
type ParticipantImpl struct {
|
||||
@@ -335,6 +337,8 @@ type ParticipantImpl struct {
|
||||
rpcLock sync.Mutex
|
||||
rpcPendingAcks map[string]*utils.DataChannelRpcPendingAckHandler
|
||||
rpcPendingResponses map[string]*utils.DataChannelRpcPendingResponseHandler
|
||||
|
||||
dataBlob *ParticipantDataBlob
|
||||
}
|
||||
|
||||
func NewParticipant(params ParticipantParams) (*ParticipantImpl, error) {
|
||||
@@ -373,6 +377,9 @@ func NewParticipant(params ParticipantParams) (*ParticipantImpl, error) {
|
||||
telemetryGuard: &telemetry.ReferenceGuard{},
|
||||
nextSubscribedDataTrackHandle: uint16(rand.Intn(256)),
|
||||
requireBroadcast: params.Grants.Metadata != "" || len(params.Grants.Attributes) != 0,
|
||||
dataBlob: NewParticipantDataBlob(ParticipantDataBlobParams{
|
||||
Logger: params.Logger,
|
||||
}),
|
||||
}
|
||||
p.setupSignalling()
|
||||
|
||||
@@ -912,6 +919,7 @@ func (p *ParticipantImpl) ToProtoWithVersion() (*livekit.ParticipantInfo, utils.
|
||||
KindDetails: grants.GetKindDetails(),
|
||||
DisconnectReason: p.CloseReason().ToDisconnectReason(),
|
||||
ClientProtocol: clientProtocol,
|
||||
Capabilities: p.params.ClientInfo.GetCapabilities(),
|
||||
}
|
||||
p.lock.RUnlock()
|
||||
|
||||
@@ -1340,9 +1348,7 @@ func (p *ParticipantImpl) AddTrack(req *livekit.AddTrackRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
p.pendingTracksLock.Lock()
|
||||
ti := p.addPendingTrackLocked(req)
|
||||
p.pendingTracksLock.Unlock()
|
||||
ti := p.addPendingTrack(req)
|
||||
if ti == nil {
|
||||
return
|
||||
}
|
||||
@@ -1568,7 +1574,7 @@ func (p *ParticipantImpl) setupMigrationTimerLocked() {
|
||||
// to try and succeed. If not, close the subscriber peer connection
|
||||
// and help the remote side to narrow down its ICE candidate pool.
|
||||
//
|
||||
p.migrationTimer = time.AfterFunc(migrationWaitDuration, func() {
|
||||
p.migrationTimer = time.AfterFunc(max(p.params.MigrationWaitDuration, migrationWaitDuration), func() {
|
||||
p.clearMigrationTimer()
|
||||
|
||||
if p.IsClosed() || p.IsDisconnected() {
|
||||
@@ -2852,7 +2858,10 @@ func (p *ParticipantImpl) onSubscribedAudioCodecChange(
|
||||
return p.sendSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) addPendingTrackLocked(req *livekit.AddTrackRequest) *livekit.TrackInfo {
|
||||
func (p *ParticipantImpl) addPendingTrack(req *livekit.AddTrackRequest) *livekit.TrackInfo {
|
||||
p.pendingTracksLock.Lock()
|
||||
defer p.pendingTracksLock.Unlock()
|
||||
|
||||
if req.Sid != "" {
|
||||
track := p.GetPublishedTrack(livekit.TrackID(req.Sid))
|
||||
if track == nil {
|
||||
@@ -3249,11 +3258,11 @@ func (p *ParticipantImpl) mediaTrackReceived(
|
||||
mt = p.addMediaTrack(signalCid, ti)
|
||||
newTrack = true
|
||||
|
||||
// if the addTrackRequest is sent before participant active then it means the client tries to publish
|
||||
// before fully connected, in this case we only record the time when the participant is active since
|
||||
// if the addTrackRequest is sent before publisher peer connection is established, then it means the client tries to publish
|
||||
// before fully connected, in this case we only record the time when publisher peer connection is established since
|
||||
// we want this metric to represent the time cost by publishing.
|
||||
if activeAt := p.lastActiveAt.Load(); activeAt != nil && createdAt.Before(*activeAt) {
|
||||
createdAt = *activeAt
|
||||
if connectedAt := p.TransportManager.PublisherFirstConnectedAt(); !connectedAt.IsZero() && createdAt.Before(connectedAt) {
|
||||
createdAt = connectedAt
|
||||
}
|
||||
pubTime = time.Since(createdAt)
|
||||
p.dirty.Store(true)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2026 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
type ParticipantDataBlobParams struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type ParticipantDataBlob struct {
|
||||
params ParticipantDataBlobParams
|
||||
lock sync.Mutex
|
||||
blobs map[string]*livekit.DataBlob
|
||||
}
|
||||
|
||||
func NewParticipantDataBlob(params ParticipantDataBlobParams) *ParticipantDataBlob {
|
||||
return &ParticipantDataBlob{
|
||||
params: params,
|
||||
blobs: make(map[string]*livekit.DataBlob),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParticipantDataBlob) Add(db *livekit.DataBlob) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if db.Key == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.blobs[db.Key.String()] = db
|
||||
}
|
||||
|
||||
func (p *ParticipantDataBlob) Delete(dbKey *livekit.DataBlobKey) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if dbKey == nil {
|
||||
return
|
||||
}
|
||||
|
||||
delete(p.blobs, dbKey.String())
|
||||
}
|
||||
|
||||
func (p *ParticipantDataBlob) Get(dbKey *livekit.DataBlobKey) *livekit.DataBlob {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if dbKey == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
db, ok := p.blobs[dbKey.String()]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (p *ParticipantDataBlob) GetAll() []*livekit.DataBlob {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
all := make([]*livekit.DataBlob, 0, len(p.blobs))
|
||||
for _, db := range p.blobs {
|
||||
all = append(all, db)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2026 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/types"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
func (p *ParticipantImpl) HandleStoreDataBlobRequest(req *livekit.StoreDataBlobRequest) {
|
||||
if !p.params.EnableParticipantDataBlob {
|
||||
p.pubLogger.Warnw("data blob not enabled", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
RequestId: req.RequestId,
|
||||
Reason: livekit.RequestResponse_NOT_ALLOWED,
|
||||
Message: "data blob not enabled",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Blob == nil || req.Blob.Key == nil || len(req.Blob.Key.String()) == 0 || !p.params.LimitConfig.CheckDataBlobKeyLength(req.Blob.Key.String()) {
|
||||
p.pubLogger.Warnw("data blob is invalid", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
RequestId: req.RequestId,
|
||||
Reason: livekit.RequestResponse_INVALID_REQUEST,
|
||||
Message: "data blob is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Blob.Contents) == 0 {
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
RequestId: req.RequestId,
|
||||
Reason: livekit.RequestResponse_INVALID_REQUEST,
|
||||
Message: "data blob is empty",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !p.params.LimitConfig.CanAddDataBlob(p.dataBlob.GetAll(), req.Blob) {
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
RequestId: req.RequestId,
|
||||
Reason: livekit.RequestResponse_LIMIT_EXCEEDED,
|
||||
Message: "async attribute definition exceeds limit",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.AddDataBlob(req.Blob)
|
||||
p.listener().OnStoreDataBlob(p, req.Blob)
|
||||
p.sendStoreDataBlobResponse(req.RequestId, req.Blob.Key)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) HandleGetDataBlobRequest(req *livekit.GetDataBlobRequest) {
|
||||
if req.Key == nil {
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
RequestId: req.RequestId,
|
||||
Reason: livekit.RequestResponse_INVALID_REQUEST,
|
||||
Message: "data blob key is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.listener().OnGetDataBlob(p, req)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) AddDataBlob(dataBlob *livekit.DataBlob) {
|
||||
p.dataBlob.Add(dataBlob)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) GetDataBlob(key *livekit.DataBlobKey) *livekit.DataBlob {
|
||||
return p.dataBlob.Get(key)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) ProcessGetDataBlobRequest(req *livekit.GetDataBlobRequest, publisher types.Participant) {
|
||||
if publisher == nil {
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
RequestId: req.RequestId,
|
||||
Reason: livekit.RequestResponse_NOT_FOUND,
|
||||
Message: "participant not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dataBlob := publisher.GetDataBlob(req.Key)
|
||||
if dataBlob == nil {
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
RequestId: req.RequestId,
|
||||
Reason: livekit.RequestResponse_NOT_FOUND,
|
||||
Message: "data blob not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.sendGetDataBlobResponse(req.RequestId, dataBlob)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) GetAllDataBlob() []*livekit.DataBlob {
|
||||
return p.dataBlob.GetAll()
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright 2026 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 (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/routing/routingfakes"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
|
||||
)
|
||||
|
||||
func newParticipantWithDataBlob(t *testing.T, enabled bool, maxKeyLength int, maxSize uint32) *ParticipantImpl {
|
||||
t.Helper()
|
||||
p := newParticipantForTest("test")
|
||||
p.params.EnableParticipantDataBlob = enabled
|
||||
p.params.LimitConfig = config.LimitConfig{
|
||||
MaxDataBlobKeyLength: maxKeyLength,
|
||||
MaxDataBlobSize: maxSize,
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func lastRequestResponse(t *testing.T, sink *routingfakes.FakeMessageSink, idx int) *livekit.RequestResponse {
|
||||
t.Helper()
|
||||
msg := sink.WriteMessageArgsForCall(idx).(*livekit.SignalResponse)
|
||||
rr, ok := msg.Message.(*livekit.SignalResponse_RequestResponse)
|
||||
require.True(t, ok, "expected SignalResponse_RequestResponse, got %T", msg.Message)
|
||||
return rr.RequestResponse
|
||||
}
|
||||
|
||||
func TestHandleStoreDataBlobRequest(t *testing.T) {
|
||||
t.Run("returns NOT_ALLOWED when feature not enabled", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, false, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
req := &livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: genericKey("blob-1"),
|
||||
Contents: []byte("def"),
|
||||
},
|
||||
}
|
||||
p.HandleStoreDataBlobRequest(req)
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_NOT_ALLOWED, rr.Reason)
|
||||
require.Empty(t, p.dataBlob.GetAll())
|
||||
})
|
||||
|
||||
t.Run("returns INVALID_REQUEST when blob is nil", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
|
||||
require.Empty(t, p.dataBlob.GetAll())
|
||||
})
|
||||
|
||||
t.Run("returns INVALID_REQUEST when key is nil", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Contents: []byte("def"),
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
|
||||
})
|
||||
|
||||
t.Run("returns INVALID_REQUEST when key has no oneof set", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: &livekit.DataBlobKey{},
|
||||
Contents: []byte("def"),
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
|
||||
})
|
||||
|
||||
t.Run("returns INVALID_REQUEST when key exceeds length limit", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 5, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: genericKey(strings.Repeat("a", 64)),
|
||||
Contents: []byte("def"),
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
|
||||
})
|
||||
|
||||
t.Run("returns INVALID_REQUEST when contents is empty", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: genericKey("blob-1"),
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
|
||||
require.Empty(t, p.dataBlob.GetAll())
|
||||
})
|
||||
|
||||
t.Run("returns LIMIT_EXCEEDED when adding would breach the limit", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 16)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: genericKey("blob-1"),
|
||||
Contents: []byte(strings.Repeat("x", 32)),
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_LIMIT_EXCEEDED, rr.Reason)
|
||||
require.Empty(t, p.dataBlob.GetAll())
|
||||
})
|
||||
|
||||
t.Run("stores a valid blob, notifies listener, and sends response", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
listener := p.params.ParticipantListener.(*typesfakes.FakeLocalParticipantListener)
|
||||
|
||||
key := genericKey("blob-1")
|
||||
contents := []byte("definition-bytes")
|
||||
blob := &livekit.DataBlob{Key: key, Contents: contents}
|
||||
|
||||
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
|
||||
RequestId: 42,
|
||||
Blob: blob,
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
msg := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
|
||||
response, ok := msg.Message.(*livekit.SignalResponse_StoreDataBlobResponse)
|
||||
require.True(t, ok, "expected SignalResponse_StoreDataBlobResponse, got %T", msg.Message)
|
||||
require.Equal(t, uint32(42), response.StoreDataBlobResponse.RequestId)
|
||||
require.Equal(t, key, response.StoreDataBlobResponse.Key)
|
||||
|
||||
stored := p.dataBlob.Get(key)
|
||||
require.NotNil(t, stored)
|
||||
require.Equal(t, contents, stored.Contents)
|
||||
|
||||
require.Equal(t, 1, listener.OnStoreDataBlobCallCount())
|
||||
gotParticipant, gotBlob := listener.OnStoreDataBlobArgsForCall(0)
|
||||
require.Equal(t, p, gotParticipant)
|
||||
require.Equal(t, blob, gotBlob)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleGetDataBlobRequest(t *testing.T) {
|
||||
t.Run("returns INVALID_REQUEST when key is missing", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.HandleGetDataBlobRequest(&livekit.GetDataBlobRequest{
|
||||
ParticipantIdentity: "other",
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
|
||||
})
|
||||
|
||||
t.Run("forwards request to listener when key is provided", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
listener := p.params.ParticipantListener.(*typesfakes.FakeLocalParticipantListener)
|
||||
|
||||
req := &livekit.GetDataBlobRequest{
|
||||
ParticipantIdentity: "other",
|
||||
Key: genericKey("blob-1"),
|
||||
}
|
||||
p.HandleGetDataBlobRequest(req)
|
||||
|
||||
require.Equal(t, 1, listener.OnGetDataBlobCallCount())
|
||||
gotParticipant, gotReq := listener.OnGetDataBlobArgsForCall(0)
|
||||
require.Equal(t, p, gotParticipant)
|
||||
require.Equal(t, req, gotReq)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDataBlob(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
|
||||
key := genericKey("blob-1")
|
||||
require.Nil(t, p.GetDataBlob(key))
|
||||
|
||||
blob := &livekit.DataBlob{
|
||||
Key: key,
|
||||
Contents: []byte("definition"),
|
||||
}
|
||||
p.dataBlob.Add(blob)
|
||||
got := p.GetDataBlob(key)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, key.String(), got.Key.String())
|
||||
require.Equal(t, []byte("definition"), got.Contents)
|
||||
}
|
||||
|
||||
func TestProcessGetDataBlobRequest(t *testing.T) {
|
||||
t.Run("returns NOT_FOUND when publisher is nil", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
p.ProcessGetDataBlobRequest(&livekit.GetDataBlobRequest{
|
||||
Key: genericKey("blob-1"),
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_NOT_FOUND, rr.Reason)
|
||||
require.Contains(t, rr.Message, "participant")
|
||||
})
|
||||
|
||||
t.Run("returns NOT_FOUND when publisher has no matching blob", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
publisher := &typesfakes.FakeParticipant{}
|
||||
publisher.GetDataBlobReturns(nil)
|
||||
|
||||
req := &livekit.GetDataBlobRequest{
|
||||
Key: genericKey("blob-1"),
|
||||
}
|
||||
p.ProcessGetDataBlobRequest(req, publisher)
|
||||
|
||||
require.Equal(t, 1, publisher.GetDataBlobCallCount())
|
||||
require.Equal(t, req.Key, publisher.GetDataBlobArgsForCall(0))
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
rr := lastRequestResponse(t, sink, 0)
|
||||
require.Equal(t, livekit.RequestResponse_NOT_FOUND, rr.Reason)
|
||||
})
|
||||
|
||||
t.Run("sends blob response when publisher has a matching blob", func(t *testing.T) {
|
||||
p := newParticipantWithDataBlob(t, true, 0, 0)
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
key := genericKey("blob-1")
|
||||
blob := &livekit.DataBlob{
|
||||
Key: key,
|
||||
Contents: []byte("definition-bytes"),
|
||||
}
|
||||
|
||||
publisher := &typesfakes.FakeParticipant{}
|
||||
publisher.GetDataBlobReturns(blob)
|
||||
|
||||
p.ProcessGetDataBlobRequest(&livekit.GetDataBlobRequest{
|
||||
RequestId: 42,
|
||||
Key: key,
|
||||
}, publisher)
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
msg := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
|
||||
response, ok := msg.Message.(*livekit.SignalResponse_GetDataBlobResponse)
|
||||
require.True(t, ok, "expected SignalResponse_GetDataBlobResponse, got %T", msg.Message)
|
||||
require.Equal(t, uint32(42), response.GetDataBlobResponse.RequestId)
|
||||
require.Equal(t, blob, response.GetDataBlobResponse.Blob)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2026 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"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
func newTestDataBlob() *ParticipantDataBlob {
|
||||
return NewParticipantDataBlob(ParticipantDataBlobParams{
|
||||
Logger: logger.GetLogger(),
|
||||
})
|
||||
}
|
||||
|
||||
func genericKey(name string) *livekit.DataBlobKey {
|
||||
return &livekit.DataBlobKey{
|
||||
Key: &livekit.DataBlobKey_Generic{
|
||||
Generic: name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_AddAndGet(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
key := genericKey("blob-1")
|
||||
contents := []byte("definition-bytes")
|
||||
|
||||
a.Add(&livekit.DataBlob{Key: key, Contents: contents})
|
||||
|
||||
got := a.Get(key)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, key.String(), got.Key.String())
|
||||
require.Equal(t, contents, got.Contents)
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_AddOverwrites(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
key := genericKey("blob-1")
|
||||
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("v1")})
|
||||
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("v2")})
|
||||
|
||||
got := a.Get(key)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, []byte("v2"), got.Contents)
|
||||
|
||||
require.Len(t, a.GetAll(), 1)
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_DistinctKeys(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
key1 := genericKey("blob-1")
|
||||
key2 := genericKey("blob-2")
|
||||
|
||||
a.Add(&livekit.DataBlob{Key: key1, Contents: []byte("c1")})
|
||||
a.Add(&livekit.DataBlob{Key: key2, Contents: []byte("c2")})
|
||||
|
||||
got1 := a.Get(key1)
|
||||
require.NotNil(t, got1)
|
||||
require.Equal(t, []byte("c1"), got1.Contents)
|
||||
|
||||
got2 := a.Get(key2)
|
||||
require.NotNil(t, got2)
|
||||
require.Equal(t, []byte("c2"), got2.Contents)
|
||||
|
||||
require.Len(t, a.GetAll(), 2)
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_Delete(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
key := genericKey("blob-1")
|
||||
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("definition")})
|
||||
|
||||
a.Delete(key)
|
||||
require.Nil(t, a.Get(key))
|
||||
require.Empty(t, a.GetAll())
|
||||
|
||||
// deleting a non-existent key is a no-op
|
||||
a.Delete(key)
|
||||
require.Empty(t, a.GetAll())
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_NilKey(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
// nil key should be silently ignored, not panic
|
||||
a.Add(&livekit.DataBlob{Contents: []byte("definition")})
|
||||
require.Empty(t, a.GetAll())
|
||||
|
||||
require.Nil(t, a.Get(nil))
|
||||
|
||||
a.Delete(nil)
|
||||
require.Empty(t, a.GetAll())
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_GetMissing(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
require.Nil(t, a.Get(genericKey("missing")))
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_GetAllContents(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
key1 := genericKey("blob-1")
|
||||
key2 := genericKey("blob-2")
|
||||
|
||||
a.Add(&livekit.DataBlob{Key: key1, Contents: []byte("def-1")})
|
||||
a.Add(&livekit.DataBlob{Key: key2, Contents: []byte("def-2")})
|
||||
|
||||
all := a.GetAll()
|
||||
require.Len(t, all, 2)
|
||||
for _, db := range all {
|
||||
switch key := db.Key.Key.(type) {
|
||||
case *livekit.DataBlobKey_Generic:
|
||||
switch key.Generic {
|
||||
case "blob-1":
|
||||
require.Equal(t, []byte("def-1"), db.Contents)
|
||||
case "blob-2":
|
||||
require.Equal(t, []byte("def-2"), db.Contents)
|
||||
default:
|
||||
require.Fail(t, "unexpected key", key.Generic)
|
||||
}
|
||||
default:
|
||||
require.Fail(t, "unexpected key type", "Generic")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParticipantDataBlob_ConcurrentAccess(t *testing.T) {
|
||||
a := newTestDataBlob()
|
||||
|
||||
const numGoroutines = 16
|
||||
const opsPerGoroutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
for g := 0; g < numGoroutines; g++ {
|
||||
go func(g int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < opsPerGoroutine; i++ {
|
||||
key := genericKey(fmt.Sprintf("blob-%d", g%8))
|
||||
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("v")})
|
||||
_ = a.Get(key)
|
||||
_ = a.GetAll()
|
||||
if i%3 == 0 {
|
||||
a.Delete(key)
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -368,3 +368,17 @@ func (p *ParticipantImpl) SendDataTrackSubscriberHandles(handles map[uint32]*liv
|
||||
SubHandles: handles,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendStoreDataBlobResponse(requestId uint32, key *livekit.DataBlobKey) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalStoreDataBlobResponse(&livekit.StoreDataBlobResponse{
|
||||
RequestId: requestId,
|
||||
Key: key,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendGetDataBlobResponse(requestId uint32, dataBlob *livekit.DataBlob) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalGetDataBlobResponse(&livekit.GetDataBlobResponse{
|
||||
RequestId: requestId,
|
||||
Blob: dataBlob,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1384,6 +1384,11 @@ func (r *Room) onUpdateDataSubscriptions(participant types.LocalParticipant, req
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) onGetDataBlob(participant types.LocalParticipant, req *livekit.GetDataBlobRequest) {
|
||||
publisher := r.GetParticipant(livekit.ParticipantIdentity(req.ParticipantIdentity))
|
||||
participant.ProcessGetDataBlobRequest(req, publisher)
|
||||
}
|
||||
|
||||
func (r *Room) onLeave(p types.LocalParticipant, reason types.ParticipantCloseReason) {
|
||||
r.RemoveParticipant(p.Identity(), p.ID(), reason)
|
||||
}
|
||||
@@ -1996,6 +2001,13 @@ func (l *localParticipantListener) OnUpdateDataSubscriptions(p types.LocalPartic
|
||||
l.room.onUpdateDataSubscriptions(p, req)
|
||||
}
|
||||
|
||||
func (l *localParticipantListener) OnStoreDataBlob(_p types.LocalParticipant, _dataBlob *livekit.DataBlob) {
|
||||
}
|
||||
|
||||
func (l *localParticipantListener) OnGetDataBlob(p types.LocalParticipant, req *livekit.GetDataBlobRequest) {
|
||||
l.room.onGetDataBlob(p, req)
|
||||
}
|
||||
|
||||
func (l *localParticipantListener) OnSyncState(p types.LocalParticipant, state *livekit.SyncState) error {
|
||||
return l.room.onSyncState(p, state)
|
||||
}
|
||||
|
||||
@@ -62,4 +62,6 @@ type ParticipantSignalling interface {
|
||||
SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message
|
||||
SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message
|
||||
SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message
|
||||
SignalStoreDataBlobResponse(storeDataBlobResponse *livekit.StoreDataBlobResponse) proto.Message
|
||||
SignalGetDataBlobResponse(getDataBlobResponse *livekit.GetDataBlobResponse) proto.Message
|
||||
}
|
||||
|
||||
@@ -151,6 +151,12 @@ func (s *signalhandler) HandleMessage(msg proto.Message) error {
|
||||
|
||||
case *livekit.SignalRequest_UpdateDataSubscription:
|
||||
s.params.Participant.HandleUpdateDataSubscription(msg.UpdateDataSubscription)
|
||||
|
||||
case *livekit.SignalRequest_StoreDataBlobRequest:
|
||||
s.params.Participant.HandleStoreDataBlobRequest(msg.StoreDataBlobRequest)
|
||||
|
||||
case *livekit.SignalRequest_GetDataBlobRequest:
|
||||
s.params.Participant.HandleGetDataBlobRequest(msg.GetDataBlobRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -258,3 +258,19 @@ func (s *signalling) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalStoreDataBlobResponse(storeDataBlobResponse *livekit.StoreDataBlobResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_StoreDataBlobResponse{
|
||||
StoreDataBlobResponse: storeDataBlobResponse,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalGetDataBlobResponse(getDataBlobResponse *livekit.GetDataBlobResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_GetDataBlobResponse{
|
||||
GetDataBlobResponse: getDataBlobResponse,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,3 +127,11 @@ func (u *signallingUnimplemented) SignalUnpublishDataTrackResponse(unpublishData
|
||||
func (u *signallingUnimplemented) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalStoreDataBlobResponse(storeDataBlobResponse *livekit.StoreDataBlobResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalGetDataBlobResponse(getDataBlobResponse *livekit.GetDataBlobResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
+89
-14
@@ -1007,12 +1007,12 @@ func (t *PCTransport) queueOrConfigureSender(
|
||||
enableAudioNACK bool,
|
||||
) {
|
||||
params := configureSenderParams{
|
||||
transceiver,
|
||||
enabledCodecs,
|
||||
rtcpFeedbackConfig,
|
||||
!t.params.IsOfferer,
|
||||
enableAudioStereo,
|
||||
enableAudioNACK,
|
||||
transceiver: transceiver,
|
||||
enabledCodecs: enabledCodecs,
|
||||
rtcpFeedbackConfig: rtcpFeedbackConfig,
|
||||
filterOutH264HighProfile: !t.params.IsOfferer,
|
||||
enableAudioStereo: enableAudioStereo,
|
||||
enableAudioNACK: enableAudioNACK,
|
||||
}
|
||||
if !t.params.IsOfferer {
|
||||
t.sendersPendingConfigMu.Lock()
|
||||
@@ -1021,10 +1021,17 @@ func (t *PCTransport) queueOrConfigureSender(
|
||||
return
|
||||
}
|
||||
|
||||
configureSender(params)
|
||||
// Offerer: no remote offer to echo payload types from.
|
||||
configureSender(params, nil)
|
||||
}
|
||||
|
||||
func (t *PCTransport) processSendersPendingConfig() {
|
||||
// processSendersPendingConfig configures the senders queued while answering the
|
||||
// remote's offer (single peer connection mode). offerAudioPT (mime type -> the
|
||||
// payload type the offer assigned) is parsed from the offer by the caller before
|
||||
// SetRemoteDescription, so the answer echoes the offered payload types for audio
|
||||
// codecs (and stays consistent with the forwarded RTP). It is nil when there is
|
||||
// nothing to echo.
|
||||
func (t *PCTransport) processSendersPendingConfig(offerAudioPT map[mime.MimeType]webrtc.PayloadType) {
|
||||
t.sendersPendingConfigMu.Lock()
|
||||
pending := t.sendersPendingConfig
|
||||
t.sendersPendingConfig = nil
|
||||
@@ -1037,7 +1044,7 @@ func (t *PCTransport) processSendersPendingConfig() {
|
||||
continue
|
||||
}
|
||||
|
||||
configureSender(p)
|
||||
configureSender(p, offerAudioPT)
|
||||
}
|
||||
|
||||
if len(unprocessed) != 0 {
|
||||
@@ -1432,6 +1439,13 @@ func (t *PCTransport) HasEverConnected() bool {
|
||||
return !t.firstConnectedAt.IsZero()
|
||||
}
|
||||
|
||||
func (t *PCTransport) FirstConnectedAt() time.Time {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
return t.firstConnectedAt
|
||||
}
|
||||
|
||||
func (t *PCTransport) GetICEConnectionInfo() *types.ICEConnectionInfo {
|
||||
return t.connectionDetails.GetInfo()
|
||||
}
|
||||
@@ -2340,11 +2354,19 @@ func (t *PCTransport) handleICEGatheringCompleteAnswerer() error {
|
||||
t.pendingRestartIceOffer = nil
|
||||
|
||||
t.params.Logger.Debugw("accept remote restart ice offer after ICE gathering")
|
||||
|
||||
// Parse the offer payload types before SetRemoteDescription so this does not
|
||||
// race with pion's use of the same description.
|
||||
var offerAudioPT map[mime.MimeType]webrtc.PayloadType
|
||||
if parsed, err := offer.Unmarshal(); err == nil {
|
||||
offerAudioPT = offerAudioPayloadTypes(parsed)
|
||||
}
|
||||
|
||||
if err := t.setRemoteDescription(offer); err != nil {
|
||||
return err
|
||||
}
|
||||
t.params.Handler.OnSetRemoteDescriptionOffer()
|
||||
t.processSendersPendingConfig()
|
||||
t.processSendersPendingConfig(offerAudioPT)
|
||||
|
||||
return t.createAndSendAnswer()
|
||||
}
|
||||
@@ -2889,7 +2911,7 @@ func (t *PCTransport) handleRemoteOfferReceived(sd *webrtc.SessionDescription, o
|
||||
}
|
||||
|
||||
t.params.Handler.OnSetRemoteDescriptionOffer()
|
||||
t.processSendersPendingConfig()
|
||||
t.processSendersPendingConfig(offerAudioPayloadTypes(parsed))
|
||||
|
||||
rtxRepairs := nonSimulcastRTXRepairsFromSDP(parsed, t.params.Logger)
|
||||
if len(rtxRepairs) > 0 {
|
||||
@@ -3074,7 +3096,7 @@ type configureSenderParams struct {
|
||||
enableAudioNACK bool
|
||||
}
|
||||
|
||||
func configureSender(params configureSenderParams) {
|
||||
func configureSender(params configureSenderParams, offerAudioPT map[mime.MimeType]webrtc.PayloadType) {
|
||||
configureSenderCodecs(
|
||||
params.transceiver,
|
||||
params.enabledCodecs,
|
||||
@@ -3083,14 +3105,14 @@ func configureSender(params configureSenderParams) {
|
||||
)
|
||||
|
||||
if params.transceiver.Kind() == webrtc.RTPCodecTypeAudio {
|
||||
configureSenderAudio(params.transceiver, params.enableAudioStereo, params.enableAudioNACK)
|
||||
configureSenderAudio(params.transceiver, params.enableAudioStereo, params.enableAudioNACK, offerAudioPT)
|
||||
}
|
||||
}
|
||||
|
||||
// configure subscriber transceiver for audio stereo and nack
|
||||
// pion doesn't support per transciver codec configuration, so the nack of this session will be disabled
|
||||
// forever once it is first disabled by a transceiver.
|
||||
func configureSenderAudio(tr *webrtc.RTPTransceiver, stereo bool, nack bool) {
|
||||
func configureSenderAudio(tr *webrtc.RTPTransceiver, stereo bool, nack bool, offerAudioPT map[mime.MimeType]webrtc.PayloadType) {
|
||||
sender := tr.Sender()
|
||||
if sender == nil {
|
||||
return
|
||||
@@ -3114,12 +3136,65 @@ func configureSenderAudio(tr *webrtc.RTPTransceiver, stereo bool, nack bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// When answering a subscriber's offer (single peer connection mode), echo
|
||||
// the payload type the offer assigned for this codec instead of the server's
|
||||
// MediaEngine payload type. Otherwise the answer can advertise e.g. Opus on a
|
||||
// PT that was never offered, which Firefox rejects (received packets decode to
|
||||
// 0 samples / silence). The forwarded RTP already uses the offered PT.
|
||||
if len(offerAudioPT) > 0 {
|
||||
if pt, ok := offerAudioPT[mime.NormalizeMimeType(c.MimeType)]; ok {
|
||||
c.PayloadType = pt
|
||||
}
|
||||
}
|
||||
configCodecs = append(configCodecs, c)
|
||||
}
|
||||
|
||||
tr.SetCodecPreferences(configCodecs)
|
||||
}
|
||||
|
||||
// offerAudioPayloadTypes returns mime type -> payload type for the audio codecs
|
||||
// in a remote offer, so the subscriber answer can echo the offered payload types
|
||||
// (RFC 3264 6.1). The caller parses the offer before SetRemoteDescription, so this
|
||||
// does not race with pion's use of the same description.
|
||||
func offerAudioPayloadTypes(parsed *sdp.SessionDescription) map[mime.MimeType]webrtc.PayloadType {
|
||||
if parsed == nil {
|
||||
return nil
|
||||
}
|
||||
out := map[mime.MimeType]webrtc.PayloadType{}
|
||||
for _, md := range parsed.MediaDescriptions {
|
||||
if !strings.EqualFold(md.MediaName.Media, "audio") {
|
||||
continue
|
||||
}
|
||||
for _, a := range md.Attributes {
|
||||
if a.Key != "rtpmap" {
|
||||
continue
|
||||
}
|
||||
// value e.g. "109 opus/48000/2"
|
||||
fields := strings.Fields(a.Value)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
pt, err := strconv.Atoi(fields[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
codecName := fields[1]
|
||||
if i := strings.Index(codecName, "/"); i >= 0 {
|
||||
codecName = codecName[:i]
|
||||
}
|
||||
mt := mime.NormalizeMimeTypeCodec(codecName).ToMimeType()
|
||||
if mt == mime.MimeTypeUnknown {
|
||||
continue
|
||||
}
|
||||
out[mt] = webrtc.PayloadType(pt)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// In single peer connection mode, set up enebled codecs for sender.
|
||||
// The config provides config of direction.
|
||||
// For publisher peer connection those are publish enabled codecs
|
||||
|
||||
@@ -617,7 +617,7 @@ func TestConfigureAudioTransceiver(t *testing.T) {
|
||||
tr, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
|
||||
require.NoError(t, err)
|
||||
|
||||
configureSenderAudio(tr, testcase.stereo, testcase.nack)
|
||||
configureSenderAudio(tr, testcase.stereo, testcase.nack, nil)
|
||||
codecs := tr.Sender().GetParameters().Codecs
|
||||
for _, codec := range codecs {
|
||||
if mime.IsMimeTypeStringOpus(codec.MimeType) {
|
||||
@@ -636,6 +636,32 @@ func TestConfigureAudioTransceiver(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// When answering a subscriber offer, the sender's audio payload type must echo
|
||||
// the payload type the offer assigned (RFC 3264), otherwise Firefox decodes no
|
||||
// audio. See https://github.com/livekit/livekit/issues/4599.
|
||||
func TestConfigureAudioTransceiverEchoesOfferPayloadType(t *testing.T) {
|
||||
var me webrtc.MediaEngine
|
||||
registerCodecs(&me, []*livekit.Codec{{Mime: mime.MimeTypeOpus.String()}}, RTCPFeedbackConfig{Audio: []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}}, false)
|
||||
pc, err := webrtc.NewAPI(webrtc.WithMediaEngine(&me)).NewPeerConnection(webrtc.Configuration{})
|
||||
require.NoError(t, err)
|
||||
defer pc.Close()
|
||||
tr, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
|
||||
require.NoError(t, err)
|
||||
|
||||
// offer mapped Opus to a payload type different from the server MediaEngine's.
|
||||
const offeredOpusPT = webrtc.PayloadType(109)
|
||||
configureSenderAudio(tr, false, true, map[mime.MimeType]webrtc.PayloadType{mime.MimeTypeOpus: offeredOpusPT})
|
||||
|
||||
var found bool
|
||||
for _, codec := range tr.Sender().GetParameters().Codecs {
|
||||
if mime.IsMimeTypeStringOpus(codec.MimeType) {
|
||||
require.Equal(t, offeredOpusPT, codec.PayloadType)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "opus codec must be present in sender preferences")
|
||||
}
|
||||
|
||||
// In single-PC mode the publisher PC carries both publish and subscribe
|
||||
// directions. If the MediaEngine were built only from the publish codec list,
|
||||
// the SDP offer would not advertise some codecs in the m-section even though
|
||||
|
||||
@@ -227,6 +227,10 @@ func (t *TransportManager) HasPublisherEverConnected() bool {
|
||||
return t.publisher.HasEverConnected()
|
||||
}
|
||||
|
||||
func (t *TransportManager) PublisherFirstConnectedAt() time.Time {
|
||||
return t.publisher.FirstConnectedAt()
|
||||
}
|
||||
|
||||
func (t *TransportManager) IsPublisherEstablished() bool {
|
||||
return t.publisher.IsEstablished()
|
||||
}
|
||||
@@ -267,6 +271,14 @@ func (t *TransportManager) HasSubscriberEverConnected() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) SubscriberFirstConnectedAt() time.Time {
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.FirstConnectedAt()
|
||||
} else {
|
||||
return t.subscriber.FirstConnectedAt()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) AddTrackLocal(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
|
||||
@@ -355,6 +355,9 @@ type Participant interface {
|
||||
HandleReceivedDataTrackMessage([]byte, *datatrack.Packet, int64)
|
||||
|
||||
GetParticipantListener() ParticipantListener
|
||||
|
||||
AddDataBlob(dataBlob *livekit.DataBlob)
|
||||
GetDataBlob(key *livekit.DataBlobKey) *livekit.DataBlob
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
@@ -562,6 +565,9 @@ type LocalParticipant interface {
|
||||
HandlePublishDataTrackRequest(*livekit.PublishDataTrackRequest)
|
||||
HandleUnpublishDataTrackRequest(*livekit.UnpublishDataTrackRequest)
|
||||
HandleUpdateDataSubscription(*livekit.UpdateDataSubscription)
|
||||
HandleStoreDataBlobRequest(*livekit.StoreDataBlobRequest)
|
||||
HandleGetDataBlobRequest(*livekit.GetDataBlobRequest)
|
||||
ProcessGetDataBlobRequest(*livekit.GetDataBlobRequest, Participant)
|
||||
|
||||
HandleSignalMessage(msg proto.Message) error
|
||||
|
||||
@@ -572,6 +578,8 @@ type LocalParticipant interface {
|
||||
ClearParticipantListener()
|
||||
|
||||
GetNextSubscribedDataTrackHandle() uint16
|
||||
|
||||
GetAllDataBlob() []*livekit.DataBlob
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
@@ -621,6 +629,8 @@ type LocalParticipantListener interface {
|
||||
)
|
||||
OnUpdateSubscriptionPermission(LocalParticipant, *livekit.SubscriptionPermission) error
|
||||
OnUpdateDataSubscriptions(LocalParticipant, *livekit.UpdateDataSubscription)
|
||||
OnStoreDataBlob(LocalParticipant, *livekit.DataBlob)
|
||||
OnGetDataBlob(LocalParticipant, *livekit.GetDataBlobRequest)
|
||||
OnSyncState(LocalParticipant, *livekit.SyncState) error
|
||||
OnSimulateScenario(LocalParticipant, *livekit.SimulateScenario) error
|
||||
OnLeave(LocalParticipant, ParticipantCloseReason)
|
||||
@@ -652,6 +662,10 @@ func (*NullLocalParticipantListener) OnUpdateSubscriptionPermission(LocalPartici
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnUpdateDataSubscriptions(LocalParticipant, *livekit.UpdateDataSubscription) {
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnStoreDataBlob(LocalParticipant, *livekit.DataBlob) {
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnGetDataBlob(LocalParticipant, *livekit.GetDataBlobRequest) {
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnSyncState(LocalParticipant, *livekit.SyncState) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ type FakeLocalParticipant struct {
|
||||
activeAtReturnsOnCall map[int]struct {
|
||||
result1 time.Time
|
||||
}
|
||||
AddDataBlobStub func(*livekit.DataBlob)
|
||||
addDataBlobMutex sync.RWMutex
|
||||
addDataBlobArgsForCall []struct {
|
||||
arg1 *livekit.DataBlob
|
||||
}
|
||||
AddOnCloseStub func(string, func(types.LocalParticipant))
|
||||
addOnCloseMutex sync.RWMutex
|
||||
addOnCloseArgsForCall []struct {
|
||||
@@ -216,6 +221,16 @@ type FakeLocalParticipant struct {
|
||||
getAdaptiveStreamReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
GetAllDataBlobStub func() []*livekit.DataBlob
|
||||
getAllDataBlobMutex sync.RWMutex
|
||||
getAllDataBlobArgsForCall []struct {
|
||||
}
|
||||
getAllDataBlobReturns struct {
|
||||
result1 []*livekit.DataBlob
|
||||
}
|
||||
getAllDataBlobReturnsOnCall map[int]struct {
|
||||
result1 []*livekit.DataBlob
|
||||
}
|
||||
GetAnswerStub func() (webrtc.SessionDescription, uint32, error)
|
||||
getAnswerMutex sync.RWMutex
|
||||
getAnswerArgsForCall []struct {
|
||||
@@ -305,6 +320,17 @@ type FakeLocalParticipant struct {
|
||||
getCountryReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
}
|
||||
GetDataBlobStub func(*livekit.DataBlobKey) *livekit.DataBlob
|
||||
getDataBlobMutex sync.RWMutex
|
||||
getDataBlobArgsForCall []struct {
|
||||
arg1 *livekit.DataBlobKey
|
||||
}
|
||||
getDataBlobReturns struct {
|
||||
result1 *livekit.DataBlob
|
||||
}
|
||||
getDataBlobReturnsOnCall map[int]struct {
|
||||
result1 *livekit.DataBlob
|
||||
}
|
||||
GetDataTrackTransportStub func() types.DataTrackTransport
|
||||
getDataTrackTransportMutex sync.RWMutex
|
||||
getDataTrackTransportArgsForCall []struct {
|
||||
@@ -576,6 +602,11 @@ type FakeLocalParticipant struct {
|
||||
handleAnswerArgsForCall []struct {
|
||||
arg1 *livekit.SessionDescription
|
||||
}
|
||||
HandleGetDataBlobRequestStub func(*livekit.GetDataBlobRequest)
|
||||
handleGetDataBlobRequestMutex sync.RWMutex
|
||||
handleGetDataBlobRequestArgsForCall []struct {
|
||||
arg1 *livekit.GetDataBlobRequest
|
||||
}
|
||||
HandleICERestartSDPFragmentStub func(string) (string, error)
|
||||
handleICERestartSDPFragmentMutex sync.RWMutex
|
||||
handleICERestartSDPFragmentArgsForCall []struct {
|
||||
@@ -689,6 +720,11 @@ type FakeLocalParticipant struct {
|
||||
handleSimulateScenarioReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
HandleStoreDataBlobRequestStub func(*livekit.StoreDataBlobRequest)
|
||||
handleStoreDataBlobRequestMutex sync.RWMutex
|
||||
handleStoreDataBlobRequestArgsForCall []struct {
|
||||
arg1 *livekit.StoreDataBlobRequest
|
||||
}
|
||||
HandleSyncStateStub func(*livekit.SyncState) error
|
||||
handleSyncStateMutex sync.RWMutex
|
||||
handleSyncStateArgsForCall []struct {
|
||||
@@ -986,6 +1022,12 @@ type FakeLocalParticipant struct {
|
||||
arg2 chan string
|
||||
arg3 chan error
|
||||
}
|
||||
ProcessGetDataBlobRequestStub func(*livekit.GetDataBlobRequest, types.Participant)
|
||||
processGetDataBlobRequestMutex sync.RWMutex
|
||||
processGetDataBlobRequestArgsForCall []struct {
|
||||
arg1 *livekit.GetDataBlobRequest
|
||||
arg2 types.Participant
|
||||
}
|
||||
ProtocolVersionStub func() types.ProtocolVersion
|
||||
protocolVersionMutex sync.RWMutex
|
||||
protocolVersionArgsForCall []struct {
|
||||
@@ -1594,6 +1636,38 @@ func (fake *FakeLocalParticipant) ActiveAtReturnsOnCall(i int, result1 time.Time
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) AddDataBlob(arg1 *livekit.DataBlob) {
|
||||
fake.addDataBlobMutex.Lock()
|
||||
fake.addDataBlobArgsForCall = append(fake.addDataBlobArgsForCall, struct {
|
||||
arg1 *livekit.DataBlob
|
||||
}{arg1})
|
||||
stub := fake.AddDataBlobStub
|
||||
fake.recordInvocation("AddDataBlob", []interface{}{arg1})
|
||||
fake.addDataBlobMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.AddDataBlobStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) AddDataBlobCallCount() int {
|
||||
fake.addDataBlobMutex.RLock()
|
||||
defer fake.addDataBlobMutex.RUnlock()
|
||||
return len(fake.addDataBlobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) AddDataBlobCalls(stub func(*livekit.DataBlob)) {
|
||||
fake.addDataBlobMutex.Lock()
|
||||
defer fake.addDataBlobMutex.Unlock()
|
||||
fake.AddDataBlobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) AddDataBlobArgsForCall(i int) *livekit.DataBlob {
|
||||
fake.addDataBlobMutex.RLock()
|
||||
defer fake.addDataBlobMutex.RUnlock()
|
||||
argsForCall := fake.addDataBlobArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) AddOnClose(arg1 string, arg2 func(types.LocalParticipant)) {
|
||||
fake.addOnCloseMutex.Lock()
|
||||
fake.addOnCloseArgsForCall = append(fake.addOnCloseArgsForCall, struct {
|
||||
@@ -2539,6 +2613,59 @@ func (fake *FakeLocalParticipant) GetAdaptiveStreamReturnsOnCall(i int, result1
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetAllDataBlob() []*livekit.DataBlob {
|
||||
fake.getAllDataBlobMutex.Lock()
|
||||
ret, specificReturn := fake.getAllDataBlobReturnsOnCall[len(fake.getAllDataBlobArgsForCall)]
|
||||
fake.getAllDataBlobArgsForCall = append(fake.getAllDataBlobArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.GetAllDataBlobStub
|
||||
fakeReturns := fake.getAllDataBlobReturns
|
||||
fake.recordInvocation("GetAllDataBlob", []interface{}{})
|
||||
fake.getAllDataBlobMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetAllDataBlobCallCount() int {
|
||||
fake.getAllDataBlobMutex.RLock()
|
||||
defer fake.getAllDataBlobMutex.RUnlock()
|
||||
return len(fake.getAllDataBlobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetAllDataBlobCalls(stub func() []*livekit.DataBlob) {
|
||||
fake.getAllDataBlobMutex.Lock()
|
||||
defer fake.getAllDataBlobMutex.Unlock()
|
||||
fake.GetAllDataBlobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetAllDataBlobReturns(result1 []*livekit.DataBlob) {
|
||||
fake.getAllDataBlobMutex.Lock()
|
||||
defer fake.getAllDataBlobMutex.Unlock()
|
||||
fake.GetAllDataBlobStub = nil
|
||||
fake.getAllDataBlobReturns = struct {
|
||||
result1 []*livekit.DataBlob
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetAllDataBlobReturnsOnCall(i int, result1 []*livekit.DataBlob) {
|
||||
fake.getAllDataBlobMutex.Lock()
|
||||
defer fake.getAllDataBlobMutex.Unlock()
|
||||
fake.GetAllDataBlobStub = nil
|
||||
if fake.getAllDataBlobReturnsOnCall == nil {
|
||||
fake.getAllDataBlobReturnsOnCall = make(map[int]struct {
|
||||
result1 []*livekit.DataBlob
|
||||
})
|
||||
}
|
||||
fake.getAllDataBlobReturnsOnCall[i] = struct {
|
||||
result1 []*livekit.DataBlob
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetAnswer() (webrtc.SessionDescription, uint32, error) {
|
||||
fake.getAnswerMutex.Lock()
|
||||
ret, specificReturn := fake.getAnswerReturnsOnCall[len(fake.getAnswerArgsForCall)]
|
||||
@@ -2983,6 +3110,67 @@ func (fake *FakeLocalParticipant) GetCountryReturnsOnCall(i int, result1 string)
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetDataBlob(arg1 *livekit.DataBlobKey) *livekit.DataBlob {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
ret, specificReturn := fake.getDataBlobReturnsOnCall[len(fake.getDataBlobArgsForCall)]
|
||||
fake.getDataBlobArgsForCall = append(fake.getDataBlobArgsForCall, struct {
|
||||
arg1 *livekit.DataBlobKey
|
||||
}{arg1})
|
||||
stub := fake.GetDataBlobStub
|
||||
fakeReturns := fake.getDataBlobReturns
|
||||
fake.recordInvocation("GetDataBlob", []interface{}{arg1})
|
||||
fake.getDataBlobMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetDataBlobCallCount() int {
|
||||
fake.getDataBlobMutex.RLock()
|
||||
defer fake.getDataBlobMutex.RUnlock()
|
||||
return len(fake.getDataBlobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetDataBlobCalls(stub func(*livekit.DataBlobKey) *livekit.DataBlob) {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
defer fake.getDataBlobMutex.Unlock()
|
||||
fake.GetDataBlobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetDataBlobArgsForCall(i int) *livekit.DataBlobKey {
|
||||
fake.getDataBlobMutex.RLock()
|
||||
defer fake.getDataBlobMutex.RUnlock()
|
||||
argsForCall := fake.getDataBlobArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetDataBlobReturns(result1 *livekit.DataBlob) {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
defer fake.getDataBlobMutex.Unlock()
|
||||
fake.GetDataBlobStub = nil
|
||||
fake.getDataBlobReturns = struct {
|
||||
result1 *livekit.DataBlob
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetDataBlobReturnsOnCall(i int, result1 *livekit.DataBlob) {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
defer fake.getDataBlobMutex.Unlock()
|
||||
fake.GetDataBlobStub = nil
|
||||
if fake.getDataBlobReturnsOnCall == nil {
|
||||
fake.getDataBlobReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.DataBlob
|
||||
})
|
||||
}
|
||||
fake.getDataBlobReturnsOnCall[i] = struct {
|
||||
result1 *livekit.DataBlob
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) GetDataTrackTransport() types.DataTrackTransport {
|
||||
fake.getDataTrackTransportMutex.Lock()
|
||||
ret, specificReturn := fake.getDataTrackTransportReturnsOnCall[len(fake.getDataTrackTransportArgsForCall)]
|
||||
@@ -4428,6 +4616,38 @@ func (fake *FakeLocalParticipant) HandleAnswerArgsForCall(i int) *livekit.Sessio
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleGetDataBlobRequest(arg1 *livekit.GetDataBlobRequest) {
|
||||
fake.handleGetDataBlobRequestMutex.Lock()
|
||||
fake.handleGetDataBlobRequestArgsForCall = append(fake.handleGetDataBlobRequestArgsForCall, struct {
|
||||
arg1 *livekit.GetDataBlobRequest
|
||||
}{arg1})
|
||||
stub := fake.HandleGetDataBlobRequestStub
|
||||
fake.recordInvocation("HandleGetDataBlobRequest", []interface{}{arg1})
|
||||
fake.handleGetDataBlobRequestMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.HandleGetDataBlobRequestStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleGetDataBlobRequestCallCount() int {
|
||||
fake.handleGetDataBlobRequestMutex.RLock()
|
||||
defer fake.handleGetDataBlobRequestMutex.RUnlock()
|
||||
return len(fake.handleGetDataBlobRequestArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleGetDataBlobRequestCalls(stub func(*livekit.GetDataBlobRequest)) {
|
||||
fake.handleGetDataBlobRequestMutex.Lock()
|
||||
defer fake.handleGetDataBlobRequestMutex.Unlock()
|
||||
fake.HandleGetDataBlobRequestStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleGetDataBlobRequestArgsForCall(i int) *livekit.GetDataBlobRequest {
|
||||
fake.handleGetDataBlobRequestMutex.RLock()
|
||||
defer fake.handleGetDataBlobRequestMutex.RUnlock()
|
||||
argsForCall := fake.handleGetDataBlobRequestArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleICERestartSDPFragment(arg1 string) (string, error) {
|
||||
fake.handleICERestartSDPFragmentMutex.Lock()
|
||||
ret, specificReturn := fake.handleICERestartSDPFragmentReturnsOnCall[len(fake.handleICERestartSDPFragmentArgsForCall)]
|
||||
@@ -5052,6 +5272,38 @@ func (fake *FakeLocalParticipant) HandleSimulateScenarioReturnsOnCall(i int, res
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleStoreDataBlobRequest(arg1 *livekit.StoreDataBlobRequest) {
|
||||
fake.handleStoreDataBlobRequestMutex.Lock()
|
||||
fake.handleStoreDataBlobRequestArgsForCall = append(fake.handleStoreDataBlobRequestArgsForCall, struct {
|
||||
arg1 *livekit.StoreDataBlobRequest
|
||||
}{arg1})
|
||||
stub := fake.HandleStoreDataBlobRequestStub
|
||||
fake.recordInvocation("HandleStoreDataBlobRequest", []interface{}{arg1})
|
||||
fake.handleStoreDataBlobRequestMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.HandleStoreDataBlobRequestStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleStoreDataBlobRequestCallCount() int {
|
||||
fake.handleStoreDataBlobRequestMutex.RLock()
|
||||
defer fake.handleStoreDataBlobRequestMutex.RUnlock()
|
||||
return len(fake.handleStoreDataBlobRequestArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleStoreDataBlobRequestCalls(stub func(*livekit.StoreDataBlobRequest)) {
|
||||
fake.handleStoreDataBlobRequestMutex.Lock()
|
||||
defer fake.handleStoreDataBlobRequestMutex.Unlock()
|
||||
fake.HandleStoreDataBlobRequestStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleStoreDataBlobRequestArgsForCall(i int) *livekit.StoreDataBlobRequest {
|
||||
fake.handleStoreDataBlobRequestMutex.RLock()
|
||||
defer fake.handleStoreDataBlobRequestMutex.RUnlock()
|
||||
argsForCall := fake.handleStoreDataBlobRequestArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) HandleSyncState(arg1 *livekit.SyncState) error {
|
||||
fake.handleSyncStateMutex.Lock()
|
||||
ret, specificReturn := fake.handleSyncStateReturnsOnCall[len(fake.handleSyncStateArgsForCall)]
|
||||
@@ -6680,6 +6932,39 @@ func (fake *FakeLocalParticipant) PerformRpcArgsForCall(i int) (*livekit.Perform
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) ProcessGetDataBlobRequest(arg1 *livekit.GetDataBlobRequest, arg2 types.Participant) {
|
||||
fake.processGetDataBlobRequestMutex.Lock()
|
||||
fake.processGetDataBlobRequestArgsForCall = append(fake.processGetDataBlobRequestArgsForCall, struct {
|
||||
arg1 *livekit.GetDataBlobRequest
|
||||
arg2 types.Participant
|
||||
}{arg1, arg2})
|
||||
stub := fake.ProcessGetDataBlobRequestStub
|
||||
fake.recordInvocation("ProcessGetDataBlobRequest", []interface{}{arg1, arg2})
|
||||
fake.processGetDataBlobRequestMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.ProcessGetDataBlobRequestStub(arg1, arg2)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) ProcessGetDataBlobRequestCallCount() int {
|
||||
fake.processGetDataBlobRequestMutex.RLock()
|
||||
defer fake.processGetDataBlobRequestMutex.RUnlock()
|
||||
return len(fake.processGetDataBlobRequestArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) ProcessGetDataBlobRequestCalls(stub func(*livekit.GetDataBlobRequest, types.Participant)) {
|
||||
fake.processGetDataBlobRequestMutex.Lock()
|
||||
defer fake.processGetDataBlobRequestMutex.Unlock()
|
||||
fake.ProcessGetDataBlobRequestStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) ProcessGetDataBlobRequestArgsForCall(i int) (*livekit.GetDataBlobRequest, types.Participant) {
|
||||
fake.processGetDataBlobRequestMutex.RLock()
|
||||
defer fake.processGetDataBlobRequestMutex.RUnlock()
|
||||
argsForCall := fake.processGetDataBlobRequestArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipant) ProtocolVersion() types.ProtocolVersion {
|
||||
fake.protocolVersionMutex.Lock()
|
||||
ret, specificReturn := fake.protocolVersionReturnsOnCall[len(fake.protocolVersionArgsForCall)]
|
||||
|
||||
@@ -42,6 +42,12 @@ type FakeLocalParticipantListener struct {
|
||||
arg1 types.Participant
|
||||
arg2 types.DataTrack
|
||||
}
|
||||
OnGetDataBlobStub func(types.LocalParticipant, *livekit.GetDataBlobRequest)
|
||||
onGetDataBlobMutex sync.RWMutex
|
||||
onGetDataBlobArgsForCall []struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 *livekit.GetDataBlobRequest
|
||||
}
|
||||
OnLeaveStub func(types.LocalParticipant, types.ParticipantCloseReason)
|
||||
onLeaveMutex sync.RWMutex
|
||||
onLeaveArgsForCall []struct {
|
||||
@@ -82,6 +88,12 @@ type FakeLocalParticipantListener struct {
|
||||
onStateChangeArgsForCall []struct {
|
||||
arg1 types.LocalParticipant
|
||||
}
|
||||
OnStoreDataBlobStub func(types.LocalParticipant, *livekit.DataBlob)
|
||||
onStoreDataBlobMutex sync.RWMutex
|
||||
onStoreDataBlobArgsForCall []struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 *livekit.DataBlob
|
||||
}
|
||||
OnSubscribeStatusChangedStub func(types.LocalParticipant, livekit.ParticipantID, bool)
|
||||
onSubscribeStatusChangedMutex sync.RWMutex
|
||||
onSubscribeStatusChangedArgsForCall []struct {
|
||||
@@ -331,6 +343,39 @@ func (fake *FakeLocalParticipantListener) OnDataTrackUnpublishedArgsForCall(i in
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnGetDataBlob(arg1 types.LocalParticipant, arg2 *livekit.GetDataBlobRequest) {
|
||||
fake.onGetDataBlobMutex.Lock()
|
||||
fake.onGetDataBlobArgsForCall = append(fake.onGetDataBlobArgsForCall, struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 *livekit.GetDataBlobRequest
|
||||
}{arg1, arg2})
|
||||
stub := fake.OnGetDataBlobStub
|
||||
fake.recordInvocation("OnGetDataBlob", []interface{}{arg1, arg2})
|
||||
fake.onGetDataBlobMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.OnGetDataBlobStub(arg1, arg2)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnGetDataBlobCallCount() int {
|
||||
fake.onGetDataBlobMutex.RLock()
|
||||
defer fake.onGetDataBlobMutex.RUnlock()
|
||||
return len(fake.onGetDataBlobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnGetDataBlobCalls(stub func(types.LocalParticipant, *livekit.GetDataBlobRequest)) {
|
||||
fake.onGetDataBlobMutex.Lock()
|
||||
defer fake.onGetDataBlobMutex.Unlock()
|
||||
fake.OnGetDataBlobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnGetDataBlobArgsForCall(i int) (types.LocalParticipant, *livekit.GetDataBlobRequest) {
|
||||
fake.onGetDataBlobMutex.RLock()
|
||||
defer fake.onGetDataBlobMutex.RUnlock()
|
||||
argsForCall := fake.onGetDataBlobArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnLeave(arg1 types.LocalParticipant, arg2 types.ParticipantCloseReason) {
|
||||
fake.onLeaveMutex.Lock()
|
||||
fake.onLeaveArgsForCall = append(fake.onLeaveArgsForCall, struct {
|
||||
@@ -556,6 +601,39 @@ func (fake *FakeLocalParticipantListener) OnStateChangeArgsForCall(i int) types.
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnStoreDataBlob(arg1 types.LocalParticipant, arg2 *livekit.DataBlob) {
|
||||
fake.onStoreDataBlobMutex.Lock()
|
||||
fake.onStoreDataBlobArgsForCall = append(fake.onStoreDataBlobArgsForCall, struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 *livekit.DataBlob
|
||||
}{arg1, arg2})
|
||||
stub := fake.OnStoreDataBlobStub
|
||||
fake.recordInvocation("OnStoreDataBlob", []interface{}{arg1, arg2})
|
||||
fake.onStoreDataBlobMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.OnStoreDataBlobStub(arg1, arg2)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnStoreDataBlobCallCount() int {
|
||||
fake.onStoreDataBlobMutex.RLock()
|
||||
defer fake.onStoreDataBlobMutex.RUnlock()
|
||||
return len(fake.onStoreDataBlobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnStoreDataBlobCalls(stub func(types.LocalParticipant, *livekit.DataBlob)) {
|
||||
fake.onStoreDataBlobMutex.Lock()
|
||||
defer fake.onStoreDataBlobMutex.Unlock()
|
||||
fake.OnStoreDataBlobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnStoreDataBlobArgsForCall(i int) (types.LocalParticipant, *livekit.DataBlob) {
|
||||
fake.onStoreDataBlobMutex.RLock()
|
||||
defer fake.onStoreDataBlobMutex.RUnlock()
|
||||
argsForCall := fake.onStoreDataBlobArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantListener) OnSubscribeStatusChanged(arg1 types.LocalParticipant, arg2 livekit.ParticipantID, arg3 bool) {
|
||||
fake.onSubscribeStatusChangedMutex.Lock()
|
||||
fake.onSubscribeStatusChangedArgsForCall = append(fake.onSubscribeStatusChangedArgsForCall, struct {
|
||||
|
||||
@@ -13,6 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type FakeParticipant struct {
|
||||
AddDataBlobStub func(*livekit.DataBlob)
|
||||
addDataBlobMutex sync.RWMutex
|
||||
addDataBlobArgsForCall []struct {
|
||||
arg1 *livekit.DataBlob
|
||||
}
|
||||
CanSkipBroadcastStub func() bool
|
||||
canSkipBroadcastMutex sync.RWMutex
|
||||
canSkipBroadcastArgsForCall []struct {
|
||||
@@ -78,6 +83,17 @@ type FakeParticipant struct {
|
||||
result1 float64
|
||||
result2 bool
|
||||
}
|
||||
GetDataBlobStub func(*livekit.DataBlobKey) *livekit.DataBlob
|
||||
getDataBlobMutex sync.RWMutex
|
||||
getDataBlobArgsForCall []struct {
|
||||
arg1 *livekit.DataBlobKey
|
||||
}
|
||||
getDataBlobReturns struct {
|
||||
result1 *livekit.DataBlob
|
||||
}
|
||||
getDataBlobReturnsOnCall map[int]struct {
|
||||
result1 *livekit.DataBlob
|
||||
}
|
||||
GetLoggerStub func() logger.Logger
|
||||
getLoggerMutex sync.RWMutex
|
||||
getLoggerArgsForCall []struct {
|
||||
@@ -361,6 +377,38 @@ type FakeParticipant struct {
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) AddDataBlob(arg1 *livekit.DataBlob) {
|
||||
fake.addDataBlobMutex.Lock()
|
||||
fake.addDataBlobArgsForCall = append(fake.addDataBlobArgsForCall, struct {
|
||||
arg1 *livekit.DataBlob
|
||||
}{arg1})
|
||||
stub := fake.AddDataBlobStub
|
||||
fake.recordInvocation("AddDataBlob", []interface{}{arg1})
|
||||
fake.addDataBlobMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.AddDataBlobStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) AddDataBlobCallCount() int {
|
||||
fake.addDataBlobMutex.RLock()
|
||||
defer fake.addDataBlobMutex.RUnlock()
|
||||
return len(fake.addDataBlobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) AddDataBlobCalls(stub func(*livekit.DataBlob)) {
|
||||
fake.addDataBlobMutex.Lock()
|
||||
defer fake.addDataBlobMutex.Unlock()
|
||||
fake.AddDataBlobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) AddDataBlobArgsForCall(i int) *livekit.DataBlob {
|
||||
fake.addDataBlobMutex.RLock()
|
||||
defer fake.addDataBlobMutex.RUnlock()
|
||||
argsForCall := fake.addDataBlobArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) CanSkipBroadcast() bool {
|
||||
fake.canSkipBroadcastMutex.Lock()
|
||||
ret, specificReturn := fake.canSkipBroadcastReturnsOnCall[len(fake.canSkipBroadcastArgsForCall)]
|
||||
@@ -692,6 +740,67 @@ func (fake *FakeParticipant) GetAudioLevelReturnsOnCall(i int, result1 float64,
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) GetDataBlob(arg1 *livekit.DataBlobKey) *livekit.DataBlob {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
ret, specificReturn := fake.getDataBlobReturnsOnCall[len(fake.getDataBlobArgsForCall)]
|
||||
fake.getDataBlobArgsForCall = append(fake.getDataBlobArgsForCall, struct {
|
||||
arg1 *livekit.DataBlobKey
|
||||
}{arg1})
|
||||
stub := fake.GetDataBlobStub
|
||||
fakeReturns := fake.getDataBlobReturns
|
||||
fake.recordInvocation("GetDataBlob", []interface{}{arg1})
|
||||
fake.getDataBlobMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) GetDataBlobCallCount() int {
|
||||
fake.getDataBlobMutex.RLock()
|
||||
defer fake.getDataBlobMutex.RUnlock()
|
||||
return len(fake.getDataBlobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) GetDataBlobCalls(stub func(*livekit.DataBlobKey) *livekit.DataBlob) {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
defer fake.getDataBlobMutex.Unlock()
|
||||
fake.GetDataBlobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) GetDataBlobArgsForCall(i int) *livekit.DataBlobKey {
|
||||
fake.getDataBlobMutex.RLock()
|
||||
defer fake.getDataBlobMutex.RUnlock()
|
||||
argsForCall := fake.getDataBlobArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) GetDataBlobReturns(result1 *livekit.DataBlob) {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
defer fake.getDataBlobMutex.Unlock()
|
||||
fake.GetDataBlobStub = nil
|
||||
fake.getDataBlobReturns = struct {
|
||||
result1 *livekit.DataBlob
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) GetDataBlobReturnsOnCall(i int, result1 *livekit.DataBlob) {
|
||||
fake.getDataBlobMutex.Lock()
|
||||
defer fake.getDataBlobMutex.Unlock()
|
||||
fake.GetDataBlobStub = nil
|
||||
if fake.getDataBlobReturnsOnCall == nil {
|
||||
fake.getDataBlobReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.DataBlob
|
||||
})
|
||||
}
|
||||
fake.getDataBlobReturnsOnCall[i] = struct {
|
||||
result1 *livekit.DataBlob
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeParticipant) GetLogger() logger.Logger {
|
||||
fake.getLoggerMutex.Lock()
|
||||
ret, specificReturn := fake.getLoggerReturnsOnCall[len(fake.getLoggerArgsForCall)]
|
||||
|
||||
+20
-10
@@ -485,19 +485,29 @@ func (h *AgentHandler) CheckEnabled(ctx context.Context, req *rpc.CheckEnabledRe
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *AgentHandler) DrainConnections(interval time.Duration) {
|
||||
// jitter drain start
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(interval))))
|
||||
func (h *AgentHandler) DrainConnections(interval time.Duration, force bool) {
|
||||
if !force {
|
||||
// jitter drain start
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(interval))))
|
||||
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
for _, w := range h.workers {
|
||||
w.Close()
|
||||
<-t.C
|
||||
for _, w := range h.workers {
|
||||
w.Close()
|
||||
<-t.C
|
||||
}
|
||||
} else {
|
||||
// drain as quickly as possible when forced
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
for _, w := range h.workers {
|
||||
w.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -538,6 +538,7 @@ func (r *RoomManager) StartSession(
|
||||
FireOnTrackBySdp: true,
|
||||
UseSinglePeerConnection: pi.UseSinglePeerConnection,
|
||||
EnableDataTracks: r.config.EnableDataTracks,
|
||||
EnableParticipantDataBlob: r.config.EnableParticipantDataBlob,
|
||||
EnableRTPStreamRestartDetection: r.config.RTC.EnableRTPStreamRestartDetection,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+21
-11
@@ -401,9 +401,6 @@ func (s *RTCService) serve(w http.ResponseWriter, r *http.Request, needsJoinRequ
|
||||
return
|
||||
}
|
||||
|
||||
prometheus.IncrementParticipantJoin(1)
|
||||
joinDuration = time.Since(startedAt)
|
||||
|
||||
pLogger = pLogger.WithValues("connID", cr.ConnectionID)
|
||||
if !pi.Reconnect && initialResponse.GetJoin() != nil {
|
||||
joinRoomID := livekit.RoomID(initialResponse.GetJoin().GetRoom().GetSid())
|
||||
@@ -445,6 +442,7 @@ func (s *RTCService) serve(w http.ResponseWriter, r *http.Request, needsJoinRequ
|
||||
// upgrade only once the basics are good to go
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
prometheus.IncrementParticipantJoinUpgradeFail(1)
|
||||
resolveLogger(true)
|
||||
HandleError(w, r, http.StatusInternalServerError, err, getLoggerFields()...)
|
||||
return
|
||||
@@ -465,12 +463,17 @@ func (s *RTCService) serve(w http.ResponseWriter, r *http.Request, needsJoinRequ
|
||||
pLogger.Debugw("sending initial response", "response", logger.Proto(initialResponse))
|
||||
count, err := sigConn.WriteResponse(initialResponse)
|
||||
if err != nil {
|
||||
prometheus.IncrementParticipantJoinWriteInitialResponseFail(1)
|
||||
resolveLogger(true)
|
||||
pLogger.Warnw("could not write initial response", err)
|
||||
return
|
||||
}
|
||||
signalStats.AddBytes(uint64(count), true)
|
||||
|
||||
prometheus.IncrementParticipantJoin(1)
|
||||
joinDuration = time.Since(startedAt)
|
||||
prometheus.RecordSessionJoinLatency(int(pi.Client.GetProtocol()), joinDuration)
|
||||
|
||||
pLogger.Debugw(
|
||||
"new client WS connected",
|
||||
"reconnect", pi.Reconnect,
|
||||
@@ -625,20 +628,27 @@ func (s *RTCService) serve(w http.ResponseWriter, r *http.Request, needsJoinRequ
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RTCService) DrainConnections(interval time.Duration) {
|
||||
func (s *RTCService) DrainConnections(interval time.Duration, force bool) {
|
||||
s.mu.Lock()
|
||||
conns := maps.Clone(s.connections)
|
||||
s.mu.Unlock()
|
||||
|
||||
// jitter drain start
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(interval))))
|
||||
if !force {
|
||||
// jitter drain start
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(interval))))
|
||||
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
|
||||
for c := range conns {
|
||||
_ = c.Close()
|
||||
<-t.C
|
||||
for c := range conns {
|
||||
_ = c.Close()
|
||||
<-t.C
|
||||
}
|
||||
} else {
|
||||
// drain as quickly as possible when forced
|
||||
for c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ package datachannel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/pion/datachannel"
|
||||
"github.com/pion/transport/v4/deadline"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -149,43 +149,41 @@ func GetNodeStats(nodeStartedAt int64, prevStats []*livekit.NodeStats, rateInter
|
||||
promSysPacketGauge.WithLabelValues("dropped").Set(float64(sysDroppedPackets - sysDroppedPacketsStart))
|
||||
|
||||
stats := &livekit.NodeStats{
|
||||
StartedAt: nodeStartedAt,
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
NumRooms: roomCurrent.Load(),
|
||||
NumClients: participantCurrent.Load(),
|
||||
NumTracksIn: trackPublishedCurrent.Load(),
|
||||
NumTracksOut: trackSubscribedCurrent.Load(),
|
||||
NumTrackPublishAttempts: trackPublishAttempts.Load(),
|
||||
NumTrackPublishSuccess: trackPublishSuccess.Load(),
|
||||
NumTrackPublishCancels: trackPublishCancels.Load(),
|
||||
NumTrackSubscribeAttempts: trackSubscribeAttempts.Load(),
|
||||
NumTrackSubscribeSuccess: trackSubscribeSuccess.Load(),
|
||||
NumTrackSubscribeCancels: trackSubscribeCancels.Load(),
|
||||
BytesIn: bytesIn.Load(),
|
||||
BytesOut: bytesOut.Load(),
|
||||
PacketsIn: packetsIn.Load(),
|
||||
PacketsOut: packetsOut.Load(),
|
||||
RetransmitBytesOut: retransmitBytes.Load(),
|
||||
RetransmitPacketsOut: retransmitPackets.Load(),
|
||||
NackTotal: nackTotal.Load(),
|
||||
ParticipantSignalConnected: participantSignalConnected.Load(),
|
||||
ParticipantSignalFailed: participantSignalFailed.Load(),
|
||||
ParticipantSignalValidationFailed: participantSignalValidationFailed.Load(),
|
||||
ParticipantRtcInit: participantRTCInit.Load(),
|
||||
ParticipantRtcConnected: participantRTCConnected.Load(),
|
||||
ParticipantRtcCanceled: participantRTCCanceled.Load(),
|
||||
ParticipantRtcActive: participantRTCActive.Load(),
|
||||
ForwardLatency: forwardLatency.Load(),
|
||||
ForwardJitter: forwardJitter.Load(),
|
||||
NumCpus: uint32(cpuStats.NumCPU()), // this will round down to the nearest integer
|
||||
CpuLoad: float32(cpuStats.GetCPULoad()),
|
||||
MemoryTotal: memTotal,
|
||||
MemoryUsed: memUsed,
|
||||
LoadAvgLast1Min: float32(loadAvg.Loadavg1),
|
||||
LoadAvgLast5Min: float32(loadAvg.Loadavg5),
|
||||
LoadAvgLast15Min: float32(loadAvg.Loadavg15),
|
||||
SysPacketsOut: sysPackets,
|
||||
SysPacketsDropped: sysDroppedPackets,
|
||||
StartedAt: nodeStartedAt,
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
NumRooms: roomCurrent.Load(),
|
||||
NumClients: participantCurrent.Load(),
|
||||
NumTracksIn: trackPublishedCurrent.Load(),
|
||||
NumTracksOut: trackSubscribedCurrent.Load(),
|
||||
NumTrackPublishAttempts: trackPublishAttempts.Load(),
|
||||
NumTrackPublishSuccess: trackPublishSuccess.Load(),
|
||||
NumTrackPublishCancels: trackPublishCancels.Load(),
|
||||
NumTrackSubscribeAttempts: trackSubscribeAttempts.Load(),
|
||||
NumTrackSubscribeSuccess: trackSubscribeSuccess.Load(),
|
||||
NumTrackSubscribeCancels: trackSubscribeCancels.Load(),
|
||||
BytesIn: bytesIn.Load(),
|
||||
BytesOut: bytesOut.Load(),
|
||||
PacketsIn: packetsIn.Load(),
|
||||
PacketsOut: packetsOut.Load(),
|
||||
RetransmitBytesOut: retransmitBytes.Load(),
|
||||
RetransmitPacketsOut: retransmitPackets.Load(),
|
||||
NackTotal: nackTotal.Load(),
|
||||
ParticipantSignalConnected: participantSignalConnected.Load(),
|
||||
ParticipantRtcInit: participantRTCInit.Load(),
|
||||
ParticipantRtcConnected: participantRTCConnected.Load(),
|
||||
ParticipantRtcCanceled: participantRTCCanceled.Load(),
|
||||
ParticipantRtcActive: participantRTCActive.Load(),
|
||||
ForwardLatency: forwardLatency.Load(),
|
||||
ForwardJitter: forwardJitter.Load(),
|
||||
NumCpus: uint32(cpuStats.NumCPU()), // this will round down to the nearest integer
|
||||
CpuLoad: float32(cpuStats.GetCPULoad()),
|
||||
MemoryTotal: memTotal,
|
||||
MemoryUsed: memUsed,
|
||||
LoadAvgLast1Min: float32(loadAvg.Loadavg1),
|
||||
LoadAvgLast5Min: float32(loadAvg.Loadavg5),
|
||||
LoadAvgLast15Min: float32(loadAvg.Loadavg15),
|
||||
SysPacketsOut: sysPackets,
|
||||
SysPacketsDropped: sysDroppedPackets,
|
||||
}
|
||||
|
||||
for _, rateInterval := range rateIntervals {
|
||||
|
||||
@@ -36,22 +36,20 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
bytesIn atomic.Uint64
|
||||
bytesOut atomic.Uint64
|
||||
packetsIn atomic.Uint64
|
||||
packetsOut atomic.Uint64
|
||||
nackTotal atomic.Uint64
|
||||
retransmitBytes atomic.Uint64
|
||||
retransmitPackets atomic.Uint64
|
||||
participantSignalConnected atomic.Uint64
|
||||
participantSignalFailed atomic.Uint64
|
||||
participantSignalValidationFailed atomic.Uint64
|
||||
participantRTCConnected atomic.Uint64
|
||||
participantRTCInit atomic.Uint64
|
||||
participantRTCCanceled atomic.Uint64
|
||||
participantRTCActive atomic.Uint64
|
||||
forwardLatency atomic.Uint32
|
||||
forwardJitter atomic.Uint32
|
||||
bytesIn atomic.Uint64
|
||||
bytesOut atomic.Uint64
|
||||
packetsIn atomic.Uint64
|
||||
packetsOut atomic.Uint64
|
||||
nackTotal atomic.Uint64
|
||||
retransmitBytes atomic.Uint64
|
||||
retransmitPackets atomic.Uint64
|
||||
participantSignalConnected atomic.Uint64
|
||||
participantRTCConnected atomic.Uint64
|
||||
participantRTCInit atomic.Uint64
|
||||
participantRTCCanceled atomic.Uint64
|
||||
participantRTCActive atomic.Uint64
|
||||
forwardLatency atomic.Uint32
|
||||
forwardJitter atomic.Uint32
|
||||
|
||||
promPacketLabels = []string{"direction", "transmission", "country"}
|
||||
promPacketTotal *prometheus.CounterVec
|
||||
@@ -305,29 +303,39 @@ func IncrementParticipantJoin(join uint32) {
|
||||
|
||||
func IncrementParticipantJoinFail(fail uint32) {
|
||||
if fail > 0 {
|
||||
participantSignalFailed.Add(uint64(fail))
|
||||
promParticipantJoin.WithLabelValues("signal_failed").Add(float64(fail))
|
||||
}
|
||||
}
|
||||
|
||||
func IncrementParticipantJoinValidationFail(validationFail uint32) {
|
||||
if validationFail > 0 {
|
||||
participantSignalValidationFailed.Add(uint64(validationFail))
|
||||
promParticipantJoin.WithLabelValues("signal_validation_failed").Add(float64(validationFail))
|
||||
}
|
||||
}
|
||||
|
||||
func IncrementParticipantRtcInit(join uint32) {
|
||||
if join > 0 {
|
||||
participantRTCInit.Add(uint64(join))
|
||||
promParticipantJoin.WithLabelValues("rtc_init").Add(float64(join))
|
||||
func IncrementParticipantJoinUpgradeFail(upgradeFail uint32) {
|
||||
if upgradeFail > 0 {
|
||||
promParticipantJoin.WithLabelValues("signal_upgrade_failed").Add(float64(upgradeFail))
|
||||
}
|
||||
}
|
||||
|
||||
func IncrementParticipantRtcConnected(join uint32) {
|
||||
if join > 0 {
|
||||
participantRTCConnected.Add(uint64(join))
|
||||
promParticipantJoin.WithLabelValues("rtc_connected").Add(float64(join))
|
||||
func IncrementParticipantJoinWriteInitialResponseFail(writeInitialResponseFail uint32) {
|
||||
if writeInitialResponseFail > 0 {
|
||||
promParticipantJoin.WithLabelValues("signal_write_initial_response_failed").Add(float64(writeInitialResponseFail))
|
||||
}
|
||||
}
|
||||
|
||||
func IncrementParticipantRtcInit(init uint32) {
|
||||
if init > 0 {
|
||||
participantRTCInit.Add(uint64(init))
|
||||
promParticipantJoin.WithLabelValues("rtc_init").Add(float64(init))
|
||||
}
|
||||
}
|
||||
|
||||
func IncrementParticipantRtcConnected(connected uint32) {
|
||||
if connected > 0 {
|
||||
participantRTCConnected.Add(uint64(connected))
|
||||
promParticipantJoin.WithLabelValues("rtc_connected").Add(float64(connected))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,10 +346,10 @@ func IncrementParticipantRtcActive(active uint32) {
|
||||
}
|
||||
}
|
||||
|
||||
func IncrementParticipantRtcCanceled(numCancels uint64) {
|
||||
if numCancels > 0 {
|
||||
participantRTCCanceled.Add(numCancels)
|
||||
promParticipantJoin.WithLabelValues("rtc_canceled").Add(float64(numCancels))
|
||||
func IncrementParticipantRtcCanceled(canceled uint64) {
|
||||
if canceled > 0 {
|
||||
participantRTCCanceled.Add(canceled)
|
||||
promParticipantJoin.WithLabelValues("rtc_canceled").Add(float64(canceled))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ var (
|
||||
promTrackSubscribedCurrent *prometheus.GaugeVec
|
||||
promTrackPublishCounter *prometheus.CounterVec
|
||||
promTrackSubscribeCounter *prometheus.CounterVec
|
||||
promSessionJoinLatency *prometheus.HistogramVec
|
||||
promSessionStartTime *prometheus.HistogramVec
|
||||
promSessionDuration *prometheus.HistogramVec
|
||||
promPubSubTime *prometheus.HistogramVec
|
||||
@@ -99,6 +100,13 @@ func initRoomStats(nodeID string, nodeType livekit.NodeType) {
|
||||
Name: "subscribe_counter",
|
||||
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
|
||||
}, []string{"state", "error"})
|
||||
promSessionJoinLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: livekitNamespace,
|
||||
Subsystem: "session",
|
||||
Name: "join_latency_ms",
|
||||
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
|
||||
Buckets: prometheus.ExponentialBucketsRange(10, 10000, 15),
|
||||
}, []string{"protocol_version"})
|
||||
promSessionStartTime = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: livekitNamespace,
|
||||
Subsystem: "session",
|
||||
@@ -134,6 +142,7 @@ func initRoomStats(nodeID string, nodeType livekit.NodeType) {
|
||||
prometheus.MustRegister(promTrackSubscribedCurrent)
|
||||
prometheus.MustRegister(promTrackPublishCounter)
|
||||
prometheus.MustRegister(promTrackSubscribeCounter)
|
||||
prometheus.MustRegister(promSessionJoinLatency)
|
||||
prometheus.MustRegister(promSessionStartTime)
|
||||
prometheus.MustRegister(promSessionDuration)
|
||||
prometheus.MustRegister(promPubSubTime)
|
||||
@@ -271,6 +280,10 @@ func RecordTrackSubscribeCancels(numCancels int32) {
|
||||
promTrackSubscribeCounter.WithLabelValues("cancel", "").Add(float64(numCancels))
|
||||
}
|
||||
|
||||
func RecordSessionJoinLatency(protocolVersion int, d time.Duration) {
|
||||
promSessionJoinLatency.WithLabelValues(strconv.Itoa(protocolVersion)).Observe(float64(d.Milliseconds()))
|
||||
}
|
||||
|
||||
func RecordSessionStartTime(protocolVersion int, d time.Duration) {
|
||||
promSessionStartTime.WithLabelValues(strconv.Itoa(protocolVersion)).Observe(float64(d.Milliseconds()))
|
||||
}
|
||||
|
||||
@@ -80,9 +80,13 @@ func setupSingleNodeTest(name string) (*service.LivekitServer, func()) {
|
||||
}
|
||||
|
||||
func setupMultiNodeTest(name string) (*service.LivekitServer, *service.LivekitServer, func()) {
|
||||
return setupMultiNodeTestWithConfig(name, nil)
|
||||
}
|
||||
|
||||
func setupMultiNodeTestWithConfig(name string, configUpdater func(*config.Config)) (*service.LivekitServer, *service.LivekitServer, func()) {
|
||||
logger.Infow("----------------STARTING TEST----------------", "test", name)
|
||||
s1 := createMultiNodeServer(guid.New(nodeID1), defaultServerPort)
|
||||
s2 := createMultiNodeServer(guid.New(nodeID2), secondServerPort)
|
||||
s1 := createMultiNodeServer(guid.New(nodeID1), defaultServerPort, configUpdater)
|
||||
s2 := createMultiNodeServer(guid.New(nodeID2), secondServerPort, configUpdater)
|
||||
go s1.Start()
|
||||
go s2.Start()
|
||||
|
||||
@@ -190,7 +194,7 @@ func createSingleNodeServer(configUpdater func(*config.Config)) *service.Livekit
|
||||
return s
|
||||
}
|
||||
|
||||
func createMultiNodeServer(nodeID string, port uint32) *service.LivekitServer {
|
||||
func createMultiNodeServer(nodeID string, port uint32, configUpdater func(*config.Config)) *service.LivekitServer {
|
||||
var err error
|
||||
conf, err := config.NewConfig("", true, nil, nil)
|
||||
if err != nil {
|
||||
@@ -202,6 +206,9 @@ func createMultiNodeServer(nodeID string, port uint32) *service.LivekitServer {
|
||||
conf.Redis.Address = "localhost:6379"
|
||||
conf.Keys = map[string]string{testApiKey: testApiSecret}
|
||||
conf.EnableDataTracks = true
|
||||
if configUpdater != nil {
|
||||
configUpdater(conf)
|
||||
}
|
||||
|
||||
currentNode, err := routing.NewLocalNode(conf)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/rtc"
|
||||
"github.com/livekit/livekit-server/pkg/testutils"
|
||||
"github.com/livekit/livekit-server/test/client"
|
||||
@@ -425,3 +426,129 @@ func TestCloseDisconnectedParticipantOnSignalClose(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiNodeDataBlob(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
return
|
||||
}
|
||||
|
||||
_, _, finish := setupMultiNodeTestWithConfig("TestMultiNodeDataBlob", func(c *config.Config) {
|
||||
c.EnableParticipantDataBlob = true
|
||||
c.Limit.MaxDataBlobSize = 1024
|
||||
})
|
||||
defer finish()
|
||||
|
||||
for _, testRTCServicePath := range testRTCServicePaths {
|
||||
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
||||
pubCapture := &dataBlobCapture{}
|
||||
subCapture := &dataBlobCapture{}
|
||||
|
||||
// publisher on node 1, subscriber on node 2
|
||||
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, &client.Options{
|
||||
AutoSubscribe: true,
|
||||
SignalResponseInterceptor: pubCapture.interceptor(),
|
||||
})
|
||||
sub := createRTCClient("sub", secondServerPort, testRTCServicePath, &client.Options{
|
||||
AutoSubscribe: true,
|
||||
SignalResponseInterceptor: subCapture.interceptor(),
|
||||
})
|
||||
waitUntilConnected(t, pub, sub)
|
||||
defer stopClients(pub, sub)
|
||||
|
||||
// wait for both nodes to see each other so the get request routes correctly
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if sub.GetRemoteParticipant(pub.ID()) == nil {
|
||||
return "sub does not see pub yet"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
key := &livekit.DataBlobKey{
|
||||
Key: &livekit.DataBlobKey_Generic{
|
||||
Generic: "blob-multinode",
|
||||
},
|
||||
}
|
||||
contents := []byte("multinode-content")
|
||||
|
||||
require.NoError(t, pub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_StoreDataBlobRequest{
|
||||
StoreDataBlobRequest: &livekit.StoreDataBlobRequest{
|
||||
RequestId: 1,
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: key,
|
||||
Contents: contents,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
resp := pubCapture.takeStoreResponse()
|
||||
if resp == nil {
|
||||
return "publisher did not receive store response"
|
||||
}
|
||||
if resp.RequestId != 1 {
|
||||
return fmt.Sprintf("expected store response request id 1, got %d", resp.RequestId)
|
||||
}
|
||||
if resp.Key == nil {
|
||||
return "store response missing key"
|
||||
}
|
||||
if resp.Key.String() != key.String() {
|
||||
return fmt.Sprintf("expected stored blob key %s, got %s", key.String(), resp.Key.String())
|
||||
}
|
||||
return ""
|
||||
})
|
||||
require.Equal(t, 0, pubCapture.requestResponseCount(), "publisher should not receive an error response on success")
|
||||
|
||||
// subscriber on a different node asks for the blob; the request routes
|
||||
// across nodes to the publisher.
|
||||
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
||||
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
||||
ParticipantIdentity: "pub",
|
||||
Key: key,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
resp := subCapture.takeBlobResponse()
|
||||
if resp == nil {
|
||||
return "subscriber did not receive blob response"
|
||||
}
|
||||
if resp.Blob == nil {
|
||||
return "blob response missing blob"
|
||||
}
|
||||
if resp.Blob.Key.String() != key.String() {
|
||||
return fmt.Sprintf("expected data blob key %s, got %s", key.String(), resp.Blob.Key.String())
|
||||
}
|
||||
if string(resp.Blob.Contents) != string(contents) {
|
||||
return fmt.Sprintf("expected contents %q, got %q", contents, resp.Blob.Contents)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// requesting an unknown publisher identity should return NOT_FOUND
|
||||
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
||||
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
||||
ParticipantIdentity: "unknown-publisher",
|
||||
Key: key,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rr := subCapture.takeRequestResponse()
|
||||
if rr == nil {
|
||||
return "subscriber did not receive RequestResponse for unknown publisher"
|
||||
}
|
||||
if rr.Reason != livekit.RequestResponse_NOT_FOUND {
|
||||
return fmt.Sprintf("expected NOT_FOUND, got %s", rr.Reason)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1526,3 +1526,299 @@ func TestTurnAuthFailure(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// dataBlobCapture buffers RequestResponse, StoreDataBlobResponse, and GetDataBlobResponse messages
|
||||
// sent to a test client so they can be asserted on. Other messages flow through to the
|
||||
// default handler.
|
||||
type dataBlobCapture struct {
|
||||
mu sync.Mutex
|
||||
requestResponses []*livekit.RequestResponse
|
||||
storeResponses []*livekit.StoreDataBlobResponse
|
||||
blobResponses []*livekit.GetDataBlobResponse
|
||||
}
|
||||
|
||||
func (c *dataBlobCapture) interceptor() testclient.SignalResponseInterceptor {
|
||||
return func(msg *livekit.SignalResponse, next testclient.SignalResponseHandler) error {
|
||||
switch m := msg.Message.(type) {
|
||||
case *livekit.SignalResponse_RequestResponse:
|
||||
c.mu.Lock()
|
||||
c.requestResponses = append(c.requestResponses, m.RequestResponse)
|
||||
c.mu.Unlock()
|
||||
case *livekit.SignalResponse_StoreDataBlobResponse:
|
||||
c.mu.Lock()
|
||||
c.storeResponses = append(c.storeResponses, m.StoreDataBlobResponse)
|
||||
c.mu.Unlock()
|
||||
case *livekit.SignalResponse_GetDataBlobResponse:
|
||||
c.mu.Lock()
|
||||
c.blobResponses = append(c.blobResponses, m.GetDataBlobResponse)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
return next(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *dataBlobCapture) takeRequestResponse() *livekit.RequestResponse {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.requestResponses) == 0 {
|
||||
return nil
|
||||
}
|
||||
rr := c.requestResponses[0]
|
||||
c.requestResponses = c.requestResponses[1:]
|
||||
return rr
|
||||
}
|
||||
|
||||
func (c *dataBlobCapture) takeStoreResponse() *livekit.StoreDataBlobResponse {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.storeResponses) == 0 {
|
||||
return nil
|
||||
}
|
||||
sr := c.storeResponses[0]
|
||||
c.storeResponses = c.storeResponses[1:]
|
||||
return sr
|
||||
}
|
||||
|
||||
func (c *dataBlobCapture) takeBlobResponse() *livekit.GetDataBlobResponse {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.blobResponses) == 0 {
|
||||
return nil
|
||||
}
|
||||
sr := c.blobResponses[0]
|
||||
c.blobResponses = c.blobResponses[1:]
|
||||
return sr
|
||||
}
|
||||
|
||||
func (c *dataBlobCapture) requestResponseCount() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.requestResponses)
|
||||
}
|
||||
|
||||
func setupDataBlobServer(t *testing.T, name string, enable bool) (*service.LivekitServer, func()) {
|
||||
logger.Infow("----------------STARTING TEST----------------", "test", name)
|
||||
s := createSingleNodeServer(func(c *config.Config) {
|
||||
c.EnableParticipantDataBlob = enable
|
||||
c.Limit.MaxDataBlobSize = 1024
|
||||
})
|
||||
go func() {
|
||||
if err := s.Start(); err != nil {
|
||||
logger.Errorw("server returned error", err)
|
||||
}
|
||||
}()
|
||||
waitForServerToStart(s)
|
||||
return s, func() {
|
||||
s.Stop(true)
|
||||
logger.Infow("----------------FINISHING TEST----------------", "test", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleNodeDataBlob(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
return
|
||||
}
|
||||
|
||||
_, finish := setupDataBlobServer(t, "TestSingleNodeDataBlob", true)
|
||||
defer finish()
|
||||
|
||||
for _, testRTCServicePath := range testRTCServicePaths {
|
||||
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
||||
pubCapture := &dataBlobCapture{}
|
||||
subCapture := &dataBlobCapture{}
|
||||
|
||||
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, &testclient.Options{
|
||||
AutoSubscribe: true,
|
||||
SignalResponseInterceptor: pubCapture.interceptor(),
|
||||
})
|
||||
sub := createRTCClient("sub", defaultServerPort, testRTCServicePath, &testclient.Options{
|
||||
AutoSubscribe: true,
|
||||
SignalResponseInterceptor: subCapture.interceptor(),
|
||||
})
|
||||
waitUntilConnected(t, pub, sub)
|
||||
defer stopClients(pub, sub)
|
||||
|
||||
key := &livekit.DataBlobKey{
|
||||
Key: &livekit.DataBlobKey_Generic{
|
||||
Generic: "blob-1",
|
||||
},
|
||||
}
|
||||
contents := []byte("definition-bytes")
|
||||
|
||||
// publisher stores a blob
|
||||
require.NoError(t, pub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_StoreDataBlobRequest{
|
||||
StoreDataBlobRequest: &livekit.StoreDataBlobRequest{
|
||||
RequestId: 1,
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: key,
|
||||
Contents: contents,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
resp := pubCapture.takeStoreResponse()
|
||||
if resp == nil {
|
||||
return "publisher did not receive store response"
|
||||
}
|
||||
if resp.RequestId != 1 {
|
||||
return fmt.Sprintf("expected store response request id 1, got %d", resp.RequestId)
|
||||
}
|
||||
if resp.Key == nil {
|
||||
return "store response missing key"
|
||||
}
|
||||
if resp.Key.String() != key.String() {
|
||||
return fmt.Sprintf("expected stored blob key %s, got %s", key.String(), resp.Key.String())
|
||||
}
|
||||
return ""
|
||||
})
|
||||
require.Equal(t, 0, pubCapture.requestResponseCount(), "publisher should not receive an error response on success")
|
||||
|
||||
// subscriber asks for the blob
|
||||
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
||||
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
||||
ParticipantIdentity: "pub",
|
||||
Key: key,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
resp := subCapture.takeBlobResponse()
|
||||
if resp == nil {
|
||||
return "subscriber did not receive blob response"
|
||||
}
|
||||
if resp.Blob == nil {
|
||||
return "blob response missing blob"
|
||||
}
|
||||
if resp.Blob.Key.String() != key.String() {
|
||||
return fmt.Sprintf("expected blob key %s, got %s", key.String(), resp.Blob.Key.String())
|
||||
}
|
||||
if string(resp.Blob.Contents) != string(contents) {
|
||||
return fmt.Sprintf("expected contents %q, got %q", contents, resp.Blob.Contents)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// subscriber asks for an unknown blob on a known publisher
|
||||
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
||||
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
||||
ParticipantIdentity: "pub",
|
||||
Key: &livekit.DataBlobKey{
|
||||
Key: &livekit.DataBlobKey_Generic{
|
||||
Generic: "does-not-exist",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rr := subCapture.takeRequestResponse()
|
||||
if rr == nil {
|
||||
return "subscriber did not receive RequestResponse for missing blob"
|
||||
}
|
||||
if rr.Reason != livekit.RequestResponse_NOT_FOUND {
|
||||
return fmt.Sprintf("expected NOT_FOUND, got %s", rr.Reason)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// subscriber asks for a blob on an unknown publisher identity
|
||||
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
||||
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
||||
ParticipantIdentity: "unknown-publisher",
|
||||
Key: key,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rr := subCapture.takeRequestResponse()
|
||||
if rr == nil {
|
||||
return "subscriber did not receive RequestResponse for unknown publisher"
|
||||
}
|
||||
if rr.Reason != livekit.RequestResponse_NOT_FOUND {
|
||||
return fmt.Sprintf("expected NOT_FOUND, got %s", rr.Reason)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
// publisher sends an invalid blob (empty key)
|
||||
require.NoError(t, pub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_StoreDataBlobRequest{
|
||||
StoreDataBlobRequest: &livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Contents: contents,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rr := pubCapture.takeRequestResponse()
|
||||
if rr == nil {
|
||||
return "publisher did not receive RequestResponse for invalid define"
|
||||
}
|
||||
if rr.Reason != livekit.RequestResponse_INVALID_REQUEST {
|
||||
return fmt.Sprintf("expected INVALID_REQUEST, got %s", rr.Reason)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleNodeDataBlobDisabled(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
return
|
||||
}
|
||||
|
||||
_, finish := setupDataBlobServer(t, "TestSingleNodeDataBlobDisabled", false)
|
||||
defer finish()
|
||||
|
||||
for _, testRTCServicePath := range testRTCServicePaths {
|
||||
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
||||
pubCapture := &dataBlobCapture{}
|
||||
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, &testclient.Options{
|
||||
AutoSubscribe: true,
|
||||
SignalResponseInterceptor: pubCapture.interceptor(),
|
||||
})
|
||||
waitUntilConnected(t, pub)
|
||||
defer stopClients(pub)
|
||||
|
||||
require.NoError(t, pub.SendRequest(&livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_StoreDataBlobRequest{
|
||||
StoreDataBlobRequest: &livekit.StoreDataBlobRequest{
|
||||
Blob: &livekit.DataBlob{
|
||||
Key: &livekit.DataBlobKey{
|
||||
Key: &livekit.DataBlobKey_Generic{
|
||||
Generic: "blob-1",
|
||||
},
|
||||
},
|
||||
Contents: []byte("definition-bytes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
rr := pubCapture.takeRequestResponse()
|
||||
if rr == nil {
|
||||
return "publisher did not receive RequestResponse"
|
||||
}
|
||||
if rr.Reason != livekit.RequestResponse_NOT_ALLOWED {
|
||||
return fmt.Sprintf("expected NOT_ALLOWED, got %s", rr.Reason)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
|
||||
package version
|
||||
|
||||
const Version = "1.13.1"
|
||||
const Version = "1.13.2"
|
||||
|
||||
Reference in New Issue
Block a user