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:
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user