Squashed 'livekit-server/' changes from 154b4d26..46c43095

46c43095 fix goreleaser workflow, version 1.13.1 (#4577)
e0815be2 chore: improve docker test shutdown reliability (#4576)
bfd9deff expose TCPFallbackRTTThreshold and AllowUDPUnstableFallback via config (#4556)
b93c1e16 Release v1.13.0. (#4573)
fd452212 Update mediatransportutil to get ICE candidate timeout config (#4572)
8be8c74a Update github workflows (#4463)
c4e41872 Update go deps to v1.17.2 (#4462)
dc8e0310 Update go deps to v4 (#4482)
20fd1ad2 turn: allow for providing secret via file (#4564)
6590570d Pin pion/dtls to v3.1.2 (#4570)
cdbbee1f deps: bump protocol + psrpc to latest tips (#4565)
d290de81 Correct config comment (#4563)
77ecf920 rtc: report participant session end time on room move (#4561)
63be96f6 Prevent panic from nil(illegal) syncState.Subscriptions message (#4560)
835ef1b3 Metrics for participant active, i. e. fully established. (#4557)
5bd42534 Document of advertise_internal_ip and external_ip_only (#4554)
356ae211 Config documentation for advertise_internal_ip and skip_external_ip_validation (#4552)
7c319a67 rtc: prevent duration reporting for inactive participants (#4550)
2dd5e632 telemetry: split webhook-processed hook out of NewTelemetryService (#4548)
222177a9 service: prevent nil deref in validate with wrapped join request (#4547)
dd7580b4 Protect against nil clientInfo (#4546)
145689e6 Start tracking Twirp method request latency in prometheus too, not just in logs (#4545)
cde89627 rtc: emit per-data-track bytes via BytesTrackStats (#4540)
2e22911d Remove backwards compatibility support for TURN auth. (#4539)
062d1219 Use NACKQuueInterface type. (#4538)
7f08b04c Add IsIntentionalDisconnect helper (#4537)
1ab2bf04 Clean up packet size logging (#4536)
8ab92a80 Don't require media sections when joining (#4535)
019a6640 rtc: report participant kind code and details (#4534)
77595d38 TEL-336: fix sip error categorization (#4528)
f303f499 Always enable rtx codec (#4533)
e4a8a55c Check Less and LessEq in version compare. (#4532)
37eb7a32 Release v1.12.0 (#4529)
4a7b1e85 Create NACK tracker only once. (#4527)
89faaeba Apply ttl check only when authenticate allocation creating (#4526)
b32933b0 Log details of RTCP packets. (#4525)
8b79ec9e Support SIP auth realm for inbound. (#4522)
4b8db3cf Add integration test for TURN auth failures (#4524)
ef2e5efe Log large packets receive/send. (#4521)
d1236750 feat: auto create rooms for tokens with the RoomCreate grant (#4320)
7a3e595b apply room tags from JWT grant room configuration (#4518)
ab7fdeab add AssignmentHook to AssignJob; propagate websocket write errors (#4516)
cf20c9cd Add expiry to TURN password. (#4515)
20d4a3a1 Populate data track loggers with context (#4514)
12fff29a allow setting agent job assignment url (#4512)
ba366fc7 Fix SIP media config upgrade. (#4511)
8fbc5adf update protocol for protojson (#4510)
3de6f517 Add TURN permission handler. (#4505)
8ffcef93 Update protocol to support SIP media config. (#4509)
1ab1e072 test: verify upstream and downstream connection stats end-to-end (#4508)
c4fd71a5 Fix sense check in DeltaInfo gathering (#4507)
803999ef rename agent environment to deployment (#4506)
bacc21e6 add helper to check for agent worker endpoint (#4503)
253f977d add duration seconds reporting (#4500)
ffab3bd3 add agent environment (#4498)
ccdf23c8 Use mediatransportutil/codec package, no functional change (#4497)
680703f2 Include reception reoprts in receiver report callback. (#4496)
f51798bc Fix publish-only limitations being incorrectly applied to receivers (#4495)
a002337d Legacy TrackInfo.Simulcast flag. (#4493)
af1dcc88 Add CloseWithReason to agent SignalConn interface (#4492)
d7c2daf1 report all simulcast layers (#4491)
c1ad2b22 Misc optimisations. (#4490)
19b9e8c0 Additional data tracks logging (#4489)
743d9c8b add support for client capabilities (#4461)
fc47e478 Close peer connection unconditionally to unblock set local/remote (#4485)
639406eb Update module github.com/pion/ice/v4 to v4.2.3 (#4481)
dc6b7505 reduce some heap use in packet path (#4478)
f3b80b28 fix: wrap IPv6 addresses in brackets in UDP TURN URLs (RFC 3986) (#4476)
3a7f2628 Turn off transceiver re-use on Safari. (#4474)
d84f3d7a add more types to signum (#4473)
701a37c2 Convert sort.Slice -> slices.SortFunc (#4472)
85be9d70 Avoid stream allocator event data cast to interface and back. (#4471)
b43685e8 Keep a shadow copy of tracks for use by different stream allocator state (#4470)
27c2b149 Consolidate RTCP packets and do RTCP callback outside lock. (#4469)
31083307 do not log data track stats if not started (#4468)
9ee06635 feat(pion/ice): replace deprecated NAT1To1 with SetAddressRewriteRules (#4466)
8ccad68d Release v1.11.0 (#4459)
dbf5cf61 Store concrete ICE candidate for remote candidates. (#4458)
2a04bc3c fix publisher frame count reporting for simulcast streams (#4457)
1d804737 fix: limit join request and WHIP request body to http.DefaultMaxHeaderBytes (#4450)
3cfb71e7 Use Muted in TrackInfo to propagated published track muted. (#4453)
69aa9479 Some drive-by clean up (#4452)
6c81f678 Add subscriber stream start event notification (#4449)
ce1bf47b Revert "fix: ensure num_participants is accurate in webhook events (#4265) (#…" (#4448)
cdb0769c fix: ensure num_participants is accurate in webhook events (#4265) (#4422)
c91e79af Switch to stdlib maps, slices (#4445)
ea7b9c6f Update module github.com/livekit/protocol to v1.45.3 (#4435)
97378368 Update go deps (major) (#3179)
d6aef547 Update go deps (#3862)
afc9feae Update github workflows (#4331)
4b385612 chore: pin GH commits and switch to golangci-lint (#4444)
2974ba87 Unsubscribe from data track on close (#4443)
5dc4e90d Apply IPFilter when get local ip (#4440)
88c77dc6 compute agent dispatch affinity from target load (#4442)
8fe99377 Log join duration. (#4433)
0a503a57 Add `Close` method for UpDataTrackManager and call it on participant (#4432)
55912dff Add some simple data track stats (#4431)
050909e6 Enable data tracks by default. (#4429)
72c7e65c chore: log API key during worker registration (#4428)
8a67dd1b Do not close publisher peer connection to aid migration. (#4427)
91e90c10 Add some more logging around migration. (#4426)
c6ddc879 isExpectedToResume is based on whether flushing or not. (#4425)
7d06cfca Keep subscription synchronous when publisher is expected to resume. (#4424)
934f8598 Clean up data track observers on unsubscribe. (#4421)
9674ac48 Cleaning up some logs and standardising log frequency. (#4420)
7b925304 Drop time inverted packets in RED -> Opus conversion. (#4418)
4d8d232a ensure participant init is correctly serialized for logging (#4417)
4fe80877 Log time inversion between incoming packets (#4415)
248d7394 Guard against timestamp inversion in RED -> Opus conversion. (#4414)
9ab8c1d5 clear track notifier observers on subscription teardown (#4413)
397cd09a Embedded turn test (#4412)
56326654 Prepare release v1.10.1 (#4408)
e9b113c8 Make the TURN bind address configurable and allow for multiple addresses. (#4315)
4bc5e6bb Address malformed H264/H265 parsing issues. (#4407)
77a0a4fc AV1 parser overflow fix. (#4405)
ff7fd7ed feat(agent-dispatch): add job restart policy (#4401)
34bd1e08 do not log roll over for padding only packets (#4396)
13d02ee9 add deadline to dtls connect context (#4395)
9055a349 Path check helpers (#4392)
1f1eeb68 Fallback to servicestore if rpc is unavailable (#4391)
59e9bb41 Fix TURN server URL (#4389)
9e0a7e54 Close both peer connections to aid migration. (#4382)
9474c807 route participant reads through PSRPC instead of Redis (#4387)
a5333a86 add packet trailer stripping support (#4361)
0e3d765d Release v1.10.0 (#4380)
bc3aeaf3 Update grpc to address CVE-2026-33186 (#4381)
8cdd6f4c Replace deprecated io/ioutil with io in whipservice (#4375)
89410df7 handle AGENT_ERROR disconnect reason (#4339)
8f984c77 Fix repair stream ID reporting for RTX pairing. (#4369)
cdfaacfc Restart nacker on OOB sequence number restart. (#4368)
4a9e0045 Update protocol. (#4367)
750d5904 Add API to restart lite stats. (#4366)
c8bb2578 Rename log field "pID" to "participantID" for consistency (#4365)
77fc74a7 Do not block all ext ID determination on stream allocator listener (#4364)
90a46fab Do not kick off migration of closed participant (#4363)
5dc2e7b1 Switch data track extension to 1-byte ID/length. (#4362)
7323ad02 Sample data send error logging. (#4358)
0d34e455 Add option to not re-use transceiver in e2ee. (#4356)
95225ff2 don't require media section for dual peerconnection mode (#4354)
e9639538 Refine ipv6 support (#4352)
b34b0472 Add StopEgress function to the EgressLauncher interface (#4353)
69235ed2 update readme (#4340)
db1a8046 defensive check for peer connection instance (#4350)
cb7dc2d0 TEL-405: support originating calls from custom domains (#4349)
7eaaaada Mark last run of grow bucket outside goroutine. (#4348)
caa47522 Add option to require media sections when participant joining (#4347)
087050d1 Release v1.9.12 (#4346)
493e87df Fix SIP client timeout. (#4345)
52c28a93 Log a bit more details of out-of-order TWCC feedback report. (#4343)
516aeabf Use ParticipantTelemetryListener of LocalParticipant. (#4342)
b3510565 Exclude ice restart case from offer answer id mismatch warning (#4341)
303657bc feat: make INSTALL_PATH overridable in install script (#3954)
9d418689 Send participant left event after track unpublished for moved (#4334)
bab91868 do not discount packets lost on duplicate packets (#4333)
939794cf mark + restart (#4329)
75f9c462 Add debug for receiver restart. (#4328)
74891f30 Protect against incorrect temporal layer. (#4327)
f51b2732 Update pion/webrtc and deps to update dtls (#4326)
b81bac0e Key telemetry stats worker using combination of roomID, participantID  (#4323)
2d40449f Update self-hosting deployment documentation link (#4312)
03e90dd7 Fix for some CodeQL reported issues (#4314)
77c858f0 Log stats worker when it not closable. (#4313)
a6035212 ESP32 client info (#4267)
478e486a Copilot suggested improvement to Github Actions permissions (#4310)
cbd2f82d Copilot suggested improvement to Github Actions permissions (#4311)
a9b8d40d Publish is always on publisher peer connection. (#4307)
8ae56406 generate & log egressID for start egress request (#4303)
35d7ef88 Avoid alloc in RTPStatsReceiver.Update (#4302)
bb744916 More optimisation in RTPStats module. (#4298)
cefd5da9 Optimise some bits in rtpstats_receiver (#4297)
52a4b848 Read client protocol from query param (#4294)
195b17f6 Populate client_protocol field in ParticipantInfo (#4293)
370e0a4d Set up audio config in audio level module when config is updated. (#4290)
f3e9b688 Do not increase max expected layer on track info update. (#4285)
a9849340 Avoid logger data race. (#4284)
97016627 Do not hold lock when creating buffer (#4283)
6b68e3d5 Create buffer if needed when a PLI is requested. (#4282)
3cca7180 use separate allocation for signal stats telemetry guard (#4281)
d2bae34d refresh telemetry guard on participant move (#4280)
333f0349 clear reference guard when resetting signal stats (#4279)
d1bab17b Add session duration and participant kind to closing log. (#4277)
1e689e1a Reducing some info level logs. (#4274)
88facc02 adds a test to ensure agent worker errors cause disconnection (#4273)
76a41a7a Generate config flags (#4268)
700e1788 require participant broadcast when metadata/attributes are set in token (#4266)
b61799ec Ignore parse addr error when add remote candidate (#4264)
01bd966f Add silent frame for pcmu/a (#4258)
0c33b8c6 chore: move codecs/mime stuff to protocol (#4255)
165c1735 Update livekit protocol to v1.44.0 (#4254)
343f12b8 Wrapping SIP errors for invalid argument and not found (#4253)
aea044c5 Defer setting clock rate in RTPStats module till codec is bound. (#4250)
d9f716c1 FIx receiver restart race (#4248)
0508ee9f remove copy/paste left over line (#4246)
40408407 Release v1.9.11 (#4245)
f8675507 Do not remove participant from cache on disconnect. (#4241)
a35a6ae7 Add participant option for data track auto-subscribe. (#4240)
07572511 Wrapping the invalid request errors for CreateSipParticipant (#4239)
843d8c3e Update Pion transport package. (#4237)
a05690d2 Changing field naming of data track packet (#4235)
c4015008 Clear participant version cache on disconnect (#4234)
18db4ec1 Log modified timeout of API context. (#4232)
ac20ccda Log timeout in API. (#4231)
541a7d6c Change some logs to debugw (#4229)
52ab3374 Return on SDP fragment read error. (#4228)
7fae5ac9 Do not restart receiver on codec change mid-session. (#4225)
dafdc36e Add option to force simuclast codec. (#4226)
0a7dd40b Use only layer 0 for SVC codecs. (#4224)
4ec0f8f4 Support OpenTelemetry tracing. Add Jaeger support. (#4222)
80ba93fa Do NACK updates as soon as flow state is processed. (#4221)
4405afe2 Use atomic pointer and return interface from RED transformer constructors (#4220)
b649c2fe Remove method not needed from REDTransformer. (#4219)
f0080f35 Remove enable arrival time forwarding method. (#4217)
335f4c33 Swap result sink atomically rather than closing and setting. (#4216)
46651c19 Release v1.9.10 (#4214)
08ac4ecd Support preserving external supplied time. (#4212)
f6efccce report video size from media data for whip (#4211)
d92f6a79 return iceservers for whip (#4210)
1a4758ed Skip restart callback if external. (#4208)
dde4fb49 configurable dependency descriptor restart (#4207)
08793bea Use active at time to check for track not bound timeout. (#4206)
3606ce54 Do not warn about track not bound if participant is not ready. (#4205)
b8ddd0f9 Taking interface{} -> any modernize bits (#4204)
b91cd2e4 Rework receiver restart. (#4202)
bb00c864 Restart API on receiver. (#4200)
25ece1e9 Minor refactor in buffer base and audio level (#4198)
599002f8 ignore PLI requests for non-video (#4196)
2510b946 Taking a bunch of go modernize suggestions. (#4194)
ed8e6afc Handle repair SSRC of simulcast tracks during migration. (#4193)
c6bf7a27 Fix logging key and other clean up around stream restart. (#4192)
3cb9abb6 Update pion/webrtc to v4.2.1 (#4191)
8b0efb8c Resolve RTX pair via OnTrack also. (#4190)
381bce03 Return extended sequence number only and not packet. (#4189)
6bcbf54e Always instantiate nacker when using out-of-band sequence numbers. (#4187)
e71184de Store buffer after creating it.  (#4186)
7c8ea115 Refactor receiver and buffer into Base and higher layer.  (#4185)
4104b827 update protocol (#4183)
cd99fec2 Make new path for signalling v1.5 support. (#4180)
32cd0370 Flush the ext packets on restart/close and release packets. (#4179)
1df1316b Move OnDataTrackMessage to participant listener to support replay. (#4178)
e7601251 Make data message naming a bit more consistent. (#4177)
a04e566d Use published track for model access in data down track. (#4176)
47c86be1 Add support for TURN static auth secret credentials (#3796)
24559a28 chore(deps): update github workflows to v6 (#3866)
0a824386 add explicit room exists servicestore op (#4175)
39bd077d Release v1.9.9 (#4174)
cbb2c617 Publish/Unpublish counter match. (#4173)
fb849edc Minor clean up (#4172)
c28e5e45 fix(kindToProto): update protocol (#4171)
47324abd Drop run away receiver reports. (#4170)
462ec324 prevent uint overflow setting packet not found count (#4169)
5c841b8e Some logging changes. (#4168)
2f2d0a57 skip lost sequence number ranges in getIntervalStats (#4166)
898ebe05 clean up manual roomservice log redaction (#4165)
3e417253 move delete to oss service store (#4164)
5964efbb Ensure subscribe data track handles are unique (#4162)
a26c4830 Add support for RTP stream restart. (#4161)
386f0b38 fix typo in clearing index when removing track from room track manager (#4158)
0abfb251 deregister observability function when participant is closed (#4157)
97aba5e7 Consistently undo update to sequence number and timestamp when the (#4156)
2317c295 Fix panic while removing track from room track manager. (#4153)
a0a28ac5 Avoid duplicate track add to room track manager.  (#4152)
f01008f8 Revert telemetry stats worker wait configuration. (#4151)
ca4b56d2 Handle case of sequence number jump just after start. (#4150)
97099cae Configurable telemetry stats worker clean up wait. (#4148)
d7db7cb3 chore: fix a large number of spelling issues (#4147)
cadf2ad9 Release v1.9.8 (#4145)
498304cd defensive nil check (#4144)
20f6a497 Store ddParser in atomic.Pointer (#4143)
037cb906 release ext packet if patching fails (#4142)
dd598ef2 Release ExtPacket if dependency descriptor or other parsing fails (#4141)
35b0e2bc update webrtc to 4.1.8 to pick up DTLS fingerprint check during handshake (#4140)
1c1a836c Mark RTCP buffer Write as noinline. (#4138)
ea9b2177 protocol deps to get inactive file adjusted memory usage. (#4137)
64f3d1e9 switch participant callbacks to room to listener interface (#4136)
c6e6c021 add debug metric for tracking references (#4134)
a30c79fa Use isEnding to indicate if down track could be resumed. (#4132)
8c241ecf Fix RTCP reader leak in DownTrack. (#4131)
3eef869a Do not pause rid in SDP (#4129)
8e01e595 Release 1.9.7 (#4128)
7c1a0fab Fix concurrent map access. (#4127)
14446b1c Let participant close remove the published tracks. (#4125)
fa0633aa move utils.WrapAround to mediatransportutil (#4124)
f8706cd4 Update pion/ice to stop gather first on close (#4123)
7954748d Data tracks (#4089)
7158d983 log bucket growth (#4122)
04b35eb6 Release v1.9.6 (#4121)
ebdcead5 Update mediatransportutil to get bucket packet size limit. (#4120)
411b09f6 Release v1.9.5 (#4119)
8dcf235a Update pion/ice - attempt to address tcp packet conn close hang (#4116)
64c65143 Update mediatransportutil (#4115)
0a2943bb Clean up bits added to debug peer connection close hang. (#4114)
9c483a69 Use released version v1.8.41 of pion/sctp (#4113)
35c79a57 Update SCTP hash, had the wrong one in previous PR (#4111)
e0fbbef1 Update pion/sctp with RFC9260 revert (#4110)
f3c80917 Try SCTP with read deadline to unblock abort. (#4109)
bd5382da Splitting transport close timeout logs. (#4108)
6d4154b8 Update pion/ice. (#4107)
a6418ae2 Log more peer conenction state on close timeout. (#4105)
06d99974 Check for cancel on unsubscription/source track going away. (#4104)
7f10e18b Record join/publish/subscribe cancellations. (#4102)
40293632 Clear stereo=1 if stereo is not enabled. (#4101)
70f6def3 Add checks for participant and sub-components close. (#4100)
ffbabcc7 Switch forwarding latency log to Debugw (#4098)
27d82a72 Fix "address" typo in transport logs (addddress → address) (#4097)
37a06821 logger proto redaction. (#4090)
54cf7d46 Control latency of lossy data channel (#4088)
5175c1af Lock x/tools at 0.37.0 (#4085)
d510fff1 Downgrade x/tools to be able to make a release (#4084)
c3ea5890 Prepare release v1.9.4. (#4083)
3a128e61 protocol bump for SIP error mapping and validation (#4081)
c3964ba2 Use sync.Pool for objects in packet path. (#4066)
f8b994d4 Forwarding latency measurement tweaks. (#4080)
f4929f09 Revert "Revert pion/transpor to v3.0.8 (#4073)" (#4074)
a04d9c48 Revert pion/transpor to v3.0.8 (#4073)
2d5054ad kind details for connector (#4072)
a272e28a Log raeson for subscriber not being to determine codec. (#4071)
b9b4eec9 Update pion/transport to v3.1.1 (#4070)
b23d093c update protocol (#4069)
4ce07bed Higher resolution forwarding latency histogram. (#4067)
858db7ab fix(deps): update module github.com/livekit/protocol to v1.43.0 (#4015)
1dc9b8fc Use buffered indicator to exclude from forwarding latency.  (#4062)
f117ee51 Track start up delay.  (#4061)
4872f205 Return write count from WriteRTP. (#4059)
d0ba46b4 Log write count atomic. (#4057)
ae5fb7e8 Add packet to forwarding stats only if packet is forwarded. (#4056)
f6909192 Update PsRPC to get redis pipeliner implementation. (#4055)
ca3c507b Prevent invalid track access while peer connection is shutting down. (#4054)
9ca6ee00 Use replace so that x/tools does not get overridden (#4048)
b9323eab chore(deps): downgrade x/tools for counterfeiter (#4047)
2f1e6c36 Prep release v1.9.3 (#4046)
9d5c351d Fix prom units for forwarding latency/jitter. (#4045)
e183657c Add prom histogram for forwarding latency and jitter. (#4044)
1eefeb30 Enable AbsCaptureTimeURI in RTC configuration (#4043)
075a7576 Use simulcast codec as default policy for audio track (#4040)
c264b504 Don't warn 0 payload type for PCMU (#4039)
32fc3525 Broadcast cond var on RTX write. (#4038)
061eb8b4 AddDownTrack to regressed codec after restarting forwarder. (#4037)
c87eb8ed fix: add missing Unlock() in AddReceiver (#4036)
70444924 if RingingTimeout is provided, deadline should be set to that timeout. (#4018)
ab906d71 Prevent leakage of previous codec after codec regression. (#4035)
79b03f97 Log queueing latency when encountering high forwarding latency (#4034)
29117b14 set max layer in allocation (#4033)
15b19ccd Remove ~ from rid which indicates disabled layer to get the actual rid (#4032)
34e16a87 Check more conditions for opportunistic alloc. (#4031)
81fbd355 Use the optimal allocation function for opportunistic allocation. (#4030)
a2ce73e0 Do not bind buffer if codec is invalid. (#4028)
cef6fdb7 Correct direction for request/response for prom counters. (#4027)
5042c06c Use rtp converter from protocol/utils/rtputil (#4020)
5a426d15 Use rtp converter from protocol/utils (#4019)
35fb8877 feat: use env var for GOARCH (#4012)
c0397696 Issue #1 only: Fix spatial layer initialization in Forwarder (#4003)
2afbf0e8 Some golang modernisation bits. (#4016)
484f784a Prepare release v1.9.2 (#4011)
ad074ed2 counterfeiter needs an older version of x/tools (#4009)
e63e8b6f Include mid -> trackID in both SDP offer and answer. (#4007)
781dfede Do not call receiver methods under settings lock. (#4006)
69ff25a0 Use answer with mid -> trackID mapping when in single peer connection (#4005)
fe912acf Update pion/webrtc to prevent GetStats panic. (#4004)
7930dcde Do not try to read stats from peer connection after close. (#4002)
ca0d5ee9 Count request/response packets on both client and server side. (#4001)
dd62eb00 Resort to full search for requested quality is not available. (#4000)
f6ca82d1 Revert to using silence packets for audio dummy start. (#3999)
0e2c59c8 Sort codec layers when adding track (#3998)
100bb46a Adding ProviderInfo to GetSIPTrunkAuthenticationResponse (#3993)
a8d4df66 "Power of Two Random Choices" option for node selection  (#3785)
a20bbe34 Log RPC details. (#3991)
158496bc Increment RTP timestamp on padding when using dummy start. (#3989)
4f6ed65d Limit check to red + opus when looking for primary codec match. (#3988)
a87f6c4b Allow passing inline trunk for outbound calls. (#3987)
bf06596f Support Opus mixed with RED when encrypted. (#3986)
ea208a1c Add encryption datapacket type (#3869)
2a6adbe8 Use padding only packets for dummy start of audio. (#3984)
be018f97 Provide the InputVideo/AudioState to Ingress in WHIPRTCConnectionNotify (#3982)
146bd969 Do not panic of redis is not configured (#3981)
01337ba7 Do not start forawarding on out-of-order packet. (#3985)
c7f625d6 Do not force codec regression between opus and red. (#3980)
3bd20ddb Revert unintentional change to not handle transport fallback on (#3970)
89a2f46c Update deps to fix redis issue when 1 cluster address is provided (#3969)
060719d1 add config for user data recording (#3966)
b3ee219c fix stats worker closed condition (#3965)
3d737031 add idempotent reference count to telemetry stats worker (#3964)
735c663a Update protocol for EventKey helper. (#3963)
646b9de8 Add node_ip to config-sample.yaml (#3960)
0bf7b178 avoid logging on small values (#3958)
00ff2ab9 Adjust for hold time when fowarding RTCP report. (#3956)
e180be06 short circuit participant broadcast filter in livestream mode (#3955)
bfba6fee Adjust stream allocator ping interval based on state. (#3951)
3837006b Revert "Switch ops queue a singly linked list. (#3949)" (#3950)
990c5faf feat: server rpc apis (#3904)
80b11662 Switch ops queue a singly linked list. (#3949)
56ee2328 handle terminated job requests (#3948)
49f9b9c8 Flush stats when there are no packets. (#3947)
e6a3df1e ForwarStats.GetStats needs to be public (#3946)
824d116b Tweaks tresholds for logging high forwarding latency/jitter. (#3945)
408492e0 Log some information around high forwarding latency. (#3944)
6a41fae5 Use microseconds for forwarding stats. (#3943)
856e0871 mediatransportutil to log local address when validating external IP (#3942)
40101cf7 Update protocol for SipCreateParticipant (#3939)
b07e7a38 Use difference in key frame counter to stop seeder. (#3936)
d7f92878 Avoid matching on empty track id. (#3937)
56fb2885 Do DD restart only if DD structure is present. (#3935)
86facce9 More debugging of DD jump (#3934)
6058a3f6 Add debugging from DD frame number wrap around. (#3933)
dc6825c0 mediatransportutil crash fix for logging local address (#3930)
d6f0588f Update mediatransportutil to log external IP found via STUN. (#3929)
2c30a064 Fix dynacast subscriber node clearing on move participant. (#3926)
6489237e Simulcast audio fixes (#3925)
9f0ab870 Wait for `SetRemoteDescription` before configuring senders. (#3924)
df6c26db Subscrbed audio codecs - update from remote nodes. (#3921)
798fa761 Support simulcasting of audio (#3920)
f4a06cf0 Clean code as there is no oss sweeper for ingress (#3918)
5f561b4f Include agent_name as a participant attribute (#3914)
782a35e8 update protocol for psrpc (#3915)
eee2001a Set publisher codec preferences after setting remote description (#3913)
fc995533 add incoming request id to request response message (#3912)
76645fad Rpcs for ingress proxy WHIP (#3911)
991a4a4f Refactor subscribedTrack + mediaTrackSubscriptions. (#3908)
e16b3ba9 Use gzip reader pool (#3903)
17c34921 update protocol for sip api change (#3902)
2f43a575 Release candidate for v1.9.1 (#3899)
07c40cf3 Use `RequestResponse` to report protocol handling errors (#3895)
98352fd0 Prevent race in determining BWE type. (#3891)
f7291fda Do not send both asb-send-time and twcc. (#3890)
21b42fa6 Do not advertise NACK for RED. (#3889)
6633bf93 Use departure timeout from room preset. (#3888)
38f7906e Handle migration better in single peer connection case. (#3886)
5026de2b handle frame number wrap back in svc (#3885)
091e3c13 Revert to using answer for migration case. (#3884)
2aeadf14 init ua parser once (#3883)
998a9f94 Switch known rids from 012 -> 210, used by OBS. (#3882)
890fd942 Single peer connection mode (#3873)
bfe98eaa fix: ensure the participant kind is set on refresh tokens (#3881)
8d270e2a chunk room updates (#3880)
b4e146c5 update mediatransport util for ice port 3478 (#3877)
dc3a7753 Fix timeout handing in StopEgress (#3876)
d62336e1 Remove unnecessary check (#3870)
c58e5d23 Update golang Docker tag to v1.25 (#3864)
98d577ee Update module github.com/livekit/protocol to v1.40.0 (#3865)
afbf541e Update pion deps (#3863)
b660c3b5 Extract video size from media stream (#3856)
456b8709 Fix missed unlock (#3861)
d500806e Handle no codecs in track info. (#3859)
11b240d6 Log track settings more. (#3853)
1aa0f963 Log signal messages on media node. (#3852)
b182d07b Log signal messages as debug. (#3851)
a370bb20 Support G.711 A-law and U-law (#3849)
fa5f4ef3 Populate SDP cid in track info when available. (#3845)
eed27885 Send `participant_connection_aborted` when participant session is closed (#3848)
61e59346 Update go deps (#3439)
1b228913 Support video layer mode from client and make most of the code mime aware (#3843)
f2da4444 Support per simulcast codec layers. (#3840)
f275f592 handle SyncState in join request (#3839)
5d44cf6d Use wrapped join request to be able to support compressed and uncompressed. (#3838)
5ca16264 Support join request as proto + base64 encoded query param (#3836)
7dea1012 Clean up missed v2 pieces (#3837)
34a49130 Delete v2 signalling (#3835)
1fe33716 Fix: RingingTimeout was being skipped for transferParticipant (#3831)
5751692a deps (#3829)
db4bc127 Get to the point of connecting publisher PC and using it for async signalling (#3822)
5e483e75 update readme (#3809)
e3155b14 Get to the point of establishing subscriber peer connection. (#3821)
a7ce1382 HTTP DELETE of participant session (#3819)
01de0e36 Do not send leave if nil (to older clients) (#3817)
10103449 Add country label to edge prom stats. (#3816)
68387b41 Minor tweak to keep RPC type at service level. (#3815)
a75295fc More v2 signalling changes (#3814)
b20db94d Validation end point for v2 signalling. (#3811)
f2f595f4 update readme (#3808)
fffc2ac0 Use signalling utils from protocol (#3807)
f5fc82d3 Filling out messages unlikely to change in v2. (#3806)
1c99b9ad Split signal segmenter and reassembler. (#3805)
0a1bfd30 Signal handling interfaces and participant specific HTTP PATCH. (#3804)
7837c8e5 starting signaller interface (#3802)
18ce5244 Handle Metadata field from RoomConfig (#3798)
2a6a9b8a Grouping all signal messages into participant_signal. (#3801)
078c01fa Signal v2: envelope and fragments as wire message format. (#3800)
b9a44c3f Signalling V2 protocol implementation start (#3794)
ba702a53 forward agent id to job state (#3786)
1f31d430 Map ErrNoResponse to ErrRequestTimedOut in StopEgress to avoid returning 503 (#3788)
51bbe8c5 Set participant active when peerconnection connected (#3790)
40028dc3 Normalize known rids. (#3779)
ddd92329 Return default layer for invalid rid + track info combination. (#3778)
8c033ce9 Enable H265 by default (#3773)
7678e087 Set rids for all codecs. (#3772)
5d636acf Limit taking rids from SDP only in WHIP path. (#3771)
4d09e5b5 Log SDP rids to understand the mapping better. (#3770)
c69f1aae Revert "Temporary change: use pre-defined rids" (#3769)
8197438e bounds check layer index (#3768)
d11da5f5 Temporary change: use pre-defined rids (#3767)
cb4da533 fix(deps): update module github.com/livekit/protocol to v1.39.3 (#3733)
d6d2b6d8 feat(cli-flags): add option for cpu profiling (#3765)
9fc4ddbe ClearAllReceivers interface is used to pause relay tracks. (#3761)
1216113b Do not need to just clean up receivers. Remove that interface. (#3760)
ef6c38ce Log previous allocation to see changes. (#3759)
01bf9685 SVC with RID -> spatial layer mapping (#3754)
c481396f offer could be nil when migrating. (#3752)
8c2fc0bc Fix svc encoding for chrome mobile on iOS (#3751)
e467daa0 move egress roomID load to launcher (#3748)
3783ebb3 feat(cli): update to urfave/cli/v3 (#3745)
03d3fcab Fix data packet ParticipantIdentity override logic in participant.go (#3735)
068b4366 reuse compiled client config scripts (#3743)
e754a860 return error when moving egree/agent participant (#3741)
7542cf07 remove unused code (#3740)
9d569e2f Take ClientInfo from request. (#3738)
80774263 chore: set workerid on job creation (#3737)
5549ab55 Revert clearing RIDs. (#3732)
ae967313 Clear rids if not present in SDP. (#3731)
0e033907 Return highest available layer if requested quality is higher than (#3729)
9ce737db Add log for dropping out of order reliable message (#3728)
1b95e818 Don't check bindState on downtrack.Bind (#3726)
670f927f Set and use rid/spatial layer in TrackInfo. (#3724)
a9e29116 Add Id to SDP signalling messages. (#3722)
4ec828ce Fix bug with SDP rid, clear only overflow. (#3723)
8f6c3a9b Clear rids from default for layers not published. (#3721)
ce07740e Add simulcast support for WHIP. (#3719)
e98fb94f Create client config manager in room manager constructor. (#3718)
fdf9b852 e2e reliability for data channel (#3716)
35dda8ea swap pub/sub track metrics (#3717)
1d9a4366 Do not require create permission for WHIP participant. (#3715)
e0aea17a Flush stats on close (#3713)
630aa7d9 implement observability for room metrics (#3712)
e7f0294e remove unused ws signal read loop (#3709)
77f70b18 for real, pick up protocol change for webhooks queue length bucnkets (#3707)
7b180646 protocol dep for webhook stats buckets (#3706)
b0ab95ba warn about credentials when used in tokens (#3705)
a72ce30f Small changes to add/use helper functions for length checks. (#3704)
425f6bb3 Allow passing extra attributes to RTC endpoint. (#3693)
758e1762 Add a trend check before declaring joint queuing region. (#3701)
fe81e411 Adds Devin to readme so it auto updates DeepWiki weekly (#3699)
09dede35 version bump v1.9.0 (#3698)
fc867c5b Webhook prom stats (#3697)
0e17916f Do not use Redis pipeline for SIP delete. Fixes Redis clustering support. (#3694)
1b760393 WHIP support. (#3692)
e4f7d81b add client ip to agent worker registration (#3675)
6b849a87 update mediatransportutil for sctp congestion control (#3673)
83b189b0 Add ServerInfo to ReconnectResponse (#3671)
5f87a35b Prevent operating on swapped out map. (#3670)
13b55a80 move agent token (#3669)
c9385edd handle agent worker jwt (#3668)
3b359d8b Use logger resolver reset to reset contexts. (#3665)
dbb70e0f Fix dynacast quality for moving out tracks (#3664)
0a5f3c2a resolve new room name logger earlier when moving participant (#3662)
2df05517 Revert unbound transceiver stop. (#3661)
5172af15 ~Send initial participant update only after a participant becomes active.~ - General clean up (#3655)
5d4d86f8 protocol to pick resolver values replace (#3659)
7f8e6323 Send self participant update immediately. (#3656)
11630878 Use unordered for lossy data channel. (#3653)
aee34ffe log request for agent dispatch api (#3650)
793b383a Add Moving participant to another room (#3648)
2fff36cb Stub MoveParticipant so that cloud can include the latest protocol. (#3646)
d4ab1142 Redact address (#3643)
b83190a3 protocol update to fix memory stats path (#3642)
a1f4e88e Update protocol to latest, got bit by tag (#3641)
8d3902af Protocol to pick up cgroups v2 memory path fix (#3640)
2f002388 Use participant close reason in remove. (#3639)
58822c26 Include clientInfo in connectivity logs. (#3638)
6d6393a6 Take AudioFeatures from AddTrack. (#3635)
08670412 Limit buffer queue before Bind. (#3634)
9f5bc9b9 Avoid synthesising duplicate feature. (#3632)
847239c3 Disable vp9 for safari 18.4 (#3631)
f69ab680 Populate the sender identity when translating to user packet. (#3628)
e1490558 Forward data between WHIP client and non-WHIP client (#3627)
6739e7bc Broadcast inside lock (#3626)
f24152b4 Call Broadcast in lock scope. (#3625)
b760918a Use logger from request context. (#3623)
34a2e2c1 Check for multiple layers for managed track. (#3622)
4955ebe4 Forward transfer headers to internal request (#3615)
d9ee9214 Set up RTX for WHIP publish (#3619)
d8cf5439 Determine TURN connection type and no fallback for TURN/TLS. (#3612)
d0d212fd Fix WHIP ICE restart. (#3616)
5e7f8a12 Update mediatransportutil for max sctp message (65K) (#3611)
28dfac14 Use exported GetEgressNotifyOptions (#3604)
75236bef protocol update to fix IPv6 SDP fragment parsing (#3603)
2130980d Add basic video support to WHIP. (#3602)
e5cbb227 Allow specifying extra webhooks with egress requests (#3597)
7e16106a Add OnSubscirberReady callback on LocalParticipant. (#3600)
5c2d96b9 Check DestinationRoom of VideoGrant for participant forwarding (#3599)
2e236a19 Revert participant state ACTIVE change. (#3598)
35ac5f56 Add support for WHIP ICE Trickle/Restart. (#3596)
ec2dff96 Fix SIP updates when replacing slices. (#3592)
e24fe77b map PEER_CONNECTION_DISCONNECTED -> CONNECTION_TIMEOUT (#3591)
6ee6eb43 Do not drop audio codecs (#3590)
68357ba6 List audio codecs after video codecs. (#3589)
05a891ff Fix rule (had an extra bracket) (#3588)
d7c41091 Exclude RED from enabled codecs for Flutter + 2.4.2 + Android. (#3587)
ee08aede skip out of order participant state updates (#3583)
15a8d9a2 Break track published fuse when there are no tracks (#3581)
35f83c51 Replace Promise with Fuse. (#3580)
07fe9b72 Prevent migration race. (#3579)
ac8082ff Use older SDP module to accommodate bad SDP. (#3578)
1c8307c7 Use cgroup for memstats. (#3573)
e9be0fca log SDP offer on error (#3577)
3238ab8d Calculate rates for memory used and total. (#3570)
d08487bf Unlabeled (pass through) data channels. (#3567)
52ce18d5 fix: revert recent changes to determine simulcast from sdp (#3565)
cdfbb106 Audio uses signal SignalCid and SdpCid. (#3564)
ed5e2f16 Keep simulcast information tied to receiver. (#3563)
ad010cfc chore(logs): log VLS type for VP9/AV1 (#3561)
8cc17f8f Rework node stats a bit. (#3555)
15f56551 fix(video): determine svc/simulcast from SDP for advanced codecs (#3549)
2b6a46f4 Handle `prefer_regression` for backup codec (#3554)
b0abb0ae Add option to use different pacer with send side bwe. (#3552)
26822b6b ParseUsername utility for TURN user name. (#3547)
55909ed7 log the initial join response (#3546)
97fcb82a Fix: Return NotFoundErr instead of Unavailable when the participant does not exist in UpdateParticipant. (#3543)
75d0e18e Implement SIP update API. (#3141)
e118aff1 Fire track subscribed when the subscriber connected (#3540)
13417c01 Send mute event only on change (#3537)
7f4c4597 Stubs for SIP update API. (#3533)
fe673bb2 Send regressed codec upstream stats to analytics. (#3532)
8eb81388 Use a generation to counter to stop key frame seeder on codec change (#3531)
188470a2 Do not accept unsupported track type in AddTrack (#3530)
507fc9cf Do not instantiate 0 sized sequencer. (#3529)
20bddfea Clean up published track on participant removal. (#3527)
65d8aa28 Handle subscribe race with track close better. (#3526)
a6cb00b3 Reduce seeder duration to 30s and also do not force send PLI. (#3525)
c8233205 Add a key frame seeder in up track. (#3524)
0f61ff3a Remove redundant log (#3523)
7685cd25 Log ParticipantInit on signal start to get a picture of join params (#3522)
ac9e62ef add server agent load threshold config (#3520)
cd5d32f0 Add pID and connID to log context to make it easier to search using pID. (#3518)
2d9aa6dd Update api call info method (#3515)
b3779a90 WebHookConfig (#3517)
6121b9af Check ForwardParticipant room name (#3514)
9a7c9442 mediatransportutil update (#3511)
50ab47c1 Log packet drops/forward. (#3510)
139d1b13 Add ForwardParticipant method to room service (#3507)
6c04909f Use atomic to store codec. (#3505)
7f6afe05 Prevent bind lock deadlock on muted. (#3504)
48063df5 load mime type before calling writeBlankFrameRTP (#3502)
d2e6cd15 Do not bind lock across flush which could take time (#3501)
47896f50 Update protocol and IO service. (#3499)
1dc42eef Bump github.com/go-jose/go-jose/v3 from 3.0.3 to 3.0.4 (#3497)
3a35cbc4 Log migration complete only when coming from sync (#3496)
c2f17a10 refactor: using slices.Contains to simplify the code (#3495)
01e51dbd fix: fix the wrong error return value (#3493)
ff9115b2 Disable dd parser for vp8 if extension is not found (#3492)
c3e06f05 Do not attempt to create objects for URL ingresses as the ingress service will do so (#3491)
f0edfbba Fix receiver rtt/jitter. (#3487)
05dfd30d Take RTT and jitter from receiver view while reporting track stats for (#3483)
04ed5683 Don't issue TrackPublished/Unpublished event on migrated track (#3482)
1cffe30c Use a RED transformer to consolidate both RED -> Opus OR Opus -> RED (#3481)
591888f7 Fix missing RTCP sender report when forwarding RED as Opus. (#3480)

git-subtree-dir: livekit-server
git-subtree-split: 46c4309554d37d23ee8da88a8a7e02a68fba09c1
This commit is contained in:
2026-06-25 23:54:33 +09:00
parent 0da97ebd21
commit 6bd7fac875
291 changed files with 37631 additions and 15381 deletions
+33 -4
View File
@@ -16,11 +16,16 @@ package service
import (
"context"
"fmt"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/protocol/agent"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
)
type AgentDispatchService struct {
@@ -45,11 +50,16 @@ func NewAgentDispatchService(
}
func (ag *AgentDispatchService) CreateDispatch(ctx context.Context, req *livekit.CreateAgentDispatchRequest) (*livekit.AgentDispatch, error) {
AppendLogFields(ctx, "room", req.Room, "request", logger.Proto(redactCreateAgentDispatchRequest(req)))
err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, twirpAuthError(err)
}
if err := agent.ValidateDeployment(req.GetDeployment()); err != nil {
return nil, psrpc.NewError(psrpc.InvalidArgument, err)
}
if ag.roomAllocator.AutoCreateEnabled(ctx) {
err := ag.roomAllocator.SelectRoomNode(ctx, livekit.RoomName(req.Room), "")
if err != nil {
@@ -63,15 +73,18 @@ func (ag *AgentDispatchService) CreateDispatch(ctx context.Context, req *livekit
}
dispatch := &livekit.AgentDispatch{
Id: guid.New(guid.AgentDispatchPrefix),
AgentName: req.AgentName,
Room: req.Room,
Metadata: req.Metadata,
Id: guid.New(guid.AgentDispatchPrefix),
AgentName: req.AgentName,
Room: req.Room,
Metadata: req.Metadata,
RestartPolicy: req.RestartPolicy,
Deployment: req.Deployment,
}
return ag.agentDispatchClient.CreateDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), dispatch)
}
func (ag *AgentDispatchService) DeleteDispatch(ctx context.Context, req *livekit.DeleteAgentDispatchRequest) (*livekit.AgentDispatch, error) {
AppendLogFields(ctx, "room", req.Room, "request", logger.Proto(req))
err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, twirpAuthError(err)
@@ -81,6 +94,7 @@ func (ag *AgentDispatchService) DeleteDispatch(ctx context.Context, req *livekit
}
func (ag *AgentDispatchService) ListDispatch(ctx context.Context, req *livekit.ListAgentDispatchRequest) (*livekit.ListAgentDispatchResponse, error) {
AppendLogFields(ctx, "room", req.Room, "request", logger.Proto(req))
err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, twirpAuthError(err)
@@ -88,3 +102,18 @@ func (ag *AgentDispatchService) ListDispatch(ctx context.Context, req *livekit.L
return ag.agentDispatchClient.ListDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
}
func redactCreateAgentDispatchRequest(req *livekit.CreateAgentDispatchRequest) *livekit.CreateAgentDispatchRequest {
if req.Metadata == "" {
return req
}
clone := utils.CloneProto(req)
// replace with size of metadata to provide visibility on request size
if clone.Metadata != "" {
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
}
return clone
}
+63 -50
View File
@@ -17,7 +17,6 @@ package service
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"slices"
@@ -43,13 +42,19 @@ import (
"github.com/livekit/psrpc"
)
const agentWorkerLoadTarget = 0.65
type AgentSocketUpgrader struct {
websocket.Upgrader
}
func (u AgentSocketUpgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*websocket.Conn, agent.WorkerProtocolVersion, bool) {
func (u AgentSocketUpgrader) Upgrade(
w http.ResponseWriter,
r *http.Request,
responseHeader http.Header,
) (
conn *websocket.Conn,
registration agent.WorkerRegistration,
ok bool,
) {
if u.CheckOrigin == nil {
// allow connections from any origin, since script may be hosted anywhere
// security is enforced by access tokens
@@ -61,29 +66,31 @@ func (u AgentSocketUpgrader) Upgrade(w http.ResponseWriter, r *http.Request, res
// reject non websocket requests
if !websocket.IsWebSocketUpgrade(r) {
w.WriteHeader(404)
return nil, 0, false
return
}
// require a claim
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil || !claims.Video.Agent {
handleError(w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return nil, 0, false
HandleError(w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return
}
registration = agent.MakeWorkerRegistration()
registration.ClientIP = GetClientIP(r)
// upgrade
conn, err := u.Upgrader.Upgrade(w, r, responseHeader)
if err != nil {
handleError(w, r, http.StatusInternalServerError, err)
return nil, 0, false
HandleError(w, r, http.StatusInternalServerError, err)
return
}
var protocol agent.WorkerProtocolVersion = agent.CurrentProtocol
if pv, err := strconv.Atoi(r.FormValue("protocol")); err == nil {
protocol = agent.WorkerProtocolVersion(pv)
registration.Protocol = agent.WorkerProtocolVersion(pv)
}
return conn, protocol, true
return conn, registration, true
}
func DispatchAgentWorkerSignal(c agent.SignalConn, h agent.WorkerSignalHandler, l logger.Logger) bool {
@@ -105,8 +112,8 @@ func DispatchAgentWorkerSignal(c agent.SignalConn, h agent.WorkerSignalHandler,
return true
}
func HandshakeAgentWorker(c agent.SignalConn, serverInfo *livekit.ServerInfo, protocol agent.WorkerProtocolVersion, l logger.Logger) (r agent.WorkerRegistration, ok bool) {
wr := agent.NewWorkerRegisterer(c, serverInfo, protocol)
func HandshakeAgentWorker(c agent.SignalConn, serverInfo *livekit.ServerInfo, registration agent.WorkerRegistration, l logger.Logger) (r agent.WorkerRegistration, ok bool) {
wr := agent.NewWorkerRegisterer(c, serverInfo, registration)
if err := c.SetReadDeadline(wr.Deadline()); err != nil {
return
}
@@ -136,6 +143,7 @@ type AgentHandler struct {
workers map[string]*agent.Worker
jobToWorker map[livekit.JobID]*agent.Worker
keyProvider auth.KeyProvider
targetLoad float32
namespaceWorkers map[workerKey][]*agent.Worker
roomKeyCount int
@@ -150,12 +158,14 @@ type AgentHandler struct {
}
type workerKey struct {
agentName string
namespace string
jobType livekit.JobType
agentName string
namespace string
jobType livekit.JobType
deployment string
}
func NewAgentService(conf *config.Config,
func NewAgentService(
conf *config.Config,
currentNode routing.LocalNode,
bus psrpc.MessageBus,
keyProvider auth.KeyProvider,
@@ -180,6 +190,7 @@ func NewAgentService(conf *config.Config,
keyProvider,
logger.GetLogger(),
serverInfo,
conf.Agents.TargetLoad,
agent.RoomAgentTopic,
agent.PublisherAgentTopic,
agent.ParticipantAgentTopic,
@@ -187,9 +198,9 @@ func NewAgentService(conf *config.Config,
return s, nil
}
func (s *AgentService) ServeHTTP(writer http.ResponseWriter, r *http.Request) {
if conn, protocol, ok := s.upgrader.Upgrade(writer, r, nil); ok {
s.HandleConnection(r.Context(), NewWSSignalConnection(conn), protocol)
func (s *AgentService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if conn, registration, ok := s.upgrader.Upgrade(w, r, nil); ok {
s.HandleConnection(r.Context(), NewWSSignalConnection(conn), registration)
conn.Close()
}
}
@@ -199,6 +210,7 @@ func NewAgentHandler(
keyProvider auth.KeyProvider,
logger logger.Logger,
serverInfo *livekit.ServerInfo,
targetLoad float32,
roomTopic string,
publisherTopic string,
participantTopic string,
@@ -211,14 +223,15 @@ func NewAgentHandler(
namespaceWorkers: make(map[workerKey][]*agent.Worker),
serverInfo: serverInfo,
keyProvider: keyProvider,
targetLoad: targetLoad,
roomTopic: roomTopic,
publisherTopic: publisherTopic,
participantTopic: participantTopic,
}
}
func (h *AgentHandler) HandleConnection(ctx context.Context, conn agent.SignalConn, protocol agent.WorkerProtocolVersion) {
registration, ok := HandshakeAgentWorker(conn, h.serverInfo, protocol, h.logger)
func (h *AgentHandler) HandleConnection(ctx context.Context, conn agent.SignalConn, registration agent.WorkerRegistration) {
registration, ok := HandshakeAgentWorker(conn, h.serverInfo, registration, h.logger)
if !ok {
return
}
@@ -243,13 +256,13 @@ func (h *AgentHandler) registerWorker(w *agent.Worker) {
h.workers[w.ID] = w
key := workerKey{w.AgentName, w.Namespace, w.JobType}
key := workerKey{w.AgentName, w.Namespace, w.JobType, w.Deployment}
workers := h.namespaceWorkers[key]
created := len(workers) == 0
if created {
nameTopic := agent.GetAgentTopic(w.AgentName, w.Namespace)
nameTopic := agent.GetAgentTopic(w.AgentName, w.Namespace, w.Deployment)
var typeTopic string
switch w.JobType {
case livekit.JobType_JT_ROOM:
@@ -260,8 +273,6 @@ func (h *AgentHandler) registerWorker(w *agent.Worker) {
typeTopic = h.participantTopic
}
fmt.Println(">>> register worker", typeTopic)
err := h.agentServer.RegisterJobRequestTopic(nameTopic, typeTopic)
if err != nil {
h.mu.Unlock()
@@ -310,7 +321,7 @@ func (h *AgentHandler) deregisterWorker(w *agent.Worker) {
delete(h.workers, w.ID)
key := workerKey{w.AgentName, w.Namespace, w.JobType}
key := workerKey{w.AgentName, w.Namespace, w.JobType, w.Deployment}
workers, ok := h.namespaceWorkers[key]
if !ok {
@@ -332,7 +343,7 @@ func (h *AgentHandler) deregisterWorker(w *agent.Worker) {
)
delete(h.namespaceWorkers, key)
topic := agent.GetAgentTopic(w.AgentName, w.Namespace)
topic := agent.GetAgentTopic(w.AgentName, w.Namespace, w.Deployment)
switch w.JobType {
case livekit.JobType_JT_ROOM:
@@ -383,7 +394,7 @@ func (h *AgentHandler) JobRequest(ctx context.Context, job *livekit.Job) (*rpc.J
logger = logger.WithValues("participant", job.Participant.Identity)
}
key := workerKey{job.AgentName, job.Namespace, job.Type}
key := workerKey{job.AgentName, job.Namespace, job.Type, job.Deployment}
attempted := make(map[*agent.Worker]struct{})
for {
selected, err := h.selectWorkerWeightedByLoad(key, attempted)
@@ -395,28 +406,30 @@ func (h *AgentHandler) JobRequest(ctx context.Context, job *livekit.Job) (*rpc.J
logger := logger.WithValues("workerID", selected.ID)
attempted[selected] = struct{}{}
state, err := selected.AssignJob(ctx, job)
if err != nil {
state, err := selected.AssignJob(ctx, job, nil)
switch state.GetStatus() {
case livekit.JobStatus_JS_RUNNING:
logger.Infow("assigned job to worker", "apiKey", selected.APIKey())
h.mu.Lock()
h.jobToWorker[livekit.JobID(job.Id)] = selected
h.mu.Unlock()
err = h.agentServer.RegisterJobTerminateTopic(job.Id)
if err != nil {
logger.Errorw("failed to register JobTerminate handler", err)
}
fallthrough
case livekit.JobStatus_JS_SUCCESS:
return &rpc.JobRequestResponse{
State: state,
}, nil
default:
retry := utils.ErrorIsOneOf(err, agent.ErrWorkerNotAvailable, agent.ErrWorkerClosed)
logger.Warnw("failed to assign job to worker", err, "retry", retry)
if retry {
continue // Try another worker
if !retry {
return nil, err
}
return nil, err
}
logger.Infow("assigned job to worker")
h.mu.Lock()
h.jobToWorker[livekit.JobID(job.Id)] = selected
h.mu.Unlock()
err = h.agentServer.RegisterJobTerminateTopic(job.Id)
if err != nil {
logger.Errorw("failed to register JobTerminate handler", err)
}
return &rpc.JobRequestResponse{
State: state,
}, nil
}
}
@@ -426,12 +439,12 @@ func (h *AgentHandler) JobRequestAffinity(ctx context.Context, job *livekit.Job)
var affinity float32
for _, w := range h.workers {
if w.AgentName != job.AgentName || w.Namespace != job.Namespace || w.JobType != job.Type {
if w.AgentName != job.AgentName || w.Namespace != job.Namespace || w.JobType != job.Type || w.Deployment != job.Deployment {
continue
}
if w.Status() == livekit.WorkerStatus_WS_AVAILABLE {
affinity += max(0, agentWorkerLoadTarget-w.Load())
affinity += max(0, h.targetLoad-w.Load())
}
}
+19 -6
View File
@@ -58,7 +58,7 @@ func NewAPIKeyAuthMiddleware(provider auth.KeyProvider) *APIKeyAuthMiddleware {
}
func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if r.URL != nil && r.URL.Path == "/rtc/validate" {
if r.URL != nil && (r.URL.Path == "/rtc/validate" || r.URL.Path == "/rtc/v1/validate") {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
@@ -67,7 +67,7 @@ func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
if authHeader != "" {
if !strings.HasPrefix(authHeader, bearerPrefix) {
handleError(w, r, http.StatusUnauthorized, ErrMissingAuthorization)
HandleError(w, r, http.StatusUnauthorized, ErrMissingAuthorization)
return
}
@@ -80,19 +80,19 @@ func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
if authToken != "" {
v, err := auth.ParseAPIToken(authToken)
if err != nil {
handleError(w, r, http.StatusUnauthorized, ErrInvalidAuthorizationToken)
HandleError(w, r, http.StatusUnauthorized, ErrInvalidAuthorizationToken)
return
}
secret := m.provider.GetSecret(v.APIKey())
if secret == "" {
handleError(w, r, http.StatusUnauthorized, errors.New("invalid API key: "+v.APIKey()))
HandleError(w, r, http.StatusUnauthorized, errors.New("invalid API key: "+v.APIKey()))
return
}
grants, err := v.Verify(secret)
_, grants, err := v.Verify(secret)
if err != nil {
handleError(w, r, http.StatusUnauthorized, errors.New("invalid token: "+authToken+", error: "+err.Error()))
HandleError(w, r, http.StatusUnauthorized, errors.New("invalid token: "+authToken+", error: "+err.Error()))
return
}
@@ -219,6 +219,19 @@ func EnsureSIPCallPermission(ctx context.Context) error {
return nil
}
func EnsureDestRoomPermission(ctx context.Context, source livekit.RoomName, destination livekit.RoomName) error {
claims := GetGrants(ctx)
if claims == nil || claims.Video == nil {
return ErrPermissionDenied
}
if !claims.Video.RoomAdmin || source != livekit.RoomName(claims.Video.Room) || destination != livekit.RoomName(claims.Video.DestinationRoom) {
return ErrPermissionDenied
}
return nil
}
// wraps authentication errors around Twirp
func twirpAuthError(err error) error {
return twirp.NewError(twirp.Unauthenticated, err.Error())
-42
View File
@@ -19,12 +19,8 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
)
//counterfeiter:generate . IOClient
@@ -35,41 +31,3 @@ type IOClient interface {
CreateIngress(ctx context.Context, req *livekit.IngressInfo) (*emptypb.Empty, error)
UpdateIngressState(ctx context.Context, req *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error)
}
type egressLauncher struct {
client rpc.EgressClient
io IOClient
}
func NewEgressLauncher(client rpc.EgressClient, io IOClient) rtc.EgressLauncher {
if client == nil {
return nil
}
return &egressLauncher{
client: client,
io: io,
}
}
func (s *egressLauncher) StartEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
if s.client == nil {
return nil, ErrEgressNotConnected
}
// Ensure we have an Egress ID
if req.EgressId == "" {
req.EgressId = guid.New(utils.EgressPrefix)
}
info, err := s.client.StartEgress(ctx, "", req)
if err != nil {
return nil, err
}
_, err = s.io.CreateEgress(ctx, info)
if err != nil {
logger.Errorw("failed to create egress", err)
}
return info, nil
}
+17 -10
View File
@@ -15,27 +15,31 @@
package service_test
import (
"context"
"fmt"
"log"
"net"
"os"
"testing"
"time"
"go.uber.org/atomic"
"github.com/ory/dockertest/v3"
mobyclient "github.com/moby/moby/client"
"github.com/ory/dockertest/v4"
)
var Docker *dockertest.Pool
var Docker dockertest.ClosablePool
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
ctx := context.Background()
pool, err := dockertest.NewPool(ctx, "")
if err != nil {
log.Fatalf("Could not construct pool: %s", err)
}
// uses pool to try to connect to Docker
err = pool.Client.Ping()
_, err = pool.Client().Ping(ctx, mobyclient.PingOptions{})
if err != nil {
log.Fatalf("Could not connect to Docker: %s", err)
}
@@ -46,7 +50,7 @@ func TestMain(m *testing.M) {
}
func waitTCPPort(t testing.TB, addr string) {
if err := Docker.Retry(func() error {
if err := Docker.Retry(t.Context(), 30*time.Second, func() error {
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Log(err)
@@ -62,15 +66,18 @@ func waitTCPPort(t testing.TB, addr string) {
var redisLast atomic.Uint32
func runRedis(t testing.TB) string {
c, err := Docker.RunWithOptions(&dockertest.RunOptions{
Name: fmt.Sprintf("lktest-redis-%d", redisLast.Inc()),
Repository: "redis", Tag: "latest",
})
c, err := Docker.Run(t.Context(),
"redis",
dockertest.WithName(fmt.Sprintf("lktest-redis-%d", redisLast.Inc())),
dockertest.WithTag("latest"),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = Docker.Purge(c)
// t.Context() is canceled before cleanup funcs run, so use a
// non-canceled context to let the container stop/remove complete.
_ = c.Close(context.Background())
})
addr := c.GetHostPort("6379/tcp")
waitTCPPort(t, addr)
+129 -33
View File
@@ -17,15 +17,21 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"github.com/twitchtv/twirp"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/protocol/egress"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/rtc"
)
type EgressService struct {
@@ -33,27 +39,41 @@ type EgressService struct {
client rpc.EgressClient
io IOClient
roomService livekit.RoomService
store ServiceStore
}
type egressLauncher struct {
client rpc.EgressClient
io IOClient
store ServiceStore
}
func NewEgressService(
client rpc.EgressClient,
launcher rtc.EgressLauncher,
store ServiceStore,
io IOClient,
rs livekit.RoomService,
) *EgressService {
return &EgressService{
client: client,
store: store,
io: io,
roomService: rs,
launcher: launcher,
}
}
func NewEgressLauncher(client rpc.EgressClient, io IOClient, store ServiceStore) rtc.EgressLauncher {
if client == nil {
return nil
}
return &egressLauncher{
client: client,
io: io,
store: store,
}
}
func (s *EgressService) StartRoomCompositeEgress(ctx context.Context, req *livekit.RoomCompositeEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"room", req.RoomName,
"baseUrl", req.CustomBaseUrl,
"outputType", egress.GetOutputType(req),
@@ -61,7 +81,10 @@ func (s *EgressService) StartRoomCompositeEgress(ctx context.Context, req *livek
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_RoomComposite{
RoomComposite: req,
},
@@ -69,19 +92,23 @@ func (s *EgressService) StartRoomCompositeEgress(ctx context.Context, req *livek
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartWebEgress(ctx context.Context, req *livekit.WebEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"url", req.Url,
"outputType", egress.GetOutputType(req),
}
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, "", &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_Web{
Web: req,
},
@@ -89,12 +116,14 @@ func (s *EgressService) StartWebEgress(ctx context.Context, req *livekit.WebEgre
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartParticipantEgress(ctx context.Context, req *livekit.ParticipantEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"room", req.RoomName,
"identity", req.Identity,
"outputType", egress.GetOutputType(req),
@@ -102,7 +131,9 @@ func (s *EgressService) StartParticipantEgress(ctx context.Context, req *livekit
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_Participant{
Participant: req,
},
@@ -110,12 +141,14 @@ func (s *EgressService) StartParticipantEgress(ctx context.Context, req *livekit
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartTrackCompositeEgress(ctx context.Context, req *livekit.TrackCompositeEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"room", req.RoomName,
"audioTrackID", req.AudioTrackId,
"videoTrackID", req.VideoTrackId,
@@ -124,7 +157,9 @@ func (s *EgressService) StartTrackCompositeEgress(ctx context.Context, req *live
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_TrackComposite{
TrackComposite: req,
},
@@ -132,19 +167,23 @@ func (s *EgressService) StartTrackCompositeEgress(ctx context.Context, req *live
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartTrackEgress(ctx context.Context, req *livekit.TrackEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{"room", req.RoomName, "trackID", req.TrackId}
fields := []any{"room", req.RoomName, "trackID", req.TrackId}
if t := reflect.TypeOf(req.Output); t != nil {
fields = append(fields, "outputType", t.String())
}
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_Track{
Track: req,
},
@@ -152,26 +191,76 @@ func (s *EgressService) StartTrackEgress(ctx context.Context, req *livekit.Track
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) startEgress(ctx context.Context, roomName livekit.RoomName, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
func (s *EgressService) startEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
if err := EnsureRecordPermission(ctx); err != nil {
return nil, twirpAuthError(err)
} else if s.launcher == nil {
return nil, ErrEgressNotConnected
}
if roomName != "" {
room, _, err := s.store.LoadRoom(ctx, roomName, false)
if err != nil {
return nil, err
}
req.RoomId = room.Sid
}
return s.launcher.StartEgress(ctx, req)
}
func (s *egressLauncher) StartEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
if s.client == nil {
return nil, ErrEgressNotConnected
}
// Ensure we have an Egress ID
if req.EgressId == "" {
req.EgressId = guid.New(utils.EgressPrefix)
}
if req.RoomId == "" {
var roomName string
switch v := req.Request.(type) {
case *rpc.StartEgressRequest_RoomComposite:
roomName = v.RoomComposite.RoomName
case *rpc.StartEgressRequest_Web:
// no room name
case *rpc.StartEgressRequest_Participant:
roomName = v.Participant.RoomName
case *rpc.StartEgressRequest_TrackComposite:
roomName = v.TrackComposite.RoomName
case *rpc.StartEgressRequest_Track:
roomName = v.Track.RoomName
}
if roomName != "" {
room, _, err := s.store.LoadRoom(ctx, livekit.RoomName(roomName), false)
if err != nil {
return nil, err
}
req.RoomId = room.Sid
}
}
info, err := s.client.StartEgress(ctx, "", req)
if err != nil {
return nil, err
}
_, err = s.io.CreateEgress(ctx, info)
if err != nil {
logger.Errorw("failed to create egress", err)
}
return info, nil
}
func (s *egressLauncher) StopEgress(ctx context.Context, req *livekit.StopEgressRequest) (*livekit.EgressInfo, error) {
if s.client == nil {
return nil, ErrEgressNotConnected
}
return s.client.StopEgress(ctx, req.EgressId, req)
}
type LayoutMetadata struct {
Layout string `json:"layout"`
}
@@ -249,17 +338,20 @@ func (s *EgressService) ListEgress(ctx context.Context, req *livekit.ListEgressR
return s.io.ListEgress(ctx, req)
}
func (s *EgressService) StopEgress(ctx context.Context, req *livekit.StopEgressRequest) (*livekit.EgressInfo, error) {
func (s *EgressService) StopEgress(ctx context.Context, req *livekit.StopEgressRequest) (info *livekit.EgressInfo, err error) {
defer func() {
if errors.Is(err, psrpc.ErrNoResponse) {
// Do not map cases where the context times out to 503
err = psrpc.ErrRequestTimedOut
}
}()
AppendLogFields(ctx, "egressID", req.EgressId)
if err := EnsureRecordPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.client == nil {
return nil, ErrEgressNotConnected
}
info, err := s.client.StopEgress(ctx, req.EgressId, req)
info, err = s.launcher.StopEgress(ctx, req)
if err != nil {
var loadErr error
info, loadErr = s.io.GetEgress(ctx, &rpc.GetEgressRequest{EgressId: req.EgressId})
@@ -279,3 +371,7 @@ func (s *EgressService) StopEgress(ctx context.Context, req *livekit.StopEgressR
return info, nil
}
func (s *EgressService) StartEgress(ctx context.Context, req *livekit.StartEgressRequest) (*livekit.EgressInfo, error) {
return nil, errors.New("not implemented")
}
+44
View File
@@ -0,0 +1,44 @@
package service
import (
"context"
"strings"
"github.com/twitchtv/twirp"
"github.com/livekit/protocol/utils/guid"
)
type egressIDKey struct{}
func TwirpEgressID() *twirp.ServerHooks {
return &twirp.ServerHooks{
RequestRouted: func(ctx context.Context) (context.Context, error) {
// generate egressID for start egress methods for tracing egress failure before it reaches egress service.
if isStartEgressMethod(ctx) {
egressID := guid.New(guid.EgressPrefix)
ctx = WithEgressID(ctx, egressID)
AppendLogFields(ctx, "egressID", egressID)
}
return ctx, nil
},
}
}
func WithEgressID(ctx context.Context, egressID string) context.Context {
return context.WithValue(ctx, egressIDKey{}, egressID)
}
func EgressID(ctx context.Context) (string, bool) {
if egressID, ok := ctx.Value(egressIDKey{}).(string); ok && egressID != "" {
return egressID, true
}
return "", false
}
func isStartEgressMethod(ctx context.Context) bool {
if methodName, ok := twirp.MethodName(ctx); ok && strings.HasPrefix(methodName, "Start") && strings.HasSuffix(methodName, "Egress") {
return true
}
return false
}
+7
View File
@@ -22,14 +22,17 @@ var (
ErrEgressNotFound = psrpc.NewErrorf(psrpc.NotFound, "egress does not exist")
ErrEgressNotConnected = psrpc.NewErrorf(psrpc.Internal, "egress not connected (redis required)")
ErrIdentityEmpty = psrpc.NewErrorf(psrpc.InvalidArgument, "identity cannot be empty")
ErrParticipantSidEmpty = psrpc.NewErrorf(psrpc.InvalidArgument, "participant sid cannot be empty")
ErrIngressNotConnected = psrpc.NewErrorf(psrpc.Internal, "ingress not connected (redis required)")
ErrIngressNotFound = psrpc.NewErrorf(psrpc.NotFound, "ingress does not exist")
ErrIngressNonReusable = psrpc.NewErrorf(psrpc.InvalidArgument, "ingress is not reusable and cannot be modified")
ErrNameExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "name length exceeds limits")
ErrMetadataExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "metadata size exceeds limits")
ErrAttributeExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "attribute size exceeds limits")
ErrNoRoomName = psrpc.NewErrorf(psrpc.InvalidArgument, "no room name")
ErrRoomNameExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "room name length exceeds limits")
ErrParticipantIdentityExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "participant identity length exceeds limits")
ErrDestinationSameAsSourceRoom = psrpc.NewErrorf(psrpc.InvalidArgument, "destination room cannot be the same as source room")
ErrOperationFailed = psrpc.NewErrorf(psrpc.Internal, "operation cannot be completed")
ErrParticipantNotFound = psrpc.NewErrorf(psrpc.NotFound, "participant does not exist")
ErrRoomNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested room does not exist")
@@ -42,4 +45,8 @@ var (
ErrSIPTrunkNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip trunk does not exist")
ErrSIPDispatchRuleNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip dispatch rule does not exist")
ErrSIPParticipantNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip participant does not exist")
ErrInvalidMessageType = psrpc.NewErrorf(psrpc.Internal, "invalid message type")
ErrNoConnectRequest = psrpc.NewErrorf(psrpc.InvalidArgument, "no connect request")
ErrNoConnectResponse = psrpc.NewErrorf(psrpc.InvalidArgument, "no connect response")
ErrDestinationIdentityRequired = psrpc.NewErrorf(psrpc.InvalidArgument, "destination identity is required")
)
+16 -17
View File
@@ -85,7 +85,7 @@ func NewIngressService(
}
func (s *IngressService) CreateIngress(ctx context.Context, req *livekit.CreateIngressRequest) (*livekit.IngressInfo, error) {
fields := []interface{}{
fields := []any{
"inputType", req.InputType,
"name", req.Name,
}
@@ -190,20 +190,19 @@ func (s *IngressService) CreateIngressWithUrl(ctx context.Context, urlStr string
if err != nil {
return info, err
}
}
// TODO Remove this store Ingress call for URL pull as it is redundant since
// the ingress service sends a CreateIngress RPC
_, err = s.io.CreateIngress(ctx, info)
switch err {
case nil:
break
case ingress.ErrIngressOutOfDate:
// Error returned if the ingress was already created by the ingress service
err = nil
default:
logger.Errorw("could not create ingress object", err)
return nil, err
// The Ingress instance will create the ingress object when handling the URL pull ingress
} else {
_, err = s.io.CreateIngress(ctx, info)
switch err {
case nil:
break
case ingress.ErrIngressOutOfDate:
// Error returned if the ingress was already created by the ingress service
err = nil
default:
logger.Errorw("could not create ingress object", err)
return nil, err
}
}
return info, nil
@@ -218,7 +217,7 @@ func (s *IngressService) LaunchPullIngress(ctx context.Context, info *livekit.In
}
func updateEnableTranscoding(info *livekit.IngressInfo) {
// Set BypassTranscoding as well for backward compatiblity
// Set BypassTranscoding as well for backward compatibility
if info.EnableTranscoding != nil {
info.BypassTranscoding = !*info.EnableTranscoding
return
@@ -276,7 +275,7 @@ func updateInfoUsingRequest(req *livekit.UpdateIngressRequest, info *livekit.Ing
}
func (s *IngressService) UpdateIngress(ctx context.Context, req *livekit.UpdateIngressRequest) (*livekit.IngressInfo, error) {
fields := []interface{}{
fields := []any{
"ingress", req.IngressId,
"name", req.Name,
}
+7 -1
View File
@@ -28,6 +28,7 @@ import (
//counterfeiter:generate . ObjectStore
type ObjectStore interface {
ServiceStore
OSSServiceStore
// enable locking on a specific room to prevent race
// returns a (lock uuid, error)
@@ -43,11 +44,16 @@ type ObjectStore interface {
//counterfeiter:generate . ServiceStore
type ServiceStore interface {
LoadRoom(ctx context.Context, roomName livekit.RoomName, includeInternal bool) (*livekit.Room, *livekit.RoomInternal, error)
DeleteRoom(ctx context.Context, roomName livekit.RoomName) error
RoomExists(ctx context.Context, roomName livekit.RoomName) (bool, error)
// ListRooms returns currently active rooms. if names is not nil, it'll filter and return
// only rooms that match
ListRooms(ctx context.Context, roomNames []livekit.RoomName) ([]*livekit.Room, error)
}
type OSSServiceStore interface {
DeleteRoom(ctx context.Context, roomName livekit.RoomName) error
HasParticipant(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (bool, error)
LoadParticipant(ctx context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error)
ListParticipants(ctx context.Context, roomName livekit.RoomName) ([]*livekit.ParticipantInfo, error)
}
+25 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware/otelpsrpc"
"github.com/livekit/livekit-server/pkg/telemetry"
)
@@ -54,7 +55,9 @@ func NewIOInfoService(
}
if bus != nil {
ioServer, err := rpc.NewIOInfoServer(s, bus)
ioServer, err := rpc.NewIOInfoServer(s, bus,
otelpsrpc.ServerOptions(otelpsrpc.Config{}),
)
if err != nil {
return nil, err
}
@@ -86,6 +89,10 @@ func (s *IOInfoService) Stop() {
}
func (s *IOInfoService) CreateEgress(ctx context.Context, info *livekit.EgressInfo) (*emptypb.Empty, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
// check if egress already exists to avoid duplicate EgressStarted event
if _, err := s.es.LoadEgress(ctx, info.EgressId); err == nil {
return &emptypb.Empty{}, nil
@@ -103,6 +110,10 @@ func (s *IOInfoService) CreateEgress(ctx context.Context, info *livekit.EgressIn
}
func (s *IOInfoService) UpdateEgress(ctx context.Context, info *livekit.EgressInfo) (*emptypb.Empty, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
err := s.es.UpdateEgress(ctx, info)
switch info.Status {
@@ -126,6 +137,10 @@ func (s *IOInfoService) UpdateEgress(ctx context.Context, info *livekit.EgressIn
}
func (s *IOInfoService) GetEgress(ctx context.Context, req *rpc.GetEgressRequest) (*livekit.EgressInfo, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
info, err := s.es.LoadEgress(ctx, req.EgressId)
if err != nil {
logger.Errorw("failed to load egress", err)
@@ -136,6 +151,10 @@ func (s *IOInfoService) GetEgress(ctx context.Context, req *rpc.GetEgressRequest
}
func (s *IOInfoService) ListEgress(ctx context.Context, req *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
if req.EgressId != "" {
info, err := s.es.LoadEgress(ctx, req.EgressId)
if err != nil {
@@ -168,3 +187,8 @@ func (s *IOInfoService) UpdateSIPCallState(ctx context.Context, req *rpc.UpdateS
// TODO: placeholder
return &emptypb.Empty{}, nil
}
func (s *IOInfoService) RecordCallContext(context.Context, *rpc.RecordCallContextRequest) (*emptypb.Empty, error) {
// TODO: placeholder
return &emptypb.Empty{}, nil
}
+12
View File
@@ -25,6 +25,10 @@ import (
)
func (s *IOInfoService) CreateIngress(ctx context.Context, info *livekit.IngressInfo) (*emptypb.Empty, error) {
if s.is == nil {
return nil, ErrIngressNotConnected
}
err := s.is.StoreIngress(ctx, info)
if err != nil {
return nil, err
@@ -45,6 +49,10 @@ func (s *IOInfoService) GetIngressInfo(ctx context.Context, req *rpc.GetIngressI
}
func (s *IOInfoService) loadIngressFromInfoRequest(req *rpc.GetIngressInfoRequest) (info *livekit.IngressInfo, err error) {
if s.is == nil {
return nil, ErrIngressNotConnected
}
if req.IngressId != "" {
info, err = s.is.LoadIngress(context.Background(), req.IngressId)
} else if req.StreamKey != "" {
@@ -56,6 +64,10 @@ func (s *IOInfoService) loadIngressFromInfoRequest(req *rpc.GetIngressInfoReques
}
func (s *IOInfoService) UpdateIngressState(ctx context.Context, req *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error) {
if s.is == nil {
return nil, ErrIngressNotConnected
}
info, err := s.is.LoadIngress(ctx, req.IngressId)
if err != nil {
return nil, err
+18 -19
View File
@@ -30,21 +30,21 @@ import (
// matchSIPTrunk finds a SIP Trunk definition matching the request.
// Returns nil if no rules matched or an error if there are conflicting definitions.
func (s *IOInfoService) matchSIPTrunk(ctx context.Context, trunkID, calling, called string, srcIP netip.Addr) (*livekit.SIPInboundTrunkInfo, error) {
func (s *IOInfoService) matchSIPTrunk(ctx context.Context, trunkID string, call *rpc.SIPCall) (*livekit.SIPInboundTrunkInfo, error) {
if s.ss == nil {
return nil, ErrSIPNotConnected
}
if trunkID != "" {
// This is a best-effort optimization. Fallthrough to listing trunks if it doesn't work.
if tr, err := s.ss.LoadSIPInboundTrunk(ctx, trunkID); err == nil {
tr, err = sip.MatchTrunkIter(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), srcIP, calling, called)
tr, err = sip.MatchTrunkIter(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), call)
if err == nil {
return tr, nil
}
}
}
it := s.SelectSIPInboundTrunk(ctx, called)
return sip.MatchTrunkIter(it, srcIP, calling, called)
it := s.SelectSIPInboundTrunk(ctx, call.To.User)
return sip.MatchTrunkIter(it, call)
}
func (s *IOInfoService) SelectSIPInboundTrunk(ctx context.Context, called string) iters.Iter[*livekit.SIPInboundTrunkInfo] {
@@ -82,18 +82,19 @@ func (s *IOInfoService) SelectSIPDispatchRule(ctx context.Context, trunkID strin
}
func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
call := req.SIPCall()
log := logger.GetLogger()
log = log.WithValues("toUser", req.CalledNumber, "fromUser", req.CallingNumber, "src", req.SrcAddress)
if req.SrcAddress == "" {
log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
if call.SourceIp == "" {
log.Warnw("source address is not set", nil)
// TODO: return error in the next release
}
srcIP, err := netip.ParseAddr(req.SrcAddress)
if req.SrcAddress != "" && err != nil {
_, err := netip.ParseAddr(call.SourceIp)
if call.SourceIp != "" && err != nil {
log.Errorw("cannot parse source IP", err)
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
trunk, err := s.matchSIPTrunk(ctx, req.SipTrunkId, req.CallingNumber, req.CalledNumber, srcIP)
trunk, err := s.matchSIPTrunk(ctx, req.SipTrunkId, call)
if err != nil {
return nil, err
}
@@ -122,23 +123,25 @@ func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.E
if err != nil {
return nil, err
}
resp.Upgrade()
resp.SipTrunkId = trunkID
return resp, err
}
func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.GetSIPTrunkAuthenticationRequest) (*rpc.GetSIPTrunkAuthenticationResponse, error) {
call := req.SIPCall()
log := logger.GetLogger()
log = log.WithValues("toUser", req.To, "fromUser", req.From, "src", req.SrcAddress)
if req.SrcAddress == "" {
log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
if call.SourceIp == "" {
log.Warnw("source address is not set", nil)
// TODO: return error in the next release
}
srcIP, err := netip.ParseAddr(req.SrcAddress)
if req.SrcAddress != "" && err != nil {
_, err := netip.ParseAddr(call.SourceIp)
if call.SourceIp != "" && err != nil {
log.Errorw("cannot parse source IP", err)
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
trunk, err := s.matchSIPTrunk(ctx, "", req.From, req.To, srcIP)
trunk, err := s.matchSIPTrunk(ctx, "", call)
if err != nil {
return nil, err
}
@@ -147,9 +150,5 @@ func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.
return &rpc.GetSIPTrunkAuthenticationResponse{}, nil
}
log.Debugw("SIP trunk matched for auth", "sipTrunk", trunk.SipTrunkId)
return &rpc.GetSIPTrunkAuthenticationResponse{
SipTrunkId: trunk.SipTrunkId,
Username: trunk.AuthUsername,
Password: trunk.AuthPassword,
}, nil
return sip.InboundTrunkAuthPrompt(trunk)
}
+19 -4
View File
@@ -25,6 +25,8 @@ import (
"github.com/livekit/protocol/utils"
)
var _ OSSServiceStore = (*LocalStore)(nil)
// encapsulates CRUD operations for room settings
type LocalStore struct {
// map of roomName => room
@@ -84,6 +86,16 @@ func (s *LocalStore) LoadRoom(_ context.Context, roomName livekit.RoomName, incl
return room, internal, nil
}
func (s *LocalStore) RoomExists(ctx context.Context, roomName livekit.RoomName) (bool, error) {
_, _, err := s.LoadRoom(ctx, roomName, false)
if err == ErrRoomNotFound {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func (s *LocalStore) ListRooms(_ context.Context, roomNames []livekit.RoomName) ([]*livekit.Room, error) {
s.lock.RLock()
defer s.lock.RUnlock()
@@ -153,6 +165,11 @@ func (s *LocalStore) LoadParticipant(_ context.Context, roomName livekit.RoomNam
return participant, nil
}
func (s *LocalStore) HasParticipant(ctx context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) (bool, error) {
p, err := s.LoadParticipant(ctx, roomName, identity)
return p != nil, utils.ScreenError(err, ErrParticipantNotFound)
}
func (s *LocalStore) ListParticipants(_ context.Context, roomName livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
s.lock.RLock()
defer s.lock.RUnlock()
@@ -224,10 +241,8 @@ func (s *LocalStore) ListAgentDispatches(ctx context.Context, roomName livekit.R
agentJobs := s.agentJobs[roomName]
var js []*livekit.Job
if agentJobs != nil {
for _, j := range agentJobs {
js = append(js, utils.CloneProto(j))
}
for _, j := range agentJobs {
js = append(js, utils.CloneProto(j))
}
var ds []*livekit.AgentDispatch
+20 -3
View File
@@ -68,6 +68,8 @@ const (
maxRetries = 5
)
var _ OSSServiceStore = (*RedisStore)(nil)
type RedisStore struct {
rc redis.UniversalClient
unlockScript *redis.Script
@@ -195,6 +197,16 @@ func (s *RedisStore) LoadRoom(_ context.Context, roomName livekit.RoomName, incl
return room, internal, nil
}
func (s *RedisStore) RoomExists(ctx context.Context, roomName livekit.RoomName) (bool, error) {
_, _, err := s.LoadRoom(ctx, roomName, false)
if err == ErrRoomNotFound {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func (s *RedisStore) ListRooms(_ context.Context, roomNames []livekit.RoomName) ([]*livekit.Room, error) {
var items []string
var err error
@@ -205,7 +217,7 @@ func (s *RedisStore) ListRooms(_ context.Context, roomNames []livekit.RoomName)
}
} else {
names := livekit.IDsAsStrings(roomNames)
var results []interface{}
var results []any
results, err = s.rc.HMGet(s.ctx, RoomsKey, names...).Result()
if err != nil && err != redis.Nil {
return nil, errors.Wrap(err, "could not get rooms by names")
@@ -314,6 +326,11 @@ func (s *RedisStore) LoadParticipant(_ context.Context, roomName livekit.RoomNam
return &pi, nil
}
func (s *RedisStore) HasParticipant(ctx context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) (bool, error) {
p, err := s.LoadParticipant(ctx, roomName, identity)
return p != nil, utils.ScreenError(err, ErrParticipantNotFound)
}
func (s *RedisStore) ListParticipants(_ context.Context, roomName livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
key := RoomParticipantsPrefix + string(roomName)
items, err := s.rc.HVals(s.ctx, key).Result()
@@ -585,7 +602,7 @@ func (s *RedisStore) storeIngress(_ context.Context, info *livekit.IngressInfo)
}
// Retry if the key has been changed.
for i := 0; i < maxRetries; i++ {
for range maxRetries {
err := s.rc.Watch(s.ctx, txf, IngressKey)
switch err {
case redis.TxFailedErr:
@@ -660,7 +677,7 @@ func (s *RedisStore) storeIngressState(_ context.Context, ingressId string, stat
}
// Retry if the key has been changed.
for i := 0; i < maxRetries; i++ {
for range maxRetries {
err := s.rc.Watch(s.ctx, txf, IngressStatePrefix+ingressId)
switch err {
case redis.TxFailedErr:
+13 -6
View File
@@ -106,12 +106,19 @@ func (s *RedisStore) LoadSIPOutboundTrunk(ctx context.Context, id string) (*live
}
func (s *RedisStore) DeleteSIPTrunk(ctx context.Context, id string) error {
tx := s.rc.TxPipeline()
tx.HDel(s.ctx, SIPTrunkKey, id)
tx.HDel(s.ctx, SIPInboundTrunkKey, id)
tx.HDel(s.ctx, SIPOutboundTrunkKey, id)
_, err := tx.Exec(ctx)
return err
err1 := s.rc.HDel(s.ctx, SIPTrunkKey, id).Err()
err2 := s.rc.HDel(s.ctx, SIPInboundTrunkKey, id).Err()
err3 := s.rc.HDel(s.ctx, SIPOutboundTrunkKey, id).Err()
if err1 != nil {
return err1
}
if err2 != nil {
return err2
}
if err3 != nil {
return err3
}
return nil
}
func (s *RedisStore) listSIPLegacyTrunk(ctx context.Context, page *livekit.Pagination) ([]*livekit.SIPTrunkInfo, error) {
+1 -1
View File
@@ -320,7 +320,7 @@ func testIter[T listItem](
) {
ctx := context.Background()
var all []string
for i := 0; i < 250; i++ {
for i := range 250 {
id := fmt.Sprintf("%05d", i)
all = append(all, id)
err := create(ctx, id)
+6 -4
View File
@@ -174,7 +174,7 @@ func (r *StandardRoomAllocator) SelectRoomNode(ctx context.Context, roomName liv
func (r *StandardRoomAllocator) ValidateCreateRoom(ctx context.Context, roomName livekit.RoomName) error {
// when auto create is disabled, we'll check to ensure it's already created
if !r.config.Room.AutoCreate {
if !r.config.Room.AutoCreate && EnsureCreatePermission(ctx) != nil {
_, _, err := r.roomStore.LoadRoom(ctx, roomName, false)
if err != nil {
return err
@@ -208,17 +208,16 @@ func (r *StandardRoomAllocator) applyNamedRoomConfiguration(req *livekit.CreateR
conf, ok := r.config.Room.RoomConfigurations[req.RoomPreset]
if !ok {
return req, psrpc.NewErrorf(psrpc.InvalidArgument, "unknown room confguration in create room request")
return req, psrpc.NewErrorf(psrpc.InvalidArgument, "unknown room configuration in create room request")
}
clone := utils.CloneProto(req)
// Request overwrites conf
if clone.EmptyTimeout == 0 {
clone.EmptyTimeout = conf.EmptyTimeout
}
if clone.DepartureTimeout == 0 {
clone.DepartureTimeout = req.DepartureTimeout
clone.DepartureTimeout = conf.DepartureTimeout
}
if clone.MaxParticipants == 0 {
clone.MaxParticipants = conf.MaxParticipants
@@ -241,6 +240,9 @@ func (r *StandardRoomAllocator) applyNamedRoomConfiguration(req *livekit.CreateR
if !clone.SyncStreams {
clone.SyncStreams = conf.SyncStreams
}
if clone.Metadata == "" {
clone.Metadata = conf.Metadata
}
return clone, nil
}
+307 -108
View File
@@ -16,21 +16,27 @@ package service
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"maps"
"net"
"os"
"slices"
"strconv"
"sync"
"time"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/sfu"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
@@ -38,6 +44,10 @@ import (
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/sfu"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/livekit-server/pkg/clientconfiguration"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
@@ -53,8 +63,6 @@ const (
tokenDefaultTTL = 10 * time.Minute
)
var affinityEpoch = time.Date(2000, 0, 0, 0, 0, 0, 0, time.UTC)
type iceConfigCacheKey struct {
roomName livekit.RoomName
participantIdentity livekit.ParticipantIdentity
@@ -72,6 +80,7 @@ type RoomManager struct {
router routing.Router
roomAllocator RoomAllocator
roomManagerServer rpc.TypedRoomManagerServer
whipServer rpc.WHIPServer[livekit.NodeID]
roomStore ObjectStore
telemetry telemetry.TelemetryService
clientConfManager clientconfiguration.ClientConfigurationManager
@@ -84,13 +93,19 @@ type RoomManager struct {
rooms map[livekit.RoomName]*rtc.Room
roomServers utils.MultitonService[rpc.RoomTopic]
agentDispatchServers utils.MultitonService[rpc.RoomTopic]
participantServers utils.MultitonService[rpc.ParticipantTopic]
roomServers utils.MultitonService[rpc.RoomTopic]
agentDispatchServers utils.MultitonService[rpc.RoomTopic]
participantServers utils.MultitonService[rpc.ParticipantTopic]
httpSignalParticipantServers utils.MultitonService[rpc.ParticipantTopic]
whipParticipantServers utils.MultitonService[rpc.ParticipantTopic]
iceConfigCache *sutils.IceConfigCache[iceConfigCacheKey]
forwardStats *sfu.ForwardStats
rpc.UnimplementedParticipantServer
rpc.UnimplementedRoomServer
rpc.UnimplementedRoomManagerServer
}
func NewLocalRoomManager(
@@ -100,7 +115,6 @@ func NewLocalRoomManager(
router routing.Router,
roomAllocator RoomAllocator,
telemetry telemetry.TelemetryService,
clientConfManager clientconfiguration.ClientConfigurationManager,
agentClient agent.Client,
agentStore AgentStore,
egressLauncher rtc.EgressLauncher,
@@ -122,7 +136,7 @@ func NewLocalRoomManager(
roomAllocator: roomAllocator,
roomStore: roomStore,
telemetry: telemetry,
clientConfManager: clientConfManager,
clientConfManager: clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations),
egressLauncher: egressLauncher,
agentClient: agentClient,
agentStore: agentStore,
@@ -153,6 +167,18 @@ func NewLocalRoomManager(
return nil, err
}
whipService, err := newWhipService(r)
if err != nil {
return nil, err
}
r.whipServer, err = rpc.NewWHIPServer[livekit.NodeID](whipService, bus, rpc.WithDefaultServerOptions(conf.PSRPC, logger.GetLogger()))
if err != nil {
return nil, err
}
if err := r.whipServer.RegisterAllCommonTopics(currentNode.NodeID()); err != nil {
return nil, err
}
return r, nil
}
@@ -193,7 +219,7 @@ func (r *RoomManager) deleteRoom(ctx context.Context, roomName livekit.RoomName)
func (r *RoomManager) CloseIdleRooms() {
r.lock.RLock()
rooms := maps.Values(r.rooms)
rooms := slices.Collect(maps.Values(r.rooms))
r.lock.RUnlock()
for _, room := range rooms {
@@ -216,7 +242,7 @@ func (r *RoomManager) HasParticipants() bool {
func (r *RoomManager) Stop() {
// disconnect all clients
r.lock.RLock()
rooms := maps.Values(r.rooms)
rooms := slices.Collect(maps.Values(r.rooms))
r.lock.RUnlock()
for _, room := range rooms {
@@ -224,9 +250,12 @@ func (r *RoomManager) Stop() {
}
r.roomManagerServer.Kill()
r.whipServer.Kill()
r.roomServers.Kill()
r.agentDispatchServers.Kill()
r.participantServers.Kill()
r.httpSignalParticipantServers.Kill()
r.whipParticipantServers.Kill()
if r.rtcConfig != nil {
if r.rtcConfig.UDPMux != nil {
@@ -318,10 +347,12 @@ func (r *RoomManager) StartSession(
Leave: leave,
},
})
prometheus.IncrementParticipantRtcCanceled(1)
return errors.New("could not restart closed participant")
}
participant.GetLogger().Infow("resuming RTC session",
participant.GetLogger().Infow(
"resuming RTC session",
"nodeID", r.currentNode.NodeID(),
"participantInit", &pi,
"numParticipants", room.GetParticipantCount(),
@@ -343,6 +374,9 @@ func (r *RoomManager) StartSession(
return err
}
r.telemetry.ParticipantResumed(ctx, room.ToProto(), participant.ToProto(), r.currentNode.NodeID(), pi.ReconnectReason)
go room.HandleSyncState(participant, pi.SyncState)
go r.rtcSessionWorker(room, participant, requestSource)
return nil
}
@@ -371,6 +405,7 @@ func (r *RoomManager) StartSession(
Leave: leave,
},
})
prometheus.IncrementParticipantRtcCanceled(1)
return errors.New("could not restart participant")
}
@@ -381,7 +416,8 @@ func (r *RoomManager) StartSession(
sid,
false,
)
pLogger.Infow("starting RTC session",
pLogger.Infow(
"starting RTC session",
"room", room.Name(),
"nodeID", r.currentNode.NodeID(),
"numParticipants", room.GetParticipantCount(),
@@ -396,78 +432,98 @@ func (r *RoomManager) StartSession(
if pi.DisableICELite {
rtcConf.SettingEngine.SetLite(false)
}
rtcConf.UpdatePublisherConfig(pi.UseSinglePeerConnection)
// default allow forceTCP
allowFallback := true
if r.config.RTC.AllowTCPFallback != nil {
allowFallback = *r.config.RTC.AllowTCPFallback
}
// default do not force full reconnect on a publication error
reconnectOnPublicationError := false
if r.config.RTC.ReconnectOnPublicationError != nil {
reconnectOnPublicationError = *r.config.RTC.ReconnectOnPublicationError
}
// default do not force full reconnect on a subscription error
reconnectOnSubscriptionError := false
if r.config.RTC.ReconnectOnSubscriptionError != nil {
reconnectOnSubscriptionError = *r.config.RTC.ReconnectOnSubscriptionError
}
// default do not force full reconnect on a data channel error
reconnectOnDataChannelError := false
if r.config.RTC.ReconnectOnDataChannelError != nil {
reconnectOnDataChannelError = *r.config.RTC.ReconnectOnDataChannelError
}
subscriberAllowPause := r.config.RTC.CongestionControl.AllowPause
if pi.SubscriberAllowPause != nil {
subscriberAllowPause = *pi.SubscriberAllowPause
}
enabledCodecs := protoRoom.EnabledCodecs
if !slices.ContainsFunc(enabledCodecs, func(codec *livekit.Codec) bool {
return mime.IsMimeTypeStringRTX(codec.Mime)
}) {
enabledCodecs = append(enabledCodecs, &livekit.Codec{Mime: mime.MimeTypeRTX.String()})
}
participant, err = rtc.NewParticipant(rtc.ParticipantParams{
Identity: pi.Identity,
Name: pi.Name,
SID: sid,
Config: &rtcConf,
Sink: responseSink,
AudioConfig: r.config.Audio,
VideoConfig: r.config.Video,
LimitConfig: r.config.Limit,
ProtocolVersion: pv,
SessionStartTime: sessionStartTime,
Telemetry: r.telemetry,
Trailer: room.Trailer(),
PLIThrottleConfig: r.config.RTC.PLIThrottle,
CongestionControlConfig: r.config.RTC.CongestionControl,
PublishEnabledCodecs: protoRoom.EnabledCodecs,
SubscribeEnabledCodecs: protoRoom.EnabledCodecs,
Grants: pi.Grants,
Reconnect: pi.Reconnect,
Logger: pLogger,
ClientConf: clientConf,
ClientInfo: rtc.ClientInfo{ClientInfo: pi.Client},
Region: pi.Region,
AdaptiveStream: pi.AdaptiveStream,
AllowTCPFallback: allowFallback,
TURNSEnabled: r.config.IsTURNSEnabled(),
GetParticipantInfo: func(pID livekit.ParticipantID) *livekit.ParticipantInfo {
if p := room.GetParticipantByID(pID); p != nil {
return p.ToProto()
}
return nil
Identity: pi.Identity,
Name: pi.Name,
SID: sid,
Config: &rtcConf,
Sink: responseSink,
AudioConfig: r.config.Audio,
VideoConfig: r.config.Video,
LimitConfig: r.config.Limit,
ProtocolVersion: pv,
SessionStartTime: sessionStartTime,
SessionTimer: observability.NewSessionTimer(sessionStartTime),
TelemetryListener: room.ParticipantTelemetryListener(),
Trailer: room.Trailer(),
PLIThrottleConfig: r.config.RTC.PLIThrottle,
CongestionControlConfig: r.config.RTC.CongestionControl,
PublishEnabledCodecs: enabledCodecs,
SubscribeEnabledCodecs: enabledCodecs,
Grants: pi.Grants,
Reconnect: pi.Reconnect,
Logger: pLogger,
Reporter: roomobs.NewNoopParticipantSessionReporter(),
ClientConf: clientConf,
ClientInfo: rtc.ClientInfo{ClientInfo: pi.Client},
Region: pi.Region,
AdaptiveStream: pi.AdaptiveStream,
AllowTCPFallback: allowFallback,
TCPFallbackRTTThreshold: r.config.RTC.TCPFallbackRTTThreshold,
AllowUDPUnstableFallback: r.config.RTC.AllowUDPUnstableFallback,
TURNSEnabled: r.config.IsTURNSEnabled(),
ParticipantListener: room.LocalParticipantListener(),
ParticipantHelper: &roomManagerParticipantHelper{
room: room,
codecRegressionThreshold: r.config.Video.CodecRegressionThreshold,
},
ReconnectOnPublicationError: reconnectOnPublicationError,
ReconnectOnSubscriptionError: reconnectOnSubscriptionError,
ReconnectOnDataChannelError: reconnectOnDataChannelError,
VersionGenerator: r.versionGenerator,
TrackResolver: room.ResolveMediaTrackForSubscriber,
SubscriberAllowPause: subscriberAllowPause,
SubscriptionLimitAudio: r.config.Limit.SubscriptionLimitAudio,
SubscriptionLimitVideo: r.config.Limit.SubscriptionLimitVideo,
PlayoutDelay: roomInternal.GetPlayoutDelay(),
SyncStreams: roomInternal.GetSyncStreams(),
ForwardStats: r.forwardStats,
MetricConfig: r.config.Metric,
UseOneShotSignallingMode: useOneShotSignallingMode,
DataChannelMaxBufferedAmount: r.config.RTC.DataChannelMaxBufferedAmount,
DatachannelSlowThreshold: r.config.RTC.DatachannelSlowThreshold,
FireOnTrackBySdp: true,
ReconnectOnPublicationError: reconnectOnPublicationError,
ReconnectOnSubscriptionError: reconnectOnSubscriptionError,
ReconnectOnDataChannelError: reconnectOnDataChannelError,
VersionGenerator: r.versionGenerator,
SubscriberAllowPause: subscriberAllowPause,
SubscriptionLimitAudio: r.config.Limit.SubscriptionLimitAudio,
SubscriptionLimitVideo: r.config.Limit.SubscriptionLimitVideo,
PlayoutDelay: roomInternal.GetPlayoutDelay(),
SyncStreams: roomInternal.GetSyncStreams(),
ForwardStats: r.forwardStats,
MetricConfig: r.config.Metric,
UseOneShotSignallingMode: useOneShotSignallingMode,
DataChannelMaxBufferedAmount: r.config.RTC.DataChannelMaxBufferedAmount,
DatachannelSlowThreshold: r.config.RTC.DatachannelSlowThreshold,
DatachannelLossyTargetLatency: r.config.RTC.DatachannelLossyTargetLatency,
FireOnTrackBySdp: true,
UseSinglePeerConnection: pi.UseSinglePeerConnection,
EnableDataTracks: r.config.EnableDataTracks,
EnableRTPStreamRestartDetection: r.config.RTC.EnableRTPStreamRestartDetection,
})
if err != nil {
return err
@@ -478,6 +534,9 @@ func (r *RoomManager) StartSession(
opts := rtc.ParticipantOptions{
AutoSubscribe: pi.AutoSubscribe,
}
if pi.AutoSubscribeDataTrack != nil {
opts.AutoSubscribeDataTrack = *pi.AutoSubscribeDataTrack
}
iceServers := r.iceServersForParticipant(apiKey, participant, iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TLS)
if err = room.Join(participant, requestSource, &opts, iceServers); err != nil {
pLogger.Errorw("could not join room", err)
@@ -485,16 +544,28 @@ func (r *RoomManager) StartSession(
return err
}
var participantServerClosers utils.Closers
participantTopic := rpc.FormatParticipantTopic(room.Name(), participant.Identity())
participantServer := must.Get(rpc.NewTypedParticipantServer(r, r.bus))
killParticipantServer := r.participantServers.Replace(participantTopic, participantServer)
participantServerClosers = append(participantServerClosers, utils.CloseFunc(r.participantServers.Replace(participantTopic, participantServer)))
if err := participantServer.RegisterAllParticipantTopics(participantTopic); err != nil {
killParticipantServer()
participantServerClosers.Close()
pLogger.Errorw("could not join register participant topic", err)
_ = participant.Close(true, types.ParticipantCloseReasonMessageBusFailed, false)
return err
}
if useOneShotSignallingMode {
whipParticipantServer := must.Get(rpc.NewTypedWHIPParticipantServer(whipParticipantService{r}, r.bus))
participantServerClosers = append(participantServerClosers, utils.CloseFunc(r.whipParticipantServers.Replace(participantTopic, whipParticipantServer)))
if err := whipParticipantServer.RegisterAllCommonTopics(participantTopic); err != nil {
participantServerClosers.Close()
pLogger.Errorw("could not join register participant topic for rtc rest participant server", err)
_ = participant.Close(true, types.ParticipantCloseReasonMessageBusFailed, false)
return err
}
}
if err = r.roomStore.StoreParticipant(ctx, room.Name(), participant.ToProto()); err != nil {
pLogger.Errorw("could not store participant", err)
}
@@ -512,9 +583,9 @@ func (r *RoomManager) StartSession(
persistRoomForParticipantCount(room.ToProto())
clientMeta := &livekit.AnalyticsClientMeta{Region: r.currentNode.Region(), Node: string(r.currentNode.NodeID())}
r.telemetry.ParticipantJoined(ctx, protoRoom, participant.ToProto(), pi.Client, clientMeta, true)
participant.OnClose(func(p types.LocalParticipant) {
killParticipantServer()
r.telemetry.ParticipantJoined(ctx, protoRoom, participant.ToProto(), pi.Client, clientMeta, true, participant.TelemetryGuard())
participant.AddOnClose(types.ParticipantCloseKeyNormal, func(p types.LocalParticipant) {
participantServerClosers.Close()
if err := r.roomStore.DeleteParticipant(ctx, room.Name(), p.Identity()); err != nil {
pLogger.Errorw("could not delete participant", err)
@@ -523,7 +594,7 @@ func (r *RoomManager) StartSession(
// update room store with new numParticipants
proto := room.ToProto()
persistRoomForParticipantCount(proto)
r.telemetry.ParticipantLeft(ctx, proto, p.ToProto(), true)
r.telemetry.ParticipantLeft(ctx, proto, p.ToProto(), true, participant.TelemetryGuard())
})
participant.OnClaimsChanged(func(participant types.LocalParticipant) {
pLogger.Debugw("refreshing client token after claims change")
@@ -535,6 +606,13 @@ func (r *RoomManager) StartSession(
r.iceConfigCache.Put(iceConfigCacheKey{room.Name(), participant.Identity()}, iceConfig)
})
for _, addTrackRequest := range pi.AddTrackRequests {
participant.AddTrack(addTrackRequest)
}
if pi.PublisherOffer != nil {
participant.HandleOffer(pi.PublisherOffer)
}
go r.rtcSessionWorker(room, participant, requestSource)
return nil
}
@@ -599,22 +677,22 @@ func (r *RoomManager) getOrCreateRoom(ctx context.Context, createRoom *livekit.C
r.telemetry.RoomEnded(ctx, roomInfo)
prometheus.RoomEnded(time.Unix(roomInfo.CreationTime, 0))
if err := r.deleteRoom(ctx, roomName); err != nil {
newRoom.Logger.Errorw("could not delete room", err)
newRoom.Logger().Errorw("could not delete room", err)
}
newRoom.Logger.Infow("room closed")
newRoom.Logger().Infow("room closed")
})
newRoom.OnRoomUpdated(func() {
if err := r.roomStore.StoreRoom(ctx, newRoom.ToProto(), newRoom.Internal()); err != nil {
newRoom.Logger.Errorw("could not handle metadata update", err)
newRoom.Logger().Errorw("could not handle metadata update", err)
}
})
newRoom.OnParticipantChanged(func(p types.LocalParticipant) {
newRoom.OnParticipantChanged(func(p types.Participant) {
if !p.IsDisconnected() {
if err := r.roomStore.StoreParticipant(ctx, roomName, p.ToProto()); err != nil {
newRoom.Logger.Errorw("could not handle participant change", err)
newRoom.Logger().Errorw("could not handle participant change", err)
}
}
})
@@ -648,12 +726,7 @@ func (r *RoomManager) getOrCreateRoom(ctx context.Context, createRoom *livekit.C
// manages an RTC session for a participant, runs on the RTC node
func (r *RoomManager) rtcSessionWorker(room *rtc.Room, participant types.LocalParticipant, requestSource routing.MessageSource) {
pLogger := rtc.LoggerWithParticipant(
rtc.LoggerWithRoom(logger.GetLogger(), room.Name(), room.ID()),
participant.Identity(),
participant.ID(),
false,
)
pLogger := participant.GetLogger()
defer func() {
pLogger.Debugw("RTC session finishing", "connID", requestSource.ConnectionID())
requestSource.Close()
@@ -673,11 +746,13 @@ func (r *RoomManager) rtcSessionWorker(room *rtc.Room, participant types.LocalPa
select {
case <-participant.Disconnected():
return
case <-tokenTicker.C:
// refresh token with the first API Key/secret pair
if err := r.refreshToken(participant); err != nil {
pLogger.Errorw("could not refresh token", err, "connID", requestSource.ConnectionID())
}
case obj := <-requestSource.ReadChan():
if obj == nil {
if room.GetParticipantRequestSource(participant.Identity()) == requestSource {
@@ -686,8 +761,7 @@ func (r *RoomManager) rtcSessionWorker(room *rtc.Room, participant types.LocalPa
return
}
req := obj.(*livekit.SignalRequest)
if err := rtc.HandleParticipantSignal(room, participant, req, pLogger); err != nil {
if err := participant.HandleSignalMessage(obj); err != nil {
// more specific errors are already logged
// treat errors returned as fatal
return
@@ -715,6 +789,37 @@ func (r *RoomManager) roomAndParticipantForReq(ctx context.Context, req particip
return room, participant, nil
}
func (r *RoomManager) ListParticipants(ctx context.Context, req *livekit.ListParticipantsRequest) (*livekit.ListParticipantsResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
participants := room.GetParticipants()
items := make([]*livekit.ParticipantInfo, 0, len(participants))
for _, p := range participants {
items = append(items, p.ToProto())
}
return &livekit.ListParticipantsResponse{
Participants: items,
}, nil
}
func (r *RoomManager) GetParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.ParticipantInfo, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
participant := room.GetParticipant(livekit.ParticipantIdentity(req.Identity))
if participant == nil {
return nil, ErrParticipantNotFound
}
return participant.ToProto(), nil
}
func (r *RoomManager) RemoveParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.RemoveParticipantResponse, error) {
room, participant, err := r.roomAndParticipantForReq(ctx, req)
if err != nil {
@@ -738,7 +843,10 @@ func (r *RoomManager) MutePublishedTrack(ctx context.Context, req *livekit.MuteR
participant.GetLogger().Errorw("cannot unmute track, remote unmute is disabled", nil)
return nil, ErrRemoteUnmuteNoteEnabled
}
track := participant.SetTrackMuted(livekit.TrackID(req.TrackSid), req.Muted, true)
track := participant.SetTrackMuted(&livekit.MuteTrackRequest{
Sid: req.TrackSid,
Muted: req.Muted,
}, true)
return &livekit.MuteRoomTrackResponse{Track: track}, nil
}
@@ -748,31 +856,58 @@ func (r *RoomManager) UpdateParticipant(ctx context.Context, req *livekit.Update
return nil, err
}
participant.GetLogger().Debugw("updating participant",
"metadata", req.Metadata,
"permission", req.Permission,
"attributes", req.Attributes,
)
if err = participant.CheckMetadataLimits(req.Name, req.Metadata, req.Attributes); err != nil {
if err = participant.UpdateMetadata(&livekit.UpdateParticipantMetadata{
Name: req.Name,
Metadata: req.Metadata,
Attributes: req.Attributes,
}, true); err != nil {
return nil, err
}
if req.Name != "" {
participant.SetName(req.Name)
}
if req.Metadata != "" {
participant.SetMetadata(req.Metadata)
}
if req.Attributes != nil {
participant.SetAttributes(req.Attributes)
}
if req.Permission != nil {
participant.GetLogger().Debugw(
"updating participant permission",
"permission", req.Permission,
)
participant.SetPermission(req.Permission)
}
return participant.ToProto(), nil
}
func (r *RoomManager) ForwardParticipant(ctx context.Context, req *livekit.ForwardParticipantRequest) (*livekit.ForwardParticipantResponse, error) {
return nil, errors.New("not implemented")
}
func (r *RoomManager) MoveParticipant(ctx context.Context, req *livekit.MoveParticipantRequest) (*livekit.MoveParticipantResponse, error) {
return nil, errors.New("not implemented")
}
func (r *RoomManager) PerformRpc(ctx context.Context, req *livekit.PerformRpcRequest) (*livekit.PerformRpcResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.GetRoom()))
if room == nil {
return nil, ErrRoomNotFound
}
participant := room.GetParticipant(livekit.ParticipantIdentity(req.GetDestinationIdentity()))
if participant == nil {
return nil, ErrParticipantNotFound
}
resultChan := make(chan string, 1)
errorChan := make(chan error, 1)
participant.PerformRpc(req, resultChan, errorChan)
select {
case result := <-resultChan:
return &livekit.PerformRpcResponse{Payload: result}, nil
case err := <-errorChan:
return nil, err
}
}
func (r *RoomManager) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomRequest) (*livekit.DeleteRoomResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
@@ -784,7 +919,7 @@ func (r *RoomManager) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomReq
return nil, err
}
} else {
room.Logger.Infow("deleting room")
room.Logger().Infow("deleting room")
room.Close(types.ParticipantCloseReasonServiceRequestDeleteRoom)
}
return &livekit.DeleteRoomResponse{}, nil
@@ -812,7 +947,7 @@ func (r *RoomManager) SendData(ctx context.Context, req *livekit.SendDataRequest
return nil, ErrRoomNotFound
}
room.Logger.Debugw("api send data", "size", len(req.Data))
room.Logger().Debugw("api send data", "size", len(req.Data))
room.SendDataPacket(&livekit.DataPacket{
Kind: req.Kind,
DestinationIdentities: req.DestinationIdentities,
@@ -835,7 +970,7 @@ func (r *RoomManager) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat
return nil, ErrRoomNotFound
}
room.Logger.Debugw("updating room")
room.Logger().Debugw("updating room")
done := room.SetMetadata(req.Metadata)
// wait till the update is applied
<-done
@@ -903,19 +1038,20 @@ func (r *RoomManager) iceServersForParticipant(apiKey string, participant types.
if r.config.TURN.UDPPort > 0 && !tlsOnly {
// UDP TURN is used as STUN
hasSTUN = true
urls = append(urls, fmt.Sprintf("turn:%s:%d?transport=udp", r.config.RTC.NodeIP, r.config.TURN.UDPPort))
for _, ip := range r.config.RTC.NodeIP.ToStringSlice() {
urls = append(urls, fmt.Sprintf("turn:%s?transport=udp", net.JoinHostPort(ip, strconv.Itoa(int(r.config.TURN.UDPPort)))))
}
}
if r.config.TURN.TLSPort > 0 {
urls = append(urls, fmt.Sprintf("turns:%s:443?transport=tcp", r.config.TURN.Domain))
}
if len(urls) > 0 {
username := r.turnAuthHandler.CreateUsername(apiKey, participant.ID())
password, err := r.turnAuthHandler.CreatePassword(apiKey, participant.ID())
username, expiry := r.turnAuthHandler.CreateUsername(apiKey, participant.ID(), r.config.TURN.TTLSeconds)
password, err := r.turnAuthHandler.CreatePassword(apiKey, participant.ID(), expiry)
if err != nil {
participant.GetLogger().Warnw("could not create turn password", err)
hasSTUN = false
} else {
logger.Infow("created TURN password", "username", username, "password", password)
iceServers = append(iceServers, &livekit.ICEServer{
Urls: urls,
Username: username,
@@ -930,17 +1066,41 @@ func (r *RoomManager) iceServersForParticipant(apiKey string, participant types.
for _, s := range r.config.RTC.TURNServers {
scheme := "turn"
transport := "tcp"
if s.Protocol == "tls" {
switch s.Protocol {
case "tls":
scheme = "turns"
} else if s.Protocol == "udp" {
case "udp":
transport = "udp"
}
var username, credential string
if s.Secret != "" {
// Generate dynamic credentials using TURN static auth secrets
ttl := s.TTL
if ttl == 0 {
ttl = 14400 // Default 4 hours
}
expiry := time.Now().Add(time.Duration(ttl) * time.Second).Unix()
participantID := string(participant.ID())
username = fmt.Sprintf("%d:%s", expiry, participantID)
// HMAC-SHA1 signature
h := hmac.New(sha1.New, []byte(s.Secret))
h.Write([]byte(username))
credential = base64.StdEncoding.EncodeToString(h.Sum(nil))
} else {
// Use static credentials
username = s.Username
credential = s.Credential
}
is := &livekit.ICEServer{
Urls: []string{
fmt.Sprintf("%s:%s:%d?transport=%s", scheme, s.Host, s.Port, transport),
},
Username: s.Username,
Credential: s.Credential,
Username: username,
Credential: credential,
}
iceServers = append(iceServers, is)
}
@@ -967,6 +1127,7 @@ func (r *RoomManager) refreshToken(participant types.LocalParticipant) error {
token := auth.NewAccessToken(key, secret)
token.SetName(grants.Name).
SetIdentity(string(participant.Identity())).
SetKind(grants.GetParticipantKind()).
SetValidFor(tokenDefaultTTL).
SetMetadata(grants.Metadata).
SetAttributes(grants.Attributes).
@@ -1010,3 +1171,41 @@ func iceServerForStunServers(servers []string) *livekit.ICEServer {
}
return iceServer
}
// ------------------------------------
type roomManagerParticipantHelper struct {
room *rtc.Room
codecRegressionThreshold int
}
func (h *roomManagerParticipantHelper) GetParticipantInfo(pID livekit.ParticipantID) *livekit.ParticipantInfo {
if p := h.room.GetParticipantByID(pID); p != nil {
return p.ToProto()
}
return nil
}
func (h *roomManagerParticipantHelper) GetRegionSettings(ip string) *livekit.RegionSettings {
return nil
}
func (h *roomManagerParticipantHelper) GetSubscriberForwarderState(lp types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error) {
return nil, nil
}
func (h *roomManagerParticipantHelper) ResolveMediaTrack(lp types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
return h.room.ResolveMediaTrackForSubscriber(lp, trackID)
}
func (h *roomManagerParticipantHelper) ResolveDataTrack(lp types.LocalParticipant, trackID livekit.TrackID) types.DataResolverResult {
return h.room.ResolveDataTrackForSubscriber(lp, trackID)
}
func (h *roomManagerParticipantHelper) ShouldRegressCodec() bool {
return h.codecRegressionThreshold == 0 || h.room.GetParticipantCount() < h.codecRegressionThreshold
}
func (h *roomManagerParticipantHelper) GetCachedReliableDataMessage(seqs map[livekit.ParticipantID]uint32) []*types.DataMessageCache {
return h.room.GetCachedReliableDataMessage(seqs)
}
+337
View File
@@ -0,0 +1,337 @@
package service
import (
"context"
"errors"
"fmt"
"time"
"github.com/pion/webrtc/v4"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/psrpc"
)
const (
whipSessionNotifyInterval = 10 * time.Second
)
type whipService struct {
*RoomManager
ingressRpcCli rpc.IngressHandlerClient
rpc.UnimplementedWHIPServer
}
func newWhipService(rm *RoomManager) (*whipService, error) {
cli, err := rpc.NewIngressHandlerClient(rm.bus, rpc.WithDefaultClientOptions(logger.GetLogger()))
if err != nil {
return nil, err
}
return &whipService{
RoomManager: rm,
ingressRpcCli: cli,
}, nil
}
func (s whipService) Create(ctx context.Context, req *rpc.WHIPCreateRequest) (*rpc.WHIPCreateResponse, error) {
pi, err := routing.ParticipantInitFromStartSession(req.StartSession, s.RoomManager.currentNode.Region())
if err != nil {
logger.Errorw("whip service: could not create participant init", err)
return nil, err
}
prometheus.IncrementParticipantRtcInit(1)
if err = s.RoomManager.StartSession(
ctx,
*pi,
routing.NewNullMessageSource(livekit.ConnectionID(req.StartSession.ConnectionId)), // no requestSource
routing.NewNullMessageSink(livekit.ConnectionID(req.StartSession.ConnectionId)), // no responseSink
true, // useOneShotSignallingMode
); err != nil {
logger.Errorw("whip service: could not start session", err)
return nil, err
}
room := s.RoomManager.GetRoom(ctx, livekit.RoomName(req.StartSession.RoomName))
if room == nil {
logger.Errorw("whip service: could not find room", nil, "room", req.StartSession.RoomName)
return nil, ErrRoomNotFound
}
lp := room.GetParticipant(pi.Identity)
if lp == nil {
room.Logger().Errorw("whip service: could not find local participant", nil, "participant", pi.Identity)
return nil, ErrParticipantNotFound
}
if err := lp.HandleOffer(&livekit.SessionDescription{
Type: webrtc.SDPTypeOffer.String(),
Sdp: req.OfferSdp,
Id: 0,
}); err != nil {
lp.GetLogger().Errorw("whip service: could not handle offer", err)
return nil, err
}
// wait for subscriptions to resolve
// NOTE: this is outside the WHIP spec, but added as a convenience for clients doing
// one-shot signalling (i. e. send an offer and get an answer once) to publish and subscribe to
// well-known tracks (i. e. remote participant identity and track names are well known)
eg, _ := errgroup.WithContext(ctx)
for publisherIdentity, trackList := range req.SubscribedParticipantTracks {
for _, trackName := range trackList.TrackNames {
eg.Go(func() error {
for {
if lp.IsTrackNameSubscribed(livekit.ParticipantIdentity(publisherIdentity), trackName) {
return nil
}
time.Sleep(50 * time.Millisecond)
}
})
}
}
err = eg.Wait()
if err != nil {
lp.GetLogger().Errorw("whip service: could not subscribe to tracks", err)
return nil, err
}
answer, _, err := lp.GetAnswer()
if err != nil {
lp.GetLogger().Errorw("whip service: could not get answer", err)
return nil, err
}
iceSessionID, err := lp.GetPublisherICESessionUfrag()
if err != nil {
lp.GetLogger().Errorw("whip service: could not get ICE session ID", err)
return nil, err
}
if req.FromIngress {
aliveCtx, cancel := context.WithCancel(context.Background())
lp.AddOnClose(types.ParticipantCloseKeyWHIP, func(lp types.LocalParticipant) {
cancel()
go func() {
lp.GetLogger().Debugw("whip service: notify participant closed")
video, audio := getMediaStateForParticipant(lp)
_, err := s.ingressRpcCli.WHIPRTCConnectionNotify(context.Background(), string(lp.ID()), &rpc.WHIPRTCConnectionNotifyRequest{
ParticipantId: string(lp.ID()),
Closed: true,
Audio: audio,
Video: video,
}, psrpc.WithRequestTimeout(rpc.DefaultPSRPCConfig.Timeout))
if err != nil {
lp.GetLogger().Warnw("whip service: could not notify ingress of participant closed", err)
}
}()
})
go func() {
if err := s.notifySession(aliveCtx, lp); err != nil {
cancel()
}
}()
}
var iceServers []*livekit.ICEServer
apiKey, _, err := s.RoomManager.getFirstKeyPair()
if err == nil {
iceServers = s.RoomManager.iceServersForParticipant(
apiKey,
lp,
false,
)
}
return &rpc.WHIPCreateResponse{
AnswerSdp: answer.SDP,
ParticipantId: string(lp.ID()),
IceServers: iceServers,
IceSessionId: iceSessionID,
}, nil
}
func (s whipService) notifySession(ctx context.Context, participant types.Participant) error {
ticker := time.NewTicker(whipSessionNotifyInterval)
defer ticker.Stop()
err := s.sendConnectionNotify(ctx, participant)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
}
for {
select {
case <-ticker.C:
err := s.sendConnectionNotify(ctx, participant)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
}
case <-ctx.Done():
return nil
}
}
}
func (s whipService) sendConnectionNotify(ctx context.Context, participant types.Participant) error {
video, audio := getMediaStateForParticipant(participant)
_, err := s.ingressRpcCli.WHIPRTCConnectionNotify(ctx, string(participant.ID()), &rpc.WHIPRTCConnectionNotifyRequest{
ParticipantId: string(participant.ID()),
Video: video,
Audio: audio,
}, psrpc.WithRequestTimeout(rpc.DefaultPSRPCConfig.Timeout))
return err
}
func getMediaStateForParticipant(participant types.Participant) (*livekit.InputVideoState, *livekit.InputAudioState) {
pParticipant := participant.ToProto()
var video *livekit.InputVideoState
var audio *livekit.InputAudioState
for _, v := range pParticipant.Tracks {
if v == nil {
continue
}
if v.Type != livekit.TrackType_VIDEO {
continue
}
video = &livekit.InputVideoState{}
video.MimeType = v.MimeType
video.Height = v.Height
video.Width = v.Width
break
}
for _, a := range pParticipant.Tracks {
if a == nil {
continue
}
if a.Type != livekit.TrackType_AUDIO {
continue
}
audio = &livekit.InputAudioState{}
audio.MimeType = a.MimeType
audio.Channels = 1
if a.Stereo {
audio.Channels = 2
}
break
}
return video, audio
}
// -------------------------------------------
type whipParticipantService struct {
*RoomManager
}
func (r whipParticipantService) ICETrickle(ctx context.Context, req *rpc.WHIPParticipantICETrickleRequest) (*emptypb.Empty, error) {
room := r.RoomManager.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
lp := room.GetParticipantByID(livekit.ParticipantID(req.ParticipantId))
if lp == nil {
return nil, ErrParticipantNotFound
}
iceSessionID, err := lp.GetPublisherICESessionUfrag()
if err != nil {
lp.GetLogger().Warnw("whipParticipant service ice-trickle: could not get ICE session ufrag", err)
return nil, psrpc.NewError(psrpc.Internal, err)
}
if req.IceSessionId != iceSessionID {
return nil, psrpc.NewError(
psrpc.FailedPrecondition,
fmt.Errorf("ice session mismatch, expected: %s, got: %s", iceSessionID, req.IceSessionId),
)
}
if err := lp.HandleICETrickleSDPFragment(req.SdpFragment); err != nil {
return nil, psrpc.NewError(psrpc.InvalidArgument, err)
}
return &emptypb.Empty{}, nil
}
func (r whipParticipantService) ICERestart(ctx context.Context, req *rpc.WHIPParticipantICERestartRequest) (*rpc.WHIPParticipantICERestartResponse, error) {
room := r.RoomManager.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
lp := room.GetParticipantByID(livekit.ParticipantID(req.ParticipantId))
if lp == nil {
return nil, ErrParticipantNotFound
}
sdpFragment, err := lp.HandleICERestartSDPFragment(req.SdpFragment)
if err != nil {
return nil, psrpc.NewError(psrpc.InvalidArgument, err)
}
iceSessionID, err := lp.GetPublisherICESessionUfrag()
if err != nil {
lp.GetLogger().Warnw("whipParticipant service ice-restart: could not get ICE session ufrag", err)
return nil, psrpc.NewError(psrpc.Internal, err)
}
return &rpc.WHIPParticipantICERestartResponse{
IceSessionId: iceSessionID,
SdpFragment: sdpFragment,
}, nil
}
func (r whipParticipantService) DeleteSession(ctx context.Context, req *rpc.WHIPParticipantDeleteSessionRequest) (*emptypb.Empty, error) {
room := r.RoomManager.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
lp := room.GetParticipantByID(livekit.ParticipantID(req.ParticipantId))
if lp != nil {
lp.AddOnClose(types.ParticipantCloseKeyWHIP, nil)
room.RemoveParticipant(
lp.Identity(),
lp.ID(),
types.ParticipantCloseReasonClientRequestLeave,
)
}
return &emptypb.Empty{}, nil
}
// --------------------------------
+98 -90
View File
@@ -24,11 +24,10 @@ import (
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/protocol/egress"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
)
type RoomService struct {
@@ -41,6 +40,9 @@ type RoomService struct {
topicFormatter rpc.TopicFormatter
roomClient rpc.TypedRoomClient
participantClient rpc.TypedParticipantClient
rpc.UnimplementedRoomServer
rpc.UnimplementedParticipantServer
}
func NewRoomService(
@@ -69,10 +71,9 @@ func NewRoomService(
}
func (s *RoomService) CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (*livekit.Room, error) {
redactedReq := redactCreateRoomRequest(req)
RecordRequest(ctx, redactedReq)
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Name, "request", logger.Proto(redactedReq))
AppendLogFields(ctx, "room", req.Name, "request", logger.Proto(req))
if err := EnsureCreatePermission(ctx); err != nil {
return nil, twirpAuthError(err)
} else if req.Egress != nil && s.egressLauncher == nil {
@@ -127,13 +128,15 @@ func (s *RoomService) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomReq
return nil, twirpAuthError(err)
}
_, _, err := s.roomStore.LoadRoom(ctx, livekit.RoomName(req.Room), false)
exists, err := s.roomStore.RoomExists(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, err
} else if !exists {
return nil, ErrRoomNotFound
}
// ensure at least one node is available to handle the request
_, err = s.router.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: req.Room})
room, err := s.router.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: req.Room})
if err != nil {
return nil, err
}
@@ -143,13 +146,15 @@ func (s *RoomService) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomReq
return nil, err
}
err = s.roomStore.DeleteRoom(ctx, livekit.RoomName(req.Room))
if os, ok := s.roomStore.(OSSServiceStore); ok {
err = os.DeleteRoom(ctx, livekit.RoomName(req.Room))
}
res := &livekit.DeleteRoomResponse{}
RecordResponse(ctx, res)
RecordResponse(ctx, room)
return res, err
}
func (s *RoomService) ListParticipants(ctx context.Context, req *livekit.ListParticipantsRequest) (*livekit.ListParticipantsResponse, error) {
func (s *RoomService) ListParticipants(ctx context.Context, req *livekit.ListParticipantsRequest) (res *livekit.ListParticipantsResponse, err error) {
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room)
@@ -157,19 +162,29 @@ func (s *RoomService) ListParticipants(ctx context.Context, req *livekit.ListPar
return nil, twirpAuthError(err)
}
participants, err := s.roomStore.ListParticipants(ctx, livekit.RoomName(req.Room))
if s.apiConf.EnablePsrpcForGetListParticpants {
res, err = s.roomClient.ListParticipants(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
} else if store, ok := s.roomStore.(OSSServiceStore); ok {
var participants []*livekit.ParticipantInfo
participants, err = store.ListParticipants(ctx, livekit.RoomName(req.Room))
if err == nil {
res = &livekit.ListParticipantsResponse{
Participants: participants,
}
}
} else {
err = psrpc.ErrUnimplemented
}
if err != nil {
return nil, err
}
res := &livekit.ListParticipantsResponse{
Participants: participants,
}
RecordResponse(ctx, res)
return res, nil
}
func (s *RoomService) GetParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.ParticipantInfo, error) {
func (s *RoomService) GetParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (participant *livekit.ParticipantInfo, err error) {
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity)
@@ -177,7 +192,14 @@ func (s *RoomService) GetParticipant(ctx context.Context, req *livekit.RoomParti
return nil, twirpAuthError(err)
}
participant, err := s.roomStore.LoadParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
if s.apiConf.EnablePsrpcForGetListParticpants {
participant, err = s.roomClient.GetParticipant(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
} else if store, ok := s.roomStore.(OSSServiceStore); ok {
participant, err = store.LoadParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
} else {
err = psrpc.ErrUnimplemented
}
if err != nil {
return nil, err
}
@@ -195,8 +217,13 @@ func (s *RoomService) RemoveParticipant(ctx context.Context, req *livekit.RoomPa
return nil, twirpAuthError(err)
}
if _, err := s.roomStore.LoadParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)); err == ErrParticipantNotFound {
return nil, twirp.NotFoundError("participant not found")
if os, ok := s.roomStore.(OSSServiceStore); ok {
found, err := os.HasParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
if err != nil {
return nil, err
} else if !found {
return nil, ErrParticipantNotFound
}
}
res, err := s.participantClient.RemoveParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
@@ -218,7 +245,7 @@ func (s *RoomService) MutePublishedTrack(ctx context.Context, req *livekit.MuteR
}
func (s *RoomService) UpdateParticipant(ctx context.Context, req *livekit.UpdateParticipantRequest) (*livekit.ParticipantInfo, error) {
RecordRequest(ctx, redactUpdateParticipantRequest(req))
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity)
@@ -238,6 +265,15 @@ func (s *RoomService) UpdateParticipant(ctx context.Context, req *livekit.Update
return nil, twirpAuthError(err)
}
if os, ok := s.roomStore.(OSSServiceStore); ok {
found, err := os.HasParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
if err != nil {
return nil, err
} else if !found {
return nil, ErrParticipantNotFound
}
}
res, err := s.participantClient.UpdateParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
RecordResponse(ctx, res)
return res, err
@@ -262,7 +298,7 @@ func (s *RoomService) UpdateSubscriptions(ctx context.Context, req *livekit.Upda
}
func (s *RoomService) SendData(ctx context.Context, req *livekit.SendDataRequest) (*livekit.SendDataResponse, error) {
RecordRequest(ctx, redactSendDataRequest(req))
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "size", len(req.Data))
@@ -281,7 +317,7 @@ func (s *RoomService) SendData(ctx context.Context, req *livekit.SendDataRequest
}
func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.UpdateRoomMetadataRequest) (*livekit.Room, error) {
RecordRequest(ctx, redactUpdateRoomMetadataRequest(req))
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room, "size", len(req.Metadata))
maxMetadataSize := int(s.limitConf.MaxMetadataSize)
@@ -293,9 +329,11 @@ func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat
return nil, twirpAuthError(err)
}
_, _, err := s.roomStore.LoadRoom(ctx, livekit.RoomName(req.Room), false)
exists, err := s.roomStore.RoomExists(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, err
} else if !exists {
return nil, ErrRoomNotFound
}
room, err := s.roomClient.UpdateRoomMetadata(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
@@ -307,86 +345,56 @@ func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat
return room, nil
}
func redactCreateRoomRequest(req *livekit.CreateRoomRequest) *livekit.CreateRoomRequest {
if req.Egress == nil && req.Metadata == "" {
// nothing to redact
return req
func (s *RoomService) ForwardParticipant(ctx context.Context, req *livekit.ForwardParticipantRequest) (*livekit.ForwardParticipantResponse, error) {
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "participant", req.Identity)
if err := EnsureDestRoomPermission(ctx, roomName, livekit.RoomName(req.DestinationRoom)); err != nil {
return nil, twirpAuthError(err)
}
clone := utils.CloneProto(req)
if clone.Egress != nil {
if clone.Egress.Room != nil {
egress.RedactEncodedOutputs(clone.Egress.Room)
}
if clone.Egress.Participant != nil {
egress.RedactAutoEncodedOutput(clone.Egress.Participant)
}
if clone.Egress.Tracks != nil {
egress.RedactUpload(clone.Egress.Tracks)
}
if req.Room == req.DestinationRoom {
return nil, twirp.InvalidArgumentError(ErrDestinationSameAsSourceRoom.Error(), "")
}
// replace with size of metadata to provide visibility on request size
if clone.Metadata != "" {
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
}
return clone
res, err := s.participantClient.ForwardParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
RecordResponse(ctx, res)
return res, err
}
func redactUpdateParticipantRequest(req *livekit.UpdateParticipantRequest) *livekit.UpdateParticipantRequest {
if req.Metadata == "" && len(req.Attributes) == 0 {
return req
func (s *RoomService) MoveParticipant(ctx context.Context, req *livekit.MoveParticipantRequest) (*livekit.MoveParticipantResponse, error) {
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "participant", req.Identity)
if err := EnsureDestRoomPermission(ctx, roomName, livekit.RoomName(req.DestinationRoom)); err != nil {
return nil, twirpAuthError(err)
}
clone := utils.CloneProto(req)
// replace with size of metadata/attributes to provide visibility on request size
if clone.Metadata != "" {
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
if req.Room == req.DestinationRoom {
return nil, twirp.InvalidArgumentError(ErrDestinationSameAsSourceRoom.Error(), "")
}
if len(clone.Attributes) != 0 {
keysSize := 0
valuesSize := 0
for k, v := range clone.Attributes {
keysSize += len(k)
valuesSize += len(v)
}
clone.Attributes = map[string]string{
"__num_elements": fmt.Sprintf("%d", len(clone.Attributes)),
"__keys_size": fmt.Sprintf("%d", keysSize),
"__values_size": fmt.Sprintf("%d", valuesSize),
}
}
return clone
res, err := s.participantClient.MoveParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
RecordResponse(ctx, res)
return res, err
}
func redactSendDataRequest(req *livekit.SendDataRequest) *livekit.SendDataRequest {
if len(req.Data) == 0 {
return req
func (s *RoomService) PerformRpc(ctx context.Context, req *livekit.PerformRpcRequest) (*livekit.PerformRpcResponse, error) {
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "participant", req.DestinationIdentity)
if err := EnsureAdminPermission(ctx, roomName); err != nil {
return nil, twirpAuthError(err)
}
if req.DestinationIdentity == "" {
return nil, ErrDestinationIdentityRequired
}
clone := utils.CloneProto(req)
// replace with size of data to provide visibility on request size
clone.Data = []byte(fmt.Sprintf("__size: %d", len(clone.Data)))
return clone
}
func redactUpdateRoomMetadataRequest(req *livekit.UpdateRoomMetadataRequest) *livekit.UpdateRoomMetadataRequest {
if req.Metadata == "" {
return req
}
clone := utils.CloneProto(req)
// replace with size of metadata to provide visibility on request size
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
return clone
res, err := s.participantClient.PerformRpc(ctx, s.topicFormatter.ParticipantTopic(ctx, roomName, livekit.ParticipantIdentity(req.DestinationIdentity)), req)
RecordResponse(ctx, res)
return res, err
}
+329 -213
View File
@@ -16,8 +16,11 @@ package service
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"maps"
"math/rand"
"net/http"
"os"
@@ -26,31 +29,28 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/ua-parser/uap-go/uaparser"
"go.uber.org/atomic"
"golang.org/x/exp/maps"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/routing/selector"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/psrpc"
)
type RTCService struct {
router routing.MessageRouter
roomAllocator RoomAllocator
store ServiceStore
upgrader websocket.Upgrader
currentNode routing.LocalNode
config *config.Config
isDev bool
limits config.LimitConfig
parser *uaparser.Parser
telemetry telemetry.TelemetryService
mu sync.Mutex
@@ -60,20 +60,15 @@ type RTCService struct {
func NewRTCService(
conf *config.Config,
ra RoomAllocator,
store ServiceStore,
router routing.MessageRouter,
currentNode routing.LocalNode,
telemetry telemetry.TelemetryService,
) *RTCService {
s := &RTCService{
router: router,
roomAllocator: ra,
store: store,
currentNode: currentNode,
config: conf,
isDev: conf.Development,
limits: conf.Limit,
parser: uaparser.NewFromSaved(),
telemetry: telemetry,
connections: map[*websocket.Conn]struct{}{},
}
@@ -92,145 +87,294 @@ func NewRTCService(
}
func (s *RTCService) SetupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/rtc/validate", s.validate)
mux.HandleFunc("/rtc", s.v0)
mux.HandleFunc("/rtc/validate", s.v0Validate)
mux.HandleFunc("/rtc/v1", s.v1)
mux.HandleFunc("/rtc/v1/validate", s.v1Validate)
}
func (s *RTCService) validate(w http.ResponseWriter, r *http.Request) {
_, _, code, err := s.validateInternal(r)
func (s *RTCService) v0Validate(w http.ResponseWriter, r *http.Request) {
lgr := utils.GetLogger(r.Context())
_, _, code, err := s.validateInternal(lgr, r, false, true)
if err != nil {
handleError(w, r, code, err)
HandleError(w, r, code, err)
return
}
_, _ = w.Write([]byte("success"))
}
func (s *RTCService) validateInternal(r *http.Request) (livekit.RoomName, routing.ParticipantInit, int, error) {
claims := GetGrants(r.Context())
var pi routing.ParticipantInit
// require a claim
if claims == nil || claims.Video == nil {
return "", pi, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
onlyName, err := EnsureJoinPermission(r.Context())
func (s *RTCService) v1Validate(w http.ResponseWriter, r *http.Request) {
lgr := utils.GetLogger(r.Context())
_, _, code, err := s.validateInternal(lgr, r, true, true)
if err != nil {
return "", pi, http.StatusUnauthorized, err
HandleError(w, r, code, err)
return
}
if claims.Identity == "" {
return "", pi, http.StatusBadRequest, ErrIdentityEmpty
}
if limit := s.config.Limit.MaxParticipantIdentityLength; limit > 0 && len(claims.Identity) > limit {
return "", pi, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrParticipantIdentityExceedsLimits, limit)
}
roomName := livekit.RoomName(r.FormValue("room"))
reconnectParam := r.FormValue("reconnect")
reconnectReason, _ := strconv.Atoi(r.FormValue("reconnect_reason")) // 0 means unknown reason
autoSubParam := r.FormValue("auto_subscribe")
publishParam := r.FormValue("publish")
adaptiveStreamParam := r.FormValue("adaptive_stream")
participantID := r.FormValue("sid")
subscriberAllowPauseParam := r.FormValue("subscriber_allow_pause")
disableICELite := r.FormValue("disable_ice_lite")
if onlyName != "" {
roomName = onlyName
}
if limit := s.config.Limit.MaxRoomNameLength; limit > 0 && len(roomName) > limit {
return "", pi, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, limit)
}
// this is new connection for existing participant - with publish only permissions
if publishParam != "" {
// Make sure grant has GetCanPublish set,
if !claims.Video.GetCanPublish() {
return "", routing.ParticipantInit{}, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
// Make sure by default subscribe is off
claims.Video.SetCanSubscribe(false)
claims.Identity += "#" + publishParam
}
// room allocator validations
err = s.roomAllocator.ValidateCreateRoom(r.Context(), roomName)
if err != nil {
if errors.Is(err, ErrRoomNotFound) {
return "", pi, http.StatusNotFound, err
} else {
return "", pi, http.StatusInternalServerError, err
}
}
region := ""
if router, ok := s.router.(routing.Router); ok {
region = router.GetRegion()
if foundNode, err := router.GetNodeForRoom(r.Context(), roomName); err == nil {
if selector.LimitsReached(s.limits, foundNode.Stats) {
return "", pi, http.StatusServiceUnavailable, rtc.ErrLimitExceeded
}
}
}
createRequest := &livekit.CreateRoomRequest{
Name: string(roomName),
RoomPreset: claims.RoomPreset,
}
SetRoomConfiguration(createRequest, claims.GetRoomConfiguration())
pi = routing.ParticipantInit{
Reconnect: boolValue(reconnectParam),
ReconnectReason: livekit.ReconnectReason(reconnectReason),
Identity: livekit.ParticipantIdentity(claims.Identity),
Name: livekit.ParticipantName(claims.Name),
AutoSubscribe: true,
Client: s.ParseClientInfo(r),
Grants: claims,
Region: region,
CreateRoom: createRequest,
}
if pi.Reconnect {
pi.ID = livekit.ParticipantID(participantID)
}
if autoSubParam != "" {
pi.AutoSubscribe = boolValue(autoSubParam)
}
if adaptiveStreamParam != "" {
pi.AdaptiveStream = boolValue(adaptiveStreamParam)
}
if subscriberAllowPauseParam != "" {
subscriberAllowPause := boolValue(subscriberAllowPauseParam)
pi.SubscriberAllowPause = &subscriberAllowPause
}
if disableICELite != "" {
pi.DisableICELite = boolValue(disableICELite)
}
return roomName, pi, http.StatusOK, nil
_, _ = w.Write([]byte("success"))
}
func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func decodeAttributes(str string) (map[string]string, error) {
data, err := base64.URLEncoding.DecodeString(str)
if err != nil {
return nil, err
}
var attrs map[string]string
if err := json.Unmarshal(data, &attrs); err != nil {
return nil, err
}
return attrs, nil
}
var errJoinRequestTooLarge = errors.New("join request too large")
func (s *RTCService) validateInternal(
lgr logger.Logger,
r *http.Request,
needsJoinRequest bool,
strict bool,
) (livekit.RoomName, routing.ParticipantInit, int, error) {
if claims := GetGrants(r.Context()); claims == nil || claims.Video == nil {
return "", routing.ParticipantInit{}, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
var params ValidateConnectRequestParams
useSinglePeerConnection := false
joinRequest := &livekit.JoinRequest{}
wrappedJoinRequestBase64 := r.FormValue("join_request")
if wrappedJoinRequestBase64 == "" {
if needsJoinRequest {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("join_request is required")
}
params.publish = r.FormValue("publish")
attributesStrParam := r.FormValue("attributes")
if attributesStrParam != "" {
attrs, err := decodeAttributes(attributesStrParam)
if err != nil {
if strict {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot decode attributes")
}
lgr.Debugw("failed to decode attributes", "error", err)
// attrs will be empty here, so just proceed
}
params.attributes = attrs
}
} else {
useSinglePeerConnection = true
if wrappedProtoBytes, err := base64.URLEncoding.DecodeString(wrappedJoinRequestBase64); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot base64 decode wrapped join request")
} else {
wrappedJoinRequest := &livekit.WrappedJoinRequest{}
if err := proto.Unmarshal(wrappedProtoBytes, wrappedJoinRequest); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot unmarshal wrapped join request")
}
switch wrappedJoinRequest.Compression {
case livekit.WrappedJoinRequest_NONE:
if len(wrappedJoinRequest.JoinRequest) > http.DefaultMaxHeaderBytes {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errJoinRequestTooLarge
}
if err := proto.Unmarshal(wrappedJoinRequest.JoinRequest, joinRequest); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot unmarshal join request")
}
case livekit.WrappedJoinRequest_GZIP:
protoBytes, err := DecompressGzip(wrappedJoinRequest.JoinRequest)
if err != nil {
switch {
case errors.Is(err, ErrGzipTooLarge):
err = errJoinRequestTooLarge
case errors.Is(err, ErrGzipReadFailed):
err = errors.New("cannot read decompressed join request")
}
return "", routing.ParticipantInit{}, http.StatusBadRequest, err
}
if err := proto.Unmarshal(protoBytes, joinRequest); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot unmarshal join request")
}
}
params.metadata = joinRequest.Metadata
params.attributes = joinRequest.ParticipantAttributes
}
}
res, code, err := ValidateConnectRequest(
lgr,
r,
s.limits,
params,
s.router,
s.roomAllocator,
)
if err != nil {
return res.roomName, routing.ParticipantInit{}, code, err
}
pi := routing.ParticipantInit{
Identity: livekit.ParticipantIdentity(res.grants.Identity),
Name: livekit.ParticipantName(res.grants.Name),
Grants: res.grants,
Region: res.region,
CreateRoom: res.createRoomRequest,
UseSinglePeerConnection: useSinglePeerConnection,
}
if wrappedJoinRequestBase64 == "" {
pi.Reconnect = boolValue(r.FormValue("reconnect"))
pi.Client = ParseClientInfo(r)
pi.AutoSubscribe = true
if autoSubscribeParam := r.FormValue("auto_subscribe"); autoSubscribeParam != "" {
pi.AutoSubscribe = boolValue(autoSubscribeParam)
}
if autoSubscribeDataTrackParam := r.FormValue("auto_subscribe_data_track"); autoSubscribeDataTrackParam != "" {
autoSubscribeDataTrack := boolValue(autoSubscribeDataTrackParam)
pi.AutoSubscribeDataTrack = &autoSubscribeDataTrack
}
pi.AdaptiveStream = boolValue(r.FormValue("adaptive_stream"))
pi.DisableICELite = boolValue(r.FormValue("disable_ice_lite"))
reconnectReason, _ := strconv.Atoi(r.FormValue("reconnect_reason")) // 0 means unknown reason
pi.ReconnectReason = livekit.ReconnectReason(reconnectReason)
if pi.Reconnect {
pi.ID = livekit.ParticipantID(r.FormValue("sid"))
}
if subscriberAllowPauseParam := r.FormValue("subscriber_allow_pause"); subscriberAllowPauseParam != "" {
subscriberAllowPause := boolValue(subscriberAllowPauseParam)
pi.SubscriberAllowPause = &subscriberAllowPause
}
} else {
lgr.Debugw("processing join request", "joinRequest", logger.Proto(joinRequest))
if joinRequest.ClientInfo == nil {
joinRequest.ClientInfo = &livekit.ClientInfo{}
}
AugmentClientInfo(joinRequest.ClientInfo, r)
pi.Client = joinRequest.ClientInfo
pi.AutoSubscribe = joinRequest.GetConnectionSettings().GetAutoSubscribe()
autoSubscribeDataTrack := joinRequest.GetConnectionSettings().GetAutoSubscribeDataTrack()
pi.AutoSubscribeDataTrack = &autoSubscribeDataTrack
pi.AdaptiveStream = joinRequest.GetConnectionSettings().GetAdaptiveStream()
pi.DisableICELite = joinRequest.GetConnectionSettings().GetDisableIceLite()
subscriberAllowPause := joinRequest.GetConnectionSettings().GetSubscriberAllowPause()
pi.SubscriberAllowPause = &subscriberAllowPause
pi.AddTrackRequests = joinRequest.AddTrackRequests
pi.PublisherOffer = joinRequest.PublisherOffer
pi.Reconnect = joinRequest.Reconnect
pi.ReconnectReason = joinRequest.ReconnectReason
pi.ID = livekit.ParticipantID(joinRequest.ParticipantSid)
}
return res.roomName, pi, code, err
}
func (s *RTCService) v0(w http.ResponseWriter, r *http.Request) {
s.serve(w, r, false)
}
func (s *RTCService) v1(w http.ResponseWriter, r *http.Request) {
s.serve(w, r, true)
}
func (s *RTCService) serve(w http.ResponseWriter, r *http.Request, needsJoinRequest bool) {
// reject non websocket requests
if !websocket.IsWebSocketUpgrade(r) {
w.WriteHeader(404)
return
}
roomName, pi, code, err := s.validateInternal(r)
startedAt := time.Now()
var (
roomName livekit.RoomName
roomID livekit.RoomID
participantIdentity livekit.ParticipantIdentity
pID livekit.ParticipantID
joinDuration time.Duration
loggerResolved bool
pi routing.ParticipantInit
code int
err error
)
pLogger, loggerResolver := utils.GetLogger(r.Context()).WithDeferredValues()
getLoggerFields := func() []any {
return []any{
"room", roomName,
"roomID", roomID,
"participant", participantIdentity,
"participantID", pID,
"joinDuration", joinDuration,
}
}
resolveLogger := func(force bool) {
if loggerResolved {
return
}
if force {
if roomName == "" {
roomName = "unresolved"
}
if roomID == "" {
roomID = "unresolved"
}
if participantIdentity == "" {
participantIdentity = "unresolved"
}
if pID == "" {
pID = "unresolved"
}
if joinDuration == 0 {
joinDuration = time.Since(startedAt)
}
}
if roomName != "" && roomID != "" && participantIdentity != "" && pID != "" {
loggerResolved = true
loggerResolver.Resolve(getLoggerFields()...)
}
}
resetLogger := func() {
loggerResolver.Reset()
roomName = ""
roomID = ""
participantIdentity = ""
pID = ""
loggerResolved = false
}
roomName, pi, code, err = s.validateInternal(pLogger, r, needsJoinRequest, false)
if err != nil {
handleError(w, r, code, err)
prometheus.IncrementParticipantJoinValidationFail(1)
resolveLogger(true)
HandleError(w, r, code, err, getLoggerFields()...)
return
}
loggerFields := []any{
"participant", pi.Identity,
"pID", pi.ID,
"room", roomName,
"remote", false,
participantIdentity = pi.Identity
if pi.ID != "" {
pID = pi.ID
}
pLogger := utils.GetLogger(r.Context()).WithValues(loggerFields...)
pLogger.Debugw("join request validated", append(getLoggerFields(), "participantInit", &pi)...)
// give it a few attempts to start session
var cr connectionResult
@@ -251,17 +395,28 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if errors.As(err, &psrpcErr) {
status = psrpcErr.ToHttp()
}
handleError(w, r, status, err, loggerFields...)
resolveLogger(true)
HandleError(w, r, status, err, getLoggerFields()...)
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())
if joinRoomID != "" {
roomID = joinRoomID
}
pi.ID = livekit.ParticipantID(initialResponse.GetJoin().GetParticipant().GetSid())
pID = pi.ID
resolveLogger(false)
}
signalStats := telemetry.NewBytesSignalStats(r.Context(), s.telemetry)
signalStats := rtc.NewBytesSignalStats(r.Context(), s.telemetry)
if join := initialResponse.GetJoin(); join != nil {
signalStats.ResolveRoom(join.GetRoom())
signalStats.ResolveParticipant(join.GetParticipant())
@@ -277,10 +432,8 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
done := make(chan struct{})
// function exits when websocket terminates, it'll close the event reading off of request sink and response source as well
defer func() {
pLogger.Debugw("finishing WS connection",
"connID", cr.ConnectionID,
"closedByClient", closedByClient.Load(),
)
resolveLogger(true)
pLogger.Debugw("finishing WS connection", "closedByClient", closedByClient.Load())
cr.ResponseSource.Close()
cr.RequestSink.Close()
close(done)
@@ -291,7 +444,8 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// upgrade only once the basics are good to go
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
handleError(w, r, http.StatusInternalServerError, err, loggerFields...)
resolveLogger(true)
HandleError(w, r, http.StatusInternalServerError, err, getLoggerFields()...)
return
}
@@ -307,15 +461,17 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// websocket established
sigConn := NewWSSignalConnection(conn)
pLogger.Debugw("sending initial response", "response", logger.Proto(initialResponse))
count, err := sigConn.WriteResponse(initialResponse)
if err != nil {
resolveLogger(true)
pLogger.Warnw("could not write initial response", err)
return
}
signalStats.AddBytes(uint64(count), true)
pLogger.Debugw("new client WS connected",
"connID", cr.ConnectionID,
pLogger.Debugw(
"new client WS connected",
"reconnect", pi.Reconnect,
"reconnectReason", pi.ReconnectReason,
"adaptiveStream", pi.AdaptiveStream,
@@ -343,7 +499,8 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
case msg := <-cr.ResponseSource.ReadChan():
if msg == nil {
pLogger.Debugw("nothing to read from response source", "connID", cr.ConnectionID)
resolveLogger(true)
pLogger.Debugw("nothing to read from response source")
return
}
res, ok := msg.(*livekit.SignalResponse)
@@ -351,7 +508,6 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pLogger.Errorw(
"unexpected message type", nil,
"type", fmt.Sprintf("%T", msg),
"connID", cr.ConnectionID,
)
continue
}
@@ -359,17 +515,46 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch m := res.Message.(type) {
case *livekit.SignalResponse_Offer:
pLogger.Debugw("sending offer", "offer", m)
case *livekit.SignalResponse_Answer:
pLogger.Debugw("sending answer", "answer", m)
case *livekit.SignalResponse_Join:
pLogger.Debugw("sending join", "join", m)
signalStats.ResolveRoom(m.Join.GetRoom())
signalStats.ResolveParticipant(m.Join.GetParticipant())
case *livekit.SignalResponse_RoomUpdate:
updateRoomID := livekit.RoomID(m.RoomUpdate.GetRoom().GetSid())
if updateRoomID != "" {
roomID = updateRoomID
resolveLogger(false)
}
pLogger.Debugw("sending room update", "roomUpdate", m)
signalStats.ResolveRoom(m.RoomUpdate.GetRoom())
case *livekit.SignalResponse_Update:
pLogger.Debugw("sending participant update", "participantUpdate", m)
case *livekit.SignalResponse_RoomMoved:
resetLogger()
signalStats.Reset()
roomName = livekit.RoomName(m.RoomMoved.GetRoom().GetName())
moveRoomID := livekit.RoomID(m.RoomMoved.GetRoom().GetSid())
if moveRoomID != "" {
roomID = moveRoomID
}
participantIdentity = livekit.ParticipantIdentity(m.RoomMoved.GetParticipant().GetIdentity())
pID = livekit.ParticipantID(m.RoomMoved.GetParticipant().GetSid())
resolveLogger(false)
signalStats.ResolveRoom(m.RoomMoved.GetRoom())
signalStats.ResolveParticipant(m.RoomMoved.GetParticipant())
pLogger.Debugw("sending room moved", "roomMoved", m)
default:
pLogger.Debugw("sending signal response", "response", m)
}
if count, err := sigConn.WriteResponse(res); err != nil {
@@ -389,7 +574,7 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if IsWebSocketCloseError(err) {
closedByClient.Store(true)
} else {
pLogger.Errorw("error reading from websocket", err, "connID", cr.ConnectionID)
pLogger.Errorw("error reading from websocket", err)
}
return
}
@@ -428,86 +613,17 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pLogger.Debugw("received offer", "offer", m)
case *livekit.SignalRequest_Answer:
pLogger.Debugw("received answer", "answer", m)
default:
pLogger.Debugw("received signal request", "request", m)
}
if err := cr.RequestSink.WriteMessage(req); err != nil {
pLogger.Warnw("error writing to request sink", err, "connID", cr.ConnectionID)
pLogger.Warnw("error writing to request sink", err)
return
}
}
}
func (s *RTCService) ParseClientInfo(r *http.Request) *livekit.ClientInfo {
values := r.Form
ci := &livekit.ClientInfo{}
if pv, err := strconv.Atoi(values.Get("protocol")); err == nil {
ci.Protocol = int32(pv)
}
sdkString := values.Get("sdk")
switch sdkString {
case "js":
ci.Sdk = livekit.ClientInfo_JS
case "ios", "swift":
ci.Sdk = livekit.ClientInfo_SWIFT
case "android":
ci.Sdk = livekit.ClientInfo_ANDROID
case "flutter":
ci.Sdk = livekit.ClientInfo_FLUTTER
case "go":
ci.Sdk = livekit.ClientInfo_GO
case "unity":
ci.Sdk = livekit.ClientInfo_UNITY
case "reactnative":
ci.Sdk = livekit.ClientInfo_REACT_NATIVE
case "rust":
ci.Sdk = livekit.ClientInfo_RUST
case "python":
ci.Sdk = livekit.ClientInfo_PYTHON
case "cpp":
ci.Sdk = livekit.ClientInfo_CPP
case "unityweb":
ci.Sdk = livekit.ClientInfo_UNITY_WEB
case "node":
ci.Sdk = livekit.ClientInfo_NODE
}
ci.Version = values.Get("version")
ci.Os = values.Get("os")
ci.OsVersion = values.Get("os_version")
ci.Browser = values.Get("browser")
ci.BrowserVersion = values.Get("browser_version")
ci.DeviceModel = values.Get("device_model")
ci.Network = values.Get("network")
// get real address (forwarded http header) - check Cloudflare headers first, fall back to X-Forwarded-For
ci.Address = GetClientIP(r)
// attempt to parse types for SDKs that support browser as a platform
if ci.Sdk == livekit.ClientInfo_JS ||
ci.Sdk == livekit.ClientInfo_REACT_NATIVE ||
ci.Sdk == livekit.ClientInfo_FLUTTER ||
ci.Sdk == livekit.ClientInfo_UNITY {
client := s.parser.Parse(r.UserAgent())
if ci.Browser == "" {
ci.Browser = client.UserAgent.Family
ci.BrowserVersion = client.UserAgent.ToVersionString()
}
if ci.Os == "" {
ci.Os = client.Os.Family
ci.OsVersion = client.Os.ToVersionString()
}
if ci.DeviceModel == "" {
model := client.Device.Family
if model != "" && client.Device.Model != "" && model != client.Device.Model {
model += " " + client.Device.Model
}
ci.DeviceModel = model
}
}
return ci
}
func (s *RTCService) DrainConnections(interval time.Duration) {
s.mu.Lock()
conns := maps.Clone(s.connections)
+12 -6
View File
@@ -27,7 +27,7 @@ import (
"strconv"
"time"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/cors"
"github.com/twitchtv/twirp"
@@ -49,6 +49,7 @@ type LivekitServer struct {
config *config.Config
ioService *IOInfoService
rtcService *RTCService
whipService *WHIPService
agentService *AgentService
httpServer *http.Server
promServer *http.Server
@@ -70,6 +71,7 @@ func NewLivekitServer(conf *config.Config,
sipService *SIPService,
ioService *IOInfoService,
rtcService *RTCService,
whipService *WHIPService,
agentService *AgentService,
keyProvider auth.KeyProvider,
router routing.Router,
@@ -82,6 +84,7 @@ func NewLivekitServer(conf *config.Config,
config: conf,
ioService: ioService,
rtcService: rtcService,
whipService: whipService,
agentService: agentService,
router: router,
roomManager: roomManager,
@@ -100,7 +103,9 @@ func NewLivekitServer(conf *config.Config,
AllowOriginFunc: func(origin string) bool {
return true
},
AllowedMethods: []string{"OPTIONS", "HEAD", "GET", "POST", "PATCH", "DELETE"},
AllowedHeaders: []string{"*"},
ExposedHeaders: []string{"*"},
// allow preflight to be cached for a day
MaxAge: 86400,
}),
@@ -110,9 +115,10 @@ func NewLivekitServer(conf *config.Config,
middlewares = append(middlewares, NewAPIKeyAuthMiddleware(keyProvider))
}
serverOptions := []interface{}{
serverOptions := []any{
twirp.WithServerHooks(twirp.ChainHooks(
TwirpLogger(),
TwirpEgressID(),
TwirpRequestStatusReporter(),
)),
}
@@ -138,8 +144,8 @@ func NewLivekitServer(conf *config.Config,
xtwirp.RegisterServer(mux, egressServer)
xtwirp.RegisterServer(mux, ingressServer)
xtwirp.RegisterServer(mux, sipServer)
mux.Handle("/rtc", rtcService)
rtcService.SetupRoutes(mux)
whipService.SetupRoutes(mux)
mux.Handle("/agent", agentService)
mux.HandleFunc("/", s.defaultHandler)
@@ -231,7 +237,7 @@ func (s *LivekitServer) Start() error {
}
}
values := []interface{}{
values := []any{
"portHttp", s.config.Port,
"nodeID", s.currentNode.NodeID(),
"nodeIP", s.currentNode.NodeIP(),
@@ -342,7 +348,7 @@ func (s *LivekitServer) debugGoroutines(w http.ResponseWriter, _ *http.Request)
func (s *LivekitServer) debugInfo(w http.ResponseWriter, _ *http.Request) {
s.roomManager.lock.RLock()
info := make([]map[string]interface{}, 0, len(s.roomManager.rooms))
info := make([]map[string]any, 0, len(s.roomManager.rooms))
for _, room := range s.roomManager.rooms {
info = append(info, room.DebugInfo())
}
@@ -372,7 +378,7 @@ func (s *LivekitServer) healthCheck(w http.ResponseWriter, _ *http.Request) {
}
if time.Since(updatedAt) > 4*time.Second {
w.WriteHeader(http.StatusNotAcceptable)
_, _ = w.Write([]byte(fmt.Sprintf("Not Ready\nNode Updated At %s", updatedAt)))
_, _ = fmt.Fprintf(w, "Not Ready\nNode Updated At %s", updatedAt)
return
}
@@ -392,16 +392,6 @@ func (fake *FakeAgentStore) StoreAgentJobReturnsOnCall(i int, result1 error) {
func (fake *FakeAgentStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteAgentDispatchMutex.RLock()
defer fake.deleteAgentDispatchMutex.RUnlock()
fake.deleteAgentJobMutex.RLock()
defer fake.deleteAgentJobMutex.RUnlock()
fake.listAgentDispatchesMutex.RLock()
defer fake.listAgentDispatchesMutex.RUnlock()
fake.storeAgentDispatchMutex.RLock()
defer fake.storeAgentDispatchMutex.RUnlock()
fake.storeAgentJobMutex.RLock()
defer fake.storeAgentJobMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -325,14 +325,6 @@ func (fake *FakeEgressStore) UpdateEgressReturnsOnCall(i int, result1 error) {
func (fake *FakeEgressStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.listEgressMutex.RLock()
defer fake.listEgressMutex.RUnlock()
fake.loadEgressMutex.RLock()
defer fake.loadEgressMutex.RUnlock()
fake.storeEgressMutex.RLock()
defer fake.storeEgressMutex.RUnlock()
fake.updateEgressMutex.RLock()
defer fake.updateEgressMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -552,20 +552,6 @@ func (fake *FakeIngressStore) UpdateIngressStateReturnsOnCall(i int, result1 err
func (fake *FakeIngressStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteIngressMutex.RLock()
defer fake.deleteIngressMutex.RUnlock()
fake.listIngressMutex.RLock()
defer fake.listIngressMutex.RUnlock()
fake.loadIngressMutex.RLock()
defer fake.loadIngressMutex.RUnlock()
fake.loadIngressFromStreamKeyMutex.RLock()
defer fake.loadIngressFromStreamKeyMutex.RUnlock()
fake.storeIngressMutex.RLock()
defer fake.storeIngressMutex.RUnlock()
fake.updateIngressMutex.RLock()
defer fake.updateIngressMutex.RUnlock()
fake.updateIngressStateMutex.RLock()
defer fake.updateIngressStateMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
-10
View File
@@ -414,16 +414,6 @@ func (fake *FakeIOClient) UpdateIngressStateReturnsOnCall(i int, result1 *emptyp
func (fake *FakeIOClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.createEgressMutex.RLock()
defer fake.createEgressMutex.RUnlock()
fake.createIngressMutex.RLock()
defer fake.createIngressMutex.RUnlock()
fake.getEgressMutex.RLock()
defer fake.getEgressMutex.RUnlock()
fake.listEgressMutex.RLock()
defer fake.listEgressMutex.RUnlock()
fake.updateIngressStateMutex.RLock()
defer fake.updateIngressStateMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
+160 -20
View File
@@ -36,6 +36,21 @@ type FakeObjectStore struct {
deleteRoomReturnsOnCall map[int]struct {
result1 error
}
HasParticipantStub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (bool, error)
hasParticipantMutex sync.RWMutex
hasParticipantArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}
hasParticipantReturns struct {
result1 bool
result2 error
}
hasParticipantReturnsOnCall map[int]struct {
result1 bool
result2 error
}
ListParticipantsStub func(context.Context, livekit.RoomName) ([]*livekit.ParticipantInfo, error)
listParticipantsMutex sync.RWMutex
listParticipantsArgsForCall []struct {
@@ -111,6 +126,20 @@ type FakeObjectStore struct {
result1 string
result2 error
}
RoomExistsStub func(context.Context, livekit.RoomName) (bool, error)
roomExistsMutex sync.RWMutex
roomExistsArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
roomExistsReturns struct {
result1 bool
result2 error
}
roomExistsReturnsOnCall map[int]struct {
result1 bool
result2 error
}
StoreParticipantStub func(context.Context, livekit.RoomName, *livekit.ParticipantInfo) error
storeParticipantMutex sync.RWMutex
storeParticipantArgsForCall []struct {
@@ -279,6 +308,72 @@ func (fake *FakeObjectStore) DeleteRoomReturnsOnCall(i int, result1 error) {
}{result1}
}
func (fake *FakeObjectStore) HasParticipant(arg1 context.Context, arg2 livekit.RoomName, arg3 livekit.ParticipantIdentity) (bool, error) {
fake.hasParticipantMutex.Lock()
ret, specificReturn := fake.hasParticipantReturnsOnCall[len(fake.hasParticipantArgsForCall)]
fake.hasParticipantArgsForCall = append(fake.hasParticipantArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}{arg1, arg2, arg3})
stub := fake.HasParticipantStub
fakeReturns := fake.hasParticipantReturns
fake.recordInvocation("HasParticipant", []interface{}{arg1, arg2, arg3})
fake.hasParticipantMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeObjectStore) HasParticipantCallCount() int {
fake.hasParticipantMutex.RLock()
defer fake.hasParticipantMutex.RUnlock()
return len(fake.hasParticipantArgsForCall)
}
func (fake *FakeObjectStore) HasParticipantCalls(stub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (bool, error)) {
fake.hasParticipantMutex.Lock()
defer fake.hasParticipantMutex.Unlock()
fake.HasParticipantStub = stub
}
func (fake *FakeObjectStore) HasParticipantArgsForCall(i int) (context.Context, livekit.RoomName, livekit.ParticipantIdentity) {
fake.hasParticipantMutex.RLock()
defer fake.hasParticipantMutex.RUnlock()
argsForCall := fake.hasParticipantArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeObjectStore) HasParticipantReturns(result1 bool, result2 error) {
fake.hasParticipantMutex.Lock()
defer fake.hasParticipantMutex.Unlock()
fake.HasParticipantStub = nil
fake.hasParticipantReturns = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) HasParticipantReturnsOnCall(i int, result1 bool, result2 error) {
fake.hasParticipantMutex.Lock()
defer fake.hasParticipantMutex.Unlock()
fake.HasParticipantStub = nil
if fake.hasParticipantReturnsOnCall == nil {
fake.hasParticipantReturnsOnCall = make(map[int]struct {
result1 bool
result2 error
})
}
fake.hasParticipantReturnsOnCall[i] = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) ListParticipants(arg1 context.Context, arg2 livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
fake.listParticipantsMutex.Lock()
ret, specificReturn := fake.listParticipantsReturnsOnCall[len(fake.listParticipantsArgsForCall)]
@@ -615,6 +710,71 @@ func (fake *FakeObjectStore) LockRoomReturnsOnCall(i int, result1 string, result
}{result1, result2}
}
func (fake *FakeObjectStore) RoomExists(arg1 context.Context, arg2 livekit.RoomName) (bool, error) {
fake.roomExistsMutex.Lock()
ret, specificReturn := fake.roomExistsReturnsOnCall[len(fake.roomExistsArgsForCall)]
fake.roomExistsArgsForCall = append(fake.roomExistsArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.RoomExistsStub
fakeReturns := fake.roomExistsReturns
fake.recordInvocation("RoomExists", []interface{}{arg1, arg2})
fake.roomExistsMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeObjectStore) RoomExistsCallCount() int {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
return len(fake.roomExistsArgsForCall)
}
func (fake *FakeObjectStore) RoomExistsCalls(stub func(context.Context, livekit.RoomName) (bool, error)) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = stub
}
func (fake *FakeObjectStore) RoomExistsArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
argsForCall := fake.roomExistsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeObjectStore) RoomExistsReturns(result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
fake.roomExistsReturns = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) RoomExistsReturnsOnCall(i int, result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
if fake.roomExistsReturnsOnCall == nil {
fake.roomExistsReturnsOnCall = make(map[int]struct {
result1 bool
result2 error
})
}
fake.roomExistsReturnsOnCall[i] = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) StoreParticipant(arg1 context.Context, arg2 livekit.RoomName, arg3 *livekit.ParticipantInfo) error {
fake.storeParticipantMutex.Lock()
ret, specificReturn := fake.storeParticipantReturnsOnCall[len(fake.storeParticipantArgsForCall)]
@@ -807,26 +967,6 @@ func (fake *FakeObjectStore) UnlockRoomReturnsOnCall(i int, result1 error) {
func (fake *FakeObjectStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteParticipantMutex.RLock()
defer fake.deleteParticipantMutex.RUnlock()
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
fake.listRoomsMutex.RLock()
defer fake.listRoomsMutex.RUnlock()
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
fake.loadRoomMutex.RLock()
defer fake.loadRoomMutex.RUnlock()
fake.lockRoomMutex.RLock()
defer fake.lockRoomMutex.RUnlock()
fake.storeParticipantMutex.RLock()
defer fake.storeParticipantMutex.RUnlock()
fake.storeRoomMutex.RLock()
defer fake.storeRoomMutex.RUnlock()
fake.unlockRoomMutex.RLock()
defer fake.unlockRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -330,14 +330,6 @@ func (fake *FakeRoomAllocator) ValidateCreateRoomReturnsOnCall(i int, result1 er
func (fake *FakeRoomAllocator) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.autoCreateEnabledMutex.RLock()
defer fake.autoCreateEnabledMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
fake.selectRoomNodeMutex.RLock()
defer fake.selectRoomNodeMutex.RUnlock()
fake.validateCreateRoomMutex.RLock()
defer fake.validateCreateRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
+79 -244
View File
@@ -10,32 +10,6 @@ import (
)
type FakeServiceStore struct {
DeleteRoomStub func(context.Context, livekit.RoomName) error
deleteRoomMutex sync.RWMutex
deleteRoomArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
deleteRoomReturns struct {
result1 error
}
deleteRoomReturnsOnCall map[int]struct {
result1 error
}
ListParticipantsStub func(context.Context, livekit.RoomName) ([]*livekit.ParticipantInfo, error)
listParticipantsMutex sync.RWMutex
listParticipantsArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
listParticipantsReturns struct {
result1 []*livekit.ParticipantInfo
result2 error
}
listParticipantsReturnsOnCall map[int]struct {
result1 []*livekit.ParticipantInfo
result2 error
}
ListRoomsStub func(context.Context, []livekit.RoomName) ([]*livekit.Room, error)
listRoomsMutex sync.RWMutex
listRoomsArgsForCall []struct {
@@ -50,21 +24,6 @@ type FakeServiceStore struct {
result1 []*livekit.Room
result2 error
}
LoadParticipantStub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error)
loadParticipantMutex sync.RWMutex
loadParticipantArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}
loadParticipantReturns struct {
result1 *livekit.ParticipantInfo
result2 error
}
loadParticipantReturnsOnCall map[int]struct {
result1 *livekit.ParticipantInfo
result2 error
}
LoadRoomStub func(context.Context, livekit.RoomName, bool) (*livekit.Room, *livekit.RoomInternal, error)
loadRoomMutex sync.RWMutex
loadRoomArgsForCall []struct {
@@ -82,137 +41,24 @@ type FakeServiceStore struct {
result2 *livekit.RoomInternal
result3 error
}
RoomExistsStub func(context.Context, livekit.RoomName) (bool, error)
roomExistsMutex sync.RWMutex
roomExistsArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
roomExistsReturns struct {
result1 bool
result2 error
}
roomExistsReturnsOnCall map[int]struct {
result1 bool
result2 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeServiceStore) DeleteRoom(arg1 context.Context, arg2 livekit.RoomName) error {
fake.deleteRoomMutex.Lock()
ret, specificReturn := fake.deleteRoomReturnsOnCall[len(fake.deleteRoomArgsForCall)]
fake.deleteRoomArgsForCall = append(fake.deleteRoomArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.DeleteRoomStub
fakeReturns := fake.deleteRoomReturns
fake.recordInvocation("DeleteRoom", []interface{}{arg1, arg2})
fake.deleteRoomMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeServiceStore) DeleteRoomCallCount() int {
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
return len(fake.deleteRoomArgsForCall)
}
func (fake *FakeServiceStore) DeleteRoomCalls(stub func(context.Context, livekit.RoomName) error) {
fake.deleteRoomMutex.Lock()
defer fake.deleteRoomMutex.Unlock()
fake.DeleteRoomStub = stub
}
func (fake *FakeServiceStore) DeleteRoomArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
argsForCall := fake.deleteRoomArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeServiceStore) DeleteRoomReturns(result1 error) {
fake.deleteRoomMutex.Lock()
defer fake.deleteRoomMutex.Unlock()
fake.DeleteRoomStub = nil
fake.deleteRoomReturns = struct {
result1 error
}{result1}
}
func (fake *FakeServiceStore) DeleteRoomReturnsOnCall(i int, result1 error) {
fake.deleteRoomMutex.Lock()
defer fake.deleteRoomMutex.Unlock()
fake.DeleteRoomStub = nil
if fake.deleteRoomReturnsOnCall == nil {
fake.deleteRoomReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.deleteRoomReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeServiceStore) ListParticipants(arg1 context.Context, arg2 livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
fake.listParticipantsMutex.Lock()
ret, specificReturn := fake.listParticipantsReturnsOnCall[len(fake.listParticipantsArgsForCall)]
fake.listParticipantsArgsForCall = append(fake.listParticipantsArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.ListParticipantsStub
fakeReturns := fake.listParticipantsReturns
fake.recordInvocation("ListParticipants", []interface{}{arg1, arg2})
fake.listParticipantsMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeServiceStore) ListParticipantsCallCount() int {
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
return len(fake.listParticipantsArgsForCall)
}
func (fake *FakeServiceStore) ListParticipantsCalls(stub func(context.Context, livekit.RoomName) ([]*livekit.ParticipantInfo, error)) {
fake.listParticipantsMutex.Lock()
defer fake.listParticipantsMutex.Unlock()
fake.ListParticipantsStub = stub
}
func (fake *FakeServiceStore) ListParticipantsArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
argsForCall := fake.listParticipantsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeServiceStore) ListParticipantsReturns(result1 []*livekit.ParticipantInfo, result2 error) {
fake.listParticipantsMutex.Lock()
defer fake.listParticipantsMutex.Unlock()
fake.ListParticipantsStub = nil
fake.listParticipantsReturns = struct {
result1 []*livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) ListParticipantsReturnsOnCall(i int, result1 []*livekit.ParticipantInfo, result2 error) {
fake.listParticipantsMutex.Lock()
defer fake.listParticipantsMutex.Unlock()
fake.ListParticipantsStub = nil
if fake.listParticipantsReturnsOnCall == nil {
fake.listParticipantsReturnsOnCall = make(map[int]struct {
result1 []*livekit.ParticipantInfo
result2 error
})
}
fake.listParticipantsReturnsOnCall[i] = struct {
result1 []*livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) ListRooms(arg1 context.Context, arg2 []livekit.RoomName) ([]*livekit.Room, error) {
var arg2Copy []livekit.RoomName
if arg2 != nil {
@@ -283,72 +129,6 @@ func (fake *FakeServiceStore) ListRoomsReturnsOnCall(i int, result1 []*livekit.R
}{result1, result2}
}
func (fake *FakeServiceStore) LoadParticipant(arg1 context.Context, arg2 livekit.RoomName, arg3 livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error) {
fake.loadParticipantMutex.Lock()
ret, specificReturn := fake.loadParticipantReturnsOnCall[len(fake.loadParticipantArgsForCall)]
fake.loadParticipantArgsForCall = append(fake.loadParticipantArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}{arg1, arg2, arg3})
stub := fake.LoadParticipantStub
fakeReturns := fake.loadParticipantReturns
fake.recordInvocation("LoadParticipant", []interface{}{arg1, arg2, arg3})
fake.loadParticipantMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeServiceStore) LoadParticipantCallCount() int {
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
return len(fake.loadParticipantArgsForCall)
}
func (fake *FakeServiceStore) LoadParticipantCalls(stub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error)) {
fake.loadParticipantMutex.Lock()
defer fake.loadParticipantMutex.Unlock()
fake.LoadParticipantStub = stub
}
func (fake *FakeServiceStore) LoadParticipantArgsForCall(i int) (context.Context, livekit.RoomName, livekit.ParticipantIdentity) {
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
argsForCall := fake.loadParticipantArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeServiceStore) LoadParticipantReturns(result1 *livekit.ParticipantInfo, result2 error) {
fake.loadParticipantMutex.Lock()
defer fake.loadParticipantMutex.Unlock()
fake.LoadParticipantStub = nil
fake.loadParticipantReturns = struct {
result1 *livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) LoadParticipantReturnsOnCall(i int, result1 *livekit.ParticipantInfo, result2 error) {
fake.loadParticipantMutex.Lock()
defer fake.loadParticipantMutex.Unlock()
fake.LoadParticipantStub = nil
if fake.loadParticipantReturnsOnCall == nil {
fake.loadParticipantReturnsOnCall = make(map[int]struct {
result1 *livekit.ParticipantInfo
result2 error
})
}
fake.loadParticipantReturnsOnCall[i] = struct {
result1 *livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) LoadRoom(arg1 context.Context, arg2 livekit.RoomName, arg3 bool) (*livekit.Room, *livekit.RoomInternal, error) {
fake.loadRoomMutex.Lock()
ret, specificReturn := fake.loadRoomReturnsOnCall[len(fake.loadRoomArgsForCall)]
@@ -418,19 +198,74 @@ func (fake *FakeServiceStore) LoadRoomReturnsOnCall(i int, result1 *livekit.Room
}{result1, result2, result3}
}
func (fake *FakeServiceStore) RoomExists(arg1 context.Context, arg2 livekit.RoomName) (bool, error) {
fake.roomExistsMutex.Lock()
ret, specificReturn := fake.roomExistsReturnsOnCall[len(fake.roomExistsArgsForCall)]
fake.roomExistsArgsForCall = append(fake.roomExistsArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.RoomExistsStub
fakeReturns := fake.roomExistsReturns
fake.recordInvocation("RoomExists", []interface{}{arg1, arg2})
fake.roomExistsMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeServiceStore) RoomExistsCallCount() int {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
return len(fake.roomExistsArgsForCall)
}
func (fake *FakeServiceStore) RoomExistsCalls(stub func(context.Context, livekit.RoomName) (bool, error)) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = stub
}
func (fake *FakeServiceStore) RoomExistsArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
argsForCall := fake.roomExistsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeServiceStore) RoomExistsReturns(result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
fake.roomExistsReturns = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) RoomExistsReturnsOnCall(i int, result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
if fake.roomExistsReturnsOnCall == nil {
fake.roomExistsReturnsOnCall = make(map[int]struct {
result1 bool
result2 error
})
}
fake.roomExistsReturnsOnCall[i] = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
fake.listRoomsMutex.RLock()
defer fake.listRoomsMutex.RUnlock()
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
fake.loadRoomMutex.RLock()
defer fake.loadRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -171,10 +171,6 @@ func (fake *FakeSessionHandler) LoggerReturnsOnCall(i int, result1 logger.Logger
func (fake *FakeSessionHandler) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.handleSessionMutex.RLock()
defer fake.handleSessionMutex.RUnlock()
fake.loggerMutex.RLock()
defer fake.loggerMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
-190
View File
@@ -146,34 +146,6 @@ type FakeSIPStore struct {
result1 *livekit.SIPTrunkInfo
result2 error
}
SelectSIPDispatchRuleStub func(context.Context, string) ([]*livekit.SIPDispatchRuleInfo, error)
selectSIPDispatchRuleMutex sync.RWMutex
selectSIPDispatchRuleArgsForCall []struct {
arg1 context.Context
arg2 string
}
selectSIPDispatchRuleReturns struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}
selectSIPDispatchRuleReturnsOnCall map[int]struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}
SelectSIPInboundTrunkStub func(context.Context, string) ([]*livekit.SIPInboundTrunkInfo, error)
selectSIPInboundTrunkMutex sync.RWMutex
selectSIPInboundTrunkArgsForCall []struct {
arg1 context.Context
arg2 string
}
selectSIPInboundTrunkReturns struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}
selectSIPInboundTrunkReturnsOnCall map[int]struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}
StoreSIPDispatchRuleStub func(context.Context, *livekit.SIPDispatchRuleInfo) error
storeSIPDispatchRuleMutex sync.RWMutex
storeSIPDispatchRuleArgsForCall []struct {
@@ -870,136 +842,6 @@ func (fake *FakeSIPStore) LoadSIPTrunkReturnsOnCall(i int, result1 *livekit.SIPT
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPDispatchRule(arg1 context.Context, arg2 string) ([]*livekit.SIPDispatchRuleInfo, error) {
fake.selectSIPDispatchRuleMutex.Lock()
ret, specificReturn := fake.selectSIPDispatchRuleReturnsOnCall[len(fake.selectSIPDispatchRuleArgsForCall)]
fake.selectSIPDispatchRuleArgsForCall = append(fake.selectSIPDispatchRuleArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.SelectSIPDispatchRuleStub
fakeReturns := fake.selectSIPDispatchRuleReturns
fake.recordInvocation("SelectSIPDispatchRule", []interface{}{arg1, arg2})
fake.selectSIPDispatchRuleMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleCallCount() int {
fake.selectSIPDispatchRuleMutex.RLock()
defer fake.selectSIPDispatchRuleMutex.RUnlock()
return len(fake.selectSIPDispatchRuleArgsForCall)
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleCalls(stub func(context.Context, string) ([]*livekit.SIPDispatchRuleInfo, error)) {
fake.selectSIPDispatchRuleMutex.Lock()
defer fake.selectSIPDispatchRuleMutex.Unlock()
fake.SelectSIPDispatchRuleStub = stub
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleArgsForCall(i int) (context.Context, string) {
fake.selectSIPDispatchRuleMutex.RLock()
defer fake.selectSIPDispatchRuleMutex.RUnlock()
argsForCall := fake.selectSIPDispatchRuleArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleReturns(result1 []*livekit.SIPDispatchRuleInfo, result2 error) {
fake.selectSIPDispatchRuleMutex.Lock()
defer fake.selectSIPDispatchRuleMutex.Unlock()
fake.SelectSIPDispatchRuleStub = nil
fake.selectSIPDispatchRuleReturns = struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleReturnsOnCall(i int, result1 []*livekit.SIPDispatchRuleInfo, result2 error) {
fake.selectSIPDispatchRuleMutex.Lock()
defer fake.selectSIPDispatchRuleMutex.Unlock()
fake.SelectSIPDispatchRuleStub = nil
if fake.selectSIPDispatchRuleReturnsOnCall == nil {
fake.selectSIPDispatchRuleReturnsOnCall = make(map[int]struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
})
}
fake.selectSIPDispatchRuleReturnsOnCall[i] = struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPInboundTrunk(arg1 context.Context, arg2 string) ([]*livekit.SIPInboundTrunkInfo, error) {
fake.selectSIPInboundTrunkMutex.Lock()
ret, specificReturn := fake.selectSIPInboundTrunkReturnsOnCall[len(fake.selectSIPInboundTrunkArgsForCall)]
fake.selectSIPInboundTrunkArgsForCall = append(fake.selectSIPInboundTrunkArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.SelectSIPInboundTrunkStub
fakeReturns := fake.selectSIPInboundTrunkReturns
fake.recordInvocation("SelectSIPInboundTrunk", []interface{}{arg1, arg2})
fake.selectSIPInboundTrunkMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkCallCount() int {
fake.selectSIPInboundTrunkMutex.RLock()
defer fake.selectSIPInboundTrunkMutex.RUnlock()
return len(fake.selectSIPInboundTrunkArgsForCall)
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkCalls(stub func(context.Context, string) ([]*livekit.SIPInboundTrunkInfo, error)) {
fake.selectSIPInboundTrunkMutex.Lock()
defer fake.selectSIPInboundTrunkMutex.Unlock()
fake.SelectSIPInboundTrunkStub = stub
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkArgsForCall(i int) (context.Context, string) {
fake.selectSIPInboundTrunkMutex.RLock()
defer fake.selectSIPInboundTrunkMutex.RUnlock()
argsForCall := fake.selectSIPInboundTrunkArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkReturns(result1 []*livekit.SIPInboundTrunkInfo, result2 error) {
fake.selectSIPInboundTrunkMutex.Lock()
defer fake.selectSIPInboundTrunkMutex.Unlock()
fake.SelectSIPInboundTrunkStub = nil
fake.selectSIPInboundTrunkReturns = struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkReturnsOnCall(i int, result1 []*livekit.SIPInboundTrunkInfo, result2 error) {
fake.selectSIPInboundTrunkMutex.Lock()
defer fake.selectSIPInboundTrunkMutex.Unlock()
fake.SelectSIPInboundTrunkStub = nil
if fake.selectSIPInboundTrunkReturnsOnCall == nil {
fake.selectSIPInboundTrunkReturnsOnCall = make(map[int]struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
})
}
fake.selectSIPInboundTrunkReturnsOnCall[i] = struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) StoreSIPDispatchRule(arg1 context.Context, arg2 *livekit.SIPDispatchRuleInfo) error {
fake.storeSIPDispatchRuleMutex.Lock()
ret, specificReturn := fake.storeSIPDispatchRuleReturnsOnCall[len(fake.storeSIPDispatchRuleArgsForCall)]
@@ -1251,38 +1093,6 @@ func (fake *FakeSIPStore) StoreSIPTrunkReturnsOnCall(i int, result1 error) {
func (fake *FakeSIPStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteSIPDispatchRuleMutex.RLock()
defer fake.deleteSIPDispatchRuleMutex.RUnlock()
fake.deleteSIPTrunkMutex.RLock()
defer fake.deleteSIPTrunkMutex.RUnlock()
fake.listSIPDispatchRuleMutex.RLock()
defer fake.listSIPDispatchRuleMutex.RUnlock()
fake.listSIPInboundTrunkMutex.RLock()
defer fake.listSIPInboundTrunkMutex.RUnlock()
fake.listSIPOutboundTrunkMutex.RLock()
defer fake.listSIPOutboundTrunkMutex.RUnlock()
fake.listSIPTrunkMutex.RLock()
defer fake.listSIPTrunkMutex.RUnlock()
fake.loadSIPDispatchRuleMutex.RLock()
defer fake.loadSIPDispatchRuleMutex.RUnlock()
fake.loadSIPInboundTrunkMutex.RLock()
defer fake.loadSIPInboundTrunkMutex.RUnlock()
fake.loadSIPOutboundTrunkMutex.RLock()
defer fake.loadSIPOutboundTrunkMutex.RUnlock()
fake.loadSIPTrunkMutex.RLock()
defer fake.loadSIPTrunkMutex.RUnlock()
fake.selectSIPDispatchRuleMutex.RLock()
defer fake.selectSIPDispatchRuleMutex.RUnlock()
fake.selectSIPInboundTrunkMutex.RLock()
defer fake.selectSIPInboundTrunkMutex.RUnlock()
fake.storeSIPDispatchRuleMutex.RLock()
defer fake.storeSIPDispatchRuleMutex.RUnlock()
fake.storeSIPInboundTrunkMutex.RLock()
defer fake.storeSIPInboundTrunkMutex.RUnlock()
fake.storeSIPOutboundTrunkMutex.RLock()
defer fake.storeSIPOutboundTrunkMutex.RUnlock()
fake.storeSIPTrunkMutex.RLock()
defer fake.storeSIPTrunkMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
+6 -3
View File
@@ -23,6 +23,7 @@ import (
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
@@ -86,7 +87,7 @@ type defaultSessionHandler struct {
}
func (s *defaultSessionHandler) Logger(ctx context.Context) logger.Logger {
return logger.GetLogger()
return utils.GetLogger(ctx)
}
func (s *defaultSessionHandler) HandleSession(
@@ -119,8 +120,8 @@ func (s *SignalServer) Start() error {
return s.server.RegisterAllNodeTopics(s.nodeID)
}
func (r *SignalServer) Stop() {
r.server.Kill()
func (s *SignalServer) Stop() {
s.server.Kill()
}
type signalService struct {
@@ -167,6 +168,8 @@ func (r *signalService) RelaySignal(stream psrpc.ServerStream[*rpc.RelaySignalRe
reqChan,
signalRequestMessageReader{},
r.config,
prometheus.RecordSignalRequestSuccess,
prometheus.RecordSignalRequestFailure,
)
l.Debugw("signal stream closed", "error", err)
+1 -1
View File
@@ -21,7 +21,6 @@ import (
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/config"
@@ -31,6 +30,7 @@ import (
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/protojson"
"github.com/livekit/psrpc"
)
+235 -24
View File
@@ -16,6 +16,7 @@ package service
import (
"context"
"errors"
"time"
"github.com/dennwc/iters"
@@ -86,6 +87,9 @@ func (s *SIPService) CreateSIPTrunk(ctx context.Context, req *livekit.CreateSIPT
Name: req.Name,
Metadata: req.Metadata,
}
if err := info.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
// Validate all trunks including the new one first.
it, err := ListSIPInboundTrunk(ctx, s.store, &livekit.ListSIPInboundTrunkRequest{}, info.AsInbound())
@@ -120,7 +124,7 @@ func (s *SIPService) CreateSIPInboundTrunk(ctx context.Context, req *livekit.Cre
if info.SipTrunkId != "" {
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID must be empty")
}
AppendLogFields(ctx, "trunk", logger.Proto(req.Trunk))
AppendLogFields(ctx, "trunk", logger.Proto(info))
// Keep ID empty still, so that validation can print "<new>" instead of a non-existent ID in the error.
@@ -159,7 +163,7 @@ func (s *SIPService) CreateSIPOutboundTrunk(ctx context.Context, req *livekit.Cr
if info.SipTrunkId != "" {
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID must be empty")
}
AppendLogFields(ctx, "trunk", logger.Proto(req.Trunk))
AppendLogFields(ctx, "trunk", logger.Proto(info))
// No additional validation needed for outbound.
info.SipTrunkId = guid.New(utils.SIPTrunkPrefix)
@@ -169,6 +173,100 @@ func (s *SIPService) CreateSIPOutboundTrunk(ctx context.Context, req *livekit.Cr
return info, nil
}
func (s *SIPService) UpdateSIPInboundTrunk(ctx context.Context, req *livekit.UpdateSIPInboundTrunkRequest) (*livekit.SIPInboundTrunkInfo, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.store == nil {
return nil, ErrSIPNotConnected
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
AppendLogFields(ctx,
"request", logger.Proto(req),
"trunkID", req.SipTrunkId,
)
// Validate all trunks including the new one first.
info, err := s.store.LoadSIPInboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
switch a := req.Action.(type) {
default:
return nil, errors.New("missing or unsupported action")
case livekit.UpdateSIPInboundTrunkRequestAction:
info, err = a.Apply(info)
if err != nil {
return nil, err
}
}
it, err := ListSIPInboundTrunk(ctx, s.store, &livekit.ListSIPInboundTrunkRequest{
Numbers: info.Numbers,
})
if err != nil {
return nil, err
}
defer it.Close()
if err = sip.ValidateTrunksIter(it, sip.WithTrunkReplace(func(t *livekit.SIPInboundTrunkInfo) *livekit.SIPInboundTrunkInfo {
if req.SipTrunkId == t.SipTrunkId {
return info // updated one
}
return t
})); err != nil {
return nil, err
}
if err := s.store.StoreSIPInboundTrunk(ctx, info); err != nil {
return nil, err
}
return info, nil
}
func (s *SIPService) UpdateSIPOutboundTrunk(ctx context.Context, req *livekit.UpdateSIPOutboundTrunkRequest) (*livekit.SIPOutboundTrunkInfo, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.store == nil {
return nil, ErrSIPNotConnected
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
AppendLogFields(ctx,
"request", logger.Proto(req),
"trunkID", req.SipTrunkId,
)
info, err := s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
switch a := req.Action.(type) {
default:
return nil, errors.New("missing or unsupported action")
case livekit.UpdateSIPOutboundTrunkRequestAction:
info, err = a.Apply(info)
if err != nil {
return nil, err
}
}
// No additional validation needed for outbound.
if err := s.store.StoreSIPOutboundTrunk(ctx, info); err != nil {
return nil, err
}
return info, nil
}
func (s *SIPService) GetSIPInboundTrunk(ctx context.Context, req *livekit.GetSIPInboundTrunkRequest) (*livekit.GetSIPInboundTrunkResponse, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
@@ -183,6 +281,9 @@ func (s *SIPService) GetSIPInboundTrunk(ctx context.Context, req *livekit.GetSIP
trunk, err := s.store.LoadSIPInboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
@@ -203,6 +304,9 @@ func (s *SIPService) GetSIPOutboundTrunk(ctx context.Context, req *livekit.GetSI
trunk, err := s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
@@ -317,6 +421,7 @@ func (s *SIPService) CreateSIPDispatchRule(ctx context.Context, req *livekit.Cre
if s.store == nil {
return nil, ErrSIPNotConnected
}
req.DispatchRule.Upgrade()
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
@@ -326,16 +431,8 @@ func (s *SIPService) CreateSIPDispatchRule(ctx context.Context, req *livekit.Cre
"trunkID", req.TrunkIds,
)
// Keep ID empty, so that validation can print "<new>" instead of a non-existent ID in the error.
info := &livekit.SIPDispatchRuleInfo{
Rule: req.Rule,
TrunkIds: req.TrunkIds,
InboundNumbers: req.InboundNumbers,
HidePhoneNumber: req.HidePhoneNumber,
Name: req.Name,
Metadata: req.Metadata,
Attributes: req.Attributes,
RoomConfig: req.RoomConfig,
}
info := req.DispatchRuleInfo()
info.SipDispatchRuleId = ""
// Validate all rules including the new one first.
it, err := ListSIPDispatchRule(ctx, s.store, &livekit.ListSIPDispatchRuleRequest{
@@ -357,6 +454,62 @@ func (s *SIPService) CreateSIPDispatchRule(ctx context.Context, req *livekit.Cre
return info, nil
}
func (s *SIPService) UpdateSIPDispatchRule(ctx context.Context, req *livekit.UpdateSIPDispatchRuleRequest) (*livekit.SIPDispatchRuleInfo, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.store == nil {
return nil, ErrSIPNotConnected
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
AppendLogFields(ctx,
"request", logger.Proto(req),
"ruleID", req.SipDispatchRuleId,
)
// Validate all trunks including the new one first.
info, err := s.store.LoadSIPDispatchRule(ctx, req.SipDispatchRuleId)
if err != nil {
if errors.Is(err, ErrSIPDispatchRuleNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
switch a := req.Action.(type) {
default:
return nil, errors.New("missing or unsupported action")
case livekit.UpdateSIPDispatchRuleRequestAction:
info, err = a.Apply(info)
if err != nil {
return nil, err
}
}
it, err := ListSIPDispatchRule(ctx, s.store, &livekit.ListSIPDispatchRuleRequest{
TrunkIds: info.TrunkIds,
})
if err != nil {
return nil, err
}
defer it.Close()
if _, err = sip.ValidateDispatchRulesIter(it, sip.WithDispatchRuleReplace(func(t *livekit.SIPDispatchRuleInfo) *livekit.SIPDispatchRuleInfo {
if req.SipDispatchRuleId == t.SipDispatchRuleId {
return info // updated one
}
return t
})); err != nil {
return nil, err
}
if err := s.store.StoreSIPDispatchRule(ctx, info); err != nil {
return nil, err
}
return info, nil
}
func ListSIPDispatchRule(ctx context.Context, s SIPStore, req *livekit.ListSIPDispatchRuleRequest, add ...*livekit.SIPDispatchRuleInfo) (iters.Iter[*livekit.SIPDispatchRuleInfo], error) {
if s == nil {
return nil, ErrSIPNotConnected
@@ -402,6 +555,9 @@ func (s *SIPService) DeleteSIPDispatchRule(ctx context.Context, req *livekit.Del
info, err := s.store.LoadSIPDispatchRule(ctx, req.SipDispatchRuleId)
if err != nil {
if errors.Is(err, ErrSIPDispatchRuleNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
@@ -428,7 +584,7 @@ func (s *SIPService) CreateSIPParticipant(ctx context.Context, req *livekit.Crea
ireq, err := s.CreateSIPParticipantRequest(ctx, req, "", "", "", "")
if err != nil {
unlikelyLogger.Errorw("cannot create sip participant request", err)
return nil, err
return nil, wrapSIPContextError(err)
}
unlikelyLogger = unlikelyLogger.WithValues(
"callID", ireq.SipCallId,
@@ -457,7 +613,7 @@ func (s *SIPService) CreateSIPParticipant(ctx context.Context, req *livekit.Crea
resp, err := s.psrpcClient.CreateSIPParticipant(ctx, "", ireq, psrpc.WithRequestTimeout(timeout))
if err != nil {
unlikelyLogger.Errorw("cannot create sip participant", err)
return nil, err
return nil, wrapSIPContextError(err)
}
return &livekit.SIPParticipantInfo{
ParticipantId: resp.ParticipantId,
@@ -474,6 +630,10 @@ func (s *SIPService) CreateSIPParticipantRequest(ctx context.Context, req *livek
if s.store == nil {
return nil, ErrSIPNotConnected
}
req.Upgrade()
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
callID := sip.NewCallID()
log := logger.GetLogger().WithUnlikelyValues(
"callID", callID,
@@ -485,10 +645,22 @@ func (s *SIPService) CreateSIPParticipantRequest(ctx context.Context, req *livek
log = log.WithValues("projectID", projectID)
}
trunk, err := s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
log.Errorw("cannot get trunk to update sip participant", err)
return nil, err
var trunk *livekit.SIPOutboundTrunkInfo
if req.SipTrunkId != "" {
var err error
trunk, err = s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
log.Errorw("cannot get trunk to update sip participant", err)
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
}
if trunk != nil && trunk.FromHost != "" {
host = trunk.FromHost
} else if t := req.Trunk; t != nil && t.FromHost != "" {
host = t.FromHost
}
return rpc.NewCreateSIPParticipantRequest(projectID, callID, host, wsUrl, token, req, trunk)
}
@@ -510,12 +682,25 @@ func (s *SIPService) TransferSIPParticipant(ctx context.Context, req *livekit.Tr
ireq, err := s.transferSIPParticipantRequest(ctx, req)
if err != nil {
log.Errorw("cannot create transfer sip participant request", err)
return nil, err
return nil, wrapSIPContextError(err)
}
// by default we set the timeout to be 30 seconds.
// this timeout covers:
// - a network failure between this process and the LiveKit SIP bridge
// - the SIP transfer target not returning 200 OK fast enough.
// WARN: any timeout/cancellation of a SIP transfer risks leaving
// either the SIP bridge, or the SIP REFER exchange, in a "unknown" state.
timeout := 30 * time.Second
if req.RingingTimeout != nil {
timeout = req.RingingTimeout.AsDuration()
}
// it's also possible the ctx has a Deadline.
// in that case we want to use that deadline,
// or our timeout, whichover is soonest.
if deadline, ok := ctx.Deadline(); ok {
timeout = time.Until(deadline)
timeout = min(timeout, time.Until(deadline))
} else {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, timeout)
@@ -524,7 +709,7 @@ func (s *SIPService) TransferSIPParticipant(ctx context.Context, req *livekit.Tr
_, err = s.psrpcClient.TransferSIPParticipant(ctx, ireq.SipCallId, ireq, psrpc.WithRequestTimeout(timeout))
if err != nil {
log.Errorw("cannot transfer sip participant", err)
return nil, err
return nil, wrapSIPContextError(err)
}
return &emptypb.Empty{}, nil
@@ -545,6 +730,9 @@ func (s *SIPService) transferSIPParticipantRequest(ctx context.Context, req *liv
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.RoomName)); err != nil {
return nil, twirpAuthError(err)
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
resp, err := s.roomService.GetParticipant(ctx, &livekit.RoomParticipantIdentity{
Room: req.RoomName,
@@ -561,8 +749,31 @@ func (s *SIPService) transferSIPParticipantRequest(ctx context.Context, req *liv
}
return &rpc.InternalTransferSIPParticipantRequest{
SipCallId: callID,
TransferTo: req.TransferTo,
PlayDialtone: req.PlayDialtone,
SipCallId: callID,
TransferTo: req.TransferTo,
PlayDialtone: req.PlayDialtone,
Headers: req.Headers,
RingingTimeout: req.RingingTimeout,
}, nil
}
// wrapSIPContextError converts raw context.DeadlineExceeded / context.Canceled
// into psrpc-coded errors so they aren't surfaced as @code:unknown / HTTP 500
// at the Twirp boundary. psrpc errors and any error that already carries a
// gRPC status are passed through unchanged.
func wrapSIPContextError(err error) error {
if err == nil {
return nil
}
var psErr psrpc.Error
if errors.As(err, &psErr) {
return err
}
switch {
case errors.Is(err, context.DeadlineExceeded):
return psrpc.NewError(psrpc.DeadlineExceeded, err)
case errors.Is(err, context.Canceled):
return psrpc.NewError(psrpc.Canceled, err)
}
return err
}
+182 -88
View File
@@ -21,9 +21,11 @@ import (
"net"
"strconv"
"strings"
"time"
"github.com/jxskiss/base62"
"github.com/pion/turn/v4"
"github.com/pion/stun/v3"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/livekit/protocol/auth"
@@ -40,10 +42,10 @@ const (
LivekitRealm = "livekit"
allocateRetries = 50
turnMinPort = 1024
turnMaxPort = 30000
)
var ErrExpired = errors.New("expired")
func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone bool) (*turn.Server, error) {
turnConf := conf.TURN
if !turnConf.Enabled {
@@ -52,29 +54,7 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone
if turnConf.TLSPort <= 0 && turnConf.UDPPort <= 0 {
return nil, errors.New("invalid TURN ports")
}
serverConfig := turn.ServerConfig{
Realm: LivekitRealm,
AuthHandler: authHandler,
LoggerFactory: pionlogger.NewLoggerFactory(logger.GetLogger()),
}
var relayAddrGen turn.RelayAddressGenerator = &turn.RelayAddressGeneratorPortRange{
RelayAddress: net.ParseIP(conf.RTC.NodeIP),
Address: "0.0.0.0",
MinPort: turnConf.RelayPortRangeStart,
MaxPort: turnConf.RelayPortRangeEnd,
MaxRetries: allocateRetries,
}
if standalone {
relayAddrGen = telemetry.NewRelayAddressGenerator(relayAddrGen)
}
var logValues []interface{}
logValues = append(logValues, "turn.relay_range_start", turnConf.RelayPortRangeStart)
logValues = append(logValues, "turn.relay_range_end", turnConf.RelayPortRangeEnd)
if turnConf.TLSPort > 0 {
} else if turnConf.TLSPort > 0 {
if turnConf.Domain == "" {
return nil, errors.New("TURN domain required")
}
@@ -82,64 +62,129 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone
if !IsValidDomain(turnConf.Domain) {
return nil, errors.New("TURN domain is not correct")
}
if !turnConf.ExternalTLS {
cert, err := tls.LoadX509KeyPair(turnConf.CertFile, turnConf.KeyFile)
if err != nil {
return nil, errors.Wrap(err, "TURN tls cert required")
}
tlsListener, err := tls.Listen("tcp4", "0.0.0.0:"+strconv.Itoa(turnConf.TLSPort),
&tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
})
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN TCP port")
}
if standalone {
tlsListener = telemetry.NewListener(tlsListener)
}
listenerConfig := turn.ListenerConfig{
Listener: tlsListener,
RelayAddressGenerator: relayAddrGen,
}
serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig)
} else {
tcpListener, err := net.Listen("tcp4", "0.0.0.0:"+strconv.Itoa(turnConf.TLSPort))
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN TCP port")
}
if standalone {
tcpListener = telemetry.NewListener(tcpListener)
}
listenerConfig := turn.ListenerConfig{
Listener: tcpListener,
RelayAddressGenerator: relayAddrGen,
}
serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig)
}
logValues = append(logValues, "turn.portTLS", turnConf.TLSPort, "turn.externalTLS", turnConf.ExternalTLS)
}
if turnConf.UDPPort > 0 {
udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(turnConf.UDPPort))
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN UDP port")
serverConfig := turn.ServerConfig{
Realm: LivekitRealm,
AuthHandler: authHandler,
LoggerFactory: pionlogger.NewLoggerFactory(logger.GetLogger()),
}
var logValues []any
logValues = append(logValues, "turn.relay_range_start", turnConf.RelayPortRangeStart)
logValues = append(logValues, "turn.relay_range_end", turnConf.RelayPortRangeEnd)
for _, addr := range turnConf.BindAddresses {
var nodeIP string
if net.ParseIP(addr).To4() != nil {
nodeIP = conf.RTC.NodeIP.V4
} else {
nodeIP = conf.RTC.NodeIP.V6
}
if nodeIP == "" {
return nil, errors.New("no matching node IP for relay")
}
var relayAddrGen turn.RelayAddressGenerator = &turn.RelayAddressGeneratorPortRange{
RelayAddress: net.ParseIP(nodeIP),
Address: addr,
MinPort: turnConf.RelayPortRangeStart,
MaxPort: turnConf.RelayPortRangeEnd,
MaxRetries: allocateRetries,
}
if standalone {
udpListener = telemetry.NewPacketConn(udpListener, prometheus.Incoming)
relayAddrGen = telemetry.NewRelayAddressGenerator(relayAddrGen)
}
packetConfig := turn.PacketConnConfig{
PacketConn: udpListener,
RelayAddressGenerator: relayAddrGen,
permissionHandler := func(_clientAddr net.Addr, peerIP net.IP) bool {
// restricted peer IP is denied by default, unless allowed by the allow list,
if peerIP.IsLoopback() ||
peerIP.IsLinkLocalUnicast() ||
peerIP.IsLinkLocalMulticast() ||
peerIP.IsMulticast() ||
peerIP.IsPrivate() ||
peerIP.IsUnspecified() {
allowed := false
for _, cidr := range turnConf.AllowRestrictedPeerCIDRs {
if _, ipnet, err := net.ParseCIDR(cidr); err == nil {
if ipnet.Contains(peerIP) {
allowed = true
break
}
}
}
if !allowed {
return false
}
// if allowed, check deny list for overrides
}
for _, cidr := range turnConf.DenyPeerCIDRs {
if _, ipnet, err := net.ParseCIDR(cidr); err == nil {
if ipnet.Contains(peerIP) {
return false
}
}
}
return true
}
if turnConf.TLSPort > 0 {
var listener net.Listener
var listenerErr error
if turnConf.ExternalTLS {
listener, listenerErr = net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(turnConf.TLSPort)))
} else {
cert, err := tls.LoadX509KeyPair(turnConf.CertFile, turnConf.KeyFile)
if err != nil {
return nil, errors.Wrap(err, "TURN tls cert required")
}
listener, listenerErr = tls.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(turnConf.TLSPort)),
&tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
})
}
if listenerErr != nil {
return nil, errors.Wrap(listenerErr, "could not listen on TURN TCP port")
}
if standalone {
listener = telemetry.NewListener(listener)
}
listenerConfig := turn.ListenerConfig{
Listener: listener,
RelayAddressGenerator: relayAddrGen,
PermissionHandler: permissionHandler,
}
serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig)
logValues = append(logValues, "turn.portTLS", turnConf.TLSPort, "turn.externalTLS", turnConf.ExternalTLS)
}
if turnConf.UDPPort > 0 {
udpListener, err := net.ListenPacket("udp", net.JoinHostPort(addr, strconv.Itoa(turnConf.UDPPort)))
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN UDP port")
}
if standalone {
udpListener = telemetry.NewPacketConn(udpListener, prometheus.Incoming)
}
packetConfig := turn.PacketConnConfig{
PacketConn: udpListener,
RelayAddressGenerator: relayAddrGen,
PermissionHandler: permissionHandler,
}
serverConfig.PacketConnConfigs = append(serverConfig.PacketConnConfigs, packetConfig)
logValues = append(logValues, "turn.portUDP", turnConf.UDPPort)
}
serverConfig.PacketConnConfigs = append(serverConfig.PacketConnConfigs, packetConfig)
logValues = append(logValues, "turn.portUDP", turnConf.UDPPort)
}
logger.Infow("Starting TURN server", logValues...)
@@ -160,33 +205,82 @@ func NewTURNAuthHandler(keyProvider auth.KeyProvider) *TURNAuthHandler {
}
}
func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID) string {
return base62.EncodeToString([]byte(fmt.Sprintf("%s|%s", apiKey, pID)))
func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID, ttlSeconds int) (string, int64) {
expiry := time.Now().Add(time.Duration(ttlSeconds) * time.Second).Unix()
return base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", apiKey, pID, expiry)), expiry
}
func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantID) (string, error) {
func (h *TURNAuthHandler) ParseUsername(username string) (string, livekit.ParticipantID, int64, error) {
decoded, err := base62.DecodeString(username)
if err != nil {
return "", "", 0, err
}
parts := strings.Split(string(decoded), "|")
if len(parts) != 3 {
return "", "", 0, errors.New("invalid username")
}
expiry, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return "", "", 0, err
}
if expiry == 0 {
return "", "", 0, ErrExpired
}
return parts[0], livekit.ParticipantID(parts[1]), expiry, nil
}
func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) {
if expiry == 0 || time.Now().After(time.Unix(expiry, 0)) {
return "", ErrExpired
}
return h.computePassword(apiKey, pID, expiry)
}
func (h *TURNAuthHandler) computePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) {
secret := h.keyProvider.GetSecret(apiKey)
if secret == "" {
return "", ErrInvalidAPIKey
}
keyInput := fmt.Sprintf("%s|%s", secret, pID)
keyInput := fmt.Sprintf("%s|%s|%d", secret, pID, expiry)
sum := sha256.Sum256([]byte(keyInput))
return base62.EncodeToString(sum[:]), nil
}
func (h *TURNAuthHandler) HandleAuth(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
func (h *TURNAuthHandler) HandleAuth(ra *turn.RequestAttributes) (userID string, key []byte, ok bool) {
username := ra.Username
decoded, err := base62.DecodeString(username)
if err != nil {
return nil, false
return "", nil, false
}
parts := strings.Split(string(decoded), "|")
if len(parts) != 2 {
return nil, false
if len(parts) != 3 {
return "", nil, false
}
password, err := h.CreatePassword(parts[0], livekit.ParticipantID(parts[1]))
expiry, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
logger.Warnw("could not create TURN password", err, "username", username)
return nil, false
return "", nil, false
}
return turn.GenerateAuthKey(username, LivekitRealm, password), true
if expiry == 0 {
return "", nil, false
}
expiryTime := time.Unix(expiry, 0)
if time.Now().After(expiryTime) {
// TTL only applies to initial allocation. Refresh / CreatePermission /
// ChannelBind / Send / Data requests are still authenticated against the
// username/password but skip the TTL check so long-running sessions can
// keep refreshing past the credential expiry.
if ra.Method == stun.MethodAllocate {
logger.Infow("TURN credential expired", "username", decoded, "participantID", parts[1], "expiry", expiryTime, "method", ra.Method)
return "", nil, false
}
}
password, err := h.computePassword(parts[0], livekit.ParticipantID(parts[1]), expiry)
if err != nil {
logger.Warnw("could not create TURN password", err, "username", decoded)
return "", nil, false
}
return parts[1], turn.GenerateAuthKey(username, LivekitRealm, password), true
}
+208
View File
@@ -0,0 +1,208 @@
// 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 service
import (
"fmt"
"net"
"testing"
"github.com/jxskiss/base62"
"github.com/pion/stun/v3"
"github.com/pion/turn/v5"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
)
const (
turnTestAPIKey = "APITestKey"
turnTestAPISecret = "TestSecret"
)
func newTestTurnAuthHandler() *TURNAuthHandler {
return NewTURNAuthHandler(auth.NewSimpleKeyProvider(turnTestAPIKey, turnTestAPISecret))
}
func mustAuthCreds(t *testing.T, h *TURNAuthHandler, pID livekit.ParticipantID, ttlSeconds int) (username string, key []byte) {
t.Helper()
username, expiry := h.CreateUsername(turnTestAPIKey, pID, ttlSeconds)
password, err := h.CreatePassword(turnTestAPIKey, pID, expiry)
require.NoError(t, err)
return username, turn.GenerateAuthKey(username, LivekitRealm, password)
}
func TestTURNAuthHandler_HandleAuth_ValidCredentials(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_valid")
username, expectedKey := mustAuthCreds(t, h, pID, 300)
for _, method := range []stun.Method{
stun.MethodAllocate,
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
userID, key, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.True(t, ok)
require.Equal(t, string(pID), userID)
require.Equal(t, expectedKey, key)
})
}
}
func TestTURNAuthHandler_HandleAuth_ExpiredAllocateRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_expired_alloc")
username, _ := h.CreateUsername(turnTestAPIKey, pID, -60)
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: stun.MethodAllocate,
})
require.False(t, ok, "Allocate request with expired credentials must be rejected")
}
func TestTURNAuthHandler_HandleAuth_ExpiredNonAllocateAllowed(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_expired_refresh")
username, expiry := h.CreateUsername(turnTestAPIKey, pID, -60)
// CreatePassword still enforces ErrExpired on its own, but the server hands
// the same key it generated at allocation time — reproduce that by directly
// hashing without going through CreatePassword's expiry guard.
password, err := h.computePassword(turnTestAPIKey, pID, expiry)
require.NoError(t, err)
expectedKey := turn.GenerateAuthKey(username, LivekitRealm, password)
for _, method := range []stun.Method{
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
userID, key, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.True(t, ok, "Non-allocate request with expired credentials must succeed")
require.Equal(t, string(pID), userID)
require.Equal(t, expectedKey, key)
})
}
}
func TestTURNAuthHandler_HandleAuth_WrongUsernameRejected(t *testing.T) {
h := newTestTurnAuthHandler()
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: "not-base62!!!",
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: stun.MethodRefresh,
})
require.False(t, ok)
}
func TestTURNAuthHandler_HandleAuth_TwoPartUsernameRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_two_part")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s", turnTestAPIKey, pID))
for _, method := range []stun.Method{
stun.MethodAllocate,
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.False(t, ok, "Two-part username must be rejected")
})
}
}
func TestTURNAuthHandler_HandleAuth_ZeroExpiryRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_zero_expiry")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, 0))
for _, method := range []stun.Method{
stun.MethodAllocate,
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.False(t, ok, "Username with expiry=0 must be rejected")
})
}
}
func TestTURNAuthHandler_ParseUsername_TwoPartRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_parse_two_part")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s", turnTestAPIKey, pID))
_, _, _, err := h.ParseUsername(username)
require.Error(t, err)
}
func TestTURNAuthHandler_ParseUsername_ZeroExpiryRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_parse_zero_expiry")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, 0))
_, _, _, err := h.ParseUsername(username)
require.ErrorIs(t, err, ErrExpired)
}
func TestTURNAuthHandler_CreatePassword_ZeroExpiryRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_password_zero_expiry")
_, err := h.CreatePassword(turnTestAPIKey, pID, 0)
require.ErrorIs(t, err, ErrExpired)
}
+32 -12
View File
@@ -26,10 +26,11 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
)
type twirpRequestFields struct {
@@ -46,9 +47,9 @@ type twirpLoggerKey struct{}
// License: Apache-2.0
func TwirpLogger() *twirp.ServerHooks {
loggerPool := &sync.Pool{
New: func() interface{} {
New: func() any {
return &twirpLogger{
fieldsOrig: make([]interface{}, 0, 30),
fieldsOrig: make([]any, 0, 30),
}
},
}
@@ -67,12 +68,20 @@ func TwirpLogger() *twirp.ServerHooks {
type twirpLogger struct {
twirpRequestFields
fieldsOrig []interface{}
fields []interface{}
fieldsOrig []any
fields []any
startedAt time.Time
deadline time.Time
}
func AppendLogFields(ctx context.Context, fields ...interface{}) {
func (t *twirpLogger) reset() {
t.fields = t.fieldsOrig
t.error = nil
t.startedAt = time.Time{}
t.deadline = time.Time{}
}
func AppendLogFields(ctx context.Context, fields ...any) {
r, ok := ctx.Value(twirpLoggerKey{}).(*twirpLogger)
if !ok || r == nil {
return
@@ -84,6 +93,9 @@ func AppendLogFields(ctx context.Context, fields ...interface{}) {
func loggerRequestReceived(ctx context.Context, twirpLoggerPool *sync.Pool) (context.Context, error) {
r := twirpLoggerPool.Get().(*twirpLogger)
r.startedAt = time.Now()
if deadline, ok := ctx.Deadline(); ok {
r.deadline = deadline
}
r.fields = r.fieldsOrig
r.error = nil
@@ -114,7 +126,14 @@ func loggerResponseSent(ctx context.Context, twirpLoggerPool *sync.Pool) {
return
}
r.fields = append(r.fields, "duration", time.Since(r.startedAt))
duration := time.Since(r.startedAt)
r.fields = append(r.fields, "duration", duration)
if !r.deadline.IsZero() {
r.fields = append(r.fields, "requestedTimeout", r.deadline.Sub(r.startedAt))
}
if deadline, ok := ctx.Deadline(); ok {
r.fields = append(r.fields, "modifiedTimeout", deadline.Sub(r.startedAt))
}
if status, ok := twirp.StatusCode(ctx); ok {
r.fields = append(r.fields, "status", status)
@@ -126,10 +145,10 @@ func loggerResponseSent(ctx context.Context, twirpLoggerPool *sync.Pool) {
serviceMethod := "API " + r.service + "." + r.method
utils.GetLogger(ctx).WithComponent(utils.ComponentAPI).Infow(serviceMethod, r.fields...)
prometheus.RecordTwirpRequestLatency(r.service, r.method, duration)
r.fields = r.fieldsOrig
r.error = nil
// reset fields and return to pool
r.reset()
twirpLoggerPool.Put(r)
}
@@ -203,7 +222,7 @@ func statusReporterResponseSent(ctx context.Context) {
code = r.error.Code()
}
prometheus.TwirpRequestStatusCounter.WithLabelValues(r.service, r.method, statusFamily, string(code)).Add(1)
prometheus.RecordTwirpRequestStatus(r.service, r.method, statusFamily, code)
}
func statusReporterErrorReceived(ctx context.Context, e twirp.Error) context.Context {
@@ -231,6 +250,7 @@ func TwirpTelemetry(
ResponseSent: func(ctx context.Context) {
telemetryResponseSent(ctx, nodeID, getProjectID, telemetry)
},
RequestRouted: telemetryRequestRouted,
}
}
@@ -390,7 +410,7 @@ func telemetryResponseSent(
}
a.NodeId = string(nodeID)
if statusCode, ok := twirp.StatusCode(ctx); ok {
if status, err := strconv.Atoi(statusCode); err == nil {
if status, err := strconv.ParseInt(statusCode, 10, 32); err == nil {
a.Status = int32(status)
}
}
+340 -2
View File
@@ -15,29 +15,85 @@
package service
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"github.com/ua-parser/uap-go/uaparser"
"gopkg.in/yaml.v3"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/routing/selector"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func handleError(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...interface{}) {
var (
ErrGzipReadFailed = errors.New("cannot read decompressed data")
ErrGzipTooLarge = errors.New("decompressed data too large")
)
var gzipReaderPool = sync.Pool{
New: func() any { return &gzip.Reader{} },
}
func DecompressGzip(compressed []byte) ([]byte, error) {
reader := gzipReaderPool.Get().(*gzip.Reader)
defer gzipReaderPool.Put(reader)
if err := reader.Reset(bytes.NewReader(compressed)); err != nil {
return nil, fmt.Errorf("%w: %w", ErrGzipReadFailed, err)
}
out, err := io.ReadAll(io.LimitReader(reader, http.DefaultMaxHeaderBytes+1))
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrGzipReadFailed, err)
}
if len(out) > http.DefaultMaxHeaderBytes {
return nil, ErrGzipTooLarge
}
return out, nil
}
func handleError(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...any) {
keysAndValues = append(keysAndValues, "status", status)
if r != nil && r.URL != nil {
keysAndValues = append(keysAndValues, "method", r.Method, "path", r.URL.Path)
}
if !errors.Is(err, context.Canceled) && !errors.Is(r.Context().Err(), context.Canceled) {
logger.GetLogger().WithCallDepth(1).Warnw("error handling request", err, keysAndValues...)
utils.GetLogger(r.Context()).WithCallDepth(1).Warnw("error handling request", err, keysAndValues...)
}
w.WriteHeader(status)
}
func HandleError(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...any) {
handleError(w, r, status, err, keysAndValues...)
_, _ = w.Write([]byte(err.Error()))
}
func HandleErrorJson(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...any) {
handleError(w, r, status, err, keysAndValues...)
json.NewEncoder(w).Encode(struct {
Error string `json:"error"`
}{
Error: err.Error(),
})
w.Header().Add("Content-type", "application/json")
}
func boolValue(s string) bool {
return s == "1" || s == "true"
}
@@ -81,4 +137,286 @@ func SetRoomConfiguration(createRequest *livekit.CreateRoomRequest, conf *liveki
createRequest.MinPlayoutDelay = conf.MinPlayoutDelay
createRequest.MaxPlayoutDelay = conf.MaxPlayoutDelay
createRequest.SyncStreams = conf.SyncStreams
createRequest.Metadata = conf.Metadata
createRequest.Tags = conf.Tags
}
func ParseClientInfo(r *http.Request) *livekit.ClientInfo {
values := r.Form
ci := &livekit.ClientInfo{}
if pv, err := strconv.ParseInt(values.Get("protocol"), 10, 32); err == nil {
ci.Protocol = int32(pv)
}
if cp, err := strconv.ParseInt(values.Get("client_protocol"), 10, 32); err == nil {
ci.ClientProtocol = int32(cp)
}
sdkString := values.Get("sdk")
switch sdkString {
case "js":
ci.Sdk = livekit.ClientInfo_JS
case "ios", "swift":
ci.Sdk = livekit.ClientInfo_SWIFT
case "android":
ci.Sdk = livekit.ClientInfo_ANDROID
case "flutter":
ci.Sdk = livekit.ClientInfo_FLUTTER
case "go":
ci.Sdk = livekit.ClientInfo_GO
case "unity":
ci.Sdk = livekit.ClientInfo_UNITY
case "reactnative":
ci.Sdk = livekit.ClientInfo_REACT_NATIVE
case "rust":
ci.Sdk = livekit.ClientInfo_RUST
case "python":
ci.Sdk = livekit.ClientInfo_PYTHON
case "cpp":
ci.Sdk = livekit.ClientInfo_CPP
case "unityweb":
ci.Sdk = livekit.ClientInfo_UNITY_WEB
case "node":
ci.Sdk = livekit.ClientInfo_NODE
case "esp32":
ci.Sdk = livekit.ClientInfo_ESP32
}
ci.Version = values.Get("version")
ci.Os = values.Get("os")
ci.OsVersion = values.Get("os_version")
ci.Browser = values.Get("browser")
ci.BrowserVersion = values.Get("browser_version")
ci.DeviceModel = values.Get("device_model")
ci.Network = values.Get("network")
if capStr := values.Get("capabilities"); capStr != "" {
for _, name := range strings.Split(capStr, ",") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if v, ok := livekit.ClientInfo_Capability_value[name]; ok {
ci.Capabilities = append(ci.Capabilities, livekit.ClientInfo_Capability(v))
}
}
}
AugmentClientInfo(ci, r)
return ci
}
var (
userAgentParserCache *uaparser.Parser
userAgentParserInit sync.Once
)
func createUserAgentParserWithCustomRules() (*uaparser.Parser, error) {
defaultYaml := uaparser.DefinitionYaml
rules := make(map[string]any)
err := yaml.Unmarshal(defaultYaml, rules)
if err != nil {
return nil, err
}
rules["user_agent_parsers"] = append(rules["user_agent_parsers"].([]any), map[string]any{
"regex": "OBS-Studio\\/([0-9\\.]+)",
"family_replacement": "OBS Studio",
"v1_replacement": "$1",
})
customYaml, err := yaml.Marshal(rules)
if err != nil {
return nil, err
}
return uaparser.NewFromBytes([]byte(customYaml))
}
func getUserAgentParser() *uaparser.Parser {
userAgentParserInit.Do(func() {
if parser, err := createUserAgentParserWithCustomRules(); err != nil {
logger.Warnw("could not create user agent parser with custom rules, using default", err)
userAgentParserCache = uaparser.NewFromSaved()
} else {
userAgentParserCache = parser
}
})
return userAgentParserCache
}
func AugmentClientInfo(ci *livekit.ClientInfo, req *http.Request) {
if ci == nil {
return
}
// get real address (forwarded http header) - check Cloudflare headers first, fall back to X-Forwarded-For
ci.Address = GetClientIP(req)
// attempt to parse types for SDKs that support browser as a platform
if ci.Sdk == livekit.ClientInfo_JS ||
ci.Sdk == livekit.ClientInfo_REACT_NATIVE ||
ci.Sdk == livekit.ClientInfo_FLUTTER ||
ci.Sdk == livekit.ClientInfo_UNITY ||
ci.Sdk == livekit.ClientInfo_UNKNOWN {
client := getUserAgentParser().Parse(req.UserAgent())
if ci.Browser == "" {
ci.Browser = client.UserAgent.Family
ci.BrowserVersion = client.UserAgent.ToVersionString()
}
if ci.Os == "" {
ci.Os = client.Os.Family
ci.OsVersion = client.Os.ToVersionString()
}
if ci.DeviceModel == "" {
model := client.Device.Family
if model != "" && client.Device.Model != "" && model != client.Device.Model {
model += " " + client.Device.Model
}
ci.DeviceModel = model
}
}
}
type ValidateConnectRequestParams struct {
roomName livekit.RoomName
publish string
metadata string
attributes map[string]string
}
type ValidateConnectRequestResult struct {
roomName livekit.RoomName
grants *auth.ClaimGrants
region string
createRoomRequest *livekit.CreateRoomRequest
}
func ValidateConnectRequest(
lgr logger.Logger,
r *http.Request,
limitConfig config.LimitConfig,
params ValidateConnectRequestParams,
router routing.MessageRouter,
roomAllocator RoomAllocator,
) (ValidateConnectRequestResult, int, error) {
var res ValidateConnectRequestResult
// require a claim
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
roomNameInToken, err := EnsureJoinPermission(r.Context())
if err != nil {
return res, http.StatusUnauthorized, err
}
if claims.Identity == "" {
return res, http.StatusBadRequest, ErrIdentityEmpty
}
if !limitConfig.CheckParticipantIdentityLength(claims.Identity) {
return res, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrParticipantIdentityExceedsLimits, limitConfig.MaxParticipantIdentityLength)
}
if claims.RoomConfig != nil {
if err := claims.RoomConfig.CheckCredentials(); err != nil {
lgr.Warnw("credentials found in token", nil)
// TODO(dz): in a future version, we'll reject these connections
}
}
res.roomName = params.roomName
if roomNameInToken != "" {
res.roomName = roomNameInToken
}
if res.roomName == "" {
return res, http.StatusBadRequest, ErrNoRoomName
}
if !limitConfig.CheckRoomNameLength(string(res.roomName)) {
return res, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, limitConfig.MaxRoomNameLength)
}
// this is new connection for existing participant - with publish only permissions
if params.publish != "" {
// Make sure grant has GetCanPublish set,
if !claims.Video.GetCanPublish() {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
// Make sure by default subscribe is off
claims.Video.SetCanSubscribe(false)
claims.Identity += "#" + params.publish
}
// room allocator validations
err = roomAllocator.ValidateCreateRoom(r.Context(), res.roomName)
if err != nil {
if errors.Is(err, ErrRoomNotFound) {
return res, http.StatusNotFound, err
} else {
return res, http.StatusInternalServerError, err
}
}
if router, ok := router.(routing.Router); ok {
res.region = router.GetRegion()
if foundNode, err := router.GetNodeForRoom(r.Context(), res.roomName); err == nil {
if selector.LimitsReached(limitConfig, foundNode.Stats) {
return res, http.StatusServiceUnavailable, rtc.ErrLimitExceeded
}
}
}
createRequest := &livekit.CreateRoomRequest{
Name: string(res.roomName),
RoomPreset: claims.RoomPreset,
}
SetRoomConfiguration(createRequest, claims.GetRoomConfiguration())
res.createRoomRequest = createRequest
if len(params.metadata) != 0 {
// Make sure grant has GetCanUpdateOwnMetadata set
if !claims.Video.GetCanUpdateOwnMetadata() {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
claims.Metadata = params.metadata
}
// Add extra attributes to the participant
if len(params.attributes) != 0 {
// Make sure grant has GetCanUpdateOwnMetadata set
if !claims.Video.GetCanUpdateOwnMetadata() {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
if claims.Attributes == nil {
claims.Attributes = make(map[string]string, len(params.attributes))
}
for k, v := range params.attributes {
if v == "" {
continue // do not allow deleting existing attributes
}
claims.Attributes[k] = v
}
}
res.grants = claims
return res, http.StatusOK, nil
}
func IsRTCPath(path string) bool {
return path == "/rtc" || path == "/rtc/v1"
}
func IsRTCValidatePath(path string) bool {
return path == "/rtc/validate" || path == "/rtc/v1/validate"
}
func IsAgentWorkerPath(path string) bool {
return path == "/agent"
}
func IsAgentPath(path string) bool {
return strings.HasPrefix(path, "/agent")
}
+52
View File
@@ -15,7 +15,10 @@
package service_test
import (
"bytes"
"compress/gzip"
"context"
"net/http"
"testing"
"time"
@@ -75,3 +78,52 @@ func TestIsValidDomain(t *testing.T) {
require.Equal(t, service.IsValidDomain(key), result)
}
}
func compress(t *testing.T, payload []byte) []byte {
t.Helper()
var buf bytes.Buffer
gw, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
require.NoError(t, err)
_, err = gw.Write(payload)
require.NoError(t, err)
require.NoError(t, gw.Close())
return buf.Bytes()
}
func TestDecompressGzip(t *testing.T) {
t.Run("small payload", func(t *testing.T) {
out, err := service.DecompressGzip(compress(t, []byte("hello world")))
require.NoError(t, err)
require.Equal(t, []byte("hello world"), out)
})
t.Run("payload exactly at cap", func(t *testing.T) {
raw := make([]byte, http.DefaultMaxHeaderBytes)
out, err := service.DecompressGzip(compress(t, raw))
require.NoError(t, err)
require.Len(t, out, http.DefaultMaxHeaderBytes)
})
t.Run("payload one byte over capd", func(t *testing.T) {
raw := make([]byte, http.DefaultMaxHeaderBytes+1)
_, err := service.DecompressGzip(compress(t, raw))
require.ErrorIs(t, err, service.ErrGzipTooLarge)
})
t.Run("gzip decompression bomb", func(t *testing.T) {
// 100 MB of zeros
raw := make([]byte, 100<<20)
compressed := compress(t, raw)
require.Less(t, len(compressed), 1<<20,
"sanity: bomb input should compress dramatically")
_, err := service.DecompressGzip(compressed)
require.ErrorIs(t, err, service.ErrGzipTooLarge)
})
t.Run("malformed gzip compression", func(t *testing.T) {
_, err := service.DecompressGzip([]byte("not gzip data"))
require.Error(t, err)
require.Contains(t, err.Error(), "cannot read decompressed")
})
}
+544
View File
@@ -0,0 +1,544 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/pion/webrtc/v4"
"github.com/tomnomnom/linkheader"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/rtc/types"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
)
const (
cParticipantPath = "/whip/v1"
cParticipantIDPath = "/whip/v1/{participant_id}"
)
type WHIPService struct {
http.Handler
config *config.Config
router routing.Router
roomAllocator RoomAllocator
client rpc.WHIPClient[livekit.NodeID]
topicFormatter rpc.TopicFormatter
participantClient rpc.TypedWHIPParticipantClient
}
func NewWHIPService(
config *config.Config,
router routing.Router,
roomAllocator RoomAllocator,
clientParams rpc.ClientParams,
topicFormatter rpc.TopicFormatter,
participantClient rpc.TypedWHIPParticipantClient,
) (*WHIPService, error) {
client, err := rpc.NewWHIPClient[livekit.NodeID](clientParams.Args())
if err != nil {
return nil, err
}
return &WHIPService{
config: config,
router: router,
roomAllocator: roomAllocator,
client: client,
topicFormatter: topicFormatter,
participantClient: participantClient,
}, nil
}
func (s *WHIPService) SetupRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET "+cParticipantPath, s.handleGet)
mux.HandleFunc("OPTIONS "+cParticipantPath, s.handleOptions)
mux.HandleFunc("POST "+cParticipantPath, s.handleCreate)
mux.HandleFunc("GET "+cParticipantIDPath, s.handleParticipantGet)
mux.HandleFunc("PATCH "+cParticipantIDPath, s.handleParticipantPatch)
mux.HandleFunc("DELETE "+cParticipantIDPath, s.handleParticipantDelete)
}
func (s *WHIPService) handleGet(w http.ResponseWriter, r *http.Request) {
// https:/www.rfc-editor.org/rfc/rfc9725.html#name-http-usage
w.WriteHeader(http.StatusNoContent)
}
func (s *WHIPService) handleOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Methods", "PATCH, OPTIONS, GET, POST, DELETE")
w.Header().Set("Access-Control-Expose-Headers", "*")
w.WriteHeader(http.StatusOK)
// According to https://www.rfc-editor.org/rfc/rfc9725.html#name-stun-turn-server-configurat,
// ICE servers can be returned in OPTIONS response, but not recommended.
//
// Supporting that here is tricky. This would have to get region settings like the
// session CREATE POST request and send a request to get ICE servers from a
// region + media node that is selected. The issue is that a subsequent POST,
// although unlikely, may end up in a different region. Media node in one region and
// TURN in another region, although shuttling media across regions, should still work.
// But, as this is not a recommended way, not supporting it.
}
type createRequest struct {
RoomName livekit.RoomName
ParticipantInit routing.ParticipantInit
ClientIP string
OfferSDP string
SubscribedParticipantTrackNames map[string][]string
FromIngress bool
}
func (s *WHIPService) validateCreate(w http.ResponseWriter, r *http.Request) (*createRequest, int, error) {
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
return nil, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
roomName, err := EnsureJoinPermission(r.Context())
if err != nil {
return nil, http.StatusUnauthorized, err
}
if roomName == "" {
return nil, http.StatusUnauthorized, errors.New("room name cannot be empty")
}
if !s.config.Limit.CheckRoomNameLength(string(roomName)) {
return nil, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, s.config.Limit.MaxRoomNameLength)
}
if claims.Identity == "" {
return nil, http.StatusBadRequest, ErrIdentityEmpty
}
if !s.config.Limit.CheckParticipantIdentityLength(claims.Identity) {
return nil, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrParticipantIdentityExceedsLimits, s.config.Limit.MaxParticipantIdentityLength)
}
var clientInfo struct {
ClientIP string `json:"clientIp"`
SubscribedParticipantTrackNames map[string][]string `json:"subscribedParticipantTrackNames"`
}
clientInfoHeader := r.Header.Get("X-LiveKit-ClientInfo")
if clientInfoHeader != "" {
if err := json.NewDecoder(strings.NewReader(clientInfoHeader)).Decode(&clientInfo); err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("malformed json in client info header: %s", err)
}
}
fromIngress := r.Header.Get("X-Livekit-Ingress")
offerSDPBytes, err := io.ReadAll(http.MaxBytesReader(w, r.Body, http.DefaultMaxHeaderBytes))
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
return nil, http.StatusRequestEntityTooLarge, fmt.Errorf("request body exceeds %d bytes", maxErr.Limit)
}
return nil, http.StatusBadRequest, fmt.Errorf("body does not have SDP offer: %s", err)
}
if len(offerSDPBytes) == 0 {
return nil, http.StatusBadRequest, errors.New("body does not have SDP offer")
}
offerSDP := string(offerSDPBytes)
sd := &webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer,
SDP: offerSDP,
}
_, err = sd.Unmarshal()
if err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("malformed SDP offer: %s", err)
}
ci := ParseClientInfo(r)
if ci.Protocol == 0 {
// if no client info available (which will be mostly the case with WHIP clients), at least set protocol
ci.Protocol = types.CurrentProtocol
}
pi := routing.ParticipantInit{
Identity: livekit.ParticipantIdentity(claims.Identity),
Name: livekit.ParticipantName(claims.Name),
AutoSubscribe: true,
Client: ci,
Grants: claims,
CreateRoom: &livekit.CreateRoomRequest{
Name: string(roomName),
RoomPreset: claims.RoomPreset,
},
AdaptiveStream: false,
DisableICELite: true,
}
SetRoomConfiguration(pi.CreateRoom, claims.GetRoomConfiguration())
return &createRequest{
roomName,
pi,
clientInfo.ClientIP,
offerSDP,
clientInfo.SubscribedParticipantTrackNames,
fromIngress != "",
}, http.StatusOK, nil
}
func (s *WHIPService) handleCreate(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-type") != "application/sdp" {
s.handleError("Create", w, r, http.StatusBadRequest, fmt.Errorf("unsupported content-type: %s", r.Header.Get("Content-type")))
return
}
w.Header().Add("Content-type", "application/sdp")
req, status, err := s.validateCreate(w, r)
if err != nil {
s.handleError("Create", w, r, status, err)
return
}
if err := s.roomAllocator.SelectRoomNode(r.Context(), req.RoomName, ""); err != nil {
s.handleError("Create", w, r, http.StatusInternalServerError, err)
return
}
rtcNode, err := s.router.GetNodeForRoom(r.Context(), req.RoomName)
if err != nil {
s.handleError("Create", w, r, http.StatusInternalServerError, err)
return
}
connID := livekit.ConnectionID(guid.New("CO_"))
starSession, err := req.ParticipantInit.ToStartSession(req.RoomName, connID)
if err != nil {
s.handleError("Create", w, r, http.StatusInternalServerError, err)
return
}
subscribedParticipantTracks := map[string]*rpc.WHIPCreateRequest_TrackList{}
for identity, trackNames := range req.SubscribedParticipantTrackNames {
subscribedParticipantTracks[identity] = &rpc.WHIPCreateRequest_TrackList{
TrackNames: trackNames,
}
}
res, err := s.client.Create(r.Context(), livekit.NodeID(rtcNode.Id), &rpc.WHIPCreateRequest{
OfferSdp: req.OfferSDP,
StartSession: starSession,
SubscribedParticipantTracks: subscribedParticipantTracks,
FromIngress: req.FromIngress,
})
if err != nil {
s.handleError("Create", w, r, http.StatusServiceUnavailable, err)
return
}
// created resource sent in Location header:
// https://www.rfc-editor.org/rfc/rfc9725.html#name-ingest-session-setup
// using relative location
w.Header().Add("Location", fmt.Sprintf("%s/%s", cParticipantPath, res.ParticipantId))
// ICE servers as Link header(s):
// https://www.rfc-editor.org/rfc/rfc9725.html#name-stun-turn-server-configurat
var iceServerLinks []*linkheader.Link
for _, iceServer := range res.IceServers {
for _, iceURL := range iceServer.Urls {
iceServerLink := &linkheader.Link{
URL: url.PathEscape(iceURL),
Rel: "ice-server",
Params: map[string]string{},
}
if iceServer.Username != "" {
iceServerLink.Params["username"] = iceServer.Username
}
if iceServer.Credential != "" {
iceServerLink.Params["credential"] = iceServer.Credential
}
iceServerLinks = append(iceServerLinks, iceServerLink)
}
}
for _, iceServerLink := range iceServerLinks {
w.Header().Add("Link", iceServerLink.String())
}
// To support ICE Trickle/Restart, HTTP PATCH should have an ETag
// send ICE session ID (ICE ufrag is used as ID) in ETag header
// https://www.rfc-editor.org/rfc/rfc9725.html#name-http-patch-request-usage
if res.IceSessionId != "" {
w.Header().Add("ETag", res.IceSessionId)
}
// 201 Status Created
w.WriteHeader(http.StatusCreated)
// SDP answer in the response body
w.Write([]byte(res.AnswerSdp))
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Create",
"connID", connID,
"participant", req.ParticipantInit.Identity,
"room", req.RoomName,
"status", http.StatusCreated,
"response", logger.Proto(res),
)
}
func (s *WHIPService) handleParticipantGet(w http.ResponseWriter, r *http.Request) {
// https:/www.rfc-editor.org/rfc/rfc9725.html#name-http-usage
w.WriteHeader(http.StatusNoContent)
}
func (s *WHIPService) iceTrickle(
w http.ResponseWriter,
r *http.Request,
roomName livekit.RoomName,
participantIdentity livekit.ParticipantIdentity,
pID livekit.ParticipantID,
iceSessionID string,
sdpFragment string,
) {
_, err := s.participantClient.ICETrickle(
r.Context(),
s.topicFormatter.ParticipantTopic(r.Context(), roomName, participantIdentity),
&rpc.WHIPParticipantICETrickleRequest{
Room: string(roomName),
ParticipantIdentity: string(participantIdentity),
ParticipantId: string(pID),
IceSessionId: iceSessionID,
SdpFragment: sdpFragment,
},
)
if err != nil {
var pe psrpc.Error
if errors.As(err, &pe) {
switch pe.Code() {
case psrpc.NotFound:
s.handleError("Patch", w, r, http.StatusNotFound, errors.New(pe.Error()))
case psrpc.InvalidArgument:
switch pe.Error() {
case rtc.ErrInvalidSDPFragment.Error(), rtc.ErrMidMismatch.Error(), rtc.ErrICECredentialMismatch.Error():
s.handleError("Patch", w, r, http.StatusBadRequest, errors.New(pe.Error()))
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
} else {
s.handleError("Patch", w, r, http.StatusInternalServerError, nil)
}
return
}
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Patch",
"method", "ice-trickle",
"room", roomName,
"participant", participantIdentity,
"participantID", pID,
"sdpFragment", sdpFragment,
"status", http.StatusNoContent,
)
w.WriteHeader(http.StatusNoContent)
}
func (s *WHIPService) iceRestart(
w http.ResponseWriter,
r *http.Request,
roomName livekit.RoomName,
participantIdentity livekit.ParticipantIdentity,
pID livekit.ParticipantID,
sdpFragment string,
) {
res, err := s.participantClient.ICERestart(
r.Context(),
s.topicFormatter.ParticipantTopic(r.Context(), roomName, participantIdentity),
&rpc.WHIPParticipantICERestartRequest{
Room: string(roomName),
ParticipantIdentity: string(participantIdentity),
ParticipantId: string(pID),
SdpFragment: sdpFragment,
},
)
if err != nil {
var pe psrpc.Error
if errors.As(err, &pe) {
switch pe.Code() {
case psrpc.NotFound:
s.handleError("Patch", w, r, http.StatusNotFound, errors.New(pe.Error()))
case psrpc.InvalidArgument:
switch pe.Error() {
case rtc.ErrInvalidSDPFragment.Error():
s.handleError("Patch", w, r, http.StatusBadRequest, errors.New(pe.Error()))
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
} else {
s.handleError("Patch", w, r, http.StatusInternalServerError, nil)
}
return
}
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Patch",
"method", "ice-restart",
"room", roomName,
"participant", participantIdentity,
"participantID", pID,
"sdpFragment", sdpFragment,
"status", http.StatusNoContent,
"res", logger.Proto(res),
)
if res.IceSessionId != "" {
w.Header().Add("ETag", res.IceSessionId)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(res.SdpFragment))
}
func (s *WHIPService) handleParticipantPatch(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-type") != "application/trickle-ice-sdpfrag" {
s.handleError("Patch", w, r, http.StatusBadRequest, fmt.Errorf("unsupported content-type: %s", r.Header.Get("Content-type")))
return
}
w.Header().Add("Content-type", "application/trickle-ice-sdpfrag")
// https://www.rfc-editor.org/rfc/rfc9725.html#name-http-patch-request-usage
ifMatch := r.Header.Get("If-Match")
if ifMatch == "" {
s.handleError("Patch", w, r, http.StatusPreconditionRequired, errors.New("missing entity tag"))
return
}
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
s.handleError("Patch", w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return
}
roomName, err := EnsureJoinPermission(r.Context())
if err != nil {
s.handleError("Patch", w, r, http.StatusUnauthorized, err)
return
}
if roomName == "" {
s.handleError("Patch", w, r, http.StatusUnauthorized, errors.New("room name cannot be empty"))
return
}
if claims.Identity == "" {
s.handleError("Patch", w, r, http.StatusUnauthorized, errors.New("participant identity cannot be empty"))
return
}
pID := livekit.ParticipantID(r.PathValue("participant_id"))
if pID == "" {
s.handleError("Patch", w, r, http.StatusBadRequest, errors.New("participant ID cannot be empty"))
return
}
sdpFragmentBytes, err := io.ReadAll(http.MaxBytesReader(w, r.Body, http.DefaultMaxHeaderBytes))
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
s.handleError("Patch", w, r, http.StatusRequestEntityTooLarge, fmt.Errorf("request body exceeds %d bytes", maxErr.Limit))
return
}
s.handleError("Patch", w, r, http.StatusBadRequest, fmt.Errorf("body does not have SDP fragment: %s", err))
return
}
sdpFragment := string(sdpFragmentBytes)
if ifMatch == "*" {
s.iceRestart(w, r, roomName, livekit.ParticipantIdentity(claims.Identity), pID, sdpFragment)
} else {
s.iceTrickle(w, r, roomName, livekit.ParticipantIdentity(claims.Identity), pID, ifMatch, sdpFragment)
}
}
func (s *WHIPService) handleParticipantDelete(w http.ResponseWriter, r *http.Request) {
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
s.handleError("Delete", w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return
}
roomName, err := EnsureJoinPermission(r.Context())
if err != nil {
s.handleError("Delete", w, r, http.StatusUnauthorized, err)
return
}
if roomName == "" {
s.handleError("Delete", w, r, http.StatusUnauthorized, errors.New("room name cannot be empty"))
return
}
if claims.Identity == "" {
s.handleError("Delete", w, r, http.StatusUnauthorized, errors.New("participant identity cannot be empty"))
return
}
_, err = s.participantClient.DeleteSession(
r.Context(),
s.topicFormatter.ParticipantTopic(r.Context(), roomName, livekit.ParticipantIdentity(claims.Identity)),
&rpc.WHIPParticipantDeleteSessionRequest{
Room: string(roomName),
ParticipantIdentity: claims.Identity,
ParticipantId: r.PathValue("participant_id"),
},
)
if err != nil {
s.handleError("Delete", w, r, http.StatusNotFound, err)
return
}
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Delete",
"participant", claims.Identity,
"participantID", r.PathValue("participant_id"),
"room", roomName,
"status", http.StatusOK,
)
w.WriteHeader(http.StatusOK)
}
func (s *WHIPService) handleError(method string, w http.ResponseWriter, r *http.Request, status int, err error) {
sutils.GetLogger(r.Context()).Warnw(
fmt.Sprintf("API WHIP.%s", method), err,
"status", status,
)
w.WriteHeader(status)
json.NewEncoder(w).Encode(struct {
Error string `json:"error"`
}{
Error: err.Error(),
})
}
+55 -21
View File
@@ -22,17 +22,11 @@ import (
"os"
"github.com/google/wire"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"gopkg.in/yaml.v3"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/clientconfiguration"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
@@ -41,6 +35,13 @@ import (
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/webhook"
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware/otelpsrpc"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/telemetry"
)
func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*LivekitServer, error) {
@@ -51,15 +52,15 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
wire.Bind(new(ServiceStore), new(ObjectStore)),
createKeyProvider,
createWebhookNotifier,
createClientConfiguration,
createForwardStats,
getNodeStatsConfig,
routing.CreateRouter,
getLimitConf,
config.DefaultAPIConfig,
getAPIConf,
wire.Bind(new(routing.MessageRouter), new(routing.Router)),
wire.Bind(new(livekit.RoomService), new(*RoomService)),
telemetry.NewAnalyticsService,
telemetry.NewTelemetryService,
createTelemetryService,
getMessageBus,
NewIOInfoService,
wire.Bind(new(IOClient), new(*IOInfoService)),
@@ -71,15 +72,17 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
getIngressStore,
getIngressConfig,
NewIngressService,
rpc.NewSIPClient,
newSIPClient,
getSIPStore,
getSIPConfig,
NewSIPService,
NewRoomAllocator,
NewRoomService,
NewRTCService,
NewWHIPService,
NewAgentService,
NewAgentDispatchService,
getAgentConfig,
agent.NewAgentClient,
getAgentStore,
getSignalRelayConfig,
@@ -93,6 +96,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
rpc.NewTopicFormatter,
rpc.NewTypedRoomClient,
rpc.NewTypedParticipantClient,
rpc.NewTypedWHIPParticipantClient,
rpc.NewTypedAgentDispatchInternalClient,
NewLocalRoomManager,
NewTURNAuthHandler,
@@ -116,6 +120,7 @@ func InitializeRouter(conf *config.Config, currentNode routing.LocalNode) (routi
getRoomConfig,
routing.NewRoomManagerClient,
rpc.NewKeepalivePubSub,
getNodeStatsConfig,
routing.CreateRouter,
)
@@ -157,15 +162,21 @@ func createKeyProvider(conf *config.Config) (auth.KeyProvider, error) {
func createWebhookNotifier(conf *config.Config, provider auth.KeyProvider) (webhook.QueuedNotifier, error) {
wc := conf.WebHook
if len(wc.URLs) == 0 {
return nil, nil
}
secret := provider.GetSecret(wc.APIKey)
if secret == "" {
if secret == "" && len(wc.URLs) > 0 {
return nil, ErrWebHookMissingAPIKey
}
return webhook.NewDefaultNotifier(wc.APIKey, secret, wc.URLs), nil
return webhook.NewDefaultNotifier(wc, provider)
}
func createTelemetryService(notifier webhook.QueuedNotifier, analytics telemetry.AnalyticsService) telemetry.TelemetryService {
svc := telemetry.NewTelemetryService(notifier, analytics)
if notifier != nil {
notifier.RegisterProcessedHook(svc.Webhook)
}
return svc
}
func createRedisClient(conf *config.Config) (redis.UniversalClient, error) {
@@ -222,6 +233,19 @@ func getIngressConfig(conf *config.Config) *config.IngressConfig {
return &conf.Ingress
}
func newSIPClient(p rpc.ClientParams) (rpc.SIPClient, error) {
// Do not pass parameters directly, as they set timeout that is too short,
// and might set retry policy that is not acceptable for SIP methods.
// Instead, set relevant parameters manually.
return rpc.NewSIPClientWithParams(rpc.ClientParams{
Bus: p.Bus,
ClientOptions: []psrpc.ClientOption{
rpc.WithClientLogger(p.Logger),
otelpsrpc.ClientOptions(otelpsrpc.Config{}),
},
})
}
func getSIPStore(s ObjectStore) SIPStore {
switch store := s.(type) {
case *RedisStore:
@@ -235,10 +259,6 @@ func getSIPConfig(conf *config.Config) *config.SIPConfig {
return &conf.SIP
}
func createClientConfiguration() clientconfiguration.ClientConfigurationManager {
return clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations)
}
func getLimitConf(config *config.Config) config.LimitConfig {
return config.Limit
}
@@ -256,7 +276,9 @@ func getPSRPCConfig(config *config.Config) rpc.PSRPCConfig {
}
func getPSRPCClientParams(config rpc.PSRPCConfig, bus psrpc.MessageBus) rpc.ClientParams {
return rpc.NewClientParams(config, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{})
return rpc.NewClientParams(config, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{},
otelpsrpc.ClientOptions(otelpsrpc.Config{}),
)
}
func createForwardStats(conf *config.Config) *sfu.ForwardStats {
@@ -269,3 +291,15 @@ func createForwardStats(conf *config.Config) *sfu.ForwardStats {
func newInProcessTurnServer(conf *config.Config, authHandler turn.AuthHandler) (*turn.Server, error) {
return NewTurnServer(conf, authHandler, false)
}
func getNodeStatsConfig(config *config.Config) config.NodeStatsConfig {
return config.NodeStats
}
func getAgentConfig(config *config.Config) agent.Config {
return config.Agents
}
func getAPIConf(config *config.Config) config.APIConfig {
return config.API
}
+56 -24
View File
@@ -9,7 +9,6 @@ package service
import (
"fmt"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/clientconfiguration"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/sfu"
@@ -22,7 +21,8 @@ import (
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/webhook"
"github.com/livekit/psrpc"
"github.com/pion/turn/v4"
"github.com/livekit/psrpc/pkg/middleware/otelpsrpc"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"gopkg.in/yaml.v3"
@@ -37,7 +37,7 @@ import (
func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*LivekitServer, error) {
limitConfig := getLimitConf(conf)
apiConfig := config.DefaultAPIConfig()
apiConfig := getAPIConf(conf)
universalClient, err := createRedisClient(conf)
if err != nil {
return nil, err
@@ -60,7 +60,8 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
if err != nil {
return nil, err
}
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub)
nodeStatsConfig := getNodeStatsConfig(conf)
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub, nodeStatsConfig)
objectStore := createStore(universalClient)
roomAllocator, err := NewRoomAllocator(conf, router, objectStore)
if err != nil {
@@ -82,12 +83,12 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
return nil, err
}
analyticsService := telemetry.NewAnalyticsService(conf, currentNode)
telemetryService := telemetry.NewTelemetryService(queuedNotifier, analyticsService)
telemetryService := createTelemetryService(queuedNotifier, analyticsService)
ioInfoService, err := NewIOInfoService(messageBus, egressStore, ingressStore, sipStore, telemetryService)
if err != nil {
return nil, err
}
rtcEgressLauncher := NewEgressLauncher(egressClient, ioInfoService)
rtcEgressLauncher := NewEgressLauncher(egressClient, ioInfoService, objectStore)
topicFormatter := rpc.NewTopicFormatter()
roomClient, err := rpc.NewTypedRoomClient(clientParams)
if err != nil {
@@ -106,7 +107,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
return nil, err
}
agentDispatchService := NewAgentDispatchService(agentDispatchInternalClient, topicFormatter, roomAllocator, router)
egressService := NewEgressService(egressClient, rtcEgressLauncher, objectStore, ioInfoService, roomService)
egressService := NewEgressService(egressClient, rtcEgressLauncher, ioInfoService, roomService)
ingressConfig := getIngressConfig(conf)
ingressClient, err := rpc.NewIngressClient(clientParams)
if err != nil {
@@ -114,18 +115,26 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
}
ingressService := NewIngressService(ingressConfig, nodeID, messageBus, ingressClient, ingressStore, ioInfoService, telemetryService)
sipConfig := getSIPConfig(conf)
sipClient, err := rpc.NewSIPClient(messageBus)
sipClient, err := newSIPClient(clientParams)
if err != nil {
return nil, err
}
sipService := NewSIPService(sipConfig, nodeID, messageBus, sipClient, sipStore, roomService, telemetryService)
rtcService := NewRTCService(conf, roomAllocator, objectStore, router, currentNode, telemetryService)
rtcService := NewRTCService(conf, roomAllocator, router, telemetryService)
whipParticipantClient, err := rpc.NewTypedWHIPParticipantClient(clientParams)
if err != nil {
return nil, err
}
serviceWHIPService, err := NewWHIPService(conf, router, roomAllocator, clientParams, topicFormatter, whipParticipantClient)
if err != nil {
return nil, err
}
agentService, err := NewAgentService(conf, currentNode, messageBus, keyProvider)
if err != nil {
return nil, err
}
clientConfigurationManager := createClientConfiguration()
client, err := agent.NewAgentClient(messageBus)
agentConfig := getAgentConfig(conf)
client, err := agent.NewAgentClient(messageBus, agentConfig)
if err != nil {
return nil, err
}
@@ -133,7 +142,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
timedVersionGenerator := utils.NewDefaultTimedVersionGenerator()
turnAuthHandler := NewTURNAuthHandler(keyProvider)
forwardStats := createForwardStats(conf)
roomManager, err := NewLocalRoomManager(conf, objectStore, currentNode, router, roomAllocator, telemetryService, clientConfigurationManager, client, agentStore, rtcEgressLauncher, timedVersionGenerator, turnAuthHandler, messageBus, forwardStats)
roomManager, err := NewLocalRoomManager(conf, objectStore, currentNode, router, roomAllocator, telemetryService, client, agentStore, rtcEgressLauncher, timedVersionGenerator, turnAuthHandler, messageBus, forwardStats)
if err != nil {
return nil, err
}
@@ -146,7 +155,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
if err != nil {
return nil, err
}
livekitServer, err := NewLivekitServer(conf, roomService, agentDispatchService, egressService, ingressService, sipService, ioInfoService, rtcService, agentService, keyProvider, router, roomManager, signalServer, server, currentNode)
livekitServer, err := NewLivekitServer(conf, roomService, agentDispatchService, egressService, ingressService, sipService, ioInfoService, rtcService, serviceWHIPService, agentService, keyProvider, router, roomManager, signalServer, server, currentNode)
if err != nil {
return nil, err
}
@@ -176,7 +185,8 @@ func InitializeRouter(conf *config.Config, currentNode routing.LocalNode) (routi
if err != nil {
return nil, err
}
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub)
nodeStatsConfig := getNodeStatsConfig(conf)
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub, nodeStatsConfig)
return router, nil
}
@@ -217,15 +227,21 @@ func createKeyProvider(conf *config.Config) (auth.KeyProvider, error) {
func createWebhookNotifier(conf *config.Config, provider auth.KeyProvider) (webhook.QueuedNotifier, error) {
wc := conf.WebHook
if len(wc.URLs) == 0 {
return nil, nil
}
secret := provider.GetSecret(wc.APIKey)
if secret == "" {
if secret == "" && len(wc.URLs) > 0 {
return nil, ErrWebHookMissingAPIKey
}
return webhook.NewDefaultNotifier(wc.APIKey, secret, wc.URLs), nil
return webhook.NewDefaultNotifier(wc, provider)
}
func createTelemetryService(notifier webhook.QueuedNotifier, analytics telemetry.AnalyticsService) telemetry.TelemetryService {
svc := telemetry.NewTelemetryService(notifier, analytics)
if notifier != nil {
notifier.RegisterProcessedHook(svc.Webhook)
}
return svc
}
func createRedisClient(conf *config.Config) (redis.UniversalClient, error) {
@@ -282,6 +298,14 @@ func getIngressConfig(conf *config.Config) *config.IngressConfig {
return &conf.Ingress
}
func newSIPClient(p rpc.ClientParams) (rpc.SIPClient, error) {
return rpc.NewSIPClientWithParams(rpc.ClientParams{
Bus: p.Bus,
ClientOptions: []psrpc.ClientOption{rpc.WithClientLogger(p.Logger), otelpsrpc.ClientOptions(otelpsrpc.Config{})},
})
}
func getSIPStore(s ObjectStore) SIPStore {
switch store := s.(type) {
case *RedisStore:
@@ -295,10 +319,6 @@ func getSIPConfig(conf *config.Config) *config.SIPConfig {
return &conf.SIP
}
func createClientConfiguration() clientconfiguration.ClientConfigurationManager {
return clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations)
}
func getLimitConf(config2 *config.Config) config.LimitConfig {
return config2.Limit
}
@@ -316,7 +336,7 @@ func getPSRPCConfig(config2 *config.Config) rpc.PSRPCConfig {
}
func getPSRPCClientParams(config2 rpc.PSRPCConfig, bus psrpc.MessageBus) rpc.ClientParams {
return rpc.NewClientParams(config2, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{})
return rpc.NewClientParams(config2, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{}, otelpsrpc.ClientOptions(otelpsrpc.Config{}))
}
func createForwardStats(conf *config.Config) *sfu.ForwardStats {
@@ -329,3 +349,15 @@ func createForwardStats(conf *config.Config) *sfu.ForwardStats {
func newInProcessTurnServer(conf *config.Config, authHandler turn.AuthHandler) (*turn.Server, error) {
return NewTurnServer(conf, authHandler, false)
}
func getNodeStatsConfig(config2 *config.Config) config.NodeStatsConfig {
return config2.NodeStats
}
func getAgentConfig(config2 *config.Config) agent.Config {
return config2.Agents
}
func getAPIConf(config2 *config.Config) config.APIConfig {
return config2.API
}
+58 -55
View File
@@ -22,18 +22,19 @@ import (
"time"
"github.com/gorilla/websocket"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/protojson"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
const (
pingFrequency = 10 * time.Second
pingTimeout = 2 * time.Second
pingFrequency = 10 * time.Second
pingTimeout = 2 * time.Second
closeWriteTimeout = 5 * time.Second
)
type WSSignalConnection struct {
@@ -56,75 +57,77 @@ func (c *WSSignalConnection) Close() error {
return c.conn.Close()
}
func (c *WSSignalConnection) CloseWithReason(reason string) error {
msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, reason)
_ = c.conn.WriteControl(websocket.CloseMessage, msg, time.Now().Add(closeWriteTimeout))
return c.conn.Close()
}
func (c *WSSignalConnection) SetReadDeadline(deadline time.Time) error {
return c.conn.SetReadDeadline(deadline)
}
func (c *WSSignalConnection) ReadRequest() (*livekit.SignalRequest, int, error) {
for {
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
msg := &livekit.SignalRequest{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
msg := &livekit.SignalRequest{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
}
func (c *WSSignalConnection) ReadWorkerMessage() (*livekit.WorkerMessage, int, error) {
for {
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
msg := &livekit.WorkerMessage{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
msg := &livekit.WorkerMessage{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
}