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:
+44
-7
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/agent"
|
||||
"github.com/livekit/livekit-server/pkg/agent/testutils"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func TestAgent(t *testing.T) {
|
||||
testAgentName := "test_agent"
|
||||
t.Run("dispatched jobs are assigned to a worker", func(t *testing.T) {
|
||||
bus := psrpc.NewLocalMessageBus()
|
||||
|
||||
@@ -28,7 +30,7 @@ func TestAgent(t *testing.T) {
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
worker := server.SimulateAgentWorker()
|
||||
worker.Register("test", livekit.JobType_JT_ROOM)
|
||||
worker.Register(testAgentName, livekit.JobType_JT_ROOM)
|
||||
jobAssignments := worker.JobAssignments.Observe()
|
||||
|
||||
job := &livekit.Job{
|
||||
@@ -36,14 +38,19 @@ func TestAgent(t *testing.T) {
|
||||
DispatchId: guid.New(guid.AgentDispatchPrefix),
|
||||
Type: livekit.JobType_JT_ROOM,
|
||||
Room: &livekit.Room{},
|
||||
AgentName: "test",
|
||||
AgentName: testAgentName,
|
||||
}
|
||||
_, err := client.JobRequest(context.Background(), "test", agent.RoomAgentTopic, job)
|
||||
_, err := client.JobRequest(context.Background(), testAgentName, agent.RoomAgentTopic, job)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case a := <-jobAssignments.Events():
|
||||
require.EqualValues(t, job.Id, a.Job.Id)
|
||||
v, err := auth.ParseAPIToken(a.Token)
|
||||
require.NoError(t, err)
|
||||
_, claims, err := v.Verify(server.TestAPISecret)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testAgentName, claims.Attributes[agent.AgentNameAttributeKey])
|
||||
case <-time.After(time.Second):
|
||||
require.Fail(t, "job assignment timeout")
|
||||
}
|
||||
@@ -108,7 +115,7 @@ func TestAgentLoadBalancing(t *testing.T) {
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
agents := make([]*testutils.AgentWorker, totalWorkers)
|
||||
for i := 0; i < totalWorkers; i++ {
|
||||
for i := range totalWorkers {
|
||||
agents[i] = server.SimulateAgentWorker(
|
||||
testutils.WithLabel(fmt.Sprintf("agent-%d", i)),
|
||||
testutils.WithJobLoad(testutils.NewStableJobLoad(0.01)),
|
||||
@@ -128,7 +135,7 @@ func TestAgentLoadBalancing(t *testing.T) {
|
||||
}
|
||||
|
||||
// check that jobs are distributed normally
|
||||
for i := 0; i < totalWorkers; i++ {
|
||||
for i := range totalWorkers {
|
||||
label := fmt.Sprintf("agent-%d", i)
|
||||
require.GreaterOrEqual(t, jobCount[label], 0)
|
||||
require.Less(t, jobCount[label], 35) // three std deviations from the mean is 32
|
||||
@@ -147,7 +154,7 @@ func TestAgentLoadBalancing(t *testing.T) {
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
agents := make([]*testutils.AgentWorker, totalWorkers)
|
||||
for i := 0; i < totalWorkers; i++ {
|
||||
for i := range totalWorkers {
|
||||
label := fmt.Sprintf("agent-%d", i)
|
||||
if i%2 == 0 {
|
||||
// make sure we have some workers that can accept jobs
|
||||
@@ -169,7 +176,7 @@ func TestAgentLoadBalancing(t *testing.T) {
|
||||
jobCount[w.Label] = len(w.Jobs())
|
||||
}
|
||||
|
||||
for i := 0; i < totalWorkers; i++ {
|
||||
for i := range totalWorkers {
|
||||
label := fmt.Sprintf("agent-%d", i)
|
||||
|
||||
if i%2 == 0 {
|
||||
@@ -181,3 +188,33 @@ func TestAgentLoadBalancing(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectionClosedOnDispatchError(t *testing.T) {
|
||||
t.Run("connection closed when unknown message type received", func(t *testing.T) {
|
||||
bus := psrpc.NewLocalMessageBus()
|
||||
server := testutils.NewTestServer(bus)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
// register agent
|
||||
worker := server.SimulateAgentWorker()
|
||||
worker.Register("test_agent", livekit.JobType_JT_ROOM)
|
||||
responses := worker.RegisterWorkerResponses.Observe()
|
||||
select {
|
||||
case <-responses.Events():
|
||||
// registered
|
||||
case <-time.After(time.Second):
|
||||
require.Fail(t, "registration timeout")
|
||||
}
|
||||
responses.Stop()
|
||||
|
||||
// send invalid message (nil Message field triggers ErrUnknownWorkerSignal)
|
||||
worker.SendMessage(&livekit.WorkerMessage{Message: nil})
|
||||
|
||||
select {
|
||||
case <-worker.Closed():
|
||||
// connection closed
|
||||
case <-time.After(time.Second):
|
||||
require.Fail(t, "connection should have been closed after dispatch error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+25
-15
@@ -62,11 +62,13 @@ type JobRequest struct {
|
||||
// only set for participant jobs
|
||||
Participant *livekit.ParticipantInfo
|
||||
Metadata string
|
||||
AgentName string
|
||||
AgentName string
|
||||
Deployment string
|
||||
}
|
||||
|
||||
type agentClient struct {
|
||||
client rpc.AgentInternalClient
|
||||
config Config
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
@@ -87,7 +89,7 @@ type agentClient struct {
|
||||
subDone chan struct{}
|
||||
}
|
||||
|
||||
func NewAgentClient(bus psrpc.MessageBus) (Client, error) {
|
||||
func NewAgentClient(bus psrpc.MessageBus, config Config) (Client, error) {
|
||||
client, err := rpc.NewAgentInternalClient(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -95,6 +97,7 @@ func NewAgentClient(bus psrpc.MessageBus) (Client, error) {
|
||||
|
||||
c := &agentClient{
|
||||
client: client,
|
||||
config: config,
|
||||
workers: workerpool.New(50),
|
||||
subDone: make(chan struct{}),
|
||||
}
|
||||
@@ -152,21 +155,23 @@ func (c *agentClient) LaunchJob(ctx context.Context, desc *JobRequest) *serverut
|
||||
}
|
||||
|
||||
dispatcher.ForEach(func(curNs string) {
|
||||
topic := GetAgentTopic(desc.AgentName, curNs)
|
||||
topic := GetAgentTopic(desc.AgentName, curNs, desc.Deployment)
|
||||
|
||||
wg.Add(1)
|
||||
c.workers.Submit(func() {
|
||||
defer wg.Done()
|
||||
// The cached agent parameters do not provide the exact combination of available job type/agent name/namespace, so some of the JobRequest RPC may not trigger any worker
|
||||
job := &livekit.Job{
|
||||
Id: utils.NewGuid(utils.AgentJobPrefix),
|
||||
DispatchId: desc.DispatchId,
|
||||
Type: desc.JobType,
|
||||
Room: desc.Room,
|
||||
Participant: desc.Participant,
|
||||
Namespace: curNs,
|
||||
AgentName: desc.AgentName,
|
||||
Metadata: desc.Metadata,
|
||||
Id: utils.NewGuid(utils.AgentJobPrefix),
|
||||
DispatchId: desc.DispatchId,
|
||||
Type: desc.JobType,
|
||||
Room: desc.Room,
|
||||
Participant: desc.Participant,
|
||||
Namespace: curNs,
|
||||
AgentName: desc.AgentName,
|
||||
Metadata: desc.Metadata,
|
||||
EnableRecording: c.config.EnableUserDataRecording,
|
||||
Deployment: desc.Deployment,
|
||||
}
|
||||
resp, err := c.client.JobRequest(context.Background(), topic, jobTypeTopic, job)
|
||||
if err != nil {
|
||||
@@ -323,14 +328,19 @@ func (c *agentClient) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAgentTopic(agentName, namespace string) string {
|
||||
func GetAgentTopic(agentName, namespace, deployment string) string {
|
||||
var topic string
|
||||
if agentName == "" {
|
||||
// Backward compatibility
|
||||
return namespace
|
||||
topic = namespace
|
||||
} else if namespace == "" {
|
||||
// Forward compatibility once the namespace field is removed from the worker SDK
|
||||
return agentName
|
||||
topic = agentName
|
||||
} else {
|
||||
return fmt.Sprintf("%s_%s", agentName, namespace)
|
||||
topic = fmt.Sprintf("%s_%s", agentName, namespace)
|
||||
}
|
||||
if deployment != "" {
|
||||
topic += "_" + deployment
|
||||
}
|
||||
return topic
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package agent
|
||||
|
||||
const DefaultTargetLoad = 0.7
|
||||
|
||||
type Config struct {
|
||||
EnableUserDataRecording bool `yaml:"enable_user_data_recording"`
|
||||
TargetLoad float32 `yaml:"target_load,omitempty"`
|
||||
}
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"maps"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/frostbyte73/core"
|
||||
"github.com/gammazero/deque"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/agent"
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
@@ -27,18 +28,25 @@ import (
|
||||
)
|
||||
|
||||
type AgentService interface {
|
||||
HandleConnection(context.Context, agent.SignalConn, agent.WorkerProtocolVersion)
|
||||
HandleConnection(context.Context, agent.SignalConn, agent.WorkerRegistration)
|
||||
DrainConnections(time.Duration)
|
||||
}
|
||||
|
||||
type TestServer struct {
|
||||
AgentService
|
||||
TestAPIKey string
|
||||
TestAPISecret string
|
||||
}
|
||||
|
||||
func NewTestServer(bus psrpc.MessageBus) *TestServer {
|
||||
localNode, _ := routing.NewLocalNode(nil)
|
||||
return NewTestServerWithService(must.Get(service.NewAgentService(
|
||||
&config.Config{Region: "test"},
|
||||
&config.Config{
|
||||
Region: "test",
|
||||
Agents: agent.Config{
|
||||
TargetLoad: agent.DefaultTargetLoad,
|
||||
},
|
||||
},
|
||||
localNode,
|
||||
bus,
|
||||
auth.NewSimpleKeyProvider("test", "verysecretsecret"),
|
||||
@@ -46,7 +54,7 @@ func NewTestServer(bus psrpc.MessageBus) *TestServer {
|
||||
}
|
||||
|
||||
func NewTestServerWithService(s AgentService) *TestServer {
|
||||
return &TestServer{s}
|
||||
return &TestServer{s, "test", "verysecretsecret"}
|
||||
}
|
||||
|
||||
type SimulatedWorkerOptions struct {
|
||||
@@ -126,7 +134,7 @@ func (h *TestServer) SimulateAgentWorker(opts ...SimulatedWorkerOption) *AgentWo
|
||||
}
|
||||
|
||||
ctx := service.WithAPIKey(o.Context, &auth.ClaimGrants{}, "test")
|
||||
go h.HandleConnection(ctx, w, agent.CurrentProtocol)
|
||||
go h.HandleConnection(ctx, w, agent.MakeWorkerRegistration())
|
||||
|
||||
return w
|
||||
}
|
||||
@@ -204,6 +212,15 @@ func (w *AgentWorker) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *AgentWorker) CloseWithReason(_ string) error {
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
// Closed returns a channel that is closed when the connection is closed by the server
|
||||
func (w *AgentWorker) Closed() <-chan struct{} {
|
||||
return w.fuse.Watch()
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SetReadDeadline(t time.Time) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
@@ -315,7 +332,7 @@ func (w *AgentWorker) handlePong(m *livekit.WorkerPong) {
|
||||
w.WorkerPongs.Emit(m)
|
||||
}
|
||||
|
||||
func (w *AgentWorker) sendMessage(m *livekit.WorkerMessage) {
|
||||
func (w *AgentWorker) SendMessage(m *livekit.WorkerMessage) {
|
||||
select {
|
||||
case <-w.fuse.Watch():
|
||||
case w.workerMessages <- m:
|
||||
@@ -323,43 +340,43 @@ func (w *AgentWorker) sendMessage(m *livekit.WorkerMessage) {
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SendRegister(m *livekit.RegisterWorkerRequest) {
|
||||
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Register{
|
||||
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Register{
|
||||
Register: m,
|
||||
}})
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SendAvailability(m *livekit.AvailabilityResponse) {
|
||||
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Availability{
|
||||
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Availability{
|
||||
Availability: m,
|
||||
}})
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SendUpdateWorker(m *livekit.UpdateWorkerStatus) {
|
||||
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateWorker{
|
||||
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateWorker{
|
||||
UpdateWorker: m,
|
||||
}})
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SendUpdateJob(m *livekit.UpdateJobStatus) {
|
||||
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateJob{
|
||||
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateJob{
|
||||
UpdateJob: m,
|
||||
}})
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SendPing(m *livekit.WorkerPing) {
|
||||
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Ping{
|
||||
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Ping{
|
||||
Ping: m,
|
||||
}})
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SendSimulateJob(m *livekit.SimulateJobRequest) {
|
||||
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_SimulateJob{
|
||||
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_SimulateJob{
|
||||
SimulateJob: m,
|
||||
}})
|
||||
}
|
||||
|
||||
func (w *AgentWorker) SendMigrateJob(m *livekit.MigrateJobRequest) {
|
||||
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_MigrateJob{
|
||||
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_MigrateJob{
|
||||
MigrateJob: m,
|
||||
}})
|
||||
}
|
||||
@@ -411,7 +428,7 @@ func (w *AgentWorker) SimulateRoomJob(roomName string) {
|
||||
func (w *AgentWorker) Jobs() []*AgentJob {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return maps.Values(w.jobs)
|
||||
return slices.Collect(maps.Values(w.jobs))
|
||||
}
|
||||
|
||||
type stableJobLoad struct {
|
||||
|
||||
+72
-34
@@ -18,12 +18,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
pagent "github.com/livekit/protocol/agent"
|
||||
protoagent "github.com/livekit/protocol/agent"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
@@ -43,6 +45,8 @@ var (
|
||||
ErrDuplicateJobAssignment = errors.New("duplicate job assignment")
|
||||
)
|
||||
|
||||
const AgentNameAttributeKey = "lk.agent_name"
|
||||
|
||||
type WorkerProtocolVersion int
|
||||
|
||||
const CurrentProtocol = 1
|
||||
@@ -57,12 +61,15 @@ type SignalConn interface {
|
||||
ReadWorkerMessage() (*livekit.WorkerMessage, int, error)
|
||||
SetReadDeadline(time.Time) error
|
||||
Close() error
|
||||
CloseWithReason(reason string) error
|
||||
}
|
||||
|
||||
func JobStatusIsEnded(s livekit.JobStatus) bool {
|
||||
return s == livekit.JobStatus_JS_SUCCESS || s == livekit.JobStatus_JS_FAILED
|
||||
}
|
||||
|
||||
type AssignmentHook func(next func(*livekit.JobAssignment) error) func(*livekit.JobAssignment) error
|
||||
|
||||
type WorkerSignalHandler interface {
|
||||
HandleRegister(*livekit.RegisterWorkerRequest) error
|
||||
HandleAvailability(*livekit.AvailabilityResponse) error
|
||||
@@ -141,10 +148,20 @@ type WorkerRegistration struct {
|
||||
Protocol WorkerProtocolVersion
|
||||
ID string
|
||||
Version string
|
||||
AgentID string
|
||||
AgentName string
|
||||
Namespace string
|
||||
JobType livekit.JobType
|
||||
Permissions *livekit.ParticipantPermission
|
||||
ClientIP string
|
||||
Deployment string
|
||||
}
|
||||
|
||||
func MakeWorkerRegistration() WorkerRegistration {
|
||||
return WorkerRegistration{
|
||||
ID: guid.New(guid.AgentWorkerPrefix),
|
||||
Protocol: CurrentProtocol,
|
||||
}
|
||||
}
|
||||
|
||||
var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
|
||||
@@ -152,18 +169,17 @@ var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
|
||||
type WorkerRegisterer struct {
|
||||
WorkerPingHandler
|
||||
serverInfo *livekit.ServerInfo
|
||||
protocol WorkerProtocolVersion
|
||||
deadline time.Time
|
||||
|
||||
registration WorkerRegistration
|
||||
registered bool
|
||||
}
|
||||
|
||||
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, protocol WorkerProtocolVersion) *WorkerRegisterer {
|
||||
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, base WorkerRegistration) *WorkerRegisterer {
|
||||
return &WorkerRegisterer{
|
||||
WorkerPingHandler: WorkerPingHandler{conn: conn},
|
||||
serverInfo: serverInfo,
|
||||
protocol: protocol,
|
||||
registration: base,
|
||||
deadline: time.Now().Add(RegisterTimeout),
|
||||
}
|
||||
}
|
||||
@@ -185,6 +201,10 @@ func (h *WorkerRegisterer) HandleRegister(req *livekit.RegisterWorkerRequest) er
|
||||
return ErrUnknownJobType
|
||||
}
|
||||
|
||||
if err := protoagent.ValidateDeployment(req.GetDeployment()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
permissions := req.AllowedPermissions
|
||||
if permissions == nil {
|
||||
permissions = &livekit.ParticipantPermission{
|
||||
@@ -195,15 +215,12 @@ func (h *WorkerRegisterer) HandleRegister(req *livekit.RegisterWorkerRequest) er
|
||||
}
|
||||
}
|
||||
|
||||
h.registration = WorkerRegistration{
|
||||
Protocol: h.protocol,
|
||||
ID: guid.New(guid.AgentWorkerPrefix),
|
||||
Version: req.Version,
|
||||
AgentName: req.AgentName,
|
||||
Namespace: req.GetNamespace(),
|
||||
JobType: req.GetType(),
|
||||
Permissions: permissions,
|
||||
}
|
||||
h.registration.Version = req.Version
|
||||
h.registration.AgentName = req.AgentName
|
||||
h.registration.Namespace = req.GetNamespace()
|
||||
h.registration.JobType = req.GetType()
|
||||
h.registration.Permissions = permissions
|
||||
h.registration.Deployment = req.GetDeployment()
|
||||
h.registered = true
|
||||
|
||||
_, err := h.conn.WriteServerMessage(&livekit.ServerMessage{
|
||||
@@ -256,6 +273,9 @@ func NewWorker(
|
||||
logger: logger.WithValues(
|
||||
"workerID", registration.ID,
|
||||
"agentName", registration.AgentName,
|
||||
"deployment", registration.Deployment,
|
||||
"agentID", registration.AgentID,
|
||||
"version", registration.Version,
|
||||
"jobType", registration.JobType.String(),
|
||||
),
|
||||
|
||||
@@ -268,10 +288,8 @@ func NewWorker(
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) sendRequest(req *livekit.ServerMessage) {
|
||||
if _, err := w.conn.WriteServerMessage(req); err != nil {
|
||||
w.logger.Warnw("error writing to websocket", err)
|
||||
}
|
||||
func (w *Worker) APIKey() string {
|
||||
return w.apiKey
|
||||
}
|
||||
|
||||
func (w *Worker) Status() livekit.WorkerStatus {
|
||||
@@ -293,11 +311,7 @@ func (w *Worker) Logger() logger.Logger {
|
||||
func (w *Worker) RunningJobs() map[livekit.JobID]*livekit.Job {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
jobs := make(map[livekit.JobID]*livekit.Job, len(w.runningJobs))
|
||||
for k, v := range w.runningJobs {
|
||||
jobs[k] = v
|
||||
}
|
||||
return jobs
|
||||
return maps.Clone(w.runningJobs)
|
||||
}
|
||||
|
||||
func (w *Worker) RunningJobCount() int {
|
||||
@@ -316,7 +330,7 @@ func (w *Worker) GetJobState(jobID livekit.JobID) (*livekit.JobState, error) {
|
||||
return utils.CloneProto(j.State), nil
|
||||
}
|
||||
|
||||
func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobState, error) {
|
||||
func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job, hook AssignmentHook) (*livekit.JobState, error) {
|
||||
availCh := make(chan *livekit.AvailabilityResponse, 1)
|
||||
job = utils.CloneProto(job)
|
||||
jobID := livekit.JobID(job.Id)
|
||||
@@ -340,13 +354,17 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
|
||||
job.State = &livekit.JobState{}
|
||||
}
|
||||
now := time.Now()
|
||||
job.State.WorkerId = w.ID
|
||||
job.State.AgentId = w.AgentID
|
||||
job.State.UpdatedAt = now.UnixNano()
|
||||
job.State.StartedAt = now.UnixNano()
|
||||
job.State.Status = livekit.JobStatus_JS_RUNNING
|
||||
|
||||
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Availability{
|
||||
if _, err := w.conn.WriteServerMessage(&livekit.ServerMessage{Message: &livekit.ServerMessage_Availability{
|
||||
Availability: &livekit.AvailabilityRequest{Job: job},
|
||||
}})
|
||||
}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := time.NewTimer(AssignJobTimeout)
|
||||
defer timeout.Stop()
|
||||
@@ -354,20 +372,31 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
|
||||
// See handleAvailability for the response
|
||||
select {
|
||||
case res := <-availCh:
|
||||
if res.Terminate {
|
||||
job.State.EndedAt = now.UnixNano()
|
||||
job.State.Status = livekit.JobStatus_JS_SUCCESS
|
||||
return job.State, nil
|
||||
}
|
||||
|
||||
if !res.Available {
|
||||
return nil, ErrWorkerNotAvailable
|
||||
}
|
||||
|
||||
job.State.ParticipantIdentity = res.ParticipantIdentity
|
||||
attributes := res.ParticipantAttributes
|
||||
if attributes == nil {
|
||||
attributes = make(map[string]string)
|
||||
}
|
||||
attributes[AgentNameAttributeKey] = w.AgentName
|
||||
|
||||
token, err := pagent.BuildAgentToken(
|
||||
token, err := protoagent.BuildAgentToken(
|
||||
w.apiKey,
|
||||
w.apiSecret,
|
||||
job.Room.Name,
|
||||
res.ParticipantIdentity,
|
||||
res.ParticipantName,
|
||||
res.ParticipantMetadata,
|
||||
res.ParticipantAttributes,
|
||||
attributes,
|
||||
w.Permissions,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -375,10 +404,18 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// In OSS, Url is nil, and the used API Key is the same as the one used to connect the worker
|
||||
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Assignment{
|
||||
Assignment: &livekit.JobAssignment{Job: job, Url: nil, Token: token},
|
||||
}})
|
||||
send := func(a *livekit.JobAssignment) error {
|
||||
_, err := w.conn.WriteServerMessage(&livekit.ServerMessage{
|
||||
Message: &livekit.ServerMessage_Assignment{Assignment: a},
|
||||
})
|
||||
return err
|
||||
}
|
||||
if hook != nil {
|
||||
send = hook(send)
|
||||
}
|
||||
if err := send(&livekit.JobAssignment{Job: job, Token: token}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state := utils.CloneProto(job.State)
|
||||
|
||||
@@ -407,7 +444,7 @@ func (w *Worker) TerminateJob(jobID livekit.JobID, reason rpc.JobTerminateReason
|
||||
return nil, ErrJobNotFound
|
||||
}
|
||||
|
||||
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Termination{
|
||||
_, writeErr := w.conn.WriteServerMessage(&livekit.ServerMessage{Message: &livekit.ServerMessage_Termination{
|
||||
Termination: &livekit.JobTermination{
|
||||
JobId: string(jobID),
|
||||
},
|
||||
@@ -420,11 +457,12 @@ func (w *Worker) TerminateJob(jobID livekit.JobID, reason rpc.JobTerminateReason
|
||||
errorStr = "agent worker left the room"
|
||||
}
|
||||
|
||||
return w.UpdateJobStatus(&livekit.UpdateJobStatus{
|
||||
state, updateErr := w.UpdateJobStatus(&livekit.UpdateJobStatus{
|
||||
JobId: string(jobID),
|
||||
Status: status,
|
||||
Error: errorStr,
|
||||
})
|
||||
return state, multierr.Combine(writeErr, updateErr)
|
||||
}
|
||||
|
||||
func (w *Worker) UpdateMetadata(metadata string) {
|
||||
@@ -530,7 +568,7 @@ func (w *Worker) HandleSimulateJob(simulate *livekit.SimulateJobRequest) error {
|
||||
}
|
||||
|
||||
go func() {
|
||||
_, err := w.AssignJob(w.ctx, job)
|
||||
_, err := w.AssignJob(w.ctx, job, nil)
|
||||
if err != nil {
|
||||
w.logger.Errorw("unable to simulate job", err, "jobID", job.Id)
|
||||
}
|
||||
|
||||
@@ -15,30 +15,48 @@
|
||||
package clientconfiguration
|
||||
|
||||
import (
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils/must"
|
||||
)
|
||||
|
||||
// StaticConfigurations list specific device-side limitations that should be disabled at a global level
|
||||
var StaticConfigurations = []ConfigurationItem{
|
||||
// {
|
||||
// Match: &ScriptMatch{Expr: `c.protocol <= 5 || c.browser == "firefox"`},
|
||||
// Match: must.Get(NewScriptMatch(`c.protocol <= 5 || c.browser == "firefox"`)),
|
||||
// Configuration: &livekit.ClientConfiguration{ResumeConnection: livekit.ClientConfigSetting_DISABLED},
|
||||
// Merge: false,
|
||||
// },
|
||||
{
|
||||
Match: &ScriptMatch{Expr: `c.browser == "safari"`},
|
||||
Configuration: &livekit.ClientConfiguration{DisabledCodecs: &livekit.DisabledCodecs{Codecs: []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeAV1.String()},
|
||||
}}},
|
||||
Merge: false,
|
||||
},
|
||||
{
|
||||
Match: &ScriptMatch{Expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android") ||
|
||||
((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`},
|
||||
Match: must.Get(NewScriptMatch(`c.browser == "safari"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
DisabledCodecs: &livekit.DisabledCodecs{
|
||||
Publish: []*livekit.Codec{{Mime: mime.MimeTypeH264.String()}},
|
||||
Codecs: []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeAV1.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
Merge: true,
|
||||
},
|
||||
{
|
||||
Match: must.Get(NewScriptMatch(`c.browser == "safari" && c.browser_version > "18.3"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
DisabledCodecs: &livekit.DisabledCodecs{
|
||||
Publish: []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeVP9.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
Merge: true,
|
||||
},
|
||||
{
|
||||
Match: must.Get(NewScriptMatch(`(c.device_model == "xiaomi 2201117ti" && c.os == "android") ||
|
||||
((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
DisabledCodecs: &livekit.DisabledCodecs{
|
||||
Publish: []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeH264.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
Merge: false,
|
||||
|
||||
@@ -20,13 +20,14 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils/must"
|
||||
)
|
||||
|
||||
func TestScriptMatchConfiguration(t *testing.T) {
|
||||
t.Run("no merge", func(t *testing.T) {
|
||||
confs := []ConfigurationItem{
|
||||
{
|
||||
Match: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
|
||||
Match: must.Get(NewScriptMatch(`c.protocol > 5 && c.browser != "firefox"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
|
||||
},
|
||||
@@ -48,14 +49,14 @@ func TestScriptMatchConfiguration(t *testing.T) {
|
||||
t.Run("merge", func(t *testing.T) {
|
||||
confs := []ConfigurationItem{
|
||||
{
|
||||
Match: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
|
||||
Match: must.Get(NewScriptMatch(`c.protocol > 5 && c.browser != "firefox"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
|
||||
},
|
||||
Merge: true,
|
||||
},
|
||||
{
|
||||
Match: &ScriptMatch{Expr: `c.sdk == "android"`},
|
||||
Match: must.Get(NewScriptMatch(`c.sdk == "android"`)),
|
||||
Configuration: &livekit.ClientConfiguration{
|
||||
Video: &livekit.VideoConfiguration{
|
||||
HardwareEncoder: livekit.ClientConfigSetting_DISABLED,
|
||||
@@ -81,10 +82,12 @@ func TestScriptMatchConfiguration(t *testing.T) {
|
||||
|
||||
func TestScriptMatch(t *testing.T) {
|
||||
client := &livekit.ClientInfo{
|
||||
Protocol: 6,
|
||||
Browser: "chrome",
|
||||
Sdk: 3, // android
|
||||
DeviceModel: "12345",
|
||||
Protocol: 6,
|
||||
Browser: "chrome",
|
||||
Sdk: 3, // android
|
||||
DeviceModel: "12345",
|
||||
BrowserVersion: "13.2",
|
||||
Version: "2.17.1",
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
@@ -99,12 +102,26 @@ func TestScriptMatch(t *testing.T) {
|
||||
{name: "invalid expr", expr: `cc.protocol > 5`, err: true},
|
||||
{name: "unexist field", expr: `c.protocols > 5`, err: true},
|
||||
{name: "combined condition", expr: `c.protocol > 5 && (c.sdk=="android" || c.sdk=="ios")`, result: true},
|
||||
{name: "combined condition2", expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android) || ((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`, result: false},
|
||||
{name: "combined condition2", expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android") || ((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`, result: false},
|
||||
{name: "string lesser", expr: `c.browser_version < "11.3"`, result: false},
|
||||
{name: "string lesser eq", expr: `c.browser_version <= "13.2"`, result: true},
|
||||
{name: "string greater", expr: `c.browser_version > "11.3"`, result: true},
|
||||
{name: "string greater eq", expr: `c.browser_version >= "13.2"`, result: true},
|
||||
{name: "semantic lesser", expr: `c.version < "2.16.10"`, result: false},
|
||||
{name: "semantic lesser eq", expr: `c.version <= "2.17.1"`, result: true},
|
||||
{name: "semantic greater", expr: `c.version > "2.16.10"`, result: true},
|
||||
{name: "semantic greater eq", expr: `c.version >= "2.17.1"`, result: true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
match := &ScriptMatch{Expr: c.expr}
|
||||
match, err := NewScriptMatch(c.expr)
|
||||
if err != nil {
|
||||
if !c.err {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
m, err := match.Match(client)
|
||||
if c.err {
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
package clientconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/d5/tengo/v2"
|
||||
"github.com/d5/tengo/v2/token"
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
@@ -29,7 +31,19 @@ type Match interface {
|
||||
}
|
||||
|
||||
type ScriptMatch struct {
|
||||
Expr string
|
||||
compiled *tengo.Compiled
|
||||
}
|
||||
|
||||
func NewScriptMatch(expr string) (*ScriptMatch, error) {
|
||||
script := tengo.NewScript(fmt.Appendf(nil, "__res__ := (%s)", expr))
|
||||
if err := script.Add("c", &clientObject{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compiled, err := script.Compile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ScriptMatch{compiled}, nil
|
||||
}
|
||||
|
||||
// use result of eval script expression for match.
|
||||
@@ -38,17 +52,23 @@ type ScriptMatch struct {
|
||||
// browser if firefox: c.browser == "firefox"
|
||||
// combined rule : c.protocol > 5 && c.browser == "firefox"
|
||||
func (m *ScriptMatch) Match(clientInfo *livekit.ClientInfo) (bool, error) {
|
||||
res, err := tengo.Eval(context.TODO(), m.Expr, map[string]interface{}{"c": &clientObject{info: clientInfo}})
|
||||
if err != nil {
|
||||
clone := m.compiled.Clone()
|
||||
if err := clone.Set("c", &clientObject{info: clientInfo}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := clone.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res := clone.Get("__res__").Value()
|
||||
if val, ok := res.(bool); ok {
|
||||
return val, nil
|
||||
}
|
||||
return false, errors.New("invalid match expression result")
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type clientObject struct {
|
||||
tengo.ObjectImpl
|
||||
info *livekit.ClientInfo
|
||||
@@ -72,7 +92,7 @@ func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error
|
||||
case "sdk":
|
||||
return &tengo.String{Value: strings.ToLower(c.info.Sdk.String())}, nil
|
||||
case "version":
|
||||
return &tengo.String{Value: c.info.Version}, nil
|
||||
return &ruleSdkVersion{sdkVersion: c.info.Version}, nil
|
||||
case "protocol":
|
||||
return &tengo.Int{Value: int64(c.info.Protocol)}, nil
|
||||
case "os":
|
||||
@@ -84,9 +104,74 @@ func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error
|
||||
case "browser":
|
||||
return &tengo.String{Value: strings.ToLower(c.info.Browser)}, nil
|
||||
case "browser_version":
|
||||
return &tengo.String{Value: c.info.BrowserVersion}, nil
|
||||
return &ruleSdkVersion{sdkVersion: c.info.BrowserVersion}, nil
|
||||
case "address":
|
||||
return &tengo.String{Value: c.info.Address}, nil
|
||||
}
|
||||
return &tengo.Undefined{}, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
type ruleSdkVersion struct {
|
||||
tengo.ObjectImpl
|
||||
sdkVersion string
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) TypeName() string {
|
||||
return "sdkVersion"
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) String() string {
|
||||
return r.sdkVersion
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) BinaryOp(op token.Token, rhs tengo.Object) (tengo.Object, error) {
|
||||
if rhs, ok := rhs.(*tengo.String); ok {
|
||||
cmp := r.compare(rhs.Value)
|
||||
|
||||
isMatch := false
|
||||
switch op {
|
||||
case token.Greater:
|
||||
isMatch = cmp > 0
|
||||
case token.GreaterEq:
|
||||
isMatch = cmp >= 0
|
||||
case token.Less:
|
||||
isMatch = cmp < 0
|
||||
case token.LessEq:
|
||||
isMatch = cmp <= 0
|
||||
default:
|
||||
return nil, tengo.ErrInvalidOperator
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
return tengo.TrueValue, nil
|
||||
}
|
||||
return tengo.FalseValue, nil
|
||||
}
|
||||
|
||||
return nil, tengo.ErrInvalidOperator
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) Equals(rhs tengo.Object) bool {
|
||||
if rhs, ok := rhs.(*tengo.String); ok {
|
||||
return r.compare(rhs.Value) == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *ruleSdkVersion) compare(rhsSdkVersion string) int {
|
||||
if !semver.IsValid("v"+r.sdkVersion) || !semver.IsValid("v"+rhsSdkVersion) {
|
||||
// if not valid semver, do string compare
|
||||
switch {
|
||||
case r.sdkVersion < rhsSdkVersion:
|
||||
return -1
|
||||
case r.sdkVersion > rhsSdkVersion:
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
return semver.Compare("v"+r.sdkVersion, "v"+rhsSdkVersion)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
+242
-104
@@ -18,25 +18,30 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/metric"
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
|
||||
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
redisLiveKit "github.com/livekit/protocol/redis"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/protocol/webhook"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/agent"
|
||||
"github.com/livekit/livekit-server/pkg/metric"
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/pacer"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,8 +49,9 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0")
|
||||
ErrKeysNotSet = errors.New("one of key-file or keys must be provided")
|
||||
ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0")
|
||||
ErrTURNSecretFileIncorrectPermission = errors.New("turn secret file others permissions must be set to 0")
|
||||
ErrKeysNotSet = errors.New("one of key-file or keys must be provided")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -62,7 +68,7 @@ type Config struct {
|
||||
TURN TURNConfig `yaml:"turn,omitempty"`
|
||||
Ingress IngressConfig `yaml:"ingress,omitempty"`
|
||||
SIP SIPConfig `yaml:"sip,omitempty"`
|
||||
WebHook WebHookConfig `yaml:"webhook,omitempty"`
|
||||
WebHook webhook.WebHookConfig `yaml:"webhook,omitempty"`
|
||||
NodeSelector NodeSelectorConfig `yaml:"node_selector,omitempty"`
|
||||
KeyFile string `yaml:"key_file,omitempty"`
|
||||
Keys map[string]string `yaml:"keys,omitempty"`
|
||||
@@ -73,10 +79,18 @@ type Config struct {
|
||||
LogLevel string `yaml:"log_level,omitempty"`
|
||||
Logging LoggingConfig `yaml:"logging,omitempty"`
|
||||
Limit LimitConfig `yaml:"limit,omitempty"`
|
||||
Agents agent.Config `yaml:"agents,omitempty"`
|
||||
|
||||
Development bool `yaml:"development,omitempty"`
|
||||
|
||||
Metric metric.MetricConfig `yaml:"metric,omitempty"`
|
||||
Trace TracingConfig `yaml:"trace,omitempty"`
|
||||
|
||||
NodeStats NodeStatsConfig `yaml:"node_stats,omitempty"`
|
||||
|
||||
EnableDataTracks bool `yaml:"enable_data_tracks,omitempty"`
|
||||
|
||||
API APIConfig `yaml:"api,omitempty"`
|
||||
}
|
||||
|
||||
type RTCConfig struct {
|
||||
@@ -102,6 +116,18 @@ type RTCConfig struct {
|
||||
// allow TCP and TURN/TLS fallback
|
||||
AllowTCPFallback *bool `yaml:"allow_tcp_fallback,omitempty"`
|
||||
|
||||
// Signaling RTT threshold (in milliseconds) governing ICE/TCP fallback. On a UDP
|
||||
// failure, ICE/TCP is attempted only while the measured signaling RTT is below this
|
||||
// value; at or above it, supporting clients fall back directly to TURN/TLS. When 0
|
||||
// (the default), the RTT check is disabled and ICE/TCP is always attempted (for
|
||||
// clients that support it). A positive value also gates allow_udp_unstable_fallback.
|
||||
TCPFallbackRTTThreshold int `yaml:"tcp_fallback_rtt_threshold,omitempty"`
|
||||
|
||||
// When enabled, an established UDP connection reporting sustained high packet loss is
|
||||
// migrated to ICE/TCP or TURN/TLS. Requires tcp_fallback_rtt_threshold to be set
|
||||
// (> 0). Disabled by default.
|
||||
AllowUDPUnstableFallback bool `yaml:"allow_udp_unstable_fallback,omitempty"`
|
||||
|
||||
// force a reconnect on a publication error
|
||||
ReconnectOnPublicationError *bool `yaml:"reconnect_on_publication_error,omitempty"`
|
||||
|
||||
@@ -118,7 +144,13 @@ type RTCConfig struct {
|
||||
// be dropped for a slow data channel to avoid blocking the room.
|
||||
DatachannelSlowThreshold int `yaml:"datachannel_slow_threshold,omitempty"`
|
||||
|
||||
// Target latency for lossy data channels, used to drop packets to reduce latency.
|
||||
DatachannelLossyTargetLatency time.Duration `yaml:"datachannel_lossy_target_latency,omitempty"`
|
||||
|
||||
ForwardStats ForwardStatsConfig `yaml:"forward_stats,omitempty"`
|
||||
|
||||
// enable rtp stream restart detection for published tracks
|
||||
EnableRTPStreamRestartDetection bool `yaml:"enable_rtp_stream_restart_detection,omitempty"`
|
||||
}
|
||||
|
||||
type TURNServer struct {
|
||||
@@ -127,6 +159,14 @@ type TURNServer struct {
|
||||
Protocol string `yaml:"protocol,omitempty"`
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Credential string `yaml:"credential,omitempty"`
|
||||
// Secret is used for TURN static auth secrets mechanism. When provided,
|
||||
// dynamic credentials are generated using HMAC-SHA1 instead of static Username/Credential
|
||||
Secret string `yaml:"secret,omitempty"`
|
||||
// File containing the secret
|
||||
SecretFile string `yaml:"secret_file,omitempty"`
|
||||
// TTL is the time-to-live in seconds for generated credentials when using Secret.
|
||||
// Defaults to 14400 seconds (4 hours) if not specified
|
||||
TTL int `yaml:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
type CongestionControlConfig struct {
|
||||
@@ -139,8 +179,9 @@ type CongestionControlConfig struct {
|
||||
|
||||
UseSendSideBWEInterceptor bool `yaml:"use_send_side_bwe_interceptor,omitempty"`
|
||||
|
||||
UseSendSideBWE bool `yaml:"use_send_side_bwe,omitempty"`
|
||||
SendSideBWE sendsidebwe.SendSideBWEConfig `yaml:"send_side_bwe,omitempty"`
|
||||
UseSendSideBWE bool `yaml:"use_send_side_bwe,omitempty"`
|
||||
SendSideBWEPacer string `yaml:"send_side_bwe_pacer,omitempty"`
|
||||
SendSideBWE sendsidebwe.SendSideBWEConfig `yaml:"send_side_bwe,omitempty"`
|
||||
}
|
||||
|
||||
type PlayoutDelayConfig struct {
|
||||
@@ -152,6 +193,8 @@ type PlayoutDelayConfig struct {
|
||||
type VideoConfig struct {
|
||||
DynacastPauseDelay time.Duration `yaml:"dynacast_pause_delay,omitempty"`
|
||||
StreamTrackerManager sfu.StreamTrackerManagerConfig `yaml:"stream_tracker_manager,omitempty"`
|
||||
|
||||
CodecRegressionThreshold int `yaml:"codec_regression_threshold,omitempty"`
|
||||
}
|
||||
|
||||
type RoomConfig struct {
|
||||
@@ -167,6 +210,8 @@ type RoomConfig struct {
|
||||
CreateRoomEnabled bool `yaml:"create_room_enabled,omitempty"`
|
||||
CreateRoomTimeout time.Duration `yaml:"create_room_timeout,omitempty"`
|
||||
CreateRoomAttempts int `yaml:"create_room_attempts,omitempty"`
|
||||
// target room participant update batch chunk size in bytes
|
||||
UpdateBatchTargetSize int `yaml:"update_batch_target_size,omitempty"`
|
||||
// deprecated, moved to limits
|
||||
MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"`
|
||||
// deprecated, moved to limits
|
||||
@@ -187,26 +232,33 @@ type LoggingConfig struct {
|
||||
}
|
||||
|
||||
type TURNConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
Domain string `yaml:"domain,omitempty"`
|
||||
CertFile string `yaml:"cert_file,omitempty"`
|
||||
KeyFile string `yaml:"key_file,omitempty"`
|
||||
TLSPort int `yaml:"tls_port,omitempty"`
|
||||
UDPPort int `yaml:"udp_port,omitempty"`
|
||||
RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"`
|
||||
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
|
||||
ExternalTLS bool `yaml:"external_tls,omitempty"`
|
||||
}
|
||||
|
||||
type WebHookConfig struct {
|
||||
URLs []string `yaml:"urls,omitempty"`
|
||||
// key to use for webhook
|
||||
APIKey string `yaml:"api_key,omitempty"`
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
Domain string `yaml:"domain,omitempty"`
|
||||
CertFile string `yaml:"cert_file,omitempty"`
|
||||
KeyFile string `yaml:"key_file,omitempty"`
|
||||
TLSPort int `yaml:"tls_port,omitempty"`
|
||||
UDPPort int `yaml:"udp_port,omitempty"`
|
||||
RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"`
|
||||
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
|
||||
ExternalTLS bool `yaml:"external_tls,omitempty"`
|
||||
BindAddresses []string `yaml:"bind_addresses,omitempty"`
|
||||
// TTL of the TURN credentials in seconds - defaults to 300
|
||||
TTLSeconds int `yaml:"ttl_seconds,omitempty"`
|
||||
// list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to.
|
||||
// By default (i. e. empty list), all restricted peer CIDRs are denied access.
|
||||
// When not empty, only the specified CIDRs are allowed access.
|
||||
// Note that this check is applied to restricted peer CIDRs only.
|
||||
AllowRestrictedPeerCIDRs []string `yaml:"allow_restricted_peer_cidrs,omitempty"`
|
||||
// list of peer CIDRs to deny access to
|
||||
// This applies to all peer CIDRs, including restricted ones.
|
||||
// Deny list takes precedence over allow list.
|
||||
DenyPeerCIDRs []string `yaml:"deny_peer_cidrs,omitempty"`
|
||||
}
|
||||
|
||||
type NodeSelectorConfig struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
SortBy string `yaml:"sort_by,omitempty"`
|
||||
Algorithm string `yaml:"algorithm,omitempty"`
|
||||
CPULoadLimit float32 `yaml:"cpu_load_limit,omitempty"`
|
||||
SysloadLimit float32 `yaml:"sysload_limit,omitempty"`
|
||||
Regions []RegionConfig `yaml:"regions,omitempty"`
|
||||
@@ -245,6 +297,10 @@ func (l LimitConfig) CheckRoomNameLength(name string) bool {
|
||||
return l.MaxRoomNameLength == 0 || len(name) <= l.MaxRoomNameLength
|
||||
}
|
||||
|
||||
func (l LimitConfig) CheckParticipantIdentityLength(identity string) bool {
|
||||
return l.MaxParticipantIdentityLength == 0 || len(identity) <= l.MaxParticipantIdentityLength
|
||||
}
|
||||
|
||||
func (l LimitConfig) CheckParticipantNameLength(name string) bool {
|
||||
return l.MaxParticipantNameLength == 0 || len(name) <= l.MaxParticipantNameLength
|
||||
}
|
||||
@@ -281,6 +337,9 @@ type APIConfig struct {
|
||||
|
||||
// max amount of time to wait before checking for operation complete
|
||||
MaxCheckInterval time.Duration `yaml:"max_check_interval,omitempty"`
|
||||
|
||||
// Backwards compatibility for room service api calls, will enable by default and remove in a future release
|
||||
EnablePsrpcForGetListParticpants bool `yaml:"enable_psrpc_for_get_list_participants,omitempty"`
|
||||
}
|
||||
|
||||
type PrometheusConfig struct {
|
||||
@@ -295,6 +354,13 @@ type ForwardStatsConfig struct {
|
||||
ReportWindow time.Duration `yaml:"report_window,omitempty"`
|
||||
}
|
||||
|
||||
type TracingConfig struct {
|
||||
// JaegerURL configures Jaeger as a global tracer.
|
||||
//
|
||||
// The following formats are supported: <hostname>, <host>:<port>, http(s)://<host>/<path>
|
||||
JaegerURL string `yaml:"jaeger_url,omitempty"`
|
||||
}
|
||||
|
||||
func DefaultAPIConfig() APIConfig {
|
||||
return APIConfig{
|
||||
ExecutionTimeout: 2 * time.Second,
|
||||
@@ -303,6 +369,18 @@ func DefaultAPIConfig() APIConfig {
|
||||
}
|
||||
}
|
||||
|
||||
type NodeStatsConfig struct {
|
||||
StatsUpdateInterval time.Duration `yaml:"stats_update_interval,omitempty"`
|
||||
StatsRateMeasurementIntervals []time.Duration `yaml:"stats_rate_measurement_intervals,omitempty"`
|
||||
StatsMaxDelay time.Duration `yaml:"stats_max_delay,omitempty"`
|
||||
}
|
||||
|
||||
var DefaultNodeStatsConfig = NodeStatsConfig{
|
||||
StatsUpdateInterval: 2 * time.Second,
|
||||
StatsRateMeasurementIntervals: []time.Duration{10 * time.Second},
|
||||
StatsMaxDelay: 30 * time.Second,
|
||||
}
|
||||
|
||||
var DefaultConfig = Config{
|
||||
Port: 7880,
|
||||
RTC: RTCConfig{
|
||||
@@ -324,31 +402,37 @@ var DefaultConfig = Config{
|
||||
RemoteBWE: remotebwe.DefaultRemoteBWEConfig,
|
||||
UseSendSideBWEInterceptor: false,
|
||||
UseSendSideBWE: false,
|
||||
SendSideBWEPacer: string(pacer.PacerBehaviorNoQueue),
|
||||
SendSideBWE: sendsidebwe.DefaultSendSideBWEConfig,
|
||||
},
|
||||
},
|
||||
Audio: sfu.DefaultAudioConfig,
|
||||
Video: VideoConfig{
|
||||
DynacastPauseDelay: 5 * time.Second,
|
||||
StreamTrackerManager: sfu.DefaultStreamTrackerManagerConfig,
|
||||
DynacastPauseDelay: 5 * time.Second,
|
||||
StreamTrackerManager: sfu.DefaultStreamTrackerManagerConfig,
|
||||
CodecRegressionThreshold: 5,
|
||||
},
|
||||
Redis: redisLiveKit.RedisConfig{},
|
||||
Room: RoomConfig{
|
||||
AutoCreate: true,
|
||||
EnabledCodecs: []CodecSpec{
|
||||
{Mime: mime.MimeTypePCMU.String()},
|
||||
{Mime: mime.MimeTypePCMA.String()},
|
||||
{Mime: mime.MimeTypeOpus.String()},
|
||||
{Mime: mime.MimeTypeRED.String()},
|
||||
{Mime: mime.MimeTypeVP8.String()},
|
||||
{Mime: mime.MimeTypeH264.String()},
|
||||
{Mime: mime.MimeTypeVP9.String()},
|
||||
{Mime: mime.MimeTypeAV1.String()},
|
||||
{Mime: mime.MimeTypeH265.String()},
|
||||
{Mime: mime.MimeTypeRTX.String()},
|
||||
},
|
||||
EmptyTimeout: 5 * 60,
|
||||
DepartureTimeout: 20,
|
||||
CreateRoomEnabled: true,
|
||||
CreateRoomTimeout: 10 * time.Second,
|
||||
CreateRoomAttempts: 3,
|
||||
EmptyTimeout: 5 * 60,
|
||||
DepartureTimeout: 20,
|
||||
CreateRoomEnabled: true,
|
||||
CreateRoomTimeout: 10 * time.Second,
|
||||
CreateRoomAttempts: 3,
|
||||
UpdateBatchTargetSize: 128 * 1024,
|
||||
},
|
||||
Limit: LimitConfig{
|
||||
MaxMetadataSize: 64000,
|
||||
@@ -361,13 +445,16 @@ var DefaultConfig = Config{
|
||||
PionLevel: "error",
|
||||
},
|
||||
TURN: TURNConfig{
|
||||
Enabled: false,
|
||||
Enabled: false,
|
||||
BindAddresses: []string{"0.0.0.0"},
|
||||
TTLSeconds: 300,
|
||||
},
|
||||
NodeSelector: NodeSelectorConfig{
|
||||
Kind: "any",
|
||||
SortBy: "random",
|
||||
SysloadLimit: 0.9,
|
||||
CPULoadLimit: 0.9,
|
||||
Algorithm: "lowest",
|
||||
},
|
||||
SignalRelay: SignalRelayConfig{
|
||||
RetryTimeout: 7500 * time.Millisecond,
|
||||
@@ -376,12 +463,19 @@ var DefaultConfig = Config{
|
||||
StreamBufferSize: 1000,
|
||||
ConnectAttempts: 3,
|
||||
},
|
||||
PSRPC: rpc.DefaultPSRPCConfig,
|
||||
Keys: map[string]string{},
|
||||
Metric: metric.DefaultMetricConfig,
|
||||
Agents: agent.Config{
|
||||
TargetLoad: agent.DefaultTargetLoad,
|
||||
},
|
||||
PSRPC: rpc.DefaultPSRPCConfig,
|
||||
Keys: map[string]string{},
|
||||
Metric: metric.DefaultMetricConfig,
|
||||
WebHook: webhook.DefaultWebHookConfig,
|
||||
NodeStats: DefaultNodeStatsConfig,
|
||||
API: DefaultAPIConfig(),
|
||||
EnableDataTracks: true,
|
||||
}
|
||||
|
||||
func NewConfig(confString string, strictMode bool, c *cli.Context, baseFlags []cli.Flag) (*Config, error) {
|
||||
func NewConfig(confString string, strictMode bool, c *cli.Command, baseFlags []cli.Flag) (*Config, error) {
|
||||
// start with defaults
|
||||
marshalled, err := yaml.Marshal(&DefaultConfig)
|
||||
if err != nil {
|
||||
@@ -563,74 +657,110 @@ func (conf *Config) ValidateKeys() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conf *Config) LoadTURNSecrets() error {
|
||||
var otherFilter os.FileMode = 0o007
|
||||
for i, s := range conf.RTC.TURNServers {
|
||||
if s.SecretFile == "" {
|
||||
continue
|
||||
}
|
||||
if s.Secret != "" {
|
||||
logger.Warnw("both secret and secret_file are set for TURN server, the hardcoded secret will be used", nil,
|
||||
"host", s.Host, "port", s.Port)
|
||||
continue
|
||||
}
|
||||
st, err := os.Stat(s.SecretFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st.Mode().Perm()&otherFilter != 0o000 {
|
||||
return ErrTURNSecretFileIncorrectPermission
|
||||
}
|
||||
data, err := os.ReadFile(s.SecretFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading turn secret file %q: %w", s.SecretFile, err)
|
||||
}
|
||||
conf.RTC.TURNServers[i].Secret = strings.TrimSpace(string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error) {
|
||||
blankConfig := &Config{}
|
||||
defaultConfig := &DefaultConfig
|
||||
flags := make([]cli.Flag, 0)
|
||||
for name, value := range blankConfig.ToCLIFlagNames(existingFlags) {
|
||||
for name, value := range (defaultConfig).ToCLIFlagNames(existingFlags) {
|
||||
kind := value.Kind()
|
||||
if kind == reflect.Ptr {
|
||||
kind = value.Type().Elem().Kind()
|
||||
}
|
||||
|
||||
var flag cli.Flag
|
||||
envVar := fmt.Sprintf("LIVEKIT_%s", strings.ToUpper(strings.Replace(name, ".", "_", -1)))
|
||||
envVar := fmt.Sprintf("LIVEKIT_%s", strings.ToUpper(strings.ReplaceAll(name, ".", "_")))
|
||||
defaultText := cliDefaultText(value)
|
||||
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
flag = &cli.BoolFlag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.String:
|
||||
flag = &cli.StringFlag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Int, reflect.Int32:
|
||||
flag = &cli.IntFlag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Int64:
|
||||
flag = &cli.Int64Flag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
flag = &cli.UintFlag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Uint64:
|
||||
flag = &cli.Uint64Flag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Float32:
|
||||
flag = &cli.Float64Flag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Float64:
|
||||
flag = &cli.Float64Flag{
|
||||
Name: name,
|
||||
EnvVars: []string{envVar},
|
||||
Usage: generatedCLIFlagUsage,
|
||||
Hidden: hidden,
|
||||
Name: name,
|
||||
Sources: cli.EnvVars(envVar),
|
||||
Usage: generatedCLIFlagUsage,
|
||||
DefaultText: defaultText,
|
||||
Hidden: hidden,
|
||||
}
|
||||
case reflect.Slice:
|
||||
// TODO
|
||||
@@ -651,13 +781,39 @@ func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error)
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
|
||||
func cliDefaultText(value reflect.Value) string {
|
||||
if value.Kind() == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
return ""
|
||||
}
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(value.Bool())
|
||||
case reflect.String:
|
||||
return value.String()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if value.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||
return value.Interface().(time.Duration).String()
|
||||
}
|
||||
return strconv.FormatInt(value.Int(), 10)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return strconv.FormatUint(value.Uint(), 10)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return strconv.FormatFloat(value.Float(), 'f', -1, 64)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (conf *Config) updateFromCLI(c *cli.Command, baseFlags []cli.Flag) error {
|
||||
generatedFlagNames := conf.ToCLIFlagNames(baseFlags)
|
||||
for _, flag := range c.App.Flags {
|
||||
for _, flag := range c.Flags {
|
||||
flagName := flag.Names()[0]
|
||||
|
||||
// the `c.App.Name != "test"` check is needed because `c.IsSet(...)` is always false in unit tests
|
||||
if !c.IsSet(flagName) && c.App.Name != "test" {
|
||||
if !c.IsSet(flagName) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -666,34 +822,16 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
|
||||
continue
|
||||
}
|
||||
|
||||
kind := configValue.Kind()
|
||||
if kind == reflect.Ptr {
|
||||
// instantiate value to be set
|
||||
if configValue.Kind() == reflect.Ptr {
|
||||
configValue.Set(reflect.New(configValue.Type().Elem()))
|
||||
|
||||
kind = configValue.Type().Elem().Kind()
|
||||
configValue = configValue.Elem()
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
configValue.SetBool(c.Bool(flagName))
|
||||
case reflect.String:
|
||||
configValue.SetString(c.String(flagName))
|
||||
case reflect.Int, reflect.Int32, reflect.Int64:
|
||||
configValue.SetInt(c.Int64(flagName))
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
configValue.SetUint(c.Uint64(flagName))
|
||||
case reflect.Float32:
|
||||
configValue.SetFloat(c.Float64(flagName))
|
||||
case reflect.Float64:
|
||||
configValue.SetFloat(c.Float64(flagName))
|
||||
// case reflect.Slice:
|
||||
// // TODO
|
||||
// case reflect.Map:
|
||||
// // TODO
|
||||
default:
|
||||
return fmt.Errorf("unsupported generated cli flag type for config: %s is a %s", flagName, kind.String())
|
||||
value := reflect.ValueOf(c.Value(flagName))
|
||||
if value.CanConvert(configValue.Type()) {
|
||||
configValue.Set(value.Convert(configValue.Type()))
|
||||
} else {
|
||||
return fmt.Errorf("unsupported generated cli flag type for config: %s (expected %s, got %s)", flagName, configValue.Type(), value.Type())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +862,7 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
|
||||
conf.TURN.KeyFile = c.String("turn-key")
|
||||
}
|
||||
if c.IsSet("node-ip") {
|
||||
conf.RTC.NodeIP = c.String("node-ip")
|
||||
conf.RTC.NodeIP.UnmarshalString(c.String("node-ip"))
|
||||
}
|
||||
if c.IsSet("udp-port") {
|
||||
conf.RTC.UDPPort.UnmarshalString(c.String("udp-port"))
|
||||
@@ -736,7 +874,7 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
|
||||
}
|
||||
|
||||
func (conf *Config) unmarshalKeys(keys string) error {
|
||||
temp := make(map[string]interface{})
|
||||
temp := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(keys), temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+10
-13
@@ -15,11 +15,10 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config/configtest"
|
||||
)
|
||||
@@ -53,19 +52,17 @@ func TestGeneratedFlags(t *testing.T) {
|
||||
generatedFlags, err := GenerateCLIFlags(nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "test"
|
||||
app.Flags = append(app.Flags, generatedFlags...)
|
||||
c := &cli.Command{}
|
||||
c.Name = "test"
|
||||
c.Flags = append(c.Flags, generatedFlags...)
|
||||
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("rtc.use_ice_lite", true, "") // bool
|
||||
set.String("redis.address", "localhost:6379", "") // string
|
||||
set.Uint("prometheus.port", 9999, "") // uint32
|
||||
set.Bool("rtc.allow_tcp_fallback", true, "") // pointer
|
||||
set.Bool("rtc.reconnect_on_publication_error", true, "") // pointer
|
||||
set.Bool("rtc.reconnect_on_subscription_error", false, "") // pointer
|
||||
c.Set("rtc.use_ice_lite", "true")
|
||||
c.Set("redis.address", "localhost:6379")
|
||||
c.Set("prometheus.port", "9999")
|
||||
c.Set("rtc.allow_tcp_fallback", "true")
|
||||
c.Set("rtc.reconnect_on_publication_error", "true")
|
||||
c.Set("rtc.reconnect_on_subscription_error", "false")
|
||||
|
||||
c := cli.NewContext(app, set, nil)
|
||||
conf, err := NewConfig("", true, c, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/utils"
|
||||
"github.com/livekit/mediatransportutil/pkg/latency"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
@@ -49,7 +49,7 @@ type MetricTimestamperParams struct {
|
||||
type MetricTimestamper struct {
|
||||
params MetricTimestamperParams
|
||||
lock sync.Mutex
|
||||
owdEstimator *utils.OWDEstimator
|
||||
owdEstimator *latency.OWDEstimator
|
||||
lastOWDEstimatorRunAt time.Time
|
||||
batchesSinceLastOWDEstimatorRun int
|
||||
}
|
||||
@@ -57,7 +57,7 @@ type MetricTimestamper struct {
|
||||
func NewMetricTimestamper(params MetricTimestamperParams) *MetricTimestamper {
|
||||
return &MetricTimestamper{
|
||||
params: params,
|
||||
owdEstimator: utils.NewOWDEstimator(utils.OWDEstimatorParamsDefault),
|
||||
owdEstimator: latency.NewOWDEstimator(latency.OWDEstimatorParamsDefault),
|
||||
lastOWDEstimatorRunAt: time.Now().Add(-params.Config.OneWayDelayEstimatorMinInterval),
|
||||
}
|
||||
}
|
||||
|
||||
+74
-42
@@ -23,6 +23,7 @@ import (
|
||||
"go.uber.org/zap/zapcore"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
@@ -94,7 +95,7 @@ type NullMessageSource struct {
|
||||
func NewNullMessageSource(connID livekit.ConnectionID) *NullMessageSource {
|
||||
return &NullMessageSource{
|
||||
connID: connID,
|
||||
msgChan: make(chan proto.Message, 0),
|
||||
msgChan: make(chan proto.Message),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +153,13 @@ type StartParticipantSignalResults struct {
|
||||
type MessageRouter interface {
|
||||
// CreateRoom starts an rtc room
|
||||
CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (res *livekit.Room, err error)
|
||||
|
||||
// StartParticipantSignal participant signal connection is ready to start
|
||||
StartParticipantSignal(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit) (res StartParticipantSignalResults, err error)
|
||||
StartParticipantSignal(
|
||||
ctx context.Context,
|
||||
roomName livekit.RoomName,
|
||||
pi ParticipantInit,
|
||||
) (res StartParticipantSignalResults, err error)
|
||||
}
|
||||
|
||||
func CreateRouter(
|
||||
@@ -162,8 +168,9 @@ func CreateRouter(
|
||||
signalClient SignalClient,
|
||||
roomManagerClient RoomManagerClient,
|
||||
kps rpc.KeepalivePubSub,
|
||||
nodeStatsConfig config.NodeStatsConfig,
|
||||
) Router {
|
||||
lr := NewLocalRouter(node, signalClient, roomManagerClient)
|
||||
lr := NewLocalRouter(node, signalClient, roomManagerClient, nodeStatsConfig)
|
||||
|
||||
if rc != nil {
|
||||
return NewRedisRouter(lr, rc, kps)
|
||||
@@ -177,19 +184,24 @@ func CreateRouter(
|
||||
// ------------------------------------------------
|
||||
|
||||
type ParticipantInit struct {
|
||||
Identity livekit.ParticipantIdentity
|
||||
Name livekit.ParticipantName
|
||||
Reconnect bool
|
||||
ReconnectReason livekit.ReconnectReason
|
||||
AutoSubscribe bool
|
||||
Client *livekit.ClientInfo
|
||||
Grants *auth.ClaimGrants
|
||||
Region string
|
||||
AdaptiveStream bool
|
||||
ID livekit.ParticipantID
|
||||
SubscriberAllowPause *bool
|
||||
DisableICELite bool
|
||||
CreateRoom *livekit.CreateRoomRequest
|
||||
Identity livekit.ParticipantIdentity
|
||||
Name livekit.ParticipantName
|
||||
Reconnect bool
|
||||
ReconnectReason livekit.ReconnectReason
|
||||
AutoSubscribe bool
|
||||
AutoSubscribeDataTrack *bool
|
||||
Client *livekit.ClientInfo
|
||||
Grants *auth.ClaimGrants
|
||||
Region string
|
||||
AdaptiveStream bool
|
||||
ID livekit.ParticipantID
|
||||
SubscriberAllowPause *bool
|
||||
DisableICELite bool
|
||||
CreateRoom *livekit.CreateRoomRequest
|
||||
AddTrackRequests []*livekit.AddTrackRequest
|
||||
PublisherOffer *livekit.SessionDescription
|
||||
SyncState *livekit.SyncState
|
||||
UseSinglePeerConnection bool
|
||||
}
|
||||
|
||||
func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
@@ -209,6 +221,7 @@ func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
logBoolPtr("Reconnect", &pi.Reconnect)
|
||||
e.AddString("ReconnectReason", pi.ReconnectReason.String())
|
||||
logBoolPtr("AutoSubscribe", &pi.AutoSubscribe)
|
||||
logBoolPtr("AutoSubscribeDataTrack", pi.AutoSubscribeDataTrack)
|
||||
e.AddObject("Client", logger.Proto(utils.ClientInfoWithoutAddress(pi.Client)))
|
||||
e.AddObject("Grants", pi.Grants)
|
||||
e.AddString("Region", pi.Region)
|
||||
@@ -217,6 +230,10 @@ func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
logBoolPtr("SubscriberAllowPause", pi.SubscriberAllowPause)
|
||||
logBoolPtr("DisableICELite", &pi.DisableICELite)
|
||||
e.AddObject("CreateRoom", logger.Proto(pi.CreateRoom))
|
||||
e.AddArray("AddTrackRequests", logger.ProtoSlice(pi.AddTrackRequests))
|
||||
e.AddObject("PublisherOffer", logger.Proto(pi.PublisherOffer))
|
||||
e.AddObject("SyncState", logger.Proto(pi.SyncState))
|
||||
logBoolPtr("UseSinglePeerConnection", &pi.UseSinglePeerConnection)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -227,20 +244,27 @@ func (pi *ParticipantInit) ToStartSession(roomName livekit.RoomName, connectionI
|
||||
}
|
||||
|
||||
ss := &livekit.StartSession{
|
||||
RoomName: string(roomName),
|
||||
Identity: string(pi.Identity),
|
||||
Name: string(pi.Name),
|
||||
// connection id is to allow the RTC node to identify where to route the message back to
|
||||
ConnectionId: string(connectionID),
|
||||
Reconnect: pi.Reconnect,
|
||||
ReconnectReason: pi.ReconnectReason,
|
||||
AutoSubscribe: pi.AutoSubscribe,
|
||||
Client: pi.Client,
|
||||
GrantsJson: string(claims),
|
||||
AdaptiveStream: pi.AdaptiveStream,
|
||||
ParticipantId: string(pi.ID),
|
||||
DisableIceLite: pi.DisableICELite,
|
||||
CreateRoom: pi.CreateRoom,
|
||||
RoomName: string(roomName),
|
||||
Identity: string(pi.Identity),
|
||||
Name: string(pi.Name),
|
||||
ConnectionId: string(connectionID),
|
||||
Reconnect: pi.Reconnect,
|
||||
ReconnectReason: pi.ReconnectReason,
|
||||
AutoSubscribe: pi.AutoSubscribe,
|
||||
Client: pi.Client,
|
||||
GrantsJson: string(claims),
|
||||
AdaptiveStream: pi.AdaptiveStream,
|
||||
ParticipantId: string(pi.ID),
|
||||
DisableIceLite: pi.DisableICELite,
|
||||
CreateRoom: pi.CreateRoom,
|
||||
AddTrackRequests: pi.AddTrackRequests,
|
||||
PublisherOffer: pi.PublisherOffer,
|
||||
SyncState: pi.SyncState,
|
||||
UseSinglePeerConnection: pi.UseSinglePeerConnection,
|
||||
}
|
||||
if pi.AutoSubscribeDataTrack != nil {
|
||||
autoSubscribeDataTrack := *pi.AutoSubscribeDataTrack
|
||||
ss.AutoSubscribeDataTrack = &autoSubscribeDataTrack
|
||||
}
|
||||
if pi.SubscriberAllowPause != nil {
|
||||
subscriberAllowPause := *pi.SubscriberAllowPause
|
||||
@@ -257,18 +281,26 @@ func ParticipantInitFromStartSession(ss *livekit.StartSession, region string) (*
|
||||
}
|
||||
|
||||
pi := &ParticipantInit{
|
||||
Identity: livekit.ParticipantIdentity(ss.Identity),
|
||||
Name: livekit.ParticipantName(ss.Name),
|
||||
Reconnect: ss.Reconnect,
|
||||
ReconnectReason: ss.ReconnectReason,
|
||||
Client: ss.Client,
|
||||
AutoSubscribe: ss.AutoSubscribe,
|
||||
Grants: claims,
|
||||
Region: region,
|
||||
AdaptiveStream: ss.AdaptiveStream,
|
||||
ID: livekit.ParticipantID(ss.ParticipantId),
|
||||
DisableICELite: ss.DisableIceLite,
|
||||
CreateRoom: ss.CreateRoom,
|
||||
Identity: livekit.ParticipantIdentity(ss.Identity),
|
||||
Name: livekit.ParticipantName(ss.Name),
|
||||
Reconnect: ss.Reconnect,
|
||||
ReconnectReason: ss.ReconnectReason,
|
||||
Client: ss.Client,
|
||||
AutoSubscribe: ss.AutoSubscribe,
|
||||
Grants: claims,
|
||||
Region: region,
|
||||
AdaptiveStream: ss.AdaptiveStream,
|
||||
ID: livekit.ParticipantID(ss.ParticipantId),
|
||||
DisableICELite: ss.DisableIceLite,
|
||||
CreateRoom: ss.CreateRoom,
|
||||
AddTrackRequests: ss.AddTrackRequests,
|
||||
PublisherOffer: ss.PublisherOffer,
|
||||
SyncState: ss.SyncState,
|
||||
UseSinglePeerConnection: ss.UseSinglePeerConnection,
|
||||
}
|
||||
if ss.AutoSubscribeDataTrack != nil {
|
||||
autoSubscribeDataTrack := *ss.AutoSubscribeDataTrack
|
||||
pi.AutoSubscribeDataTrack = &autoSubscribeDataTrack
|
||||
}
|
||||
if ss.SubscriberAllowPause != nil {
|
||||
subscriberAllowPause := *ss.SubscriberAllowPause
|
||||
|
||||
@@ -16,11 +16,11 @@ package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
@@ -32,8 +32,8 @@ type LocalRouter struct {
|
||||
currentNode LocalNode
|
||||
signalClient SignalClient
|
||||
roomManagerClient RoomManagerClient
|
||||
nodeStatsConfig config.NodeStatsConfig
|
||||
|
||||
lock sync.RWMutex
|
||||
// channels for each participant
|
||||
requestChannels map[string]*MessageChannel
|
||||
responseChannels map[string]*MessageChannel
|
||||
@@ -44,11 +44,13 @@ func NewLocalRouter(
|
||||
currentNode LocalNode,
|
||||
signalClient SignalClient,
|
||||
roomManagerClient RoomManagerClient,
|
||||
nodeStatsConfig config.NodeStatsConfig,
|
||||
) *LocalRouter {
|
||||
return &LocalRouter{
|
||||
currentNode: currentNode,
|
||||
signalClient: signalClient,
|
||||
roomManagerClient: roomManagerClient,
|
||||
nodeStatsConfig: nodeStatsConfig,
|
||||
requestChannels: make(map[string]*MessageChannel),
|
||||
responseChannels: make(map[string]*MessageChannel),
|
||||
}
|
||||
@@ -106,7 +108,8 @@ func (r *LocalRouter) StartParticipantSignal(ctx context.Context, roomName livek
|
||||
func (r *LocalRouter) StartParticipantSignalWithNodeID(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit, nodeID livekit.NodeID) (res StartParticipantSignalResults, err error) {
|
||||
connectionID, reqSink, resSource, err := r.signalClient.StartParticipantSignal(ctx, roomName, pi, nodeID)
|
||||
if err != nil {
|
||||
logger.Errorw("could not handle new participant", err,
|
||||
logger.Errorw(
|
||||
"could not handle new participant", err,
|
||||
"room", roomName,
|
||||
"participant", pi.Identity,
|
||||
"connID", connectionID,
|
||||
@@ -146,8 +149,7 @@ func (r *LocalRouter) statsWorker() {
|
||||
if !r.isStarted.Load() {
|
||||
return
|
||||
}
|
||||
// update every 10 seconds
|
||||
<-time.After(statsUpdateInterval)
|
||||
<-time.After(r.nodeStatsConfig.StatsUpdateInterval)
|
||||
r.currentNode.UpdateNodeStats()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestMessageChannel_WriteMessageClosed(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
_ = m.WriteMessage(&livekit.SignalRequest{})
|
||||
}
|
||||
}()
|
||||
|
||||
+14
-17
@@ -20,12 +20,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
||||
)
|
||||
|
||||
type LocalNode interface {
|
||||
@@ -45,30 +43,35 @@ type LocalNodeImpl struct {
|
||||
lock sync.RWMutex
|
||||
node *livekit.Node
|
||||
|
||||
// previous stats for computing averages
|
||||
prevStats *livekit.NodeStats
|
||||
nodeStats *NodeStats
|
||||
}
|
||||
|
||||
func NewLocalNode(conf *config.Config) (*LocalNodeImpl, error) {
|
||||
nodeID := guid.New(utils.NodePrefix)
|
||||
if conf != nil && conf.RTC.NodeIP == "" {
|
||||
if conf != nil && conf.RTC.NodeIP.IsEmpty() {
|
||||
return nil, ErrIPNotSet
|
||||
}
|
||||
nowUnix := time.Now().Unix()
|
||||
l := &LocalNodeImpl{
|
||||
node: &livekit.Node{
|
||||
Id: nodeID,
|
||||
NumCpus: uint32(runtime.NumCPU()),
|
||||
State: livekit.NodeState_SERVING,
|
||||
Stats: &livekit.NodeStats{
|
||||
StartedAt: time.Now().Unix(),
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
StartedAt: nowUnix,
|
||||
UpdatedAt: nowUnix,
|
||||
},
|
||||
},
|
||||
}
|
||||
var nsc *config.NodeStatsConfig
|
||||
if conf != nil {
|
||||
l.node.Ip = conf.RTC.NodeIP
|
||||
l.node.Ip = conf.RTC.NodeIP.PrimaryIP()
|
||||
l.node.Region = conf.Region
|
||||
|
||||
nsc = &conf.NodeStats
|
||||
}
|
||||
l.nodeStats = NewNodeStats(nsc, nowUnix)
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
@@ -138,18 +141,12 @@ func (l *LocalNodeImpl) UpdateNodeStats() bool {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.prevStats == nil {
|
||||
l.prevStats = l.node.Stats
|
||||
}
|
||||
updated, computedAvg, err := prometheus.GetUpdatedNodeStats(l.node.Stats, l.prevStats)
|
||||
stats, err := l.nodeStats.UpdateAndGetNodeStats()
|
||||
if err != nil {
|
||||
logger.Errorw("could not update node stats", err)
|
||||
return false
|
||||
}
|
||||
l.node.Stats = updated
|
||||
if computedAvg {
|
||||
l.prevStats = updated
|
||||
}
|
||||
|
||||
l.node.Stats = stats
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package routing
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
||||
)
|
||||
|
||||
type NodeStats struct {
|
||||
config config.NodeStatsConfig
|
||||
startedAt int64
|
||||
|
||||
lock sync.Mutex
|
||||
statsHistory []*livekit.NodeStats
|
||||
statsHistoryWritePtr int
|
||||
}
|
||||
|
||||
func NewNodeStats(conf *config.NodeStatsConfig, startedAt int64) *NodeStats {
|
||||
n := &NodeStats{
|
||||
startedAt: startedAt,
|
||||
}
|
||||
n.UpdateConfig(conf)
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *NodeStats) UpdateConfig(conf *config.NodeStatsConfig) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if conf == nil {
|
||||
conf = &config.DefaultNodeStatsConfig
|
||||
}
|
||||
n.config = *conf
|
||||
|
||||
// set up stats history to be able to measure different rate windows
|
||||
var maxInterval time.Duration
|
||||
for _, rateInterval := range conf.StatsRateMeasurementIntervals {
|
||||
if rateInterval > maxInterval {
|
||||
maxInterval = rateInterval
|
||||
}
|
||||
}
|
||||
n.statsHistory = make([]*livekit.NodeStats, (maxInterval+conf.StatsUpdateInterval-1)/conf.StatsUpdateInterval)
|
||||
n.statsHistoryWritePtr = 0
|
||||
}
|
||||
|
||||
func (n *NodeStats) UpdateAndGetNodeStats() (*livekit.NodeStats, error) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
stats, err := prometheus.GetNodeStats(
|
||||
n.startedAt,
|
||||
append(n.statsHistory[n.statsHistoryWritePtr:], n.statsHistory[0:n.statsHistoryWritePtr]...),
|
||||
n.config.StatsRateMeasurementIntervals,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Errorw("could not update node stats", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.statsHistory[n.statsHistoryWritePtr] = stats
|
||||
n.statsHistoryWritePtr = (n.statsHistoryWritePtr + 1) % len(n.statsHistory)
|
||||
return stats, nil
|
||||
}
|
||||
@@ -33,11 +33,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// expire participant mappings after a day
|
||||
participantMappingTTL = 24 * time.Hour
|
||||
statsUpdateInterval = 2 * time.Second
|
||||
statsMaxDelaySeconds = float64(30)
|
||||
|
||||
// hash of node_id => Node proto
|
||||
NodesKey = "nodes"
|
||||
|
||||
@@ -209,11 +204,11 @@ func (r *RedisRouter) statsWorker() {
|
||||
for r.ctx.Err() == nil {
|
||||
// update periodically
|
||||
select {
|
||||
case <-time.After(statsUpdateInterval):
|
||||
case <-time.After(r.nodeStatsConfig.StatsUpdateInterval):
|
||||
r.kps.PublishPing(r.ctx, r.currentNode.NodeID(), &rpc.KeepalivePing{Timestamp: time.Now().Unix()})
|
||||
|
||||
delaySeconds := r.currentNode.SecondsSinceNodeStatsUpdate()
|
||||
if delaySeconds > statsMaxDelaySeconds {
|
||||
if delaySeconds > r.nodeStatsConfig.StatsMaxDelay.Seconds() {
|
||||
if !goroutineDumped {
|
||||
goroutineDumped = true
|
||||
buf := bytes.NewBuffer(nil)
|
||||
@@ -240,7 +235,7 @@ func (r *RedisRouter) keepaliveWorker(startedChan chan error) {
|
||||
close(startedChan)
|
||||
|
||||
for ping := range pings.Channel() {
|
||||
if time.Since(time.Unix(ping.Timestamp, 0)) > statsUpdateInterval {
|
||||
if time.Since(time.Unix(ping.Timestamp, 0)) > r.nodeStatsConfig.StatsUpdateInterval {
|
||||
logger.Infow("keep alive too old, skipping", "timestamp", ping.Timestamp)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -243,14 +243,6 @@ func (fake *FakeMessageSink) WriteMessageReturnsOnCall(i int, result1 error) {
|
||||
func (fake *FakeMessageSink) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.closeMutex.RLock()
|
||||
defer fake.closeMutex.RUnlock()
|
||||
fake.connectionIDMutex.RLock()
|
||||
defer fake.connectionIDMutex.RUnlock()
|
||||
fake.isClosedMutex.RLock()
|
||||
defer fake.isClosedMutex.RUnlock()
|
||||
fake.writeMessageMutex.RLock()
|
||||
defer fake.writeMessageMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
||||
@@ -234,14 +234,6 @@ func (fake *FakeMessageSource) ReadChanReturnsOnCall(i int, result1 <-chan proto
|
||||
func (fake *FakeMessageSource) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.closeMutex.RLock()
|
||||
defer fake.closeMutex.RUnlock()
|
||||
fake.connectionIDMutex.RLock()
|
||||
defer fake.connectionIDMutex.RUnlock()
|
||||
fake.isClosedMutex.RLock()
|
||||
defer fake.isClosedMutex.RUnlock()
|
||||
fake.readChanMutex.RLock()
|
||||
defer fake.readChanMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
||||
@@ -129,10 +129,6 @@ func (fake *FakeRoomManagerClient) CreateRoomReturnsOnCall(i int, result1 *livek
|
||||
func (fake *FakeRoomManagerClient) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.closeMutex.RLock()
|
||||
defer fake.closeMutex.RUnlock()
|
||||
fake.createRoomMutex.RLock()
|
||||
defer fake.createRoomMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
||||
@@ -845,32 +845,6 @@ func (fake *FakeRouter) UnregisterNodeReturnsOnCall(i int, result1 error) {
|
||||
func (fake *FakeRouter) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.clearRoomStateMutex.RLock()
|
||||
defer fake.clearRoomStateMutex.RUnlock()
|
||||
fake.createRoomMutex.RLock()
|
||||
defer fake.createRoomMutex.RUnlock()
|
||||
fake.drainMutex.RLock()
|
||||
defer fake.drainMutex.RUnlock()
|
||||
fake.getNodeForRoomMutex.RLock()
|
||||
defer fake.getNodeForRoomMutex.RUnlock()
|
||||
fake.getRegionMutex.RLock()
|
||||
defer fake.getRegionMutex.RUnlock()
|
||||
fake.listNodesMutex.RLock()
|
||||
defer fake.listNodesMutex.RUnlock()
|
||||
fake.registerNodeMutex.RLock()
|
||||
defer fake.registerNodeMutex.RUnlock()
|
||||
fake.removeDeadNodesMutex.RLock()
|
||||
defer fake.removeDeadNodesMutex.RUnlock()
|
||||
fake.setNodeForRoomMutex.RLock()
|
||||
defer fake.setNodeForRoomMutex.RUnlock()
|
||||
fake.startMutex.RLock()
|
||||
defer fake.startMutex.RUnlock()
|
||||
fake.startParticipantSignalMutex.RLock()
|
||||
defer fake.startParticipantSignalMutex.RUnlock()
|
||||
fake.stopMutex.RLock()
|
||||
defer fake.stopMutex.RUnlock()
|
||||
fake.unregisterNodeMutex.RLock()
|
||||
defer fake.unregisterNodeMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
||||
@@ -173,10 +173,6 @@ func (fake *FakeSignalClient) StartParticipantSignalReturnsOnCall(i int, result1
|
||||
func (fake *FakeSignalClient) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.activeCountMutex.RLock()
|
||||
defer fake.activeCountMutex.RUnlock()
|
||||
fake.startParticipantSignalMutex.RLock()
|
||||
defer fake.startParticipantSignalMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
|
||||
// AnySelector selects any available node with no limitations
|
||||
type AnySelector struct {
|
||||
SortBy string
|
||||
SortBy string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
|
||||
@@ -29,5 +30,5 @@ func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
|
||||
return nil, ErrNoAvailableNodes
|
||||
}
|
||||
|
||||
return SelectSortedNode(nodes, s.SortBy)
|
||||
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package selector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
func createTestNode(id string, cpuLoad float32, numRooms int32, numClients int32, state livekit.NodeState) *livekit.Node {
|
||||
return &livekit.Node{
|
||||
Id: id,
|
||||
State: state,
|
||||
Stats: &livekit.NodeStats{
|
||||
UpdatedAt: time.Now().Unix() - 1, // Recent update to be considered available
|
||||
CpuLoad: cpuLoad,
|
||||
NumRooms: numRooms,
|
||||
NumClients: numClients,
|
||||
NumCpus: 4,
|
||||
LoadAvgLast1Min: cpuLoad * 4, // Simulate system load
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnySelector_SelectNode_TwoChoice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sortBy string
|
||||
algorithm string
|
||||
nodes []*livekit.Node
|
||||
wantErr string
|
||||
expected string
|
||||
notExpected string
|
||||
}{
|
||||
{
|
||||
name: "successful selection with cpuload sorting",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.8, 5, 10, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.3, 2, 5, livekit.NodeState_SERVING),
|
||||
createTestNode("node3", 0.6, 3, 8, livekit.NodeState_SERVING),
|
||||
createTestNode("node4", 0.9, 6, 12, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "", // Not determinstic selection, so no specific expected node
|
||||
notExpected: "node4", // Node with highest load should not be selected
|
||||
},
|
||||
{
|
||||
name: "successful selection with rooms sorting",
|
||||
sortBy: "rooms",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 8, 15, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.4, 2, 5, livekit.NodeState_SERVING),
|
||||
createTestNode("node3", 0.6, 12, 20, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "", // Not determinstic selection, so no specific expected node
|
||||
notExpected: "node3", // Node with highest room count should not be selected
|
||||
},
|
||||
{
|
||||
name: "successful selection with clients sorting",
|
||||
sortBy: "clients",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 3, 25, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.4, 2, 5, livekit.NodeState_SERVING),
|
||||
createTestNode("node3", 0.6, 4, 30, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "", // Not determinstic selection, so no specific expected node
|
||||
notExpected: "node3", // Node with highest clients should not be selected
|
||||
},
|
||||
{
|
||||
name: "empty nodes list",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{},
|
||||
wantErr: "could not find any available nodes",
|
||||
},
|
||||
{
|
||||
name: "no available nodes - all unavailable",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
{
|
||||
Id: "node1",
|
||||
State: livekit.NodeState_SERVING,
|
||||
Stats: &livekit.NodeStats{
|
||||
UpdatedAt: time.Now().Unix() - 10, // Too old
|
||||
CpuLoad: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "could not find any available nodes",
|
||||
},
|
||||
{
|
||||
name: "no available nodes - not serving",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
{
|
||||
Id: "node1",
|
||||
State: livekit.NodeState_SHUTTING_DOWN,
|
||||
Stats: &livekit.NodeStats{
|
||||
UpdatedAt: time.Now().Unix() - 1,
|
||||
CpuLoad: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "could not find any available nodes",
|
||||
},
|
||||
{
|
||||
name: "single available node",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 3, 10, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "node1", // Should select the only available node
|
||||
notExpected: "", // No other nodes to compare against
|
||||
},
|
||||
{
|
||||
name: "two available nodes",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "twochoice",
|
||||
nodes: []*livekit.Node{
|
||||
createTestNode("node1", 0.8, 5, 15, livekit.NodeState_SERVING),
|
||||
createTestNode("node2", 0.3, 2, 5, livekit.NodeState_SERVING),
|
||||
},
|
||||
wantErr: "",
|
||||
expected: "node2", // Should select the node with lower load
|
||||
notExpected: "node1", // Should not select the node with higher load
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector := &AnySelector{
|
||||
SortBy: tt.sortBy,
|
||||
Algorithm: tt.algorithm,
|
||||
}
|
||||
|
||||
node, err := selector.SelectNode(tt.nodes)
|
||||
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
require.Nil(t, node)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, node)
|
||||
require.NotEmpty(t, node.Id)
|
||||
|
||||
// Verify the selected node is one of the available nodes
|
||||
found := false
|
||||
availableNodes := GetAvailableNodes(tt.nodes)
|
||||
for _, availableNode := range availableNodes {
|
||||
if availableNode.Id == node.Id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "Selected node should be one of the available nodes")
|
||||
|
||||
if tt.expected != "" {
|
||||
require.Equal(t, tt.expected, node.Id, "Selected node should match expected")
|
||||
}
|
||||
if tt.notExpected != "" {
|
||||
require.NotEqual(t, tt.notExpected, node.Id, "Selected node should not match not expected")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnySelector_SelectNode_TwoChoice_Probabilistic_Behavior(t *testing.T) {
|
||||
// Test that two-choice algorithm favors nodes with lower metrics
|
||||
// This test runs multiple iterations to increase confidence in the probabilistic behavior
|
||||
selector := &AnySelector{
|
||||
SortBy: "cpuload",
|
||||
Algorithm: "twochoice",
|
||||
}
|
||||
|
||||
// Create nodes where node2 has significantly lower CPU load
|
||||
nodes := []*livekit.Node{
|
||||
createTestNode("node1", 0.95, 10, 20, livekit.NodeState_SERVING), // Very high load
|
||||
createTestNode("node2", 0.1, 1, 2, livekit.NodeState_SERVING), // Low load
|
||||
createTestNode("node3", 0.5, 9, 18, livekit.NodeState_SERVING), // Medium load
|
||||
createTestNode("node4", 0.85, 8, 16, livekit.NodeState_SERVING), // High load
|
||||
}
|
||||
|
||||
// Run multiple selections and count how often the low-load node is selected
|
||||
iterations := 1000
|
||||
lowLoadSelections := 0
|
||||
higestLoadSelections := 0
|
||||
|
||||
for range iterations {
|
||||
node, err := selector.SelectNode(nodes)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, node)
|
||||
|
||||
if node.Id == "node2" {
|
||||
lowLoadSelections++
|
||||
}
|
||||
if node.Id == "node1" {
|
||||
higestLoadSelections++
|
||||
}
|
||||
}
|
||||
|
||||
// The low-load node should be selected more often than pure random (25%)
|
||||
// Due to the two-choice algorithm favoring the better node
|
||||
selectionRate := float64(lowLoadSelections) / float64(iterations)
|
||||
require.Greater(t, selectionRate, 0.4, "Two-choice algorithm should favor the low-load node more than random selection")
|
||||
require.Equal(t, higestLoadSelections, 0, "Two-choice algorithm should never favor the highest load node")
|
||||
}
|
||||
|
||||
func TestAnySelector_SelectNode_InvalidParameters(t *testing.T) {
|
||||
nodes := []*livekit.Node{
|
||||
createTestNode("node1", 0.5, 3, 10, livekit.NodeState_SERVING),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sortBy string
|
||||
algorithm string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty sortBy",
|
||||
sortBy: "",
|
||||
algorithm: "twochoice",
|
||||
wantErr: "sort by option cannot be blank",
|
||||
},
|
||||
{
|
||||
name: "empty algorithm",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "",
|
||||
wantErr: "node selector algorithm option cannot be blank",
|
||||
},
|
||||
{
|
||||
name: "unknown sortBy",
|
||||
sortBy: "invalid",
|
||||
algorithm: "twochoice",
|
||||
wantErr: "unknown sort by option",
|
||||
},
|
||||
{
|
||||
name: "unknown algorithm",
|
||||
sortBy: "cpuload",
|
||||
algorithm: "invalid",
|
||||
wantErr: "unknown node selector algorithm option",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector := &AnySelector{
|
||||
SortBy: tt.sortBy,
|
||||
Algorithm: tt.algorithm,
|
||||
}
|
||||
|
||||
node, err := selector.SelectNode(nodes)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
require.Nil(t, node)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
type CPULoadSelector struct {
|
||||
CPULoadLimit float32
|
||||
SortBy string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
func (s *CPULoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
|
||||
@@ -50,5 +51,5 @@ func (s *CPULoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return SelectSortedNode(nodes, s.SortBy)
|
||||
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCPULoadSelector_SelectNode(t *testing.T) {
|
||||
sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random"}
|
||||
sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random", Algorithm: "lowest"}
|
||||
|
||||
var nodes []*livekit.Node
|
||||
_, err := sel.SelectNode(nodes)
|
||||
@@ -39,7 +39,7 @@ func TestCPULoadSelector_SelectNode(t *testing.T) {
|
||||
|
||||
// Select a node with low load when available
|
||||
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
|
||||
for i := 0; i < 5; i++ {
|
||||
for range 5 {
|
||||
node, err := sel.SelectNode(nodes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
@@ -21,5 +21,7 @@ var (
|
||||
ErrCurrentRegionNotSet = errors.New("current region cannot be blank")
|
||||
ErrCurrentRegionUnknownLatLon = errors.New("unknown lat and lon for the current region")
|
||||
ErrSortByNotSet = errors.New("sort by option cannot be blank")
|
||||
ErrAlgorithmNotSet = errors.New("node selector algorithm option cannot be blank")
|
||||
ErrSortByUnknown = errors.New("unknown sort by option")
|
||||
ErrAlgorithmUnknown = errors.New("unknown node selector algorithm option")
|
||||
)
|
||||
|
||||
@@ -37,19 +37,21 @@ func CreateNodeSelector(conf *config.Config) (NodeSelector, error) {
|
||||
}
|
||||
switch kind {
|
||||
case "any":
|
||||
return &AnySelector{conf.NodeSelector.SortBy}, nil
|
||||
return &AnySelector{conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm}, nil
|
||||
case "cpuload":
|
||||
return &CPULoadSelector{
|
||||
CPULoadLimit: conf.NodeSelector.CPULoadLimit,
|
||||
SortBy: conf.NodeSelector.SortBy,
|
||||
SortBy: conf.NodeSelector.SortBy,
|
||||
Algorithm: conf.NodeSelector.Algorithm,
|
||||
}, nil
|
||||
case "sysload":
|
||||
return &SystemLoadSelector{
|
||||
SysloadLimit: conf.NodeSelector.SysloadLimit,
|
||||
SortBy: conf.NodeSelector.SortBy,
|
||||
SortBy: conf.NodeSelector.SortBy,
|
||||
Algorithm: conf.NodeSelector.Algorithm,
|
||||
}, nil
|
||||
case "regionaware":
|
||||
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy)
|
||||
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -57,7 +59,7 @@ func CreateNodeSelector(conf *config.Config) (NodeSelector, error) {
|
||||
return s, nil
|
||||
case "random":
|
||||
logger.Warnw("random node selector is deprecated, please switch to \"any\" or another selector", nil)
|
||||
return &AnySelector{conf.NodeSelector.SortBy}, nil
|
||||
return &AnySelector{conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm}, nil
|
||||
default:
|
||||
return nil, ErrUnsupportedSelector
|
||||
}
|
||||
|
||||
@@ -29,9 +29,10 @@ type RegionAwareSelector struct {
|
||||
regionDistances map[string]float64
|
||||
regions []config.RegionConfig
|
||||
SortBy string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string) (*RegionAwareSelector, error) {
|
||||
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string, algorithm string) (*RegionAwareSelector, error) {
|
||||
if currentRegion == "" {
|
||||
return nil, ErrCurrentRegionNotSet
|
||||
}
|
||||
@@ -41,6 +42,7 @@ func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig,
|
||||
regionDistances: make(map[string]float64),
|
||||
regions: regions,
|
||||
SortBy: sortBy,
|
||||
Algorithm: algorithm,
|
||||
}
|
||||
|
||||
var currentRC *config.RegionConfig
|
||||
@@ -94,7 +96,7 @@ func (s *RegionAwareSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node,
|
||||
nodes = nearestNodes
|
||||
}
|
||||
|
||||
return SelectSortedNode(nodes, s.SortBy)
|
||||
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
|
||||
}
|
||||
|
||||
// haversine(θ) function
|
||||
|
||||
@@ -34,6 +34,7 @@ const (
|
||||
regionEast = "us-east"
|
||||
regionSeattle = "seattle"
|
||||
sortBy = "random"
|
||||
algorithm = "lowest"
|
||||
)
|
||||
|
||||
func TestRegionAwareRouting(t *testing.T) {
|
||||
@@ -58,7 +59,7 @@ func TestRegionAwareRouting(t *testing.T) {
|
||||
nodes := []*livekit.Node{
|
||||
newTestNodeInRegion("", false),
|
||||
}
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, nil, sortBy)
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, nil, sortBy, algorithm)
|
||||
require.NoError(t, err)
|
||||
|
||||
node, err := s.SelectNode(nodes)
|
||||
@@ -74,7 +75,7 @@ func TestRegionAwareRouting(t *testing.T) {
|
||||
expectedNode,
|
||||
newTestNodeInRegion(regionEast, false),
|
||||
}
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
|
||||
require.NoError(t, err)
|
||||
s.SysloadLimit = loadLimit
|
||||
|
||||
@@ -91,7 +92,7 @@ func TestRegionAwareRouting(t *testing.T) {
|
||||
newTestNodeInRegion(regionWest, true),
|
||||
newTestNodeInRegion(regionEast, false),
|
||||
}
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
|
||||
require.NoError(t, err)
|
||||
s.SysloadLimit = loadLimit
|
||||
|
||||
@@ -107,7 +108,7 @@ func TestRegionAwareRouting(t *testing.T) {
|
||||
expectedNode,
|
||||
newTestNodeInRegion(regionEast, true),
|
||||
}
|
||||
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
|
||||
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy, algorithm)
|
||||
require.NoError(t, err)
|
||||
s.SysloadLimit = loadLimit
|
||||
|
||||
@@ -125,7 +126,7 @@ func TestRegionAwareRouting(t *testing.T) {
|
||||
expectedNode,
|
||||
expectedNode,
|
||||
}
|
||||
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
|
||||
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy, algorithm)
|
||||
require.NoError(t, err)
|
||||
s.SysloadLimit = loadLimit
|
||||
|
||||
@@ -138,7 +139,7 @@ func TestRegionAwareRouting(t *testing.T) {
|
||||
nodes := []*livekit.Node{
|
||||
newTestNodeInRegion(regionWest, true),
|
||||
}
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
|
||||
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
|
||||
require.NoError(t, err)
|
||||
|
||||
node, err := s.SelectNode(nodes)
|
||||
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
)
|
||||
|
||||
func SortByTest(t *testing.T, sortBy string) {
|
||||
sel := selector.SystemLoadSelector{SortBy: sortBy}
|
||||
sel := selector.SystemLoadSelector{SortBy: sortBy, Algorithm: "lowest"}
|
||||
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
for range 5 {
|
||||
node, err := sel.SelectNode(nodes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -38,7 +38,7 @@ func SortByTest(t *testing.T, sortBy string) {
|
||||
}
|
||||
|
||||
func TestSortByErrors(t *testing.T) {
|
||||
sel := selector.SystemLoadSelector{}
|
||||
sel := selector.SystemLoadSelector{Algorithm: "lowest"}
|
||||
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
|
||||
|
||||
// Test unset sort by option error
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
type SystemLoadSelector struct {
|
||||
SysloadLimit float32
|
||||
SortBy string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
func (s *SystemLoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
|
||||
@@ -49,5 +50,5 @@ func (s *SystemLoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return SelectSortedNode(nodes, s.SortBy)
|
||||
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
|
||||
}
|
||||
|
||||
@@ -33,12 +33,16 @@ var (
|
||||
NumCpus: 1,
|
||||
CpuLoad: 0.1,
|
||||
LoadAvgLast1Min: 0.0,
|
||||
NumRooms: 1,
|
||||
NumClients: 2,
|
||||
NumTracksIn: 4,
|
||||
NumTracksOut: 8,
|
||||
BytesInPerSec: 1000,
|
||||
BytesOutPerSec: 2000,
|
||||
NumRooms: 1,
|
||||
NumClients: 2,
|
||||
NumTracksIn: 4,
|
||||
NumTracksOut: 8,
|
||||
Rates: []*livekit.NodeStatsRate{
|
||||
{
|
||||
BytesIn: 1000,
|
||||
BytesOut: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,12 +53,16 @@ var (
|
||||
NumCpus: 1,
|
||||
CpuLoad: 0.5,
|
||||
LoadAvgLast1Min: 0.5,
|
||||
NumRooms: 5,
|
||||
NumClients: 10,
|
||||
NumTracksIn: 20,
|
||||
NumTracksOut: 200,
|
||||
BytesInPerSec: 5000,
|
||||
BytesOutPerSec: 10000,
|
||||
NumRooms: 5,
|
||||
NumClients: 10,
|
||||
NumTracksIn: 20,
|
||||
NumTracksOut: 200,
|
||||
Rates: []*livekit.NodeStatsRate{
|
||||
{
|
||||
BytesIn: 5000,
|
||||
BytesOut: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -65,18 +73,22 @@ var (
|
||||
NumCpus: 1,
|
||||
CpuLoad: 0.99,
|
||||
LoadAvgLast1Min: 2.0,
|
||||
NumRooms: 10,
|
||||
NumClients: 20,
|
||||
NumTracksIn: 40,
|
||||
NumTracksOut: 800,
|
||||
BytesInPerSec: 10000,
|
||||
BytesOutPerSec: 40000,
|
||||
NumRooms: 10,
|
||||
NumClients: 20,
|
||||
NumTracksIn: 40,
|
||||
NumTracksOut: 800,
|
||||
Rates: []*livekit.NodeStatsRate{
|
||||
{
|
||||
BytesIn: 10000,
|
||||
BytesOut: 40000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestSystemLoadSelector_SelectNode(t *testing.T) {
|
||||
sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random"}
|
||||
sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random", Algorithm: "lowest"}
|
||||
|
||||
var nodes []*livekit.Node
|
||||
_, err := sel.SelectNode(nodes)
|
||||
@@ -90,7 +102,7 @@ func TestSystemLoadSelector_SelectNode(t *testing.T) {
|
||||
|
||||
// Select a node with low load when available
|
||||
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
|
||||
for i := 0; i < 5; i++ {
|
||||
for range 5 {
|
||||
node, err := sel.SelectNode(nodes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
package selector
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/thoas/go-funk"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
)
|
||||
|
||||
const AvailableSeconds = 5
|
||||
@@ -62,54 +64,116 @@ func LimitsReached(limitConfig config.LimitConfig, nodeStats *livekit.NodeStats)
|
||||
if limitConfig.NumTracks > 0 && limitConfig.NumTracks <= nodeStats.NumTracksIn+nodeStats.NumTracksOut {
|
||||
return true
|
||||
}
|
||||
if limitConfig.BytesPerSec > 0 && limitConfig.BytesPerSec <= nodeStats.BytesInPerSec+nodeStats.BytesOutPerSec {
|
||||
|
||||
rate := &livekit.NodeStatsRate{}
|
||||
if len(nodeStats.Rates) > 0 {
|
||||
rate = nodeStats.Rates[0]
|
||||
}
|
||||
if limitConfig.BytesPerSec > 0 && limitConfig.BytesPerSec <= rate.BytesIn+rate.BytesOut {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func SelectSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
|
||||
func SelectSortedNode(nodes []*livekit.Node, sortBy string, algorithm string) (*livekit.Node, error) {
|
||||
if sortBy == "" {
|
||||
return nil, ErrSortByNotSet
|
||||
}
|
||||
if algorithm == "" {
|
||||
return nil, ErrAlgorithmNotSet
|
||||
}
|
||||
|
||||
switch algorithm {
|
||||
case "lowest": // examine all nodes and select the lowest based on sort criteria
|
||||
return selectLowestSortedNode(nodes, sortBy)
|
||||
case "twochoice": // randomly select two nodes and return the lowest based on sort criteria "Power of Two Random Choices"
|
||||
return selectTwoChoiceSortedNode(nodes, sortBy)
|
||||
default:
|
||||
return nil, ErrAlgorithmUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func selectTwoChoiceSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
|
||||
if len(nodes) <= 2 {
|
||||
return selectLowestSortedNode(nodes, sortBy)
|
||||
}
|
||||
|
||||
// randomly select two nodes
|
||||
node1, node2, err := selectTwoRandomNodes(nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// compare the two nodes based on the sort criteria
|
||||
if node1 == nil || node2 == nil {
|
||||
return nil, ErrNoAvailableNodes
|
||||
}
|
||||
|
||||
selectedNode, err := selectLowestSortedNode([]*livekit.Node{node1, node2}, sortBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return selectedNode, nil
|
||||
}
|
||||
|
||||
func selectLowestSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
|
||||
// Return a node based on what it should be sorted by for priority
|
||||
switch sortBy {
|
||||
case "random":
|
||||
idx := funk.RandomInt(0, len(nodes))
|
||||
return nodes[idx], nil
|
||||
case "sysload":
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return GetNodeSysload(nodes[i]) < GetNodeSysload(nodes[j])
|
||||
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
|
||||
return utils.Signum(GetNodeSysload(a) - GetNodeSysload(b))
|
||||
})
|
||||
return nodes[0], nil
|
||||
case "cpuload":
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Stats.CpuLoad < nodes[j].Stats.CpuLoad
|
||||
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
|
||||
return utils.Signum(a.Stats.CpuLoad - b.Stats.CpuLoad)
|
||||
})
|
||||
return nodes[0], nil
|
||||
case "rooms":
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Stats.NumRooms < nodes[j].Stats.NumRooms
|
||||
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
|
||||
return utils.Signum(a.Stats.NumRooms - b.Stats.NumRooms)
|
||||
})
|
||||
return nodes[0], nil
|
||||
case "clients":
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Stats.NumClients < nodes[j].Stats.NumClients
|
||||
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
|
||||
return utils.Signum(a.Stats.NumClients - b.Stats.NumClients)
|
||||
})
|
||||
return nodes[0], nil
|
||||
case "tracks":
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Stats.NumTracksIn+nodes[i].Stats.NumTracksOut < nodes[j].Stats.NumTracksIn+nodes[j].Stats.NumTracksOut
|
||||
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
|
||||
return utils.Signum((a.Stats.NumTracksIn + a.Stats.NumTracksOut) - (b.Stats.NumTracksIn + b.Stats.NumTracksOut))
|
||||
})
|
||||
return nodes[0], nil
|
||||
case "bytespersec":
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Stats.BytesInPerSec+nodes[i].Stats.BytesOutPerSec < nodes[j].Stats.BytesInPerSec+nodes[j].Stats.BytesOutPerSec
|
||||
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
|
||||
ratea := &livekit.NodeStatsRate{}
|
||||
if len(a.Stats.Rates) > 0 {
|
||||
ratea = a.Stats.Rates[0]
|
||||
}
|
||||
|
||||
rateb := &livekit.NodeStatsRate{}
|
||||
if len(b.Stats.Rates) > 0 {
|
||||
rateb = b.Stats.Rates[0]
|
||||
}
|
||||
return utils.Signum((ratea.BytesIn + ratea.BytesOut) - (rateb.BytesIn + rateb.BytesOut))
|
||||
})
|
||||
return nodes[0], nil
|
||||
default:
|
||||
return nil, ErrSortByUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func selectTwoRandomNodes(nodes []*livekit.Node) (*livekit.Node, *livekit.Node, error) {
|
||||
if len(nodes) < 2 {
|
||||
return nil, nil, ErrNoAvailableNodes
|
||||
}
|
||||
|
||||
shuffledIndices := rand.Perm(len(nodes))
|
||||
|
||||
return nodes[shuffledIndices[0]], nodes[shuffledIndices[1]], nil
|
||||
}
|
||||
|
||||
+25
-8
@@ -25,6 +25,7 @@ import (
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
@@ -50,7 +51,7 @@ type signalClient struct {
|
||||
}
|
||||
|
||||
func NewSignalClient(nodeID livekit.NodeID, bus psrpc.MessageBus, config config.SignalRelayConfig) (SignalClient, error) {
|
||||
c, err := rpc.NewTypedSignalClient(
|
||||
client, err := rpc.NewTypedSignalClient(
|
||||
nodeID,
|
||||
bus,
|
||||
middleware.WithClientMetrics(rpc.PSRPCMetricsObserver{}),
|
||||
@@ -63,7 +64,7 @@ func NewSignalClient(nodeID livekit.NodeID, bus psrpc.MessageBus, config config.
|
||||
return &signalClient{
|
||||
nodeID: nodeID,
|
||||
config: config,
|
||||
client: c,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -88,25 +89,27 @@ func (r *signalClient) StartParticipantSignal(
|
||||
return
|
||||
}
|
||||
|
||||
l := logger.GetLogger().WithValues(
|
||||
l := utils.GetLogger(ctx).WithValues(
|
||||
"room", roomName,
|
||||
"reqNodeID", nodeID,
|
||||
"participant", pi.Identity,
|
||||
"connID", connectionID,
|
||||
"participantInit", &pi,
|
||||
"startSession", logger.Proto(ss),
|
||||
)
|
||||
|
||||
l.Debugw("starting signal connection")
|
||||
|
||||
stream, err := r.client.RelaySignal(ctx, nodeID)
|
||||
if err != nil {
|
||||
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
|
||||
prometheus.RecordSignalRequestFailure()
|
||||
return
|
||||
}
|
||||
|
||||
err = stream.Send(&rpc.RelaySignalRequest{StartSession: ss})
|
||||
if err != nil {
|
||||
stream.Close(err)
|
||||
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
|
||||
prometheus.RecordSignalRequestFailure()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,6 +133,8 @@ func (r *signalClient) StartParticipantSignal(
|
||||
resChan,
|
||||
signalResponseMessageReader{},
|
||||
r.config,
|
||||
prometheus.RecordSignalResponseSuccess,
|
||||
prometheus.RecordSignalResponseFailure,
|
||||
)
|
||||
l.Debugw("signal stream closed", "error", err)
|
||||
|
||||
@@ -139,6 +144,8 @@ func (r *signalClient) StartParticipantSignal(
|
||||
return connectionID, sink, resChan, nil
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
|
||||
type signalRequestMessageWriter struct{}
|
||||
|
||||
func (e signalRequestMessageWriter) Write(seq uint64, close bool, msgs []proto.Message) *rpc.RelaySignalRequest {
|
||||
@@ -153,6 +160,8 @@ func (e signalRequestMessageWriter) Write(seq uint64, close bool, msgs []proto.M
|
||||
return r
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
|
||||
type signalResponseMessageReader struct{}
|
||||
|
||||
func (e signalResponseMessageReader) Read(rm *rpc.RelaySignalResponse) ([]proto.Message, error) {
|
||||
@@ -163,6 +172,8 @@ func (e signalResponseMessageReader) Read(rm *rpc.RelaySignalResponse) ([]proto.
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
type RelaySignalMessage interface {
|
||||
proto.Message
|
||||
GetSeq() uint64
|
||||
@@ -182,6 +193,8 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
|
||||
ch *MessageChannel,
|
||||
reader SignalMessageReader[RecvType],
|
||||
config config.SignalRelayConfig,
|
||||
promSignalSuccess func(),
|
||||
promSignalFailure func(),
|
||||
) error {
|
||||
r := &signalMessageReader[SendType, RecvType]{
|
||||
reader: reader,
|
||||
@@ -190,16 +203,16 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
|
||||
for msg := range stream.Channel() {
|
||||
res, err := r.Read(msg)
|
||||
if err != nil {
|
||||
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
|
||||
promSignalFailure()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range res {
|
||||
if err = ch.WriteMessage(r); err != nil {
|
||||
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
|
||||
promSignalFailure()
|
||||
return err
|
||||
}
|
||||
prometheus.MessageCounter.WithLabelValues("signal", "success").Add(1)
|
||||
promSignalSuccess()
|
||||
}
|
||||
|
||||
if msg.GetClose() {
|
||||
@@ -209,6 +222,8 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
|
||||
return stream.Err()
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
|
||||
type signalMessageReader[SendType, RecvType RelaySignalMessage] struct {
|
||||
seq uint64
|
||||
reader SignalMessageReader[RecvType]
|
||||
@@ -236,6 +251,8 @@ func (r *signalMessageReader[SendType, RecvType]) Read(msg RecvType) ([]proto.Me
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
|
||||
type SignalSinkParams[SendType, RecvType RelaySignalMessage] struct {
|
||||
Stream psrpc.Stream[SendType, RecvType]
|
||||
Logger logger.Logger
|
||||
|
||||
+30
-10
@@ -15,6 +15,7 @@
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -45,11 +46,15 @@ func (c ClientInfo) isAndroid() bool {
|
||||
return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Os, "android")
|
||||
}
|
||||
|
||||
func (c ClientInfo) isOBS() bool {
|
||||
return c.ClientInfo != nil && strings.Contains(c.ClientInfo.Browser, "OBS")
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportsAudioRED() bool {
|
||||
return !c.isFirefox() && !c.isSafari()
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportPrflxOverRelay() bool {
|
||||
func (c ClientInfo) SupportsPrflxOverRelay() bool {
|
||||
return !c.isFirefox()
|
||||
}
|
||||
|
||||
@@ -93,21 +98,36 @@ func (c ClientInfo) SupportsChangeRTPSenderEncodingActive() bool {
|
||||
}
|
||||
|
||||
func (c ClientInfo) ComplyWithCodecOrderInSDPAnswer() bool {
|
||||
return !((c.isLinux() || c.isAndroid()) && c.isFirefox())
|
||||
return (!c.isLinux() && !c.isAndroid()) || !c.isFirefox()
|
||||
}
|
||||
|
||||
// Rust SDK can't decode unknown signal message (TrackSubscribed and ErrorResponse)
|
||||
func (c ClientInfo) SupportTrackSubscribedEvent() bool {
|
||||
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_RUST && c.ClientInfo.GetProtocol() < 10)
|
||||
func (c ClientInfo) SupportsTrackSubscribedEvent() bool {
|
||||
return c.ClientInfo.GetSdk() != livekit.ClientInfo_RUST || c.ClientInfo.GetProtocol() >= 10
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportErrorResponse() bool {
|
||||
return c.SupportTrackSubscribedEvent()
|
||||
func (c ClientInfo) SupportsRequestResponse() bool {
|
||||
return c.SupportsTrackSubscribedEvent()
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportSctpZeroChecksum() bool {
|
||||
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_UNKNOWN ||
|
||||
(c.isGo() && c.compareVersion("2.4.0") < 0))
|
||||
func (c ClientInfo) SupportsSctpZeroChecksum() bool {
|
||||
return c.ClientInfo.GetSdk() != livekit.ClientInfo_UNKNOWN &&
|
||||
(!c.isGo() || c.compareVersion("2.4.0") >= 0)
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportsTransceiverReuse() bool {
|
||||
return !c.isSafari()
|
||||
}
|
||||
|
||||
func (c ClientInfo) HasCapability(cap livekit.ClientInfo_Capability) bool {
|
||||
if c.ClientInfo == nil {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(c.ClientInfo.Capabilities, cap)
|
||||
}
|
||||
|
||||
func (c ClientInfo) SupportsPacketTrailer() bool {
|
||||
return c.HasCapability(livekit.ClientInfo_CAP_PACKET_TRAILER)
|
||||
}
|
||||
|
||||
// compareVersion compares a semver against the current client SDK version
|
||||
@@ -121,7 +141,7 @@ func (c ClientInfo) compareVersion(version string) int {
|
||||
parts1 := strings.Split(version, ".")
|
||||
ints0 := make([]int, 3)
|
||||
ints1 := make([]int, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
if len(parts0) > i {
|
||||
ints0[i], _ = strconv.Atoi(parts0[i])
|
||||
}
|
||||
|
||||
+70
-29
@@ -20,13 +20,14 @@ import (
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
||||
act "github.com/livekit/livekit-server/pkg/sfu/rtpextension/abscapturetime"
|
||||
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
|
||||
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
frameMarking = "urn:ietf:params:rtp-hdrext:framemarking"
|
||||
repairedRTPStreamID = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
|
||||
frameMarkingURI = "urn:ietf:params:rtp-hdrext:framemarking"
|
||||
repairedRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
|
||||
)
|
||||
|
||||
type WebRTCConfig struct {
|
||||
@@ -79,23 +80,82 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
|
||||
rtcConf.PacketBufferSizeAudio = rtcConf.PacketBufferSize
|
||||
}
|
||||
|
||||
// publisher configuration
|
||||
publisherConfig := DirectionConfig{
|
||||
return &WebRTCConfig{
|
||||
WebRTCConfig: *webRTCConfig,
|
||||
Receiver: ReceiverConfig{
|
||||
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
|
||||
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
|
||||
},
|
||||
Publisher: getPublisherConfig(false),
|
||||
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdatePublisherConfig(consolidated bool) {
|
||||
c.Publisher = getPublisherConfig(consolidated)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdateSubscriberConfig(ccConf config.CongestionControlConfig) {
|
||||
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
|
||||
c.BufferFactory = factory
|
||||
c.SettingEngine.BufferFactory = factory.GetOrNew
|
||||
}
|
||||
|
||||
func getPublisherConfig(consolidated bool) DirectionConfig {
|
||||
if consolidated {
|
||||
return DirectionConfig{
|
||||
RTPHeaderExtension: RTPHeaderExtensionConfig{
|
||||
Audio: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.AudioLevelURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
Video: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.TransportCCURI,
|
||||
sdp.ABSSendTimeURI,
|
||||
frameMarkingURI,
|
||||
dd.ExtensionURI,
|
||||
repairedRTPStreamIDURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
},
|
||||
RTCPFeedback: RTCPFeedbackConfig{
|
||||
Audio: []webrtc.RTCPFeedback{
|
||||
{Type: webrtc.TypeRTCPFBNACK},
|
||||
},
|
||||
Video: []webrtc.RTCPFeedback{
|
||||
{Type: webrtc.TypeRTCPFBTransportCC},
|
||||
{Type: webrtc.TypeRTCPFBGoogREMB},
|
||||
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
|
||||
{Type: webrtc.TypeRTCPFBNACK},
|
||||
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return DirectionConfig{
|
||||
RTPHeaderExtension: RTPHeaderExtensionConfig{
|
||||
Audio: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.AudioLevelURI,
|
||||
//act.AbsCaptureTimeURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
Video: []string{
|
||||
sdp.SDESMidURI,
|
||||
sdp.SDESRTPStreamIDURI,
|
||||
sdp.TransportCCURI,
|
||||
frameMarking,
|
||||
frameMarkingURI,
|
||||
dd.ExtensionURI,
|
||||
repairedRTPStreamID,
|
||||
//act.AbsCaptureTimeURI,
|
||||
repairedRTPStreamIDURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
},
|
||||
RTCPFeedback: RTCPFeedbackConfig{
|
||||
@@ -110,25 +170,6 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &WebRTCConfig{
|
||||
WebRTCConfig: *webRTCConfig,
|
||||
Receiver: ReceiverConfig{
|
||||
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
|
||||
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
|
||||
},
|
||||
Publisher: publisherConfig,
|
||||
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) UpdateCongestionControl(ccConf config.CongestionControlConfig) {
|
||||
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
|
||||
}
|
||||
|
||||
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
|
||||
c.BufferFactory = factory
|
||||
c.SettingEngine.BufferFactory = factory.GetOrNew
|
||||
}
|
||||
|
||||
func getSubscriberConfig(enableTWCC bool) DirectionConfig {
|
||||
@@ -136,10 +177,10 @@ func getSubscriberConfig(enableTWCC bool) DirectionConfig {
|
||||
RTPHeaderExtension: RTPHeaderExtensionConfig{
|
||||
Video: []string{
|
||||
dd.ExtensionURI,
|
||||
//act.AbsCaptureTimeURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
Audio: []string{
|
||||
//act.AbsCaptureTimeURI,
|
||||
act.AbsCaptureTimeURI,
|
||||
},
|
||||
},
|
||||
RTCPFeedback: RTCPFeedbackConfig{
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var _ types.DataDownTrack = (*DataDownTrack)(nil)
|
||||
var _ types.DataTrackSender = (*DataDownTrack)(nil)
|
||||
|
||||
type DataDownTrackParams struct {
|
||||
Logger logger.Logger
|
||||
SubscriberID livekit.ParticipantID
|
||||
PublishDataTrack types.DataTrack
|
||||
Handle uint16
|
||||
Transport types.DataTrackTransport
|
||||
BytesTrackStats *BytesTrackStats
|
||||
}
|
||||
|
||||
type DataDownTrack struct {
|
||||
params DataDownTrackParams
|
||||
logger logger.Logger
|
||||
createdAt int64
|
||||
}
|
||||
|
||||
func NewDataDownTrack(params DataDownTrackParams) (*DataDownTrack, error) {
|
||||
d := &DataDownTrack{
|
||||
params: params,
|
||||
createdAt: time.Now().UnixNano(),
|
||||
}
|
||||
d.logger = params.Logger.WithValues("name", d.Name(), "handle", d.Handle())
|
||||
|
||||
if err := d.params.PublishDataTrack.AddDataDownTrack(d); err != nil {
|
||||
d.logger.Warnw("could not add data down track", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.logger.Infow("created data down track")
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) Close() {
|
||||
d.logger.Infow("closing data down track")
|
||||
if d.params.BytesTrackStats != nil {
|
||||
d.params.BytesTrackStats.Stop()
|
||||
}
|
||||
d.params.PublishDataTrack.DeleteDataDownTrack(d.SubscriberID())
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) Handle() uint16 {
|
||||
return d.params.Handle
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) PublishDataTrack() types.DataTrack {
|
||||
return d.params.PublishDataTrack
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) ID() livekit.TrackID {
|
||||
return d.params.PublishDataTrack.ID()
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) Name() string {
|
||||
return d.params.PublishDataTrack.Name()
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) SubscriberID() livekit.ParticipantID {
|
||||
// add `createdAt` to ensure repeated subscriptions from same subscriber to same publisher does not collide
|
||||
return livekit.ParticipantID(fmt.Sprintf("%s:%d", d.params.SubscriberID, d.createdAt))
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) WritePacket(data []byte, packet *datatrack.Packet, _arrivalTime int64) {
|
||||
forwardedPacket := *packet
|
||||
forwardedPacket.Handle = d.params.Handle
|
||||
buf, err := forwardedPacket.Marshal()
|
||||
if err != nil {
|
||||
d.logger.Warnw("could not marshal data track message", err)
|
||||
return
|
||||
}
|
||||
if err := d.params.Transport.SendDataTrackMessage(buf); err != nil {
|
||||
d.logger.Warnw("could not send data track message", err)
|
||||
return
|
||||
}
|
||||
if d.params.BytesTrackStats != nil {
|
||||
d.params.BytesTrackStats.AddBytes(uint64(len(buf)), true)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataDownTrack) UpdateSubscriptionOptions(subscriptionOptions *livekit.DataTrackSubscriptionOptions) {
|
||||
// DT-TODO
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/frostbyte73/core"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
sfuutils "github.com/livekit/livekit-server/pkg/sfu/utils"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
errReceiverClosed = errors.New("datatrack is closed")
|
||||
)
|
||||
|
||||
var _ types.DataTrack = (*DataTrack)(nil)
|
||||
|
||||
type DataTrackParams struct {
|
||||
Logger logger.Logger
|
||||
ParticipantID func() livekit.ParticipantID
|
||||
ParticipantIdentity livekit.ParticipantIdentity
|
||||
BytesTrackStats *BytesTrackStats
|
||||
}
|
||||
|
||||
type DataTrack struct {
|
||||
params DataTrackParams
|
||||
|
||||
logger logger.Logger
|
||||
|
||||
lock sync.Mutex
|
||||
dti *livekit.DataTrackInfo
|
||||
subscribedTracks map[livekit.ParticipantID]types.DataDownTrack
|
||||
|
||||
downTrackSpreader *sfuutils.DownTrackSpreader[types.DataTrackSender]
|
||||
|
||||
stats *dataTrackStats
|
||||
|
||||
closed core.Fuse
|
||||
}
|
||||
|
||||
func NewDataTrack(params DataTrackParams, dti *livekit.DataTrackInfo) *DataTrack {
|
||||
d := &DataTrack{
|
||||
params: params,
|
||||
dti: dti,
|
||||
subscribedTracks: make(map[livekit.ParticipantID]types.DataDownTrack),
|
||||
}
|
||||
d.logger = params.Logger.WithValues("name", d.Name(), "handle", dti.PubHandle)
|
||||
d.downTrackSpreader = sfuutils.NewDownTrackSpreader[types.DataTrackSender](sfuutils.DownTrackSpreaderParams{
|
||||
Threshold: 20,
|
||||
Logger: d.logger,
|
||||
})
|
||||
d.stats = newDataTrackStats(dataTrackStatsParams{Logger: d.logger})
|
||||
d.logger.Infow("created data track", "dataTrackInfo", logger.Proto(d.dti))
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DataTrack) Close() {
|
||||
d.logger.Infow("closing data track")
|
||||
d.closed.Break()
|
||||
|
||||
d.stats.Close()
|
||||
if d.params.BytesTrackStats != nil {
|
||||
d.params.BytesTrackStats.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataTrack) PublisherID() livekit.ParticipantID {
|
||||
return d.params.ParticipantID()
|
||||
}
|
||||
|
||||
func (d *DataTrack) PublisherIdentity() livekit.ParticipantIdentity {
|
||||
return d.params.ParticipantIdentity
|
||||
}
|
||||
|
||||
func (d *DataTrack) ToProto() *livekit.DataTrackInfo {
|
||||
return utils.CloneProto(d.dti)
|
||||
}
|
||||
|
||||
func (d *DataTrack) PubHandle() uint16 {
|
||||
return uint16(d.dti.PubHandle)
|
||||
}
|
||||
|
||||
func (d *DataTrack) ID() livekit.TrackID {
|
||||
return livekit.TrackID(d.dti.Sid)
|
||||
}
|
||||
|
||||
func (d *DataTrack) Name() string {
|
||||
return d.dti.Name
|
||||
}
|
||||
|
||||
func (d *DataTrack) AddSubscriber(sub types.LocalParticipant) (types.DataDownTrack, error) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if _, ok := d.subscribedTracks[sub.ID()]; ok {
|
||||
return nil, errAlreadySubscribed
|
||||
}
|
||||
|
||||
bytesStats := NewBytesTrackStats(
|
||||
sub.GetCountry(),
|
||||
d.ID(),
|
||||
sub.ID(),
|
||||
sub.Kind(),
|
||||
sub.KindDetails(),
|
||||
sub.GetTelemetryListener(),
|
||||
sub.GetReporter(),
|
||||
)
|
||||
dataDownTrack, err := NewDataDownTrack(DataDownTrackParams{
|
||||
Logger: sub.GetLogger().WithValues("trackID", d.ID()),
|
||||
SubscriberID: sub.ID(),
|
||||
PublishDataTrack: d,
|
||||
Handle: sub.GetNextSubscribedDataTrackHandle(),
|
||||
Transport: sub.GetDataTrackTransport(),
|
||||
BytesTrackStats: bytesStats,
|
||||
})
|
||||
if err != nil {
|
||||
bytesStats.Stop()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.subscribedTracks[sub.ID()] = dataDownTrack
|
||||
return dataDownTrack, nil
|
||||
}
|
||||
|
||||
func (d *DataTrack) RemoveSubscriber(subID livekit.ParticipantID) {
|
||||
d.lock.Lock()
|
||||
dataDownTrack, ok := d.subscribedTracks[subID]
|
||||
delete(d.subscribedTracks, subID)
|
||||
d.lock.Unlock()
|
||||
|
||||
if ok {
|
||||
dataDownTrack.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataTrack) IsSubscriber(subID livekit.ParticipantID) bool {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
_, ok := d.subscribedTracks[subID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *DataTrack) AddDataDownTrack(dts types.DataTrackSender) error {
|
||||
if d.closed.IsBroken() {
|
||||
return errReceiverClosed
|
||||
}
|
||||
|
||||
if d.downTrackSpreader.HasDownTrack(dts.SubscriberID()) {
|
||||
d.logger.Infow("subscriberID already exists, replacing data downtrack", "subscriberID", dts.SubscriberID())
|
||||
}
|
||||
|
||||
d.downTrackSpreader.Store(dts)
|
||||
d.logger.Infow("data downtrack added", "subscriberID", dts.SubscriberID())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataTrack) DeleteDataDownTrack(subscriberID livekit.ParticipantID) {
|
||||
d.downTrackSpreader.Free(subscriberID)
|
||||
d.logger.Infow("data downtrack deleted", "subscriberID", subscriberID)
|
||||
}
|
||||
|
||||
func (d *DataTrack) HandlePacket(data []byte, packet *datatrack.Packet, arrivalTime int64) {
|
||||
d.stats.Update(packet, arrivalTime, len(data))
|
||||
if d.params.BytesTrackStats != nil {
|
||||
d.params.BytesTrackStats.AddBytes(uint64(len(data)), false)
|
||||
}
|
||||
|
||||
d.downTrackSpreader.Broadcast(func(dts types.DataTrackSender) {
|
||||
dts.WritePacket(data, packet, arrivalTime)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datatrack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type ExtensionParticipantSid struct {
|
||||
participantID livekit.ParticipantID
|
||||
}
|
||||
|
||||
func NewExtensionParticipantSid(participantID livekit.ParticipantID) (*ExtensionParticipantSid, error) {
|
||||
if len(participantID) >= 256 {
|
||||
return nil, errors.New("participantID too long")
|
||||
}
|
||||
|
||||
return &ExtensionParticipantSid{participantID}, nil
|
||||
}
|
||||
|
||||
func (e *ExtensionParticipantSid) ParticipantID() livekit.ParticipantID {
|
||||
return e.participantID
|
||||
}
|
||||
|
||||
func (e *ExtensionParticipantSid) Marshal() (Extension, error) {
|
||||
data := make([]byte, len(e.participantID))
|
||||
copy(data, e.participantID)
|
||||
return Extension{
|
||||
id: uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID),
|
||||
data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *ExtensionParticipantSid) Unmarshal(ext Extension) error {
|
||||
if ext.id != uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID) {
|
||||
return errors.New("invalid extension ID")
|
||||
}
|
||||
|
||||
if len(ext.data) == 0 {
|
||||
return errors.New("empty extension data")
|
||||
}
|
||||
|
||||
e.participantID = livekit.ParticipantID(ext.data)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datatrack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtensionParticipantSid(t *testing.T) {
|
||||
longTestParticipantID := livekit.ParticipantID(make([]byte, 256))
|
||||
_, err := NewExtensionParticipantSid(longTestParticipantID)
|
||||
require.Error(t, err)
|
||||
|
||||
testParticipantID := livekit.ParticipantID("test")
|
||||
extParticipantSid, err := NewExtensionParticipantSid(testParticipantID)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedExt := Extension{
|
||||
id: uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID),
|
||||
data: []byte{'t', 'e', 's', 't'},
|
||||
}
|
||||
ext, err := extParticipantSid.Marshal()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedExt, ext)
|
||||
|
||||
var unmarshaled ExtensionParticipantSid
|
||||
err = unmarshaled.Unmarshal(ext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testParticipantID, unmarshaled.ParticipantID())
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datatrack
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
errHeaderSizeInsufficient = errors.New("data track packet header size insufficient")
|
||||
errBufferSizeInsufficient = errors.New("data track packet buffer size insufficient")
|
||||
errExtensionSizeInsufficient = errors.New("data track packet extension size insufficient")
|
||||
errExtensionNotFound = errors.New("data track packet extension not found")
|
||||
errExtensionSizeTooBig = errors.New("extension size is too big")
|
||||
)
|
||||
|
||||
const (
|
||||
headerLength = 12
|
||||
|
||||
versionShift = 5
|
||||
versionMask = (1 << 3) - 1
|
||||
|
||||
startOfFrameShift = 4
|
||||
startOfFrameMask = (1 << 1) - 1
|
||||
|
||||
finalOfFrameShift = 3
|
||||
finalOfFrameMask = (1 << 1) - 1
|
||||
|
||||
extensionsShift = 2
|
||||
extensionsMask = (1 << 1) - 1
|
||||
|
||||
handleOffset = 2
|
||||
handleLength = 2
|
||||
|
||||
seqNumOffset = 4
|
||||
seqNumLength = 2
|
||||
|
||||
frameNumOffset = 6
|
||||
frameNumLength = 2
|
||||
|
||||
timestampOffset = 8
|
||||
timestampLength = 4
|
||||
|
||||
extensionsSizeOffset = headerLength
|
||||
extensionsSizeLength = 2
|
||||
|
||||
extensionIDLength = 1
|
||||
extensionSizeLength = 1
|
||||
)
|
||||
|
||||
type Extension struct {
|
||||
id uint8
|
||||
data []byte
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Version uint8
|
||||
IsStartOfFrame bool
|
||||
IsFinalOfFrame bool
|
||||
HasExtensions bool
|
||||
Handle uint16
|
||||
SequenceNumber uint16
|
||||
FrameNumber uint16
|
||||
Timestamp uint32
|
||||
ExtensionsSize uint16
|
||||
Extensions []Extension
|
||||
}
|
||||
|
||||
/*
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* 0 1 2 3
|
||||
┆* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* |V |S|F|X| reserved | handle |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* | sequence number | frame number |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* | timestamp |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|* Extensions Size if X=1 | Extensions... |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Each extension
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* 0 1 2 3
|
||||
┆* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
┆* | Extension ID | Extension size| Extension payload |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
End of all extensions
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|* padded to 4 byte boundary if aggregate of `Extensions Size` |
|
||||
|* field and all extensions do not end on a 4 byte boundary |
|
||||
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
|
||||
func (h *Header) Unmarshal(buf []byte) (int, error) {
|
||||
if len(buf) < headerLength {
|
||||
return 0, fmt.Errorf("%w: %d < %d", errHeaderSizeInsufficient, len(buf), headerLength)
|
||||
}
|
||||
|
||||
hdrSize := headerLength
|
||||
h.Version = buf[0] >> versionShift & versionMask
|
||||
h.IsStartOfFrame = (buf[0] >> startOfFrameShift & startOfFrameMask) > 0
|
||||
h.IsFinalOfFrame = (buf[0] >> finalOfFrameShift & finalOfFrameMask) > 0
|
||||
h.HasExtensions = (buf[0] >> extensionsShift & extensionsMask) > 0
|
||||
|
||||
h.Handle = binary.BigEndian.Uint16(buf[handleOffset : handleOffset+handleLength])
|
||||
h.SequenceNumber = binary.BigEndian.Uint16(buf[seqNumOffset : seqNumOffset+seqNumLength])
|
||||
h.FrameNumber = binary.BigEndian.Uint16(buf[frameNumOffset : frameNumOffset+frameNumLength])
|
||||
h.Timestamp = binary.BigEndian.Uint32(buf[timestampOffset : timestampOffset+timestampLength])
|
||||
|
||||
if h.HasExtensions {
|
||||
extensionsSize := (binary.BigEndian.Uint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength])+1)*4 - extensionsSizeLength
|
||||
hdrSize += extensionsSizeLength
|
||||
|
||||
extensionHeaderSize := extensionIDLength + extensionSizeLength
|
||||
remainingSize := int(extensionsSize)
|
||||
idx := extensionsSizeOffset + extensionsSizeLength
|
||||
for remainingSize != 0 {
|
||||
// read extension header
|
||||
if len(buf[idx:]) < extensionIDLength || remainingSize < extensionIDLength {
|
||||
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), extensionIDLength)
|
||||
}
|
||||
id := buf[idx]
|
||||
if id == 0 {
|
||||
// end of extensions, padding has started
|
||||
hdrSize += remainingSize
|
||||
break
|
||||
}
|
||||
|
||||
if len(buf[idx+1:]) < extensionSizeLength || remainingSize < extensionSizeLength {
|
||||
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), extensionSizeLength)
|
||||
}
|
||||
size := int(buf[idx+1])
|
||||
|
||||
remainingSize -= extensionHeaderSize
|
||||
idx += extensionHeaderSize
|
||||
hdrSize += extensionHeaderSize
|
||||
|
||||
// read extension data
|
||||
if len(buf[idx:]) < size || remainingSize < size {
|
||||
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), size)
|
||||
}
|
||||
h.Extensions = append(h.Extensions, Extension{id: id, data: buf[idx : idx+size]})
|
||||
|
||||
remainingSize -= size
|
||||
idx += size
|
||||
hdrSize += size
|
||||
}
|
||||
h.ExtensionsSize = extensionsSize - uint16(remainingSize)
|
||||
}
|
||||
|
||||
return hdrSize, nil
|
||||
}
|
||||
|
||||
func (h *Header) MarshalSize() int {
|
||||
extensionsSize := 0
|
||||
if h.HasExtensions {
|
||||
extensionsSize += extensionsSizeLength
|
||||
for _, ext := range h.Extensions {
|
||||
extensionsSize += len(ext.data) + extensionIDLength + extensionSizeLength
|
||||
}
|
||||
}
|
||||
|
||||
return headerLength + (extensionsSize+3)/4*4
|
||||
}
|
||||
|
||||
func (h *Header) MarshalTo(buf []byte) (int, error) {
|
||||
if len(buf) < headerLength {
|
||||
return 0, fmt.Errorf("%w: %d < %d", errHeaderSizeInsufficient, len(buf), headerLength)
|
||||
}
|
||||
|
||||
hdrSize := headerLength
|
||||
buf[0] = h.Version << versionShift
|
||||
if h.IsStartOfFrame {
|
||||
buf[0] |= (1 << startOfFrameShift)
|
||||
}
|
||||
if h.IsFinalOfFrame {
|
||||
buf[0] |= (1 << finalOfFrameShift)
|
||||
}
|
||||
if h.HasExtensions {
|
||||
buf[0] |= (1 << extensionsShift)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(buf[handleOffset:handleOffset+handleLength], h.Handle)
|
||||
binary.BigEndian.PutUint16(buf[seqNumOffset:seqNumOffset+seqNumLength], h.SequenceNumber)
|
||||
binary.BigEndian.PutUint16(buf[frameNumOffset:frameNumOffset+frameNumLength], h.FrameNumber)
|
||||
binary.BigEndian.PutUint32(buf[timestampOffset:timestampOffset+timestampLength], h.Timestamp)
|
||||
|
||||
if h.HasExtensions {
|
||||
extensionsSize := (extensionsSizeLength + h.ExtensionsSize + 3) / 4 * 4
|
||||
binary.BigEndian.PutUint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength], (extensionsSize/4)-1)
|
||||
hdrSize += extensionsSizeLength
|
||||
|
||||
addedSize := 0
|
||||
idx := extensionsSizeOffset + extensionsSizeLength
|
||||
for _, ext := range h.Extensions {
|
||||
buf[idx] = ext.id
|
||||
if len(ext.data) > 255 {
|
||||
return 0, fmt.Errorf("%w: %d > 255", errExtensionSizeTooBig, len(ext.data))
|
||||
}
|
||||
buf[idx+extensionIDLength] = byte(len(ext.data))
|
||||
copy(buf[idx+extensionIDLength+extensionSizeLength:], ext.data)
|
||||
|
||||
extSize := len(ext.data) + extensionIDLength + extensionSizeLength
|
||||
idx += extSize
|
||||
hdrSize += extSize
|
||||
addedSize += extSize
|
||||
}
|
||||
|
||||
paddingSize := extensionsSize - extensionsSizeLength - uint16(addedSize)
|
||||
for i := range paddingSize {
|
||||
buf[idx+int(i)] = 0
|
||||
}
|
||||
idx += int(paddingSize)
|
||||
hdrSize += int(paddingSize)
|
||||
}
|
||||
|
||||
return hdrSize, nil
|
||||
}
|
||||
|
||||
func (h *Header) AddExtension(ext Extension) {
|
||||
for i, existingExt := range h.Extensions {
|
||||
if existingExt.id == ext.id {
|
||||
h.ExtensionsSize -= uint16(len(existingExt.data) + extensionIDLength + extensionSizeLength)
|
||||
h.Extensions[i].data = ext.data
|
||||
h.ExtensionsSize += uint16(len(h.Extensions[i].data) + extensionIDLength + extensionSizeLength)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.Extensions = append(h.Extensions, ext)
|
||||
h.ExtensionsSize += uint16(len(ext.data) + extensionIDLength + extensionSizeLength)
|
||||
h.HasExtensions = true
|
||||
}
|
||||
|
||||
func (h *Header) GetExtension(id uint8) (Extension, error) {
|
||||
for _, ext := range h.Extensions {
|
||||
if ext.id == id {
|
||||
return ext, nil
|
||||
}
|
||||
}
|
||||
return Extension{}, fmt.Errorf("%w, id: %d", errExtensionNotFound, id)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
type Packet struct {
|
||||
Header
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func (p *Packet) Unmarshal(buf []byte) error {
|
||||
hdrSize, err := p.Header.Unmarshal(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Payload = buf[hdrSize:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) Marshal() ([]byte, error) {
|
||||
buf := make([]byte, p.Header.MarshalSize()+len(p.Payload))
|
||||
if err := p.MarshalTo(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (p *Packet) MarshalTo(buf []byte) error {
|
||||
size := p.Header.MarshalSize() + len(p.Payload)
|
||||
if len(buf) < size {
|
||||
return fmt.Errorf("%w: %d < %d", errBufferSizeInsufficient, len(buf), size)
|
||||
}
|
||||
|
||||
hdrSize, err := p.Header.MarshalTo(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copy(buf[hdrSize:], p.Payload)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datatrack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPacket(t *testing.T) {
|
||||
t.Run("without extension", func(t *testing.T) {
|
||||
payload := make([]byte, 6)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: true,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x18, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
0xfb, 0xfa,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
})
|
||||
|
||||
t.Run("with extension", func(t *testing.T) {
|
||||
payload := make([]byte, 4)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: false,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x04, 0x01, 0x10,
|
||||
0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x72,
|
||||
0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74,
|
||||
0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
|
||||
ext, err := unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var extParticipantSid ExtensionParticipantSid
|
||||
require.NoError(t, extParticipantSid.Unmarshal(ext))
|
||||
require.Equal(t, livekit.ParticipantID("test_participant"), extParticipantSid.ParticipantID())
|
||||
})
|
||||
|
||||
t.Run("with extension padding", func(t *testing.T) {
|
||||
payload := make([]byte, 4)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: false,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x0b,
|
||||
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
|
||||
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
|
||||
ext, err := unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var extParticipantSid ExtensionParticipantSid
|
||||
require.NoError(t, extParticipantSid.Unmarshal(ext))
|
||||
require.Equal(t, livekit.ParticipantID("participant"), extParticipantSid.ParticipantID())
|
||||
})
|
||||
|
||||
t.Run("replace extension", func(t *testing.T) {
|
||||
payload := make([]byte, 4)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: true,
|
||||
IsFinalOfFrame: false,
|
||||
Handle: 3333,
|
||||
SequenceNumber: 6666,
|
||||
FrameNumber: 9999,
|
||||
Timestamp: 0xdeadbeef,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x0b,
|
||||
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
|
||||
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
// replace existing extension ID and ensure that marshalled packet is updated
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err = packet.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedRawPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x04, 0x01, 0x10,
|
||||
0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x72,
|
||||
0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74,
|
||||
0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
require.Equal(t, expectedRawPacket, rawPacket)
|
||||
|
||||
var unmarshaled Packet
|
||||
err = unmarshaled.Unmarshal(rawPacket)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, packet, &unmarshaled)
|
||||
|
||||
ext, err := unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var extParticipantSid ExtensionParticipantSid
|
||||
require.NoError(t, extParticipantSid.Unmarshal(ext))
|
||||
require.Equal(t, livekit.ParticipantID("test_participant"), extParticipantSid.ParticipantID())
|
||||
})
|
||||
|
||||
t.Run("bad packet", func(t *testing.T) {
|
||||
var unmarshaled Packet
|
||||
// extensions size too small
|
||||
badPacket := []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x02, 0x01, 0x0b,
|
||||
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
|
||||
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
err := unmarshaled.Unmarshal(badPacket)
|
||||
require.Error(t, err)
|
||||
|
||||
// get an invalid extension id
|
||||
badPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x02, 0x0b,
|
||||
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
|
||||
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
err = unmarshaled.Unmarshal(badPacket)
|
||||
require.NoError(t, err)
|
||||
_, err = unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
|
||||
require.Error(t, err)
|
||||
|
||||
// extension payload size bigger than payload
|
||||
badPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x0d,
|
||||
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
|
||||
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
err = unmarshaled.Unmarshal(badPacket)
|
||||
require.Error(t, err)
|
||||
|
||||
// extension payload size smaller than payload
|
||||
badPacket = []byte{
|
||||
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
|
||||
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x07,
|
||||
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
|
||||
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
|
||||
}
|
||||
err = unmarshaled.Unmarshal(badPacket)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datatrack
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateRawDataPackets(handle uint16, seqNum uint16, frameNum uint16, numFrames int, frameSize int, frameDuration time.Duration) [][]byte {
|
||||
if seqNum == 0 {
|
||||
seqNum = uint16(rand.Intn(256) + 1)
|
||||
}
|
||||
if frameNum == 0 {
|
||||
frameNum = uint16(rand.Intn(256) + 1)
|
||||
}
|
||||
timestamp := uint32(rand.Intn(1024))
|
||||
|
||||
packetsPerFrame := (frameSize + 255) / 256 // using 256 bytes of payload per packet
|
||||
if packetsPerFrame == 0 {
|
||||
return nil
|
||||
}
|
||||
numPackets := packetsPerFrame * numFrames
|
||||
rawPackets := make([][]byte, 0, numPackets)
|
||||
for range numFrames {
|
||||
remainingSize := frameSize
|
||||
for packetIdx := range packetsPerFrame {
|
||||
payloadSize := min(remainingSize, 256)
|
||||
payload := make([]byte, payloadSize)
|
||||
for i := range len(payload) {
|
||||
payload[i] = byte(255 - i)
|
||||
}
|
||||
packet := &Packet{
|
||||
Header: Header{
|
||||
Version: 0,
|
||||
IsStartOfFrame: packetIdx == 0,
|
||||
IsFinalOfFrame: packetIdx == packetsPerFrame-1,
|
||||
Handle: handle,
|
||||
SequenceNumber: seqNum,
|
||||
FrameNumber: frameNum,
|
||||
Timestamp: timestamp,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
|
||||
if ext, err := extParticipantSid.Marshal(); err == nil {
|
||||
packet.AddExtension(ext)
|
||||
}
|
||||
}
|
||||
rawPacket, err := packet.Marshal()
|
||||
if err == nil {
|
||||
rawPackets = append(rawPackets, rawPacket)
|
||||
}
|
||||
seqNum++
|
||||
remainingSize -= payloadSize
|
||||
}
|
||||
frameNum++
|
||||
timestamp += uint32(90000 * frameDuration.Seconds())
|
||||
}
|
||||
|
||||
return rawPackets
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
)
|
||||
|
||||
type dataTrackStatsParams struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type dataTrackStats struct {
|
||||
params dataTrackStatsParams
|
||||
|
||||
lock sync.Mutex
|
||||
startTime int64
|
||||
endTime int64
|
||||
highestSequenceNumber uint16
|
||||
numPackets int
|
||||
numPacketsLost int
|
||||
numPacketsOutOfOrder int
|
||||
numFrames int // count of `F` tagged packets, i. e. packets with final packet of frame marker
|
||||
numBytes int
|
||||
}
|
||||
|
||||
func newDataTrackStats(params dataTrackStatsParams) *dataTrackStats {
|
||||
return &dataTrackStats{
|
||||
params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dataTrackStats) Update(packet *datatrack.Packet, arrivalTime int64, payloadLength int) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.numBytes += payloadLength
|
||||
|
||||
if d.endTime != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if d.startTime == 0 {
|
||||
d.startTime = arrivalTime
|
||||
d.highestSequenceNumber = packet.SequenceNumber
|
||||
d.numPackets = 1
|
||||
} else {
|
||||
diff := packet.SequenceNumber - d.highestSequenceNumber
|
||||
switch {
|
||||
case diff == 0: // duplicate
|
||||
return
|
||||
|
||||
case diff > (1 << 15): // out of order
|
||||
d.numPackets++
|
||||
d.numPacketsOutOfOrder++
|
||||
if d.numPacketsLost > 0 {
|
||||
d.numPacketsLost--
|
||||
}
|
||||
|
||||
default: // in order
|
||||
d.numPackets++
|
||||
d.numPacketsLost += int(diff) - 1
|
||||
d.highestSequenceNumber = packet.SequenceNumber
|
||||
}
|
||||
}
|
||||
|
||||
if packet.IsFinalOfFrame {
|
||||
d.numFrames++
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dataTrackStats) Close() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.endTime = mono.UnixNano()
|
||||
|
||||
if d.startTime != 0 {
|
||||
duration := time.Duration(d.endTime - d.startTime).Seconds()
|
||||
fps := float64(d.numFrames) / duration
|
||||
|
||||
d.params.Logger.Infow(
|
||||
"data track stats",
|
||||
"duration", duration,
|
||||
"numPackets", d.numPackets,
|
||||
"numPacketsLost", d.numPacketsLost,
|
||||
"numPacketsOutOfOrder", d.numPacketsOutOfOrder,
|
||||
"numFrames", d.numFrames,
|
||||
"fps", fps,
|
||||
"numBytes", d.numBytes,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -23,20 +24,41 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
func TestSubscribedMaxQuality(t *testing.T) {
|
||||
type testDynacastManagerListener struct {
|
||||
onSubscribedMaxQualityChange func(subscribedQualties []*livekit.SubscribedCodec)
|
||||
onSubscribedAudioCodecChange func(subscribedCodecs []*livekit.SubscribedAudioCodec)
|
||||
}
|
||||
|
||||
func (t *testDynacastManagerListener) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
_maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
t.onSubscribedMaxQualityChange(subscribedQualities)
|
||||
}
|
||||
|
||||
func (t *testDynacastManagerListener) OnDynacastSubscribedAudioCodecChange(
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
t.onSubscribedAudioCodecChange(codecs)
|
||||
}
|
||||
|
||||
func TestSubscribedMaxQuality(t *testing.T) {
|
||||
t.Run("subscribers muted", func(t *testing.T) {
|
||||
dm := NewDynacastManager(DynacastManagerParams{})
|
||||
var lock sync.Mutex
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
|
||||
@@ -72,21 +94,20 @@ func TestSubscribedMaxQuality(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("subscribers max quality", func(t *testing.T) {
|
||||
dm := NewDynacastManager(DynacastManagerParams{
|
||||
DynacastPauseDelay: 100 * time.Millisecond,
|
||||
})
|
||||
|
||||
lock := sync.RWMutex{}
|
||||
lock.Lock()
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
lock.Unlock()
|
||||
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
|
||||
dm.(*dynacastManagerVideo).maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
|
||||
mime.MimeTypeVP8: livekit.VideoQuality_LOW,
|
||||
mime.MimeTypeAV1: livekit.VideoQuality_LOW,
|
||||
}
|
||||
@@ -279,98 +300,224 @@ func TestSubscribedMaxQuality(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCodecRegression(t *testing.T) {
|
||||
dm := NewDynacastManager(DynacastManagerParams{})
|
||||
var lock sync.Mutex
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
t.Run("codec regression video", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
|
||||
|
||||
expectedSubscribedQualities := []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
dm.HandleCodecRegression(mime.MimeTypeAV1, mime.MimeTypeVP8)
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// av1 quality change should be forwarded to vp8
|
||||
// av1 quality change of node should be ignored
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
|
||||
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
|
||||
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_HIGH},
|
||||
})
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
|
||||
t.Run("codec regression audio", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0)
|
||||
|
||||
expectedSubscribedQualities := []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
dm := NewDynacastManagerAudio(DynacastManagerAudioParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedAudioCodecChange: func(subscribedCodecs []*livekit.SubscribedAudioCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedCodecs = subscribedCodecs
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
})
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
dm.NotifySubscription("s1", mime.MimeTypeRED, true)
|
||||
|
||||
dm.HandleCodecRegression(mime.MimeTypeAV1, mime.MimeTypeVP8)
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
expectedSubscribedCodecs := []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
dm.HandleCodecRegression(mime.MimeTypeRED, mime.MimeTypeOpus)
|
||||
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// RED disable as subscriber or subscriber node should be ignored as it has been regressed
|
||||
dm.NotifySubscription("s1", mime.MimeTypeRED, false)
|
||||
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
|
||||
{Codec: mime.MimeTypeRED.String(), Enabled: false},
|
||||
})
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// `s1` unsubscription should turn off `opus`
|
||||
dm.NotifySubscription("s1", mime.MimeTypeOpus, false)
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// a node subscription should turn `opus` back on
|
||||
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
})
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// av1 quality change should be forwarded to vp8
|
||||
// av1 quality change of node should be ignored
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
|
||||
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
|
||||
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_HIGH},
|
||||
})
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func subscribedCodecsAsString(c1 []*livekit.SubscribedCodec) string {
|
||||
sort.Slice(c1, func(i, j int) bool { return c1[i].Codec < c1[j].Codec })
|
||||
var s1 string
|
||||
slices.SortFunc(c1, func(a, b *livekit.SubscribedCodec) int {
|
||||
return strings.Compare(a.Codec, b.Codec)
|
||||
})
|
||||
|
||||
var s1 strings.Builder
|
||||
for _, c := range c1 {
|
||||
s1 += c.String()
|
||||
s1.WriteString(c.String())
|
||||
}
|
||||
return s1
|
||||
return s1.String()
|
||||
}
|
||||
|
||||
func subscribedAudioCodecsAsString(c1 []*livekit.SubscribedAudioCodec) string {
|
||||
slices.SortFunc(c1, func(a, b *livekit.SubscribedAudioCodec) int {
|
||||
return strings.Compare(a.Codec, b.Codec)
|
||||
})
|
||||
|
||||
var s1 strings.Builder
|
||||
for _, c := range c1 {
|
||||
s1.WriteString(c.String())
|
||||
}
|
||||
return s1.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
)
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerAudio)(nil)
|
||||
var _ dynacastQualityListener = (*dynacastManagerAudio)(nil)
|
||||
|
||||
type DynacastManagerAudioParams struct {
|
||||
Listener DynacastManagerListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type dynacastManagerAudio struct {
|
||||
params DynacastManagerAudioParams
|
||||
|
||||
subscribedCodecs map[mime.MimeType]bool
|
||||
committedSubscribedCodecs map[mime.MimeType]bool
|
||||
|
||||
isClosed bool
|
||||
|
||||
*dynacastManagerBase
|
||||
}
|
||||
|
||||
func NewDynacastManagerAudio(params DynacastManagerAudioParams) DynacastManager {
|
||||
if params.Logger == nil {
|
||||
params.Logger = logger.GetLogger()
|
||||
}
|
||||
d := &dynacastManagerAudio{
|
||||
params: params,
|
||||
subscribedCodecs: make(map[mime.MimeType]bool),
|
||||
committedSubscribedCodecs: make(map[mime.MimeType]bool),
|
||||
}
|
||||
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
|
||||
Logger: params.Logger,
|
||||
OpsQueueDepth: 4,
|
||||
OnRestart: func() {
|
||||
d.committedSubscribedCodecs = make(map[mime.MimeType]bool)
|
||||
},
|
||||
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
|
||||
dq := newDynacastQualityAudio(dynacastQualityAudioParams{
|
||||
MimeType: mimeType,
|
||||
Listener: d,
|
||||
Logger: d.params.Logger,
|
||||
})
|
||||
return dq
|
||||
},
|
||||
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
|
||||
d.subscribedCodecs[fromMime] = false
|
||||
|
||||
// if the new codec is not added, notify the publisher to start publishing
|
||||
if _, ok := d.subscribedCodecs[toMime]; !ok {
|
||||
d.subscribedCodecs[toMime] = true
|
||||
}
|
||||
},
|
||||
OnUpdateNeeded: d.update,
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
// It is possible for tracks to be in pending close state. When track
|
||||
// is waiting to be closed, a node is not streaming a track. This can
|
||||
// be used to force an update announcing that subscribed codec is disabled,
|
||||
// i.e. indicating not pulling track any more.
|
||||
func (d *dynacastManagerAudio) ForceEnable(enabled bool) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
for mime := range d.committedSubscribedCodecs {
|
||||
d.committedSubscribedCodecs[mime] = enabled
|
||||
}
|
||||
|
||||
d.enqueueSubscribedChange()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
dq := d.getOrCreateDynacastQuality(mime)
|
||||
if dq != nil {
|
||||
dq.NotifySubscription(subscriberID, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
for _, codec := range codecs {
|
||||
dq := d.getOrCreateDynacastQuality(mime.NormalizeMimeType(codec.Codec))
|
||||
if dq != nil {
|
||||
dq.NotifySubscriptionNode(nodeID, codec.Enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) OnUpdateAudioCodecForMime(mime mime.MimeType, enabled bool) {
|
||||
d.lock.Lock()
|
||||
if _, ok := d.regressedCodec[mime]; !ok {
|
||||
d.subscribedCodecs[mime] = enabled
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.update(false)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) update(force bool) {
|
||||
d.lock.Lock()
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"processing subscribed codec change",
|
||||
"force", force,
|
||||
"committedSubscribedCodecs", d.committedSubscribedCodecs,
|
||||
"subscribedCodecs", d.subscribedCodecs,
|
||||
)
|
||||
|
||||
if len(d.subscribedCodecs) == 0 {
|
||||
// no mime has been added, nothing to update
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// add or remove of a mime triggers an update
|
||||
changed := len(d.subscribedCodecs) != len(d.committedSubscribedCodecs)
|
||||
if !changed {
|
||||
for mime, enabled := range d.subscribedCodecs {
|
||||
if ce, ok := d.committedSubscribedCodecs[mime]; ok {
|
||||
if ce != enabled {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !force && !changed {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"committing subscribed codec change",
|
||||
"force", force,
|
||||
"committedSubscribedCoecs", d.committedSubscribedCodecs,
|
||||
"subscribedcodecs", d.subscribedCodecs,
|
||||
)
|
||||
|
||||
// commit change
|
||||
d.committedSubscribedCodecs = make(map[mime.MimeType]bool, len(d.subscribedCodecs))
|
||||
for mime, enabled := range d.subscribedCodecs {
|
||||
d.committedSubscribedCodecs[mime] = enabled
|
||||
}
|
||||
|
||||
d.enqueueSubscribedChange()
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) enqueueSubscribedChange() {
|
||||
if d.isClosed || d.params.Listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
subscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0, len(d.committedSubscribedCodecs))
|
||||
for mime, enabled := range d.committedSubscribedCodecs {
|
||||
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedAudioCodec{
|
||||
Codec: mime.String(),
|
||||
Enabled: enabled,
|
||||
})
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"subscribedAudioCodecChange",
|
||||
"subscribedCodecs", logger.ProtoSlice(subscribedCodecs),
|
||||
)
|
||||
d.notifyOpsQueue.Enqueue(func() {
|
||||
d.params.Listener.OnDynacastSubscribedAudioCodecChange(subscribedCodecs)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
)
|
||||
|
||||
type dynacastManagerBaseParams struct {
|
||||
Logger logger.Logger
|
||||
OpsQueueDepth uint
|
||||
OnRestart func()
|
||||
OnDynacastQualityCreate func(mimeType mime.MimeType) dynacastQuality
|
||||
OnRegressCodec func(fromMime, toMime mime.MimeType)
|
||||
OnUpdateNeeded func(force bool)
|
||||
}
|
||||
|
||||
type dynacastManagerBase struct {
|
||||
params dynacastManagerBaseParams
|
||||
|
||||
lock sync.RWMutex
|
||||
regressedCodec map[mime.MimeType]struct{}
|
||||
dynacastQuality map[mime.MimeType]dynacastQuality
|
||||
|
||||
notifyOpsQueue *utils.OpsQueue
|
||||
|
||||
isClosed bool
|
||||
|
||||
dynacastManagerNull
|
||||
dynacastQualityListenerNull
|
||||
}
|
||||
|
||||
func newDynacastManagerBase(params dynacastManagerBaseParams) *dynacastManagerBase {
|
||||
if params.OpsQueueDepth == 0 {
|
||||
params.OpsQueueDepth = 4
|
||||
}
|
||||
d := &dynacastManagerBase{
|
||||
params: params,
|
||||
regressedCodec: make(map[mime.MimeType]struct{}),
|
||||
dynacastQuality: make(map[mime.MimeType]dynacastQuality),
|
||||
notifyOpsQueue: utils.NewOpsQueue(utils.OpsQueueParams{
|
||||
Name: "dynacast-notify",
|
||||
MinSize: params.OpsQueueDepth,
|
||||
FlushOnStop: true,
|
||||
Logger: params.Logger,
|
||||
}),
|
||||
}
|
||||
d.notifyOpsQueue.Start()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) AddCodec(mime mime.MimeType) {
|
||||
d.getOrCreateDynacastQuality(mime)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) HandleCodecRegression(fromMime, toMime mime.MimeType) {
|
||||
fromDq := d.getOrCreateDynacastQuality(fromMime)
|
||||
|
||||
d.lock.Lock()
|
||||
if d.isClosed {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if fromDq == nil {
|
||||
// should not happen as we have added the codec on setup receiver
|
||||
d.params.Logger.Warnw("regression from codec not found", nil, "mime", fromMime, "toMime", toMime)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
d.regressedCodec[fromMime] = struct{}{}
|
||||
d.params.OnRegressCodec(fromMime, toMime)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.OnUpdateNeeded(false)
|
||||
|
||||
fromDq.Stop()
|
||||
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) Restart() {
|
||||
d.lock.Lock()
|
||||
d.params.OnRestart()
|
||||
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.lock.Unlock()
|
||||
|
||||
for _, dq := range dqs {
|
||||
dq.Restart()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) Close() {
|
||||
d.notifyOpsQueue.Stop()
|
||||
|
||||
d.lock.Lock()
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.dynacastQuality = make(map[mime.MimeType]dynacastQuality)
|
||||
|
||||
d.isClosed = true
|
||||
d.lock.Unlock()
|
||||
|
||||
for _, dq := range dqs {
|
||||
dq.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// There are situations like track unmute or streaming from a different node
|
||||
// where subscription changes needs to sent to the provider immediately.
|
||||
// This bypasses any debouncing and forces a subscription change update
|
||||
// with immediate effect.
|
||||
func (d *dynacastManagerBase) ForceUpdate() {
|
||||
d.params.OnUpdateNeeded(true)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.lock.Unlock()
|
||||
for _, dq := range dqs {
|
||||
dq.ClearSubscriberNodes()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) getOrCreateDynacastQuality(mimeType mime.MimeType) dynacastQuality {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if d.isClosed || mimeType == mime.MimeTypeUnknown {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dq := d.dynacastQuality[mimeType]; dq != nil {
|
||||
return dq
|
||||
}
|
||||
|
||||
dq := d.params.OnDynacastQualityCreate(mimeType)
|
||||
dq.Start()
|
||||
|
||||
d.dynacastQuality[mimeType] = dq
|
||||
return dq
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) getDynacastQualitiesLocked() []dynacastQuality {
|
||||
return slices.Collect(maps.Values(d.dynacastQuality))
|
||||
}
|
||||
@@ -15,148 +15,85 @@
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
)
|
||||
|
||||
type DynacastManagerParams struct {
|
||||
var _ DynacastManager = (*dynacastManagerVideo)(nil)
|
||||
var _ dynacastQualityListener = (*dynacastManagerVideo)(nil)
|
||||
|
||||
type DynacastManagerVideoParams struct {
|
||||
DynacastPauseDelay time.Duration
|
||||
Listener DynacastManagerListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type DynacastManager struct {
|
||||
params DynacastManagerParams
|
||||
type dynacastManagerVideo struct {
|
||||
params DynacastManagerVideoParams
|
||||
|
||||
lock sync.RWMutex
|
||||
regressedCodec map[mime.MimeType]struct{}
|
||||
dynacastQuality map[mime.MimeType]*DynacastQuality
|
||||
maxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
|
||||
committedMaxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
|
||||
|
||||
maxSubscribedQualityDebounce func(func())
|
||||
maxSubscribedQualityDebouncePending bool
|
||||
|
||||
qualityNotifyOpQueue *utils.OpsQueue
|
||||
|
||||
isClosed bool
|
||||
|
||||
onSubscribedMaxQualityChange func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)
|
||||
*dynacastManagerBase
|
||||
}
|
||||
|
||||
func NewDynacastManager(params DynacastManagerParams) *DynacastManager {
|
||||
func NewDynacastManagerVideo(params DynacastManagerVideoParams) DynacastManager {
|
||||
if params.Logger == nil {
|
||||
params.Logger = logger.GetLogger()
|
||||
}
|
||||
d := &DynacastManager{
|
||||
d := &dynacastManagerVideo{
|
||||
params: params,
|
||||
regressedCodec: make(map[mime.MimeType]struct{}),
|
||||
dynacastQuality: make(map[mime.MimeType]*DynacastQuality),
|
||||
maxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
|
||||
committedMaxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
|
||||
qualityNotifyOpQueue: utils.NewOpsQueue(utils.OpsQueueParams{
|
||||
Name: "quality-notify",
|
||||
MinSize: 64,
|
||||
FlushOnStop: true,
|
||||
Logger: params.Logger,
|
||||
}),
|
||||
}
|
||||
if params.DynacastPauseDelay > 0 {
|
||||
d.maxSubscribedQualityDebounce = debounce.New(params.DynacastPauseDelay)
|
||||
}
|
||||
d.qualityNotifyOpQueue.Start()
|
||||
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
|
||||
Logger: params.Logger,
|
||||
OpsQueueDepth: 64,
|
||||
OnRestart: func() {
|
||||
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
|
||||
},
|
||||
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
|
||||
dq := newDynacastQualityVideo(dynacastQualityVideoParams{
|
||||
MimeType: mimeType,
|
||||
Listener: d,
|
||||
Logger: d.params.Logger,
|
||||
})
|
||||
return dq
|
||||
},
|
||||
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
|
||||
d.maxSubscribedQuality[fromMime] = livekit.VideoQuality_OFF
|
||||
|
||||
// if the new codec is not added, notify the publisher to start publishing
|
||||
if _, ok := d.maxSubscribedQuality[toMime]; !ok {
|
||||
d.maxSubscribedQuality[toMime] = livekit.VideoQuality_HIGH
|
||||
}
|
||||
},
|
||||
OnUpdateNeeded: d.update,
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DynacastManager) OnSubscribedMaxQualityChange(f func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)) {
|
||||
d.lock.Lock()
|
||||
d.onSubscribedMaxQualityChange = f
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *DynacastManager) AddCodec(mime mime.MimeType) {
|
||||
d.getOrCreateDynacastQuality(mime)
|
||||
}
|
||||
|
||||
func (d *DynacastManager) HandleCodecRegression(fromMime, toMime mime.MimeType) {
|
||||
fromDq := d.getOrCreateDynacastQuality(fromMime)
|
||||
|
||||
d.lock.Lock()
|
||||
if d.isClosed {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if fromDq == nil {
|
||||
// should not happen as we have added the codec on setup receiver
|
||||
d.params.Logger.Warnw("regression from codec not found", nil, "mime", fromMime)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
d.regressedCodec[fromMime] = struct{}{}
|
||||
d.maxSubscribedQuality[fromMime] = livekit.VideoQuality_OFF
|
||||
|
||||
// if the new codec is not added, notify the publisher to start publishing
|
||||
if _, ok := d.maxSubscribedQuality[toMime]; !ok {
|
||||
d.maxSubscribedQuality[toMime] = livekit.VideoQuality_HIGH
|
||||
}
|
||||
|
||||
d.lock.Unlock()
|
||||
d.update(false)
|
||||
|
||||
fromDq.Stop()
|
||||
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
|
||||
}
|
||||
|
||||
func (d *DynacastManager) Restart() {
|
||||
d.lock.Lock()
|
||||
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
|
||||
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.lock.Unlock()
|
||||
|
||||
for _, dq := range dqs {
|
||||
dq.Restart()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DynacastManager) Close() {
|
||||
d.qualityNotifyOpQueue.Stop()
|
||||
|
||||
d.lock.Lock()
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.dynacastQuality = make(map[mime.MimeType]*DynacastQuality)
|
||||
|
||||
d.isClosed = true
|
||||
d.lock.Unlock()
|
||||
|
||||
for _, dq := range dqs {
|
||||
dq.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// THere are situations like track unmute or streaming from a different node
|
||||
// where subscribed quality needs to sent to the provider immediately.
|
||||
// This bypasses any debouncing and forces a subscribed quality update
|
||||
// with immediate effect.
|
||||
func (d *DynacastManager) ForceUpdate() {
|
||||
d.update(true)
|
||||
}
|
||||
|
||||
// It is possible for tracks to be in pending close state. When track
|
||||
// is waiting to be closed, a node is not streaming a track. This can
|
||||
// be used to force an update announcing that subscribed quality is OFF,
|
||||
// i.e. indicating not pulling track any more.
|
||||
func (d *DynacastManager) ForceQuality(quality livekit.VideoQuality) {
|
||||
func (d *dynacastManagerVideo) ForceQuality(quality livekit.VideoQuality) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
@@ -167,14 +104,21 @@ func (d *DynacastManager) ForceQuality(quality livekit.VideoQuality) {
|
||||
d.enqueueSubscribedQualityChange()
|
||||
}
|
||||
|
||||
func (d *DynacastManager) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, mime mime.MimeType, quality livekit.VideoQuality) {
|
||||
func (d *dynacastManagerVideo) NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
) {
|
||||
dq := d.getOrCreateDynacastQuality(mime)
|
||||
if dq != nil {
|
||||
dq.NotifySubscriberMaxQuality(subscriberID, quality)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DynacastManager) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
|
||||
func (d *dynacastManagerVideo) NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
for _, quality := range qualities {
|
||||
dq := d.getOrCreateDynacastQuality(quality.CodecMime)
|
||||
if dq != nil {
|
||||
@@ -183,34 +127,10 @@ func (d *DynacastManager) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DynacastManager) getOrCreateDynacastQuality(mimeType mime.MimeType) *DynacastQuality {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if d.isClosed || mimeType == mime.MimeTypeUnknown {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dq := d.dynacastQuality[mimeType]; dq != nil {
|
||||
return dq
|
||||
}
|
||||
|
||||
dq := NewDynacastQuality(DynacastQualityParams{
|
||||
MimeType: mimeType,
|
||||
Logger: d.params.Logger,
|
||||
})
|
||||
dq.OnSubscribedMaxQualityChange(d.updateMaxQualityForMime)
|
||||
dq.Start()
|
||||
|
||||
d.dynacastQuality[mimeType] = dq
|
||||
return dq
|
||||
}
|
||||
|
||||
func (d *DynacastManager) getDynacastQualitiesLocked() []*DynacastQuality {
|
||||
return maps.Values(d.dynacastQuality)
|
||||
}
|
||||
|
||||
func (d *DynacastManager) updateMaxQualityForMime(mime mime.MimeType, maxQuality livekit.VideoQuality) {
|
||||
func (d *dynacastManagerVideo) OnUpdateMaxQualityForMime(
|
||||
mime mime.MimeType,
|
||||
maxQuality livekit.VideoQuality,
|
||||
) {
|
||||
d.lock.Lock()
|
||||
if _, ok := d.regressedCodec[mime]; !ok {
|
||||
d.maxSubscribedQuality[mime] = maxQuality
|
||||
@@ -220,10 +140,11 @@ func (d *DynacastManager) updateMaxQualityForMime(mime mime.MimeType, maxQuality
|
||||
d.update(false)
|
||||
}
|
||||
|
||||
func (d *DynacastManager) update(force bool) {
|
||||
func (d *dynacastManagerVideo) update(force bool) {
|
||||
d.lock.Lock()
|
||||
|
||||
d.params.Logger.Debugw("processing quality change",
|
||||
d.params.Logger.Debugw(
|
||||
"processing quality change",
|
||||
"force", force,
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
@@ -260,7 +181,8 @@ func (d *DynacastManager) update(force bool) {
|
||||
|
||||
if downgradesOnly && d.maxSubscribedQualityDebounce != nil {
|
||||
if !d.maxSubscribedQualityDebouncePending {
|
||||
d.params.Logger.Debugw("debouncing quality downgrade",
|
||||
d.params.Logger.Debugw(
|
||||
"debouncing quality downgrade",
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
)
|
||||
@@ -269,7 +191,8 @@ func (d *DynacastManager) update(force bool) {
|
||||
})
|
||||
d.maxSubscribedQualityDebouncePending = true
|
||||
} else {
|
||||
d.params.Logger.Debugw("quality downgrade waiting for debounce",
|
||||
d.params.Logger.Debugw(
|
||||
"quality downgrade waiting for debounce",
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
)
|
||||
@@ -285,7 +208,8 @@ func (d *DynacastManager) update(force bool) {
|
||||
d.maxSubscribedQualityDebouncePending = false
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw("committing quality change",
|
||||
d.params.Logger.Debugw(
|
||||
"committing quality change",
|
||||
"force", force,
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
@@ -293,16 +217,14 @@ func (d *DynacastManager) update(force bool) {
|
||||
|
||||
// commit change
|
||||
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality, len(d.maxSubscribedQuality))
|
||||
for mime, quality := range d.maxSubscribedQuality {
|
||||
d.committedMaxSubscribedQuality[mime] = quality
|
||||
}
|
||||
maps.Copy(d.committedMaxSubscribedQuality, d.maxSubscribedQuality)
|
||||
|
||||
d.enqueueSubscribedQualityChange()
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *DynacastManager) enqueueSubscribedQualityChange() {
|
||||
if d.isClosed || d.onSubscribedMaxQualityChange == nil {
|
||||
func (d *dynacastManagerVideo) enqueueSubscribedQualityChange() {
|
||||
if d.isClosed || d.params.Listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -343,7 +265,7 @@ func (d *DynacastManager) enqueueSubscribedQualityChange() {
|
||||
"subscribedCodecs", subscribedCodecs,
|
||||
"maxSubscribedQualities", maxSubscribedQualities,
|
||||
)
|
||||
d.qualityNotifyOpQueue.Enqueue(func() {
|
||||
d.onSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
|
||||
d.notifyOpsQueue.Enqueue(func() {
|
||||
d.params.Listener.OnDynacastSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityAudio)(nil)
|
||||
|
||||
type dynacastQualityAudioParams struct {
|
||||
MimeType mime.MimeType
|
||||
Listener dynacastQualityListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
// dynacastQualityAudio manages enable a single receiver of a media track
|
||||
type dynacastQualityAudio struct {
|
||||
params dynacastQualityAudioParams
|
||||
|
||||
// quality level enable/disable
|
||||
lock sync.RWMutex
|
||||
initialized bool
|
||||
subscriberEnables map[livekit.ParticipantID]bool
|
||||
subscriberNodeEnables map[livekit.NodeID]bool
|
||||
enabled bool
|
||||
regressTo dynacastQuality
|
||||
|
||||
dynacastQualityNull
|
||||
}
|
||||
|
||||
func newDynacastQualityAudio(params dynacastQualityAudioParams) dynacastQuality {
|
||||
return &dynacastQualityAudio{
|
||||
params: params,
|
||||
subscriberEnables: make(map[livekit.ParticipantID]bool),
|
||||
subscriberNodeEnables: make(map[livekit.NodeID]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Start() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Restart() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Stop() {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber codec enable",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberID", subscriberID,
|
||||
"enabled", enabled,
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
delete(d.subscriberEnables, subscriberID)
|
||||
} else {
|
||||
d.subscriberEnables[subscriberID] = true
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber node codec enabled",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberNodeID", nodeID,
|
||||
"enabled", enabled,
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
// the downstream node will synthesize correct enable (its dynacast manager has codec regression), just ignore it
|
||||
d.params.Logger.Debugw(
|
||||
"ignoring node codec change, regressed to another dynacast quality",
|
||||
"mime", d.params.MimeType,
|
||||
"regressedMime", d.regressTo.Mime(),
|
||||
)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
delete(d.subscriberNodeEnables, nodeID)
|
||||
} else {
|
||||
d.subscriberNodeEnables[nodeID] = true
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
d.subscriberNodeEnables = make(map[livekit.NodeID]bool)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Mime() mime.MimeType {
|
||||
return d.params.MimeType
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) RegressTo(other dynacastQuality) {
|
||||
d.lock.Lock()
|
||||
d.regressTo = other
|
||||
d.lock.Unlock()
|
||||
|
||||
other.Restart()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) reset() {
|
||||
d.lock.Lock()
|
||||
d.initialized = false
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) updateQualityChange(force bool) {
|
||||
d.lock.Lock()
|
||||
enabled := len(d.subscriberEnables) != 0 || len(d.subscriberNodeEnables) != 0
|
||||
if enabled == d.enabled && d.initialized && !force {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
d.initialized = true
|
||||
d.enabled = enabled
|
||||
d.params.Logger.Debugw(
|
||||
"notifying enabled change",
|
||||
"mime", d.params.MimeType,
|
||||
"enabled", d.enabled,
|
||||
"subscriberNodeEnables", d.subscriberNodeEnables,
|
||||
"subscribedEnables", d.subscriberEnables,
|
||||
"force", force,
|
||||
)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.Listener.OnUpdateAudioCodecForMime(d.params.MimeType, enabled)
|
||||
}
|
||||
@@ -18,23 +18,26 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityVideo)(nil)
|
||||
|
||||
const (
|
||||
initialQualityUpdateWait = 10 * time.Second
|
||||
)
|
||||
|
||||
type DynacastQualityParams struct {
|
||||
type dynacastQualityVideoParams struct {
|
||||
MimeType mime.MimeType
|
||||
Listener dynacastQualityListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
// DynacastQuality manages max subscribed quality of a single receiver of a media track
|
||||
type DynacastQuality struct {
|
||||
params DynacastQualityParams
|
||||
// dynacastQualityVideo manages max subscribed quality of a single receiver of a media track
|
||||
type dynacastQualityVideo struct {
|
||||
params dynacastQualityVideoParams
|
||||
|
||||
// quality level enable/disable
|
||||
lock sync.RWMutex
|
||||
@@ -43,38 +46,32 @@ type DynacastQuality struct {
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality
|
||||
maxSubscribedQuality livekit.VideoQuality
|
||||
maxQualityTimer *time.Timer
|
||||
regressTo *DynacastQuality
|
||||
regressTo dynacastQuality
|
||||
|
||||
onSubscribedMaxQualityChange func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)
|
||||
dynacastQualityNull
|
||||
}
|
||||
|
||||
func NewDynacastQuality(params DynacastQualityParams) *DynacastQuality {
|
||||
return &DynacastQuality{
|
||||
func newDynacastQualityVideo(params dynacastQualityVideoParams) dynacastQuality {
|
||||
return &dynacastQualityVideo{
|
||||
params: params,
|
||||
maxSubscriberQuality: make(map[livekit.ParticipantID]livekit.VideoQuality),
|
||||
maxSubscriberNodeQuality: make(map[livekit.NodeID]livekit.VideoQuality),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) Start() {
|
||||
func (d *dynacastQualityVideo) Start() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) Restart() {
|
||||
func (d *dynacastQualityVideo) Restart() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) Stop() {
|
||||
func (d *dynacastQualityVideo) Stop() {
|
||||
d.stopMaxQualityTimer()
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) OnSubscribedMaxQualityChange(f func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
d.onSubscribedMaxQualityChange = f
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
|
||||
func (d *dynacastQualityVideo) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber max quality",
|
||||
"mime", d.params.MimeType,
|
||||
@@ -99,7 +96,7 @@ func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.Partic
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
|
||||
func (d *dynacastQualityVideo) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber node max quality",
|
||||
"mime", d.params.MimeType,
|
||||
@@ -110,8 +107,12 @@ func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
// the downstream node will synthesize correct quality notify (its dynacast manager has codec regression), just ignore it
|
||||
d.params.Logger.Debugw(
|
||||
"ignoring node quality change, regressed to another dynacast quality",
|
||||
"mime", d.params.MimeType,
|
||||
"regressedMime", d.regressTo.Mime(),
|
||||
)
|
||||
d.lock.Unlock()
|
||||
r.params.Logger.Debugw("ignoring node quality change, regressed to another dynacast quality", "mime", d.params.MimeType)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,7 +126,19 @@ func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) RegressTo(other *DynacastQuality) {
|
||||
func (d *dynacastQualityVideo) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Mime() mime.MimeType {
|
||||
return d.params.MimeType
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) RegressTo(other dynacastQuality) {
|
||||
d.lock.Lock()
|
||||
d.regressTo = other
|
||||
maxSubscriberQuality := d.maxSubscriberQuality
|
||||
@@ -134,33 +147,41 @@ func (d *DynacastQuality) RegressTo(other *DynacastQuality) {
|
||||
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
|
||||
d.lock.Unlock()
|
||||
|
||||
other.lock.Lock()
|
||||
other.Replace(maxSubscriberQuality, maxSubscriberNodeQuality)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
) {
|
||||
d.lock.Lock()
|
||||
for subID, quality := range maxSubscriberQuality {
|
||||
if otherQuality, ok := other.maxSubscriberQuality[subID]; ok {
|
||||
if oldQuality, ok := d.maxSubscriberQuality[subID]; ok {
|
||||
// no QUALITY_OFF in the map
|
||||
if quality > otherQuality {
|
||||
other.maxSubscriberQuality[subID] = quality
|
||||
if quality > oldQuality {
|
||||
d.maxSubscriberQuality[subID] = quality
|
||||
}
|
||||
} else {
|
||||
other.maxSubscriberQuality[subID] = quality
|
||||
d.maxSubscriberQuality[subID] = quality
|
||||
}
|
||||
}
|
||||
|
||||
for nodeID, quality := range maxSubscriberNodeQuality {
|
||||
if otherQuality, ok := other.maxSubscriberNodeQuality[nodeID]; ok {
|
||||
if oldQuality, ok := d.maxSubscriberNodeQuality[nodeID]; ok {
|
||||
// no QUALITY_OFF in the map
|
||||
if quality > otherQuality {
|
||||
other.maxSubscriberNodeQuality[nodeID] = quality
|
||||
if quality > oldQuality {
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
} else {
|
||||
other.maxSubscriberNodeQuality[nodeID] = quality
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
}
|
||||
other.lock.Unlock()
|
||||
other.Restart()
|
||||
d.lock.Unlock()
|
||||
|
||||
d.Restart()
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) reset() {
|
||||
func (d *dynacastQualityVideo) reset() {
|
||||
d.lock.Lock()
|
||||
d.initialized = false
|
||||
d.lock.Unlock()
|
||||
@@ -168,7 +189,7 @@ func (d *DynacastQuality) reset() {
|
||||
d.startMaxQualityTimer()
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) updateQualityChange(force bool) {
|
||||
func (d *dynacastQualityVideo) updateQualityChange(force bool) {
|
||||
d.lock.Lock()
|
||||
maxSubscribedQuality := livekit.VideoQuality_OFF
|
||||
for _, subQuality := range d.maxSubscriberQuality {
|
||||
@@ -189,22 +210,20 @@ func (d *DynacastQuality) updateQualityChange(force bool) {
|
||||
|
||||
d.initialized = true
|
||||
d.maxSubscribedQuality = maxSubscribedQuality
|
||||
d.params.Logger.Debugw("notifying quality change",
|
||||
d.params.Logger.Debugw(
|
||||
"notifying quality change",
|
||||
"mime", d.params.MimeType,
|
||||
"maxSubscriberQuality", d.maxSubscriberQuality,
|
||||
"maxSubscriberNodeQuality", d.maxSubscriberNodeQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
"force", force,
|
||||
)
|
||||
onSubscribedMaxQualityChange := d.onSubscribedMaxQualityChange
|
||||
d.lock.Unlock()
|
||||
|
||||
if onSubscribedMaxQualityChange != nil {
|
||||
onSubscribedMaxQualityChange(d.params.MimeType, maxSubscribedQuality)
|
||||
}
|
||||
d.params.Listener.OnUpdateMaxQualityForMime(d.params.MimeType, maxSubscribedQuality)
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) startMaxQualityTimer() {
|
||||
func (d *dynacastQualityVideo) startMaxQualityTimer() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
@@ -219,7 +238,7 @@ func (d *DynacastQuality) startMaxQualityTimer() {
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DynacastQuality) stopMaxQualityTimer() {
|
||||
func (d *dynacastQualityVideo) stopMaxQualityTimer() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
type DynacastManagerListener interface {
|
||||
OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
)
|
||||
|
||||
OnDynacastSubscribedAudioCodecChange(codecs []*livekit.SubscribedAudioCodec)
|
||||
}
|
||||
|
||||
var _ DynacastManagerListener = (*DynacastManagerListenerNull)(nil)
|
||||
|
||||
type DynacastManagerListenerNull struct {
|
||||
}
|
||||
|
||||
func (d *DynacastManagerListenerNull) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
}
|
||||
func (d *DynacastManagerListenerNull) OnDynacastSubscribedAudioCodecChange(
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
type DynacastManager interface {
|
||||
AddCodec(mime mime.MimeType)
|
||||
HandleCodecRegression(fromMime, toMime mime.MimeType)
|
||||
Restart()
|
||||
Close()
|
||||
ForceUpdate()
|
||||
ForceQuality(quality livekit.VideoQuality)
|
||||
ForceEnable(enabled bool)
|
||||
|
||||
NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
)
|
||||
NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
)
|
||||
|
||||
NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
)
|
||||
NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
)
|
||||
ClearSubscriberNodes()
|
||||
}
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerNull)(nil)
|
||||
|
||||
type dynacastManagerNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastManagerNull) AddCodec(mime mime.MimeType) {}
|
||||
func (d *dynacastManagerNull) HandleCodecRegression(fromMime, toMime mime.MimeType) {}
|
||||
func (d *dynacastManagerNull) Restart() {}
|
||||
func (d *dynacastManagerNull) Close() {}
|
||||
func (d *dynacastManagerNull) ForceUpdate() {}
|
||||
func (d *dynacastManagerNull) ForceQuality(quality livekit.VideoQuality) {}
|
||||
func (d *dynacastManagerNull) ForceEnable(enabled bool) {}
|
||||
func (d *dynacastManagerNull) NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) ClearSubscriberNodes() {}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type dynacastQualityListener interface {
|
||||
OnUpdateMaxQualityForMime(mimeType mime.MimeType, maxQuality livekit.VideoQuality)
|
||||
OnUpdateAudioCodecForMime(mimeType mime.MimeType, enabled bool)
|
||||
}
|
||||
|
||||
var _ dynacastQualityListener = (*dynacastQualityListenerNull)(nil)
|
||||
|
||||
type dynacastQualityListenerNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityListenerNull) OnUpdateMaxQualityForMime(
|
||||
mimeType mime.MimeType,
|
||||
maxQuality livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityListenerNull) OnUpdateAudioCodecForMime(
|
||||
mimeType mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type dynacastQuality interface {
|
||||
Start()
|
||||
Restart()
|
||||
Stop()
|
||||
|
||||
NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality)
|
||||
NotifySubscription(subscriberID livekit.ParticipantID, enabled bool)
|
||||
|
||||
NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality)
|
||||
NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool)
|
||||
ClearSubscriberNodes()
|
||||
|
||||
Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
)
|
||||
|
||||
Mime() mime.MimeType
|
||||
RegressTo(other dynacastQuality)
|
||||
}
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityNull)(nil)
|
||||
|
||||
type dynacastQualityNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityNull) Start() {}
|
||||
func (d *dynacastQualityNull) Restart() {}
|
||||
func (d *dynacastQualityNull) Stop() {}
|
||||
func (d *dynacastQualityNull) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
|
||||
}
|
||||
func (d *dynacastQualityNull) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {}
|
||||
func (d *dynacastQualityNull) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
|
||||
}
|
||||
func (d *dynacastQualityNull) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {}
|
||||
func (d *dynacastQualityNull) ClearSubscriberNodes() {}
|
||||
func (d *dynacastQualityNull) Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastQualityNull) Mime() mime.MimeType { return mime.MimeTypeUnknown }
|
||||
func (d *dynacastQualityNull) RegressTo(other dynacastQuality) {}
|
||||
+22
-20
@@ -29,6 +29,7 @@ import (
|
||||
|
||||
type EgressLauncher interface {
|
||||
StartEgress(context.Context, *rpc.StartEgressRequest) (*livekit.EgressInfo, error)
|
||||
StopEgress(context.Context, *livekit.StopEgressRequest) (*livekit.EgressInfo, error)
|
||||
}
|
||||
|
||||
func StartParticipantEgress(
|
||||
@@ -42,16 +43,17 @@ func StartParticipantEgress(
|
||||
) error {
|
||||
if req, err := startParticipantEgress(ctx, launcher, opts, identity, roomName, roomID); err != nil {
|
||||
// send egress failed webhook
|
||||
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
|
||||
Event: webhook.EventEgressEnded,
|
||||
EgressInfo: &livekit.EgressInfo{
|
||||
RoomId: string(roomID),
|
||||
RoomName: string(roomName),
|
||||
Status: livekit.EgressStatus_EGRESS_FAILED,
|
||||
Error: err.Error(),
|
||||
Request: &livekit.EgressInfo_Participant{Participant: req},
|
||||
},
|
||||
})
|
||||
|
||||
info := &livekit.EgressInfo{
|
||||
RoomId: string(roomID),
|
||||
RoomName: string(roomName),
|
||||
Status: livekit.EgressStatus_EGRESS_FAILED,
|
||||
Error: err.Error(),
|
||||
Request: &livekit.EgressInfo_Participant{Participant: req},
|
||||
}
|
||||
|
||||
ts.NotifyEgressEvent(ctx, webhook.EventEgressEnded, info)
|
||||
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -103,16 +105,16 @@ func StartTrackEgress(
|
||||
) error {
|
||||
if req, err := startTrackEgress(ctx, launcher, opts, track, roomName, roomID); err != nil {
|
||||
// send egress failed webhook
|
||||
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
|
||||
Event: webhook.EventEgressEnded,
|
||||
EgressInfo: &livekit.EgressInfo{
|
||||
RoomId: string(roomID),
|
||||
RoomName: string(roomName),
|
||||
Status: livekit.EgressStatus_EGRESS_FAILED,
|
||||
Error: err.Error(),
|
||||
Request: &livekit.EgressInfo_Track{Track: req},
|
||||
},
|
||||
})
|
||||
|
||||
info := &livekit.EgressInfo{
|
||||
RoomId: string(roomID),
|
||||
RoomName: string(roomName),
|
||||
Status: livekit.EgressStatus_EGRESS_FAILED,
|
||||
Error: err.Error(),
|
||||
Request: &livekit.EgressInfo_Track{Track: req},
|
||||
}
|
||||
ts.NotifyEgressEvent(ctx, webhook.EventEgressEnded, info)
|
||||
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -32,9 +32,6 @@ var (
|
||||
ErrEmptyParticipantID = errors.New("participant ID cannot be empty")
|
||||
ErrMissingGrants = errors.New("VideoGrant is missing")
|
||||
ErrInternalError = errors.New("internal error")
|
||||
ErrNameExceedsLimits = errors.New("name length exceeds limits")
|
||||
ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits")
|
||||
ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits")
|
||||
|
||||
// Track subscription related
|
||||
ErrNoTrackPermission = errors.New("participant is not allowed to subscribe to this track")
|
||||
|
||||
+117
-122
@@ -20,146 +20,67 @@ import (
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
protoCodecs "github.com/livekit/protocol/codecs"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
var OpusCodecCapability = webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeOpus.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "minptime=10;useinbandfec=1",
|
||||
}
|
||||
var RedCodecCapability = webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRED.String(),
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "111/111",
|
||||
}
|
||||
var videoRTX = webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRTX.String(),
|
||||
ClockRate: 90000,
|
||||
}
|
||||
|
||||
func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedback RTCPFeedbackConfig, filterOutH264HighProfile bool) error {
|
||||
opusCodec := OpusCodecCapability
|
||||
opusCodec.RTCPFeedback = rtcpFeedback.Audio
|
||||
var opusPayload webrtc.PayloadType
|
||||
if IsCodecEnabled(codecs, opusCodec) {
|
||||
opusPayload = 111
|
||||
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: opusCodec,
|
||||
PayloadType: opusPayload,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
// audio codecs
|
||||
if IsCodecEnabled(codecs, protoCodecs.OpusCodecParameters.RTPCodecCapability) {
|
||||
cp := protoCodecs.OpusCodecParameters
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if IsCodecEnabled(codecs, RedCodecCapability) {
|
||||
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: RedCodecCapability,
|
||||
PayloadType: 63,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
if IsCodecEnabled(codecs, protoCodecs.RedCodecParameters.RTPCodecCapability) {
|
||||
if err := me.RegisterCodec(protoCodecs.RedCodecParameters, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rtxEnabled := IsCodecEnabled(codecs, videoRTX)
|
||||
for _, codec := range []webrtc.RTPCodecParameters{protoCodecs.PCMUCodecParameters, protoCodecs.PCMACodecParameters} {
|
||||
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
continue
|
||||
}
|
||||
|
||||
h264HighProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
ClockRate: 90000,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 96,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=0",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 98,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeVP9.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "profile-id=1",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 100,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 125,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 108,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: h264HighProfileFmtp,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 123,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeAV1.String(),
|
||||
ClockRate: 90000,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 35,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH265,
|
||||
ClockRate: 90000,
|
||||
RTCPFeedback: rtcpFeedback.Video,
|
||||
},
|
||||
PayloadType: 116,
|
||||
},
|
||||
} {
|
||||
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == h264HighProfileFmtp {
|
||||
cp := codec
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// video codecs
|
||||
rtxEnabled := IsCodecEnabled(codecs, protoCodecs.VideoRTXCodecParameters.RTPCodecCapability)
|
||||
for _, codec := range protoCodecs.VideoCodecsParameters {
|
||||
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == protoCodecs.H264HighProfileFmtp {
|
||||
continue
|
||||
}
|
||||
if mime.IsMimeTypeStringRTX(codec.MimeType) {
|
||||
continue
|
||||
}
|
||||
if IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
if rtxEnabled {
|
||||
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: mime.MimeTypeRTX.String(),
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: fmt.Sprintf("apt=%d", codec.PayloadType),
|
||||
},
|
||||
PayloadType: codec.PayloadType + 1,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
|
||||
continue
|
||||
}
|
||||
|
||||
cp := codec
|
||||
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Video
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !rtxEnabled {
|
||||
continue
|
||||
}
|
||||
|
||||
cp = protoCodecs.VideoRTXCodecParameters
|
||||
cp.RTPCodecCapability.SDPFmtpLine = fmt.Sprintf("apt=%d", codec.PayloadType)
|
||||
cp.PayloadType = codec.PayloadType + 1
|
||||
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -215,3 +136,77 @@ func selectAlternativeVideoCodec(enabledCodecs []*livekit.Codec) string {
|
||||
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
|
||||
return mime.MimeTypeVP8.String()
|
||||
}
|
||||
|
||||
func selectAlternativeAudioCodec(enabledCodecs []*livekit.Codec) string {
|
||||
for _, c := range enabledCodecs {
|
||||
if mime.IsMimeTypeStringAudio(c.Mime) {
|
||||
return c.Mime
|
||||
}
|
||||
}
|
||||
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
|
||||
return mime.MimeTypeOpus.String()
|
||||
}
|
||||
|
||||
// mergeCodecsByMime returns a union b, deduplicated by mime type.
|
||||
func mergeCodecsByMime(a, b []*livekit.Codec) []*livekit.Codec {
|
||||
merged := make([]*livekit.Codec, 0, len(a)+len(b))
|
||||
merged = append(merged, a...)
|
||||
for _, c := range b {
|
||||
seen := false
|
||||
for _, existing := range a {
|
||||
if mime.IsMimeTypeStringEqual(c.Mime, existing.Mime) {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
merged = append(merged, c)
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func filterCodecs(
|
||||
codecs []webrtc.RTPCodecParameters,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
filterOutH264HighProfile bool,
|
||||
) []webrtc.RTPCodecParameters {
|
||||
filteredCodecs := make([]webrtc.RTPCodecParameters, 0, len(codecs))
|
||||
for _, c := range codecs {
|
||||
if filterOutH264HighProfile && isH264HighProfile(c.RTPCodecCapability.SDPFmtpLine) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, enabledCodec := range enabledCodecs {
|
||||
if mime.NormalizeMimeType(enabledCodec.Mime) == mime.NormalizeMimeType(c.RTPCodecCapability.MimeType) {
|
||||
if !mime.IsMimeTypeStringEqual(c.RTPCodecCapability.MimeType, mime.MimeTypeRTX.String()) {
|
||||
if mime.IsMimeTypeStringVideo(c.RTPCodecCapability.MimeType) {
|
||||
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Video
|
||||
} else {
|
||||
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Audio
|
||||
}
|
||||
}
|
||||
filteredCodecs = append(filteredCodecs, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredCodecs
|
||||
}
|
||||
|
||||
func isH264HighProfile(fmtp string) bool {
|
||||
params := strings.Split(fmtp, ";")
|
||||
for _, param := range params {
|
||||
parts := strings.Split(param, "=")
|
||||
if len(parts) == 2 {
|
||||
if parts[0] == "profile-level-id" {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6184#section-8.1
|
||||
// hex value 0x64 for profile_idc is high profile
|
||||
return strings.HasPrefix(parts[1], "64")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
|
||||
+348
-122
@@ -15,7 +15,6 @@
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -24,8 +23,11 @@ import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/observability/roomobs"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/dynacast"
|
||||
@@ -33,77 +35,100 @@ import (
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/connectionquality"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/interceptor"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
util "github.com/livekit/mediatransportutil"
|
||||
)
|
||||
|
||||
var _ types.LocalMediaTrack = (*MediaTrack)(nil)
|
||||
|
||||
// MediaTrack represents a WebRTC track that needs to be forwarded
|
||||
// Implements MediaTrack and PublishedTrack interface
|
||||
type MediaTrack struct {
|
||||
params MediaTrackParams
|
||||
numUpTracks atomic.Uint32
|
||||
buffer *buffer.Buffer
|
||||
everSubscribed atomic.Bool
|
||||
|
||||
*MediaTrackReceiver
|
||||
*MediaLossProxy
|
||||
|
||||
dynacastManager *dynacast.DynacastManager
|
||||
dynacastManager dynacast.DynacastManager
|
||||
|
||||
lock sync.RWMutex
|
||||
|
||||
rttFromXR atomic.Bool
|
||||
|
||||
enableRegression bool
|
||||
backupCodecPolicy livekit.BackupCodecPolicy
|
||||
regressionTargetCodec mime.MimeType
|
||||
regressionTargetCodecReceived bool
|
||||
|
||||
onSubscribedMaxQualityChange func(
|
||||
trackID livekit.TrackID,
|
||||
trackInfo *livekit.TrackInfo,
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) error
|
||||
onSubscribedAudioCodecChange func(
|
||||
trackID livekit.TrackID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) error
|
||||
}
|
||||
|
||||
type MediaTrackParams struct {
|
||||
SignalCid string
|
||||
SdpCid string
|
||||
ParticipantID livekit.ParticipantID
|
||||
ParticipantIdentity livekit.ParticipantIdentity
|
||||
ParticipantVersion uint32
|
||||
BufferFactory *buffer.Factory
|
||||
ReceiverConfig ReceiverConfig
|
||||
SubscriberConfig DirectionConfig
|
||||
PLIThrottleConfig sfu.PLIThrottleConfig
|
||||
AudioConfig sfu.AudioConfig
|
||||
VideoConfig config.VideoConfig
|
||||
Telemetry telemetry.TelemetryService
|
||||
Logger logger.Logger
|
||||
SimTracks map[uint32]SimulcastTrackInfo
|
||||
OnRTCP func([]rtcp.Packet)
|
||||
ForwardStats *sfu.ForwardStats
|
||||
OnTrackEverSubscribed func(livekit.TrackID)
|
||||
ParticipantID func() livekit.ParticipantID
|
||||
ParticipantIdentity livekit.ParticipantIdentity
|
||||
ParticipantVersion uint32
|
||||
ParticipantCountry string
|
||||
ParticipantKind livekit.ParticipantInfo_Kind
|
||||
ParticipantKindDetails []livekit.ParticipantInfo_KindDetail
|
||||
BufferFactory *buffer.Factory
|
||||
ReceiverConfig ReceiverConfig
|
||||
SubscriberConfig DirectionConfig
|
||||
PLIThrottleConfig sfu.PLIThrottleConfig
|
||||
AudioConfig sfu.AudioConfig
|
||||
VideoConfig config.VideoConfig
|
||||
TelemetryListener types.ParticipantTelemetryListener
|
||||
Logger logger.Logger
|
||||
Reporter roomobs.TrackReporter
|
||||
SimTracks map[uint32]interceptor.SimulcastTrackInfo
|
||||
OnRTCP func([]rtcp.Packet)
|
||||
ForwardStats *sfu.ForwardStats
|
||||
OnTrackEverSubscribed func(livekit.TrackID)
|
||||
ShouldRegressCodec func() bool
|
||||
PreferVideoSizeFromMedia bool
|
||||
EnableRTPStreamRestartDetection bool
|
||||
UpdateTrackInfoByVideoSizeChange bool
|
||||
ForceBackupCodecPolicySimulcast bool
|
||||
}
|
||||
|
||||
func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
|
||||
t := &MediaTrack{
|
||||
params: params,
|
||||
params: params,
|
||||
backupCodecPolicy: ti.BackupCodecPolicy,
|
||||
}
|
||||
|
||||
// TODO: disable codec regression until simulcast-codec clients knows that
|
||||
if ti.BackupCodecPolicy == livekit.BackupCodecPolicy_REGRESSION && len(ti.Codecs) > 1 {
|
||||
t.enableRegression = true
|
||||
if t.params.ForceBackupCodecPolicySimulcast {
|
||||
t.backupCodecPolicy = livekit.BackupCodecPolicy_SIMULCAST
|
||||
}
|
||||
|
||||
if t.backupCodecPolicy != livekit.BackupCodecPolicy_SIMULCAST && len(ti.Codecs) > 1 {
|
||||
t.regressionTargetCodec = mime.NormalizeMimeType(ti.Codecs[1].MimeType)
|
||||
t.params.Logger.Debugw("track enabled codec regression", "regressionCodec", t.regressionTargetCodec)
|
||||
}
|
||||
|
||||
t.MediaTrackReceiver = NewMediaTrackReceiver(MediaTrackReceiverParams{
|
||||
MediaTrack: t,
|
||||
IsRelayed: false,
|
||||
ParticipantID: params.ParticipantID,
|
||||
ParticipantIdentity: params.ParticipantIdentity,
|
||||
ParticipantVersion: params.ParticipantVersion,
|
||||
ReceiverConfig: params.ReceiverConfig,
|
||||
SubscriberConfig: params.SubscriberConfig,
|
||||
AudioConfig: params.AudioConfig,
|
||||
Telemetry: params.Telemetry,
|
||||
Logger: params.Logger,
|
||||
RegressionTargetCodec: t.regressionTargetCodec,
|
||||
MediaTrack: t,
|
||||
IsRelayed: false,
|
||||
ParticipantID: params.ParticipantID,
|
||||
ParticipantIdentity: params.ParticipantIdentity,
|
||||
ParticipantVersion: params.ParticipantVersion,
|
||||
ReceiverConfig: params.ReceiverConfig,
|
||||
SubscriberConfig: params.SubscriberConfig,
|
||||
AudioConfig: params.AudioConfig,
|
||||
TelemetryListener: params.TelemetryListener,
|
||||
Logger: params.Logger,
|
||||
RegressionTargetCodec: t.regressionTargetCodec,
|
||||
PreferVideoSizeFromMedia: params.PreferVideoSizeFromMedia,
|
||||
}, ti)
|
||||
|
||||
if ti.Type == livekit.TrackType_AUDIO {
|
||||
@@ -118,27 +143,57 @@ func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
|
||||
t.MediaTrackReceiver.OnMediaLossFeedback(t.MediaLossProxy.HandleMaxLossFeedback)
|
||||
}
|
||||
|
||||
if ti.Type == livekit.TrackType_VIDEO {
|
||||
t.dynacastManager = dynacast.NewDynacastManager(dynacast.DynacastManagerParams{
|
||||
switch ti.Type {
|
||||
case livekit.TrackType_VIDEO:
|
||||
t.dynacastManager = dynacast.NewDynacastManagerVideo(dynacast.DynacastManagerVideoParams{
|
||||
DynacastPauseDelay: params.VideoConfig.DynacastPauseDelay,
|
||||
Listener: t,
|
||||
Logger: params.Logger,
|
||||
})
|
||||
t.MediaTrackReceiver.OnSetupReceiver(func(mime mime.MimeType) {
|
||||
|
||||
case livekit.TrackType_AUDIO:
|
||||
if len(ti.Codecs) > 1 {
|
||||
t.dynacastManager = dynacast.NewDynacastManagerAudio(dynacast.DynacastManagerAudioParams{
|
||||
Listener: t,
|
||||
Logger: params.Logger,
|
||||
})
|
||||
}
|
||||
}
|
||||
t.MediaTrackReceiver.OnSetupReceiver(func(mime mime.MimeType) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.AddCodec(mime)
|
||||
})
|
||||
t.MediaTrackReceiver.OnSubscriberMaxQualityChange(
|
||||
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, layer int32) {
|
||||
}
|
||||
})
|
||||
t.MediaTrackReceiver.OnSubscriberMaxQualityChange(
|
||||
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, layer int32) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.NotifySubscriberMaxQuality(
|
||||
subscriberID,
|
||||
mimeType,
|
||||
buffer.SpatialLayerToVideoQuality(layer, t.MediaTrackReceiver.TrackInfo()),
|
||||
buffer.GetVideoQualityForSpatialLayer(
|
||||
mimeType,
|
||||
layer,
|
||||
t.MediaTrackReceiver.TrackInfo(),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
t.MediaTrackReceiver.OnCodecRegression(func(old, new webrtc.RTPCodecParameters) {
|
||||
t.dynacastManager.HandleCodecRegression(mime.NormalizeMimeType(old.MimeType), mime.NormalizeMimeType(new.MimeType))
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
t.MediaTrackReceiver.OnSubscriberAudioCodecChange(
|
||||
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, enabled bool) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.NotifySubscription(subscriberID, mimeType, enabled)
|
||||
}
|
||||
},
|
||||
)
|
||||
t.MediaTrackReceiver.OnCodecRegression(func(old, new webrtc.RTPCodecParameters) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.HandleCodecRegression(
|
||||
mime.NormalizeMimeType(old.MimeType),
|
||||
mime.NormalizeMimeType(new.MimeType),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
@@ -151,24 +206,20 @@ func (t *MediaTrack) OnSubscribedMaxQualityChange(
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) error,
|
||||
) {
|
||||
if t.dynacastManager == nil {
|
||||
return
|
||||
}
|
||||
t.lock.Lock()
|
||||
t.onSubscribedMaxQualityChange = f
|
||||
t.lock.Unlock()
|
||||
}
|
||||
|
||||
handler := func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality) {
|
||||
if f != nil && !t.IsMuted() {
|
||||
_ = f(t.ID(), t.ToProto(), subscribedQualities, maxSubscribedQualities)
|
||||
}
|
||||
|
||||
for _, q := range maxSubscribedQualities {
|
||||
receiver := t.Receiver(q.CodecMime)
|
||||
if receiver != nil {
|
||||
receiver.SetMaxExpectedSpatialLayer(buffer.VideoQualityToSpatialLayer(q.Quality, t.MediaTrackReceiver.TrackInfo()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.dynacastManager.OnSubscribedMaxQualityChange(handler)
|
||||
func (t *MediaTrack) OnSubscribedAudioCodecChange(
|
||||
f func(
|
||||
trackID livekit.TrackID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) error,
|
||||
) {
|
||||
t.lock.Lock()
|
||||
t.onSubscribedAudioCodecChange = f
|
||||
t.lock.Unlock()
|
||||
}
|
||||
|
||||
func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
|
||||
@@ -177,47 +228,84 @@ func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quali
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) SignalCid() string {
|
||||
return t.params.SignalCid
|
||||
func (t *MediaTrack) NotifySubscriptionNode(nodeID livekit.NodeID, codecs []*livekit.SubscribedAudioCodec) {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.NotifySubscriptionNode(nodeID, codecs)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) HasSdpCid(cid string) bool {
|
||||
if t.params.SdpCid == cid {
|
||||
return true
|
||||
func (t *MediaTrack) ClearSubscriberNodes() {
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.ClearSubscriberNodes()
|
||||
}
|
||||
}
|
||||
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == cid {
|
||||
return true
|
||||
func (t *MediaTrack) HasSignalCid(cid string) bool {
|
||||
if cid != "" {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == cid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *MediaTrack) HasSdpCid(cid string) bool {
|
||||
if cid != "" {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == cid || c.SdpCid == cid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *MediaTrack) GetMimeTypeForSdpCid(cid string) mime.MimeType {
|
||||
if cid != "" {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == cid || c.SdpCid == cid {
|
||||
return mime.NormalizeMimeType(c.MimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mime.MimeTypeUnknown
|
||||
}
|
||||
|
||||
func (t *MediaTrack) GetCidsForMimeType(mimeType mime.MimeType) (string, string) {
|
||||
ti := t.MediaTrackReceiver.TrackInfoClone()
|
||||
for _, c := range ti.Codecs {
|
||||
if mime.NormalizeMimeType(c.MimeType) == mimeType {
|
||||
return c.Cid, c.SdpCid
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func (t *MediaTrack) ToProto() *livekit.TrackInfo {
|
||||
return t.MediaTrackReceiver.TrackInfoClone()
|
||||
}
|
||||
|
||||
func (t *MediaTrack) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
|
||||
t.MediaTrackReceiver.UpdateCodecCid(codecs)
|
||||
}
|
||||
|
||||
// AddReceiver adds a new RTP receiver to the track, returns true when receiver represents a new codec
|
||||
func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRemote, mid string) bool {
|
||||
// and if a receiver was added successfully
|
||||
func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRemote, mid string) (bool, bool) {
|
||||
var newCodec bool
|
||||
ssrc := uint32(track.SSRC())
|
||||
buff, rtcpReader := t.params.BufferFactory.GetBufferPair(ssrc)
|
||||
if buff == nil || rtcpReader == nil {
|
||||
t.params.Logger.Errorw("could not retrieve buffer pair", nil)
|
||||
return newCodec
|
||||
return newCodec, false
|
||||
}
|
||||
|
||||
var lastRR uint32
|
||||
rtcpReader.OnPacket(func(bytes []byte) {
|
||||
pkts, err := rtcp.Unmarshal(bytes)
|
||||
if err != nil {
|
||||
t.params.Logger.Errorw("could not unmarshal RTCP", err)
|
||||
t.params.Logger.Errorw("could not unmarshal RTCP", err, "size", len(bytes))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -226,7 +314,13 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
case *rtcp.SourceDescription:
|
||||
case *rtcp.SenderReport:
|
||||
if pkt.SSRC == uint32(track.SSRC()) {
|
||||
buff.SetSenderReportData(pkt.RTPTime, pkt.NTPTime, pkt.PacketCount, pkt.OctetCount)
|
||||
buff.SetSenderReportData(&livekit.RTCPSenderReportState{
|
||||
RtpTimestamp: pkt.RTPTime,
|
||||
NtpTimestamp: pkt.NTPTime,
|
||||
Packets: pkt.PacketCount,
|
||||
Octets: uint64(pkt.OctetCount),
|
||||
At: mono.UnixNano(),
|
||||
})
|
||||
}
|
||||
case *rtcp.ExtendedReport:
|
||||
rttFromXR:
|
||||
@@ -255,13 +349,27 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
t.lock.Lock()
|
||||
var regressCodec bool
|
||||
mimeType := mime.NormalizeMimeType(track.Codec().MimeType)
|
||||
layer := buffer.RidToSpatialLayer(track.RID(), ti)
|
||||
layer := buffer.GetSpatialLayerForRid(mimeType, track.RID(), ti)
|
||||
if layer < 0 {
|
||||
t.params.Logger.Warnw(
|
||||
"AddReceiver failed due to negative layer", nil,
|
||||
"rid", track.RID(),
|
||||
"layer", layer,
|
||||
"ssrc", track.SSRC(),
|
||||
"codec", track.Codec(),
|
||||
"trackInfo", logger.Proto(ti),
|
||||
)
|
||||
t.lock.Unlock()
|
||||
return newCodec, false
|
||||
}
|
||||
|
||||
t.params.Logger.Debugw(
|
||||
"AddReceiver",
|
||||
"rid", track.RID(),
|
||||
"layer", layer,
|
||||
"ssrc", track.SSRC(),
|
||||
"codec", track.Codec(),
|
||||
"trackInfo", logger.Proto(ti),
|
||||
)
|
||||
wr := t.MediaTrackReceiver.Receiver(mimeType)
|
||||
if wr == nil {
|
||||
@@ -276,6 +384,11 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
switch len(ti.Codecs) {
|
||||
case 0:
|
||||
// audio track
|
||||
t.params.Logger.Warnw(
|
||||
"unexpected 0 codecs in track info", nil,
|
||||
"mime", mimeType,
|
||||
"track", logger.Proto(ti),
|
||||
)
|
||||
priority = 0
|
||||
case 1:
|
||||
// older clients or non simulcast-codec, mime type only set later
|
||||
@@ -285,9 +398,13 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
}
|
||||
}
|
||||
if priority < 0 {
|
||||
t.params.Logger.Warnw("could not find codec for webrtc receiver", nil, "webrtcCodec", mimeType, "track", logger.Proto(ti))
|
||||
t.params.Logger.Warnw(
|
||||
"could not find codec for webrtc receiver", nil,
|
||||
"mime", mimeType,
|
||||
"track", logger.Proto(ti),
|
||||
)
|
||||
t.lock.Unlock()
|
||||
return false
|
||||
return newCodec, false
|
||||
}
|
||||
|
||||
newWR := sfu.NewWebRTCReceiver(
|
||||
@@ -300,11 +417,11 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
sfu.WithPliThrottleConfig(t.params.PLIThrottleConfig),
|
||||
sfu.WithAudioConfig(t.params.AudioConfig),
|
||||
sfu.WithLoadBalanceThreshold(20),
|
||||
sfu.WithStreamTrackers(),
|
||||
sfu.WithForwardStats(t.params.ForwardStats),
|
||||
sfu.WithEnableRTPStreamRestartDetection(t.params.EnableRTPStreamRestartDetection),
|
||||
)
|
||||
newWR.OnCloseHandler(func() {
|
||||
t.MediaTrackReceiver.SetClosing()
|
||||
t.MediaTrackReceiver.SetClosing(false)
|
||||
t.MediaTrackReceiver.ClearReceiver(mimeType, false)
|
||||
if t.MediaTrackReceiver.TryClose() {
|
||||
if t.dynacastManager != nil {
|
||||
@@ -312,15 +429,61 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
}
|
||||
}
|
||||
})
|
||||
// SIMULCAST-CODEC-TODO: these need to be receiver/mime aware, setting it up only for primary now
|
||||
if priority == 0 {
|
||||
newWR.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
|
||||
key := telemetry.StatsKeyForTrack(livekit.StreamType_UPSTREAM, t.PublisherID(), t.ID(), ti.Source, ti.Type)
|
||||
t.params.Telemetry.TrackStats(key, stat)
|
||||
})
|
||||
|
||||
newWR.OnMaxLayerChange(t.onMaxLayerChange)
|
||||
// SIMULCAST-CODEC-TODO: these need to be receiver/mime aware, setting it up only for primary now
|
||||
statsKey := telemetry.StatsKeyForTrack(
|
||||
t.params.ParticipantCountry,
|
||||
livekit.StreamType_UPSTREAM,
|
||||
t.PublisherID(),
|
||||
t.ID(),
|
||||
ti.Source,
|
||||
ti.Type,
|
||||
)
|
||||
for _, c := range ti.Codecs {
|
||||
for _, l := range c.Layers {
|
||||
t.params.Reporter.ReportLayer(roomobs.PackTrackLayer(l.Height, l.Width))
|
||||
}
|
||||
}
|
||||
newWR.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
|
||||
// send for only one codec, either primary (priority == 0) OR regressed codec
|
||||
t.lock.RLock()
|
||||
regressionTargetCodecReceived := t.regressionTargetCodecReceived
|
||||
t.lock.RUnlock()
|
||||
if priority == 0 || regressionTargetCodecReceived {
|
||||
t.params.TelemetryListener.OnTrackStats(statsKey, stat)
|
||||
|
||||
if cs, ok := telemetry.CondenseStat(stat); ok {
|
||||
t.params.Reporter.Tx(func(tx roomobs.TrackTx) {
|
||||
tx.ParticipantSession().ReportKind(t.params.ParticipantKind.String())
|
||||
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(t.params.ParticipantKind))
|
||||
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(t.params.ParticipantKindDetails))
|
||||
tx.ReportName(ti.Name)
|
||||
tx.ReportKind(roomobs.TrackKindPub)
|
||||
tx.ReportType(roomobs.TrackTypeFromProto(ti.Type))
|
||||
tx.ReportSource(roomobs.TrackSourceFromProto(ti.Source))
|
||||
tx.ReportMime(mime.NormalizeMimeType(ti.MimeType).ReporterType())
|
||||
tx.ReportDuration(uint16(cs.EndTime.Sub(cs.StartTime).Milliseconds()))
|
||||
tx.ReportFrames(uint16(cs.Frames))
|
||||
tx.ReportRecvBytes(uint32(cs.Bytes))
|
||||
tx.ReportRecvPackets(cs.Packets)
|
||||
tx.ReportPacketsLost(cs.PacketsLost)
|
||||
tx.ReportScore(stat.Score)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
newWR.OnMaxLayerChange(func(mimeType mime.MimeType, maxLayer int32) {
|
||||
// send for only one codec, either primary (priority == 0) OR regressed codec
|
||||
t.lock.RLock()
|
||||
regressionTargetCodecReceived := t.regressionTargetCodecReceived
|
||||
t.lock.RUnlock()
|
||||
if priority == 0 || regressionTargetCodecReceived {
|
||||
t.MediaTrackReceiver.NotifyMaxLayerChange(mimeType, maxLayer)
|
||||
}
|
||||
})
|
||||
// SIMULCAST-CODEC-TODO END: these need to be receiver/mime aware, setting it up only for primary now
|
||||
|
||||
if t.PrimaryReceiver() == nil {
|
||||
// primary codec published, set potential codecs
|
||||
potentialCodecs := make([]webrtc.RTPCodecParameters, 0, len(ti.Codecs))
|
||||
@@ -345,8 +508,8 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
t.MediaTrackReceiver.SetupReceiver(newWR, priority, mid)
|
||||
|
||||
for ssrc, info := range t.params.SimTracks {
|
||||
if info.Mid == mid {
|
||||
t.MediaTrackReceiver.SetLayerSsrc(mimeType, info.Rid, ssrc)
|
||||
if info.Mid == mid && !info.IsRepairStream {
|
||||
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, info.StreamID, ssrc, info.RepairSSRC)
|
||||
}
|
||||
}
|
||||
wr = newWR
|
||||
@@ -355,9 +518,18 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
newWR.AddOnCodecStateChange(func(codec webrtc.RTPCodecParameters, state sfu.ReceiverCodecState) {
|
||||
t.MediaTrackReceiver.HandleReceiverCodecChange(newWR, codec, state)
|
||||
})
|
||||
|
||||
// update subscriber video layers when video size changes
|
||||
newWR.OnVideoSizeChanged(func() {
|
||||
if t.params.UpdateTrackInfoByVideoSizeChange {
|
||||
t.MediaTrackReceiver.UpdateVideoSize(mimeType, newWR.VideoSizes())
|
||||
}
|
||||
|
||||
t.MediaTrackSubscriptions.UpdateVideoLayers()
|
||||
})
|
||||
}
|
||||
|
||||
if newCodec && t.enableRegression {
|
||||
if newCodec && t.enableRegression() {
|
||||
if mimeType == t.regressionTargetCodec {
|
||||
t.params.Logger.Infow("regression target codec received", "codec", mimeType)
|
||||
t.regressionTargetCodecReceived = true
|
||||
@@ -377,21 +549,28 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
"newCodec", newCodec,
|
||||
)
|
||||
buff.Close()
|
||||
return false
|
||||
return newCodec, false
|
||||
}
|
||||
|
||||
// LK-TODO: can remove this completely when VideoLayers protocol becomes the default as it has info from client or if we decide to use TrackInfo.Simulcast
|
||||
if t.numUpTracks.Inc() > 1 || track.RID() != "" {
|
||||
// cannot only rely on numUpTracks since we fire metadata events immediately after the first layer
|
||||
t.SetSimulcast(true)
|
||||
var expectedBitrate int
|
||||
layers := buffer.GetVideoLayersForMimeType(mimeType, ti)
|
||||
if layer >= 0 && len(layers) > int(layer) {
|
||||
expectedBitrate = int(layers[layer].GetBitrate())
|
||||
}
|
||||
if err := buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, expectedBitrate); err != nil {
|
||||
t.params.Logger.Warnw(
|
||||
"binding buffer failed", err,
|
||||
"rid", track.RID(),
|
||||
"layer", layer,
|
||||
"ssrc", track.SSRC(),
|
||||
"newCodec", newCodec,
|
||||
)
|
||||
buff.Close()
|
||||
return newCodec, false
|
||||
}
|
||||
|
||||
var bitrates int
|
||||
if len(ti.Layers) > int(layer) {
|
||||
bitrates = int(ti.Layers[layer].GetBitrate())
|
||||
}
|
||||
|
||||
t.MediaTrackReceiver.SetLayerSsrc(mimeType, track.RID(), uint32(track.SSRC()))
|
||||
t.MediaTrackReceiver.MaybeSetSimulcast()
|
||||
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, track.RID(), uint32(track.SSRC()), 0)
|
||||
|
||||
if regressCodec {
|
||||
for _, c := range ti.Codecs {
|
||||
@@ -408,7 +587,7 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
}
|
||||
}
|
||||
|
||||
buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, bitrates)
|
||||
buff.OnNotifyRTX(t.MediaTrackReceiver.setLayerRtxInfo)
|
||||
|
||||
// if subscriber request fps before fps calculated, update them after fps updated.
|
||||
buff.OnFpsChanged(func() {
|
||||
@@ -416,20 +595,19 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
|
||||
})
|
||||
|
||||
buff.OnFinalRtpStats(func(stats *livekit.RTPStats) {
|
||||
t.params.Telemetry.TrackPublishRTPStats(
|
||||
context.Background(),
|
||||
t.params.ParticipantID,
|
||||
t.params.TelemetryListener.OnTrackPublishRTPStats(
|
||||
t.params.ParticipantID(),
|
||||
t.ID(),
|
||||
mimeType,
|
||||
int(layer),
|
||||
stats,
|
||||
)
|
||||
})
|
||||
return newCodec
|
||||
return newCodec, true
|
||||
}
|
||||
|
||||
func (t *MediaTrack) GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality) {
|
||||
receiver := t.PrimaryReceiver()
|
||||
receiver := t.ActiveReceiver()
|
||||
if rtcReceiver, ok := receiver.(*sfu.WebRTCReceiver); ok {
|
||||
return rtcReceiver.GetConnectionScoreAndQuality()
|
||||
}
|
||||
@@ -447,10 +625,6 @@ func (t *MediaTrack) HasPendingCodec() bool {
|
||||
return t.MediaTrackReceiver.PrimaryReceiver() == nil
|
||||
}
|
||||
|
||||
func (t *MediaTrack) onMaxLayerChange(maxLayer int32) {
|
||||
t.MediaTrackReceiver.NotifyMaxLayerChange(maxLayer)
|
||||
}
|
||||
|
||||
func (t *MediaTrack) Restart() {
|
||||
t.MediaTrackReceiver.Restart()
|
||||
|
||||
@@ -460,11 +634,10 @@ func (t *MediaTrack) Restart() {
|
||||
}
|
||||
|
||||
func (t *MediaTrack) Close(isExpectedToResume bool) {
|
||||
t.MediaTrackReceiver.SetClosing()
|
||||
t.MediaTrackReceiver.SetClosing(isExpectedToResume)
|
||||
if t.dynacastManager != nil {
|
||||
t.dynacastManager.Close()
|
||||
}
|
||||
t.MediaTrackReceiver.ClearAllReceivers(isExpectedToResume)
|
||||
t.MediaTrackReceiver.Close(isExpectedToResume)
|
||||
}
|
||||
|
||||
@@ -486,3 +659,56 @@ func (t *MediaTrack) OnTrackSubscribed() {
|
||||
go t.params.OnTrackEverSubscribed(t.ID())
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) enableRegression() bool {
|
||||
return t.backupCodecPolicy == livekit.BackupCodecPolicy_REGRESSION ||
|
||||
(t.backupCodecPolicy == livekit.BackupCodecPolicy_PREFER_REGRESSION && t.params.ShouldRegressCodec())
|
||||
}
|
||||
|
||||
func (t *MediaTrack) Logger() logger.Logger {
|
||||
return t.params.Logger
|
||||
}
|
||||
|
||||
// dynacast.DynacastManagerListtener implementation
|
||||
var _ dynacast.DynacastManagerListener = (*MediaTrack)(nil)
|
||||
|
||||
func (t *MediaTrack) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
t.lock.RLock()
|
||||
onSubscribedMaxQualityChange := t.onSubscribedMaxQualityChange
|
||||
t.lock.RUnlock()
|
||||
|
||||
if onSubscribedMaxQualityChange != nil && !t.IsMuted() {
|
||||
_ = onSubscribedMaxQualityChange(
|
||||
t.ID(),
|
||||
t.ToProto(),
|
||||
subscribedQualities,
|
||||
maxSubscribedQualities,
|
||||
)
|
||||
}
|
||||
|
||||
for _, q := range maxSubscribedQualities {
|
||||
receiver := t.Receiver(q.CodecMime)
|
||||
if receiver != nil {
|
||||
receiver.SetMaxExpectedSpatialLayer(
|
||||
buffer.GetSpatialLayerForVideoQuality(
|
||||
q.CodecMime,
|
||||
q.Quality,
|
||||
t.MediaTrackReceiver.TrackInfo(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrack) OnDynacastSubscribedAudioCodecChange(codecs []*livekit.SubscribedAudioCodec) {
|
||||
t.lock.RLock()
|
||||
onSubscribedAudioCodecChange := t.onSubscribedAudioCodecChange
|
||||
t.lock.RUnlock()
|
||||
|
||||
if onSubscribedAudioCodecChange != nil {
|
||||
_ = onSubscribedAudioCodecChange(t.ID(), codecs)
|
||||
}
|
||||
}
|
||||
|
||||
+97
-74
@@ -19,7 +19,9 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
func TestTrackInfo(t *testing.T) {
|
||||
@@ -47,128 +49,149 @@ func TestTrackInfo(t *testing.T) {
|
||||
require.Equal(t, ti.Width, outInfo.Width)
|
||||
require.Equal(t, ti.Height, outInfo.Height)
|
||||
require.Equal(t, ti.Simulcast, outInfo.Simulcast)
|
||||
|
||||
// make it simulcasted
|
||||
mt.SetSimulcast(true)
|
||||
require.True(t, mt.ToProto().Simulcast)
|
||||
}
|
||||
|
||||
func TestGetQualityForDimension(t *testing.T) {
|
||||
t.Run("landscape source", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
})
|
||||
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 200))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(200, 250))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(700, 480))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(500, 1000))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 300, 200))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 200, 250))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 700, 480))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 500, 1000))
|
||||
})
|
||||
|
||||
t.Run("portrait source", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 540,
|
||||
Height: 960,
|
||||
})
|
||||
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(200, 400))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 400))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(600, 900))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 200, 400))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 400, 400))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 400, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 600, 900))
|
||||
})
|
||||
|
||||
t.Run("layers provided", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
Layers: []*livekit.VideoLayer{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
Quality: livekit.VideoQuality_LOW,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_MEDIUM,
|
||||
Width: 960,
|
||||
Height: 540,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_HIGH,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{
|
||||
Quality: livekit.VideoQuality_LOW,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_MEDIUM,
|
||||
Width: 960,
|
||||
Height: 540,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_HIGH,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
|
||||
})
|
||||
|
||||
t.Run("highest layer with smallest dimensions", func(t *testing.T) {
|
||||
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
|
||||
mt := NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
Layers: []*livekit.VideoLayer{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
Quality: livekit.VideoQuality_LOW,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_MEDIUM,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_HIGH,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{
|
||||
Quality: livekit.VideoQuality_LOW,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_MEDIUM,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_HIGH,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1200, 800))
|
||||
|
||||
mt = NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
|
||||
mt = NewMediaTrack(MediaTrackParams{
|
||||
Logger: logger.GetLogger(),
|
||||
}, &livekit.TrackInfo{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
Layers: []*livekit.VideoLayer{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
Quality: livekit.VideoQuality_LOW,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_MEDIUM,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_HIGH,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
MimeType: mime.MimeTypeH264.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{
|
||||
Quality: livekit.VideoQuality_LOW,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_MEDIUM,
|
||||
Width: 480,
|
||||
Height: 270,
|
||||
},
|
||||
{
|
||||
Quality: livekit.VideoQuality_HIGH,
|
||||
Width: 1080,
|
||||
Height: 720,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 120, 120))
|
||||
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
|
||||
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1200, 800))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
+327
-94
@@ -15,19 +15,19 @@
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"go.uber.org/atomic"
|
||||
"golang.org/x/exp/slices"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/mediatransportutil/pkg/codec"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
@@ -35,9 +35,8 @@ import (
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/rtpstats"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
sutils "github.com/livekit/livekit-server/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -118,17 +117,18 @@ func (r *simulcastReceiver) IsRegressed() bool {
|
||||
// -----------------------------------------------------
|
||||
|
||||
type MediaTrackReceiverParams struct {
|
||||
MediaTrack types.MediaTrack
|
||||
IsRelayed bool
|
||||
ParticipantID livekit.ParticipantID
|
||||
ParticipantIdentity livekit.ParticipantIdentity
|
||||
ParticipantVersion uint32
|
||||
ReceiverConfig ReceiverConfig
|
||||
SubscriberConfig DirectionConfig
|
||||
AudioConfig sfu.AudioConfig
|
||||
Telemetry telemetry.TelemetryService
|
||||
Logger logger.Logger
|
||||
RegressionTargetCodec mime.MimeType
|
||||
MediaTrack types.MediaTrack
|
||||
IsRelayed bool
|
||||
ParticipantID func() livekit.ParticipantID
|
||||
ParticipantIdentity livekit.ParticipantIdentity
|
||||
ParticipantVersion uint32
|
||||
ReceiverConfig ReceiverConfig
|
||||
SubscriberConfig DirectionConfig
|
||||
AudioConfig sfu.AudioConfig
|
||||
TelemetryListener types.ParticipantTelemetryListener
|
||||
Logger logger.Logger
|
||||
RegressionTargetCodec mime.MimeType
|
||||
PreferVideoSizeFromMedia bool
|
||||
}
|
||||
|
||||
type MediaTrackReceiver struct {
|
||||
@@ -161,21 +161,15 @@ func NewMediaTrackReceiver(params MediaTrackReceiverParams, ti *livekit.TrackInf
|
||||
IsRelayed: params.IsRelayed,
|
||||
ReceiverConfig: params.ReceiverConfig,
|
||||
SubscriberConfig: params.SubscriberConfig,
|
||||
Telemetry: params.Telemetry,
|
||||
Logger: params.Logger,
|
||||
})
|
||||
t.MediaTrackSubscriptions.OnDownTrackCreated(t.onDownTrackCreated)
|
||||
|
||||
if ti.Muted {
|
||||
t.SetMuted(true)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) Restart() {
|
||||
hq := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.TrackInfo())
|
||||
|
||||
for _, receiver := range t.loadReceivers() {
|
||||
hq := buffer.GetSpatialLayerForVideoQuality(receiver.Mime(), livekit.VideoQuality_HIGH, t.TrackInfo())
|
||||
receiver.SetMaxExpectedSpatialLayer(hq)
|
||||
}
|
||||
}
|
||||
@@ -213,8 +207,8 @@ func (t *MediaTrackReceiver) SetupReceiver(receiver sfu.TrackReceiver, priority
|
||||
receivers = append(receivers, &simulcastReceiver{TrackReceiver: receiver, priority: priority})
|
||||
}
|
||||
|
||||
sort.Slice(receivers, func(i, j int) bool {
|
||||
return receivers[i].Priority() < receivers[j].Priority()
|
||||
slices.SortFunc(receivers, func(a, b *simulcastReceiver) int {
|
||||
return sutils.Signum(a.Priority() - b.Priority())
|
||||
})
|
||||
|
||||
if mid != "" {
|
||||
@@ -308,12 +302,16 @@ func (t *MediaTrackReceiver) HandleReceiverCodecChange(r sfu.TrackReceiver, code
|
||||
return
|
||||
}
|
||||
|
||||
t.params.Logger.Infow("regressing codec", "from", codec.MimeType, "to", backupCodecReceiver.Codec().MimeType)
|
||||
t.params.Logger.Infow(
|
||||
"regressing codec",
|
||||
"from", codec.MimeType,
|
||||
"to", backupCodecReceiver.Codec().MimeType,
|
||||
)
|
||||
|
||||
// remove old codec from potential codecs
|
||||
for i, c := range t.potentialCodecs {
|
||||
if strings.EqualFold(c.MimeType, codec.MimeType) {
|
||||
slices.Delete(t.potentialCodecs, i, i+1)
|
||||
t.potentialCodecs = slices.Delete(t.potentialCodecs, i, i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -351,8 +349,8 @@ func (t *MediaTrackReceiver) SetPotentialCodecs(codecs []webrtc.RTPCodecParamete
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(receivers, func(i, j int) bool {
|
||||
return receivers[i].Priority() < receivers[j].Priority()
|
||||
slices.SortFunc(receivers, func(a, b *simulcastReceiver) int {
|
||||
return sutils.Signum(a.Priority() - b.Priority())
|
||||
})
|
||||
t.receivers = receivers
|
||||
t.lock.Unlock()
|
||||
@@ -376,7 +374,7 @@ func (t *MediaTrackReceiver) ClearReceiver(mime mime.MimeType, isExpectedToResum
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) ClearAllReceivers(isExpectedToResume bool) {
|
||||
t.params.Logger.Debugw("clearing all receivers")
|
||||
t.params.Logger.Debugw("clearing all receivers", "isExpectedToResume", isExpectedToResume)
|
||||
t.lock.Lock()
|
||||
receivers := t.receivers
|
||||
t.receivers = nil
|
||||
@@ -408,12 +406,15 @@ func (t *MediaTrackReceiver) IsOpen() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) SetClosing() {
|
||||
func (t *MediaTrackReceiver) SetClosing(isExpectedToResume bool) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
if t.state == mediaTrackReceiverStateOpen {
|
||||
t.state = mediaTrackReceiverStateClosing
|
||||
}
|
||||
|
||||
t.isExpectedToResume = isExpectedToResume
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) TryClose() bool {
|
||||
@@ -444,6 +445,8 @@ func (t *MediaTrackReceiver) TryClose() bool {
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) Close(isExpectedToResume bool) {
|
||||
t.ClearAllReceivers(isExpectedToResume)
|
||||
|
||||
t.lock.Lock()
|
||||
if t.state == mediaTrackReceiverStateClosed {
|
||||
t.lock.Unlock()
|
||||
@@ -476,7 +479,7 @@ func (t *MediaTrackReceiver) Stream() string {
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) PublisherID() livekit.ParticipantID {
|
||||
return t.params.ParticipantID
|
||||
return t.params.ParticipantID()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) PublisherIdentity() livekit.ParticipantIdentity {
|
||||
@@ -487,19 +490,6 @@ func (t *MediaTrackReceiver) PublisherVersion() uint32 {
|
||||
return t.params.ParticipantVersion
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) IsSimulcast() bool {
|
||||
return t.TrackInfo().Simulcast
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) SetSimulcast(simulcast bool) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
trackInfo := t.TrackInfoClone()
|
||||
trackInfo.Simulcast = simulcast
|
||||
t.trackInfo.Store(trackInfo)
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) Name() string {
|
||||
return t.TrackInfo().Name
|
||||
}
|
||||
@@ -513,21 +503,19 @@ func (t *MediaTrackReceiver) SetMuted(muted bool) {
|
||||
trackInfo := t.TrackInfoClone()
|
||||
trackInfo.Muted = muted
|
||||
t.trackInfo.Store(trackInfo)
|
||||
|
||||
receivers := t.receivers
|
||||
t.lock.Unlock()
|
||||
|
||||
for _, receiver := range receivers {
|
||||
receiver.SetUpTrackPaused(muted)
|
||||
}
|
||||
|
||||
t.MediaTrackSubscriptions.SetMuted(muted)
|
||||
t.updateTrackInfoOfReceivers()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) IsEncrypted() bool {
|
||||
return t.TrackInfo().Encryption != livekit.Encryption_NONE
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) HasPacketTrailer() bool {
|
||||
return len(t.TrackInfo().GetPacketTrailerFeatures()) > 0
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) AddOnClose(f func(isExpectedToResume bool)) {
|
||||
if f == nil {
|
||||
return
|
||||
@@ -588,8 +576,10 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su
|
||||
StreamId: streamId,
|
||||
UpstreamCodecs: potentialCodecs,
|
||||
Logger: tLogger,
|
||||
DisableRed: t.TrackInfo().GetDisableRed() || !t.params.AudioConfig.ActiveREDEncoding,
|
||||
DisableRed: !IsRedEnabled(t.TrackInfo()) || !t.params.AudioConfig.ActiveREDEncoding,
|
||||
IsEncrypted: t.IsEncrypted(),
|
||||
})
|
||||
subID := sub.ID()
|
||||
subTrack, err := t.MediaTrackSubscriptions.AddSubscriber(sub, wr)
|
||||
|
||||
// media track could have been closed while adding subscription
|
||||
@@ -603,7 +593,12 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su
|
||||
t.lock.RUnlock()
|
||||
|
||||
if remove {
|
||||
_ = t.MediaTrackSubscriptions.RemoveSubscriber(sub.ID(), isExpectedToResume)
|
||||
t.params.Logger.Debugw(
|
||||
"removing subscriber on a not-open track",
|
||||
"subscriberID", subID,
|
||||
"isExpectedToResume", isExpectedToResume,
|
||||
)
|
||||
_ = t.MediaTrackSubscriptions.RemoveSubscriber(subID, isExpectedToResume)
|
||||
return nil, ErrNotOpen
|
||||
}
|
||||
|
||||
@@ -617,7 +612,7 @@ func (t *MediaTrackReceiver) RemoveSubscriber(subscriberID livekit.ParticipantID
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) removeAllSubscribersForMime(mime mime.MimeType, isExpectedToResume bool) {
|
||||
t.params.Logger.Debugw("removing all subscribers for mime", "mime", mime)
|
||||
t.params.Logger.Debugw("removing all subscribers for mime", "mime", mime, "isExpectedToResume", isExpectedToResume)
|
||||
for _, subscriberID := range t.MediaTrackSubscriptions.GetAllSubscribersForMime(mime) {
|
||||
t.RemoveSubscriber(subscriberID, isExpectedToResume)
|
||||
}
|
||||
@@ -632,14 +627,7 @@ func (t *MediaTrackReceiver) RevokeDisallowedSubscribers(allowedSubscriberIdenti
|
||||
continue
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, allowedIdentity := range allowedSubscriberIdentities {
|
||||
if subTrack.SubscriberIdentity() == allowedIdentity {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
found := slices.Contains(allowedSubscriberIdentities, subTrack.SubscriberIdentity())
|
||||
if !found {
|
||||
t.params.Logger.Infow("revoking subscription",
|
||||
"subscriber", subTrack.SubscriberIdentity(),
|
||||
@@ -658,17 +646,42 @@ func (t *MediaTrackReceiver) updateTrackInfoOfReceivers() {
|
||||
for _, r := range t.loadReceivers() {
|
||||
r.UpdateTrackInfo(ti)
|
||||
}
|
||||
|
||||
t.MediaTrackSubscriptions.SetMuted(ti.GetMuted())
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ssrc uint32) {
|
||||
func (t *MediaTrackReceiver) MaybeSetSimulcast() {
|
||||
// only primary receiver (i.e. receiver at index 0) for legacy use case
|
||||
primaryReceiver := t.PrimaryReceiver()
|
||||
if primaryReceiver == nil {
|
||||
return
|
||||
}
|
||||
if wr, ok := primaryReceiver.(*sfu.WebRTCReceiver); !ok || wr.NumUpTracks() < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfoClone()
|
||||
layer := buffer.RidToSpatialLayer(rid, trackInfo)
|
||||
if trackInfo.Simulcast {
|
||||
t.lock.Unlock()
|
||||
return
|
||||
}
|
||||
trackInfo.Simulcast = true
|
||||
t.trackInfo.Store(trackInfo)
|
||||
t.lock.Unlock()
|
||||
|
||||
t.updateTrackInfoOfReceivers()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) SetLayerSsrcsForRid(mimeType mime.MimeType, rid string, ssrc uint32, repairSSRC uint32) {
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfoClone()
|
||||
layer := buffer.GetSpatialLayerForRid(mimeType, rid, trackInfo)
|
||||
if layer == buffer.InvalidLayerSpatial {
|
||||
// non-simulcast case will not have `rid`
|
||||
layer = 0
|
||||
}
|
||||
quality := buffer.SpatialLayerToVideoQuality(layer, trackInfo)
|
||||
quality := buffer.GetVideoQualityForSpatialLayer(mimeType, layer, trackInfo)
|
||||
// set video layer ssrc info
|
||||
for i, ci := range trackInfo.Codecs {
|
||||
if mime.NormalizeMimeType(ci.MimeType) != mimeType {
|
||||
@@ -689,6 +702,20 @@ func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ss
|
||||
}
|
||||
if !ssrcFound && matchingLayer != nil {
|
||||
matchingLayer.Ssrc = ssrc
|
||||
if repairSSRC != 0 {
|
||||
matchingLayer.RepairSsrc = repairSSRC
|
||||
}
|
||||
}
|
||||
if ssrcFound && (matchingLayer.Ssrc != ssrc || matchingLayer.RepairSsrc != repairSSRC) {
|
||||
t.params.Logger.Warnw(
|
||||
"not overriding ssrc", nil,
|
||||
"rid", rid,
|
||||
"ssrc", ssrc,
|
||||
"existingSSRC", matchingLayer.Ssrc,
|
||||
"repairSSRC", repairSSRC,
|
||||
"existingRepairSSRC", matchingLayer.RepairSsrc,
|
||||
"trackInfo", logger.Proto(trackInfo),
|
||||
)
|
||||
}
|
||||
|
||||
// for client don't use simulcast codecs (old client version or single codec)
|
||||
@@ -703,13 +730,99 @@ func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ss
|
||||
t.updateTrackInfoOfReceivers()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
|
||||
func (t *MediaTrackReceiver) setLayerRtxInfo(ssrc uint32, repairSSRC uint32, rsid string) {
|
||||
t.params.Logger.Debugw("rtx notification", "ssrc", ssrc, "repairSSRC", repairSSRC, "rsid", rsid)
|
||||
if ssrc == 0 || repairSSRC == 0 || rsid == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfoClone()
|
||||
|
||||
done:
|
||||
for _, ci := range trackInfo.Codecs {
|
||||
for _, l := range ci.Layers {
|
||||
if l.Ssrc == ssrc {
|
||||
if (l.RepairSsrc != 0 && l.RepairSsrc != repairSSRC) || (l.Rid != "" && l.Rid != rsid) {
|
||||
t.params.Logger.Warnw(
|
||||
"not overriding rtx info", nil,
|
||||
"ssrc", ssrc,
|
||||
"repairSSRC", repairSSRC,
|
||||
"existingRepairSSRC", l.RepairSsrc,
|
||||
"rsid", rsid,
|
||||
"existingRid", l.Rid,
|
||||
"trackInfo", logger.Proto(trackInfo),
|
||||
)
|
||||
} else {
|
||||
l.RepairSsrc = repairSSRC
|
||||
t.params.Logger.Debugw(
|
||||
"set rtx info",
|
||||
"ssrc", ssrc,
|
||||
"repairSSRC", repairSSRC,
|
||||
"rsid", rsid,
|
||||
"trackInfo", logger.Proto(trackInfo),
|
||||
)
|
||||
}
|
||||
break done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// backwards compatibility
|
||||
for _, l := range trackInfo.Layers {
|
||||
if l.Ssrc == ssrc {
|
||||
if (l.RepairSsrc != 0 && l.RepairSsrc != repairSSRC) || (l.Rid != "" && l.Rid != rsid) {
|
||||
t.params.Logger.Warnw(
|
||||
"not overriding rtx info", nil,
|
||||
"ssrc", ssrc,
|
||||
"repairSSRC", repairSSRC,
|
||||
"existingRepairSSRC", l.RepairSsrc,
|
||||
"rsid", rsid,
|
||||
"existingRid", l.Rid,
|
||||
"trackInfo", logger.Proto(trackInfo),
|
||||
)
|
||||
} else {
|
||||
l.RepairSsrc = repairSSRC
|
||||
t.params.Logger.Debugw(
|
||||
"set rtx info",
|
||||
"ssrc", ssrc,
|
||||
"repairSSRC", repairSSRC,
|
||||
"rsid", rsid,
|
||||
"trackInfo", logger.Proto(trackInfo),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.trackInfo.Store(trackInfo)
|
||||
t.lock.Unlock()
|
||||
|
||||
// change not propagated as it is internal
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) UpdateCodecInfo(codecs []*livekit.SimulcastCodec) {
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfoClone()
|
||||
for _, c := range codecs {
|
||||
for _, origin := range trackInfo.Codecs {
|
||||
if mime.GetMimeTypeCodec(origin.MimeType) == mime.NormalizeMimeTypeCodec(c.Codec) {
|
||||
origin.Cid = c.Cid
|
||||
|
||||
if len(c.Layers) != 0 {
|
||||
clonedLayers := make([]*livekit.VideoLayer, 0, len(c.Layers))
|
||||
for _, l := range c.Layers {
|
||||
clonedLayers = append(clonedLayers, utils.CloneProto(l))
|
||||
}
|
||||
origin.Layers = clonedLayers
|
||||
|
||||
mimeType := mime.NormalizeMimeType(origin.MimeType)
|
||||
for _, layer := range origin.Layers {
|
||||
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(mimeType, layer.Quality, trackInfo)
|
||||
layer.Rid = buffer.VideoQualityToRid(mimeType, layer.Quality, trackInfo, buffer.DefaultVideoLayersRid)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -720,13 +833,49 @@ func (t *MediaTrackReceiver) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
|
||||
t.updateTrackInfoOfReceivers()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) UpdateCodecSdpCid(mimeType mime.MimeType, sdpCid string) {
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfoClone()
|
||||
for _, origin := range trackInfo.Codecs {
|
||||
if mime.NormalizeMimeType(origin.MimeType) == mimeType {
|
||||
if sdpCid != origin.Cid {
|
||||
origin.SdpCid = sdpCid
|
||||
}
|
||||
}
|
||||
}
|
||||
t.trackInfo.Store(trackInfo)
|
||||
t.lock.Unlock()
|
||||
|
||||
t.updateTrackInfoOfReceivers()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) UpdateCodecRids(mimeType mime.MimeType, rids buffer.VideoLayersRid) {
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfoClone()
|
||||
for _, origin := range trackInfo.Codecs {
|
||||
originMimeType := mime.NormalizeMimeType(origin.MimeType)
|
||||
if originMimeType != mimeType {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, layer := range origin.Layers {
|
||||
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(mimeType, layer.Quality, trackInfo)
|
||||
layer.Rid = buffer.VideoQualityToRid(mimeType, layer.Quality, trackInfo, rids)
|
||||
}
|
||||
break
|
||||
}
|
||||
t.trackInfo.Store(trackInfo)
|
||||
t.lock.Unlock()
|
||||
|
||||
t.updateTrackInfoOfReceivers()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
|
||||
updateMute := false
|
||||
clonedInfo := utils.CloneProto(ti)
|
||||
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfo()
|
||||
// patch Mid and SSRC of codecs/layers by keeping original if available
|
||||
// patch Mid/Rid and Ssrc/RtxSsrc of codecs/layers by keeping original if available
|
||||
for i, ci := range clonedInfo.Codecs {
|
||||
for _, originCi := range trackInfo.Codecs {
|
||||
if !mime.IsMimeTypeStringEqual(ci.MimeType, originCi.MimeType) {
|
||||
@@ -743,6 +892,13 @@ func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
|
||||
if originLayer.Ssrc != 0 {
|
||||
layer.Ssrc = originLayer.Ssrc
|
||||
}
|
||||
if originLayer.Rid != "" {
|
||||
layer.Rid = originLayer.Rid
|
||||
}
|
||||
|
||||
if originLayer.RepairSsrc != 0 {
|
||||
layer.RepairSsrc = originLayer.RepairSsrc
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -755,16 +911,9 @@ func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
|
||||
clonedInfo.Layers = ci.Layers
|
||||
}
|
||||
}
|
||||
if trackInfo.Muted != clonedInfo.Muted {
|
||||
updateMute = true
|
||||
}
|
||||
t.trackInfo.Store(clonedInfo)
|
||||
t.lock.Unlock()
|
||||
|
||||
if updateMute {
|
||||
t.SetMuted(clonedInfo.Muted)
|
||||
}
|
||||
|
||||
t.updateTrackInfoOfReceivers()
|
||||
}
|
||||
|
||||
@@ -776,7 +925,9 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfo()
|
||||
clonedInfo := utils.CloneProto(trackInfo)
|
||||
clonedInfo.AudioFeatures = update.Features
|
||||
|
||||
clonedInfo.AudioFeatures = sutils.DedupeSlice(update.Features)
|
||||
|
||||
clonedInfo.Stereo = false
|
||||
clonedInfo.DisableDtx = false
|
||||
for _, feature := range update.Features {
|
||||
@@ -787,6 +938,7 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
|
||||
clonedInfo.DisableDtx = true
|
||||
}
|
||||
}
|
||||
|
||||
if proto.Equal(trackInfo, clonedInfo) {
|
||||
t.lock.Unlock()
|
||||
return
|
||||
@@ -797,7 +949,7 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
|
||||
|
||||
t.updateTrackInfoOfReceivers()
|
||||
|
||||
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), clonedInfo)
|
||||
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
|
||||
t.params.Logger.Debugw("updated audio track", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
|
||||
}
|
||||
|
||||
@@ -821,10 +973,56 @@ func (t *MediaTrackReceiver) UpdateVideoTrack(update *livekit.UpdateLocalVideoTr
|
||||
|
||||
t.updateTrackInfoOfReceivers()
|
||||
|
||||
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), clonedInfo)
|
||||
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
|
||||
t.params.Logger.Debugw("updated video track", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) UpdateVideoSize(mimeType mime.MimeType, sizes []codec.VideoSize) {
|
||||
var changed bool
|
||||
t.lock.Lock()
|
||||
trackInfo := t.TrackInfo()
|
||||
clonedInfo := utils.CloneProto(trackInfo)
|
||||
var maxWidth, maxHeight uint32
|
||||
for _, size := range sizes {
|
||||
if size.Width > maxWidth {
|
||||
maxWidth = size.Width
|
||||
maxHeight = size.Height
|
||||
}
|
||||
}
|
||||
|
||||
if clonedInfo.Width != maxWidth || clonedInfo.Height != maxHeight {
|
||||
clonedInfo.Width = maxWidth
|
||||
clonedInfo.Height = maxHeight
|
||||
changed = true
|
||||
}
|
||||
|
||||
for _, c := range clonedInfo.Codecs {
|
||||
if mime.NormalizeMimeType(c.MimeType) == mimeType {
|
||||
for i, l := range c.Layers {
|
||||
if i < len(sizes) && (sizes[i].Width != 0 || sizes[i].Height != 0) &&
|
||||
(l.Width != sizes[i].Width || l.Height != sizes[i].Height) {
|
||||
l.Width = sizes[i].Width
|
||||
l.Height = sizes[i].Height
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
t.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
t.trackInfo.Store(clonedInfo)
|
||||
t.lock.Unlock()
|
||||
|
||||
t.updateTrackInfoOfReceivers()
|
||||
|
||||
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
|
||||
t.params.Logger.Debugw("updated video sizes", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) TrackInfo() *livekit.TrackInfo {
|
||||
return t.trackInfo.Load()
|
||||
}
|
||||
@@ -833,16 +1031,17 @@ func (t *MediaTrackReceiver) TrackInfoClone() *livekit.TrackInfo {
|
||||
return utils.CloneProto(t.TrackInfo())
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) NotifyMaxLayerChange(maxLayer int32) {
|
||||
func (t *MediaTrackReceiver) NotifyMaxLayerChange(mimeType mime.MimeType, maxLayer int32) {
|
||||
trackInfo := t.TrackInfo()
|
||||
quality := buffer.SpatialLayerToVideoQuality(maxLayer, trackInfo)
|
||||
quality := buffer.GetVideoQualityForSpatialLayer(mimeType, maxLayer, trackInfo)
|
||||
ti := &livekit.TrackInfo{
|
||||
Sid: trackInfo.Sid,
|
||||
Type: trackInfo.Type,
|
||||
Layers: []*livekit.VideoLayer{{Quality: quality}},
|
||||
}
|
||||
if quality != livekit.VideoQuality_OFF {
|
||||
for _, layer := range trackInfo.Layers {
|
||||
layers := buffer.GetVideoLayersForMimeType(mimeType, trackInfo)
|
||||
for _, layer := range layers {
|
||||
if layer.Quality == quality {
|
||||
ti.Layers[0].Width = layer.Width
|
||||
ti.Layers[0].Height = layer.Height
|
||||
@@ -851,12 +1050,12 @@ func (t *MediaTrackReceiver) NotifyMaxLayerChange(maxLayer int32) {
|
||||
}
|
||||
}
|
||||
|
||||
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), ti)
|
||||
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), ti)
|
||||
}
|
||||
|
||||
// GetQualityForDimension finds the closest quality to use for desired dimensions
|
||||
// affords a 20% tolerance on dimension
|
||||
func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) livekit.VideoQuality {
|
||||
func (t *MediaTrackReceiver) GetQualityForDimension(mimeType mime.MimeType, width, height uint32) livekit.VideoQuality {
|
||||
quality := livekit.VideoQuality_HIGH
|
||||
if t.Kind() == livekit.TrackType_AUDIO {
|
||||
return quality
|
||||
@@ -864,7 +1063,12 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
|
||||
|
||||
trackInfo := t.TrackInfo()
|
||||
|
||||
if trackInfo.Height == 0 {
|
||||
var mediaSizes []codec.VideoSize
|
||||
if receiver := t.Receiver(mimeType); receiver != nil {
|
||||
mediaSizes = receiver.VideoSizes()
|
||||
}
|
||||
|
||||
if trackInfo.Height == 0 && len(mediaSizes) == 0 {
|
||||
return quality
|
||||
}
|
||||
origSize := trackInfo.Height
|
||||
@@ -875,19 +1079,38 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
|
||||
requestedSize = width
|
||||
}
|
||||
|
||||
if origSize == 0 {
|
||||
for i := len(mediaSizes) - 1; i >= 0; i-- {
|
||||
if mediaSizes[i].Height > 0 {
|
||||
origSize = min(mediaSizes[i].Width, mediaSizes[i].Height)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default sizes representing qualities low - high
|
||||
layerSizes := []uint32{180, 360, origSize}
|
||||
var providedSizes []uint32
|
||||
for _, layer := range trackInfo.Layers {
|
||||
for _, layer := range buffer.GetVideoLayersForMimeType(mimeType, trackInfo) {
|
||||
providedSizes = append(providedSizes, layer.Height)
|
||||
}
|
||||
|
||||
if len(providedSizes) == 0 || providedSizes[0] == 0 || t.params.PreferVideoSizeFromMedia {
|
||||
if len(mediaSizes) > 0 {
|
||||
providedSizes = providedSizes[:0]
|
||||
for _, size := range mediaSizes {
|
||||
providedSizes = append(providedSizes, size.Height)
|
||||
}
|
||||
} else {
|
||||
t.params.Logger.Debugw("no video sizes provided by receiver, using track info sizes")
|
||||
}
|
||||
}
|
||||
|
||||
if len(providedSizes) > 0 {
|
||||
layerSizes = providedSizes
|
||||
// comparing height always
|
||||
requestedSize = height
|
||||
sort.Slice(layerSizes, func(i, j int) bool {
|
||||
return layerSizes[i] < layerSizes[j]
|
||||
})
|
||||
slices.Sort(layerSizes)
|
||||
}
|
||||
|
||||
// finds the highest layer with smallest dimensions that still satisfy client demands
|
||||
@@ -905,7 +1128,7 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) GetAudioLevel() (float64, bool) {
|
||||
receiver := t.PrimaryReceiver()
|
||||
receiver := t.ActiveReceiver()
|
||||
if receiver == nil {
|
||||
return 0, false
|
||||
}
|
||||
@@ -923,8 +1146,8 @@ func (t *MediaTrackReceiver) onDownTrackCreated(downTrack *sfu.DownTrack) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) DebugInfo() map[string]interface{} {
|
||||
info := map[string]interface{}{
|
||||
func (t *MediaTrackReceiver) DebugInfo() map[string]any {
|
||||
info := map[string]any{
|
||||
"ID": t.ID(),
|
||||
"Kind": t.Kind().String(),
|
||||
"PubMuted": t.IsMuted(),
|
||||
@@ -950,6 +1173,16 @@ func (t *MediaTrackReceiver) PrimaryReceiver() sfu.TrackReceiver {
|
||||
return receivers[0].TrackReceiver
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) ActiveReceiver() sfu.TrackReceiver {
|
||||
for _, r := range t.loadReceivers() {
|
||||
if r.IsRegressed() {
|
||||
return r.TrackReceiver
|
||||
}
|
||||
}
|
||||
|
||||
return t.PrimaryReceiver()
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) Receiver(mime mime.MimeType) sfu.TrackReceiver {
|
||||
for _, r := range t.loadReceivers() {
|
||||
if r.Mime() == mime {
|
||||
@@ -985,8 +1218,8 @@ func (t *MediaTrackReceiver) SetRTT(rtt uint32) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MediaTrackReceiver) GetTemporalLayerForSpatialFps(spatial int32, fps uint32, mime mime.MimeType) int32 {
|
||||
receiver := t.Receiver(mime)
|
||||
func (t *MediaTrackReceiver) GetTemporalLayerForSpatialFps(mimeType mime.MimeType, spatial int32, fps uint32) int32 {
|
||||
receiver := t.Receiver(mimeType)
|
||||
if receiver == nil {
|
||||
return buffer.DefaultMaxLayerTemporal
|
||||
}
|
||||
|
||||
@@ -16,21 +16,17 @@ package rtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
sutils "github.com/livekit/livekit-server/pkg/utils"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -47,6 +43,7 @@ type MediaTrackSubscriptions struct {
|
||||
|
||||
onDownTrackCreated func(downTrack *sfu.DownTrack)
|
||||
onSubscriberMaxQualityChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)
|
||||
onSubscriberAudioCodecChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)
|
||||
}
|
||||
|
||||
type MediaTrackSubscriptionsParams struct {
|
||||
@@ -56,8 +53,6 @@ type MediaTrackSubscriptionsParams struct {
|
||||
ReceiverConfig ReceiverConfig
|
||||
SubscriberConfig DirectionConfig
|
||||
|
||||
Telemetry telemetry.TelemetryService
|
||||
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
@@ -76,6 +71,10 @@ func (t *MediaTrackSubscriptions) OnSubscriberMaxQualityChange(f func(subscriber
|
||||
t.onSubscriberMaxQualityChange = f
|
||||
}
|
||||
|
||||
func (t *MediaTrackSubscriptions) OnSubscriberAudioCodecChange(f func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)) {
|
||||
t.onSubscriberAudioCodecChange = f
|
||||
}
|
||||
|
||||
func (t *MediaTrackSubscriptions) SetMuted(muted bool) {
|
||||
// update mute of all subscribed tracks
|
||||
for _, st := range t.getAllSubscribedTracks() {
|
||||
@@ -104,86 +103,32 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
|
||||
}
|
||||
t.subscribedTracksMu.Unlock()
|
||||
|
||||
var rtcpFeedback []webrtc.RTCPFeedback
|
||||
var maxTrack int
|
||||
switch t.params.MediaTrack.Kind() {
|
||||
case livekit.TrackType_AUDIO:
|
||||
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Audio
|
||||
maxTrack = t.params.ReceiverConfig.PacketBufferSizeAudio
|
||||
case livekit.TrackType_VIDEO:
|
||||
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Video
|
||||
maxTrack = t.params.ReceiverConfig.PacketBufferSizeVideo
|
||||
}
|
||||
codecs := wr.Codecs()
|
||||
for _, c := range codecs {
|
||||
c.RTCPFeedback = rtcpFeedback
|
||||
}
|
||||
|
||||
streamID := wr.StreamID()
|
||||
if sub.SupportsSyncStreamID() && t.params.MediaTrack.Stream() != "" {
|
||||
streamID = PackSyncStreamID(t.params.MediaTrack.PublisherID(), t.params.MediaTrack.Stream())
|
||||
}
|
||||
|
||||
var trailer []byte
|
||||
if t.params.MediaTrack.IsEncrypted() {
|
||||
trailer = sub.GetTrailer()
|
||||
}
|
||||
|
||||
downTrack, err := sfu.NewDownTrack(sfu.DowntrackParams{
|
||||
Codecs: codecs,
|
||||
Source: t.params.MediaTrack.Source(),
|
||||
Receiver: wr,
|
||||
BufferFactory: sub.GetBufferFactory(),
|
||||
SubID: subscriberID,
|
||||
StreamID: streamID,
|
||||
MaxTrack: maxTrack,
|
||||
PlayoutDelayLimit: sub.GetPlayoutDelayConfig(),
|
||||
Pacer: sub.GetPacer(),
|
||||
Trailer: trailer,
|
||||
Logger: LoggerWithTrack(sub.GetLogger().WithComponent(sutils.ComponentSub), trackID, t.params.IsRelayed),
|
||||
RTCPWriter: sub.WriteSubscriberRTCP,
|
||||
DisableSenderReportPassThrough: sub.GetDisableSenderReportPassThrough(),
|
||||
SupportsCodecChange: sub.SupportsCodecChange(),
|
||||
subTrack, err := NewSubscribedTrack(SubscribedTrackParams{
|
||||
ReceiverConfig: t.params.ReceiverConfig,
|
||||
SubscriberConfig: t.params.SubscriberConfig,
|
||||
Subscriber: sub,
|
||||
MediaTrack: t.params.MediaTrack,
|
||||
AdaptiveStream: sub.GetAdaptiveStream(),
|
||||
TelemetryListener: sub.GetTelemetryListener(),
|
||||
WrappedReceiver: wr,
|
||||
IsRelayed: t.params.IsRelayed,
|
||||
OnDownTrackCreated: t.onDownTrackCreated,
|
||||
OnDownTrackClosed: func(subscriberID livekit.ParticipantID) {
|
||||
t.subscribedTracksMu.Lock()
|
||||
delete(t.subscribedTracks, subscriberID)
|
||||
t.subscribedTracksMu.Unlock()
|
||||
},
|
||||
OnSubscriberMaxQualityChange: t.onSubscriberMaxQualityChange,
|
||||
OnSubscriberAudioCodecChange: t.onSubscriberAudioCodecChange,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t.onDownTrackCreated != nil {
|
||||
t.onDownTrackCreated(downTrack)
|
||||
}
|
||||
|
||||
subTrack := NewSubscribedTrack(SubscribedTrackParams{
|
||||
PublisherID: t.params.MediaTrack.PublisherID(),
|
||||
PublisherIdentity: t.params.MediaTrack.PublisherIdentity(),
|
||||
PublisherVersion: t.params.MediaTrack.PublisherVersion(),
|
||||
Subscriber: sub,
|
||||
MediaTrack: t.params.MediaTrack,
|
||||
DownTrack: downTrack,
|
||||
AdaptiveStream: sub.GetAdaptiveStream(),
|
||||
})
|
||||
|
||||
if !sub.Hidden() {
|
||||
subTrack.AddOnBind(func(err error) {
|
||||
if err == nil {
|
||||
t.params.MediaTrack.OnTrackSubscribed()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Bind callback can happen from replaceTrack, so set it up early
|
||||
var reusingTransceiver atomic.Bool
|
||||
var dtState sfu.DownTrackState
|
||||
downTrack.OnCodecNegotiated(func(codec webrtc.RTPCodecCapability) {
|
||||
if !wr.DetermineReceiver(codec) {
|
||||
if t.onSubscriberMaxQualityChange != nil {
|
||||
go func() {
|
||||
spatial := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.params.MediaTrack.ToProto())
|
||||
t.onSubscriberMaxQualityChange(downTrack.SubscriberID(), mime.NormalizeMimeType(codec.MimeType), spatial)
|
||||
}()
|
||||
}
|
||||
}
|
||||
})
|
||||
downTrack := subTrack.DownTrack()
|
||||
downTrack.OnBinding(func(err error) {
|
||||
if err != nil {
|
||||
go subTrack.Bound(err)
|
||||
@@ -207,25 +152,6 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
|
||||
subTrack.SetPublisherMuted(t.params.MediaTrack.IsMuted())
|
||||
})
|
||||
|
||||
downTrack.OnStatsUpdate(func(_ *sfu.DownTrack, stat *livekit.AnalyticsStat) {
|
||||
key := telemetry.StatsKeyForTrack(livekit.StreamType_DOWNSTREAM, subscriberID, trackID, t.params.MediaTrack.Source(), t.params.MediaTrack.Kind())
|
||||
t.params.Telemetry.TrackStats(key, stat)
|
||||
})
|
||||
|
||||
downTrack.OnMaxLayerChanged(func(dt *sfu.DownTrack, layer int32) {
|
||||
if t.onSubscriberMaxQualityChange != nil {
|
||||
t.onSubscriberMaxQualityChange(dt.SubscriberID(), dt.Mime(), layer)
|
||||
}
|
||||
})
|
||||
|
||||
downTrack.OnRttUpdate(func(_ *sfu.DownTrack, rtt uint32) {
|
||||
go sub.UpdateMediaRTT(rtt)
|
||||
})
|
||||
|
||||
downTrack.AddReceiverReportListener(func(dt *sfu.DownTrack, report *rtcp.ReceiverReport) {
|
||||
sub.HandleReceiverReport(dt, report)
|
||||
})
|
||||
|
||||
var transceiver *webrtc.RTPTransceiver
|
||||
var sender *webrtc.RTPSender
|
||||
|
||||
@@ -277,15 +203,16 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
|
||||
if transceiver == nil {
|
||||
info := t.params.MediaTrack.ToProto()
|
||||
addTrackParams := types.AddTrackParams{
|
||||
Stereo: info.Stereo,
|
||||
Red: !info.DisableRed,
|
||||
Stereo: slices.Contains(info.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO),
|
||||
Red: IsRedEnabled(info),
|
||||
}
|
||||
codecs := wr.Codecs()
|
||||
if addTrackParams.Red && (len(codecs) == 1 && mime.IsMimeTypeStringOpus(codecs[0].MimeType)) {
|
||||
addTrackParams.Red = false
|
||||
}
|
||||
|
||||
sub.VerifySubscribeParticipantInfo(subTrack.PublisherID(), subTrack.PublisherVersion())
|
||||
if sub.SupportsTransceiverReuse() {
|
||||
if sub.SupportsTransceiverReuse(t.params.MediaTrack) {
|
||||
//
|
||||
// AddTrack will create a new transceiver or re-use an unused one
|
||||
// if the attributes match. This prevents SDP from bloating
|
||||
@@ -320,6 +247,7 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
|
||||
// But, the subscription could be removed early if the published track is closed
|
||||
// while adding subscription. In those cases, subscription manager would not have set
|
||||
// the `OnClose` callback. So, set it here to handle cases of early close.
|
||||
// Subscription manager will reset this if this subscription proceeds till that point.
|
||||
subTrack.OnClose(func(isExpectedToResume bool) {
|
||||
if !isExpectedToResume {
|
||||
if err := sub.RemoveTrackLocal(sender); err != nil {
|
||||
@@ -330,10 +258,6 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
|
||||
|
||||
downTrack.SetTransceiver(transceiver)
|
||||
|
||||
downTrack.OnCloseHandler(func(isExpectedToResume bool) {
|
||||
t.downTrackClosed(sub, subTrack, isExpectedToResume)
|
||||
})
|
||||
|
||||
t.subscribedTracksMu.Lock()
|
||||
t.subscribedTracks[subscriberID] = subTrack
|
||||
t.subscribedTracksMu.Unlock()
|
||||
@@ -361,10 +285,10 @@ func (t *MediaTrackSubscriptions) closeSubscribedTrack(subTrack types.Subscribed
|
||||
}
|
||||
|
||||
if isExpectedToResume {
|
||||
dt.CloseWithFlush(false)
|
||||
dt.CloseWithFlush(false, false)
|
||||
} else {
|
||||
// flushing blocks, avoid blocking when publisher removes all its subscribers
|
||||
go dt.CloseWithFlush(true)
|
||||
go dt.CloseWithFlush(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,8 +353,8 @@ func (t *MediaTrackSubscriptions) getAllSubscribedTracksLocked() []types.Subscri
|
||||
return subTracks
|
||||
}
|
||||
|
||||
func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} {
|
||||
subscribedTrackInfo := make([]map[string]interface{}, 0)
|
||||
func (t *MediaTrackSubscriptions) DebugInfo() []map[string]any {
|
||||
subscribedTrackInfo := make([]map[string]any, 0)
|
||||
for _, val := range t.getAllSubscribedTracks() {
|
||||
if st, ok := val.(*SubscribedTrack); ok {
|
||||
subscribedTrackInfo = append(subscribedTrackInfo, st.DownTrack().DebugInfo())
|
||||
@@ -439,27 +363,3 @@ func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} {
|
||||
|
||||
return subscribedTrackInfo
|
||||
}
|
||||
|
||||
func (t *MediaTrackSubscriptions) downTrackClosed(
|
||||
sub types.LocalParticipant,
|
||||
subTrack types.SubscribedTrack,
|
||||
isExpectedToResume bool,
|
||||
) {
|
||||
// Cache transceiver for potential re-use on resume.
|
||||
// To ensure subscription manager does not re-subscribe before caching,
|
||||
// delete the subscribed track only after caching.
|
||||
if isExpectedToResume {
|
||||
dt := subTrack.DownTrack()
|
||||
tr := dt.GetTransceiver()
|
||||
if tr != nil {
|
||||
sub.CacheDownTrack(subTrack.ID(), tr, dt.GetState())
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
t.subscribedTracksMu.Lock()
|
||||
delete(t.subscribedTracks, sub.ID())
|
||||
t.subscribedTracksMu.Unlock()
|
||||
subTrack.Close(isExpectedToResume)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type MigrationDataCacheState int
|
||||
|
||||
const (
|
||||
MigrationDataCacheStateWaiting MigrationDataCacheState = iota
|
||||
MigrationDataCacheStateTimeout
|
||||
MigrationDataCacheStateDone
|
||||
)
|
||||
|
||||
type MigrationDataCache struct {
|
||||
lastSeq uint32
|
||||
pkts []*livekit.DataPacket
|
||||
state MigrationDataCacheState
|
||||
expiredAt time.Time
|
||||
}
|
||||
|
||||
func NewMigrationDataCache(lastSeq uint32, expiredAt time.Time) *MigrationDataCache {
|
||||
return &MigrationDataCache{
|
||||
lastSeq: lastSeq,
|
||||
expiredAt: expiredAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a message to the cache if there is a gap between the last sequence number and cached messages then return the cache State:
|
||||
// - MigrationDataCacheStateWaiting: waiting for the next packet (lastSeq + 1) of last sequence from old node
|
||||
// - MigrationDataCacheStateTimeout: the next packet is not received before the expiredAt, participant will
|
||||
// continue to process the reliable messages, subscribers will see the gap after the publisher migration
|
||||
// - MigrationDataCacheStateDone: the next packet is received, participant can continue to process the reliable messages
|
||||
func (c *MigrationDataCache) Add(pkt *livekit.DataPacket) MigrationDataCacheState {
|
||||
if c.state == MigrationDataCacheStateDone || c.state == MigrationDataCacheStateTimeout {
|
||||
return c.state
|
||||
}
|
||||
|
||||
if pkt.Sequence <= c.lastSeq {
|
||||
return c.state
|
||||
}
|
||||
|
||||
if pkt.Sequence == c.lastSeq+1 {
|
||||
c.state = MigrationDataCacheStateDone
|
||||
return c.state
|
||||
}
|
||||
|
||||
c.pkts = append(c.pkts, pkt)
|
||||
if time.Now().After(c.expiredAt) {
|
||||
c.state = MigrationDataCacheStateTimeout
|
||||
}
|
||||
return c.state
|
||||
}
|
||||
|
||||
func (c *MigrationDataCache) Get() []*livekit.DataPacket {
|
||||
return c.pkts
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
func TestMigrationDataCache_Add(t *testing.T) {
|
||||
expiredAt := time.Now().Add(100 * time.Millisecond)
|
||||
cache := NewMigrationDataCache(10, expiredAt)
|
||||
|
||||
pkt1 := &livekit.DataPacket{Sequence: 9}
|
||||
state := cache.Add(pkt1)
|
||||
require.Equal(t, MigrationDataCacheStateWaiting, state)
|
||||
require.Empty(t, cache.Get())
|
||||
|
||||
pkt2 := &livekit.DataPacket{Sequence: 11}
|
||||
state = cache.Add(pkt2)
|
||||
require.Equal(t, MigrationDataCacheStateDone, state)
|
||||
require.Empty(t, cache.Get())
|
||||
|
||||
pkt3 := &livekit.DataPacket{Sequence: 12}
|
||||
state = cache.Add(pkt3)
|
||||
require.Equal(t, MigrationDataCacheStateDone, state)
|
||||
require.Empty(t, cache.Get())
|
||||
|
||||
cache2 := NewMigrationDataCache(20, time.Now().Add(10*time.Millisecond))
|
||||
pkt4 := &livekit.DataPacket{Sequence: 22}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
state = cache2.Add(pkt4)
|
||||
require.Equal(t, MigrationDataCacheStateTimeout, state)
|
||||
require.Len(t, cache2.Get(), 1)
|
||||
require.Equal(t, uint32(22), cache2.Get()[0].Sequence)
|
||||
}
|
||||
+1704
-585
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
)
|
||||
|
||||
func (p *ParticipantImpl) HandlePublishDataTrackRequest(req *livekit.PublishDataTrackRequest) {
|
||||
if !p.CanPublishData() || !p.params.EnableDataTracks {
|
||||
p.pubLogger.Warnw("no permission to publish data track", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_NOT_ALLOWED,
|
||||
Message: "does not have permission to publish data",
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.PubHandle == 0 || req.PubHandle > 65535 {
|
||||
p.pubLogger.Warnw("invalid data track handle", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_INVALID_HANDLE,
|
||||
Message: "handle should be > 0 AND < 65536",
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Name) == 0 || len(req.Name) > 256 {
|
||||
p.pubLogger.Warnw("invalid data track name", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_INVALID_NAME,
|
||||
Message: "name should not be empty and should not exceed 256 characters",
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
publishedDataTracks := p.UpDataTrackManager.GetPublishedDataTracks()
|
||||
for _, dt := range publishedDataTracks {
|
||||
message := ""
|
||||
reason := livekit.RequestResponse_OK
|
||||
switch {
|
||||
case dt.PubHandle() == uint16(req.PubHandle):
|
||||
message = "a data track with same handle already exists"
|
||||
reason = livekit.RequestResponse_DUPLICATE_HANDLE
|
||||
case dt.Name() == req.Name:
|
||||
message = "a data track with same name already exists"
|
||||
reason = livekit.RequestResponse_DUPLICATE_NAME
|
||||
}
|
||||
if message != "" {
|
||||
p.pubLogger.Warnw(
|
||||
"cannot publish duplicate data track", nil,
|
||||
"req", logger.Proto(req),
|
||||
"existing", logger.Proto(dt.ToProto()),
|
||||
)
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
Request: &livekit.RequestResponse_PublishDataTrack{
|
||||
PublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dti := &livekit.DataTrackInfo{
|
||||
PubHandle: req.PubHandle,
|
||||
Sid: guid.New(utils.DataTrackPrefix),
|
||||
Name: req.Name,
|
||||
Encryption: req.Encryption,
|
||||
}
|
||||
dt := NewDataTrack(
|
||||
DataTrackParams{
|
||||
Logger: p.params.Logger.WithValues("trackID", dti.Sid),
|
||||
ParticipantID: p.ID,
|
||||
ParticipantIdentity: p.params.Identity,
|
||||
BytesTrackStats: NewBytesTrackStats(
|
||||
p.params.Country,
|
||||
livekit.TrackID(dti.Sid),
|
||||
p.ID(),
|
||||
p.Kind(),
|
||||
p.KindDetails(),
|
||||
p.params.TelemetryListener,
|
||||
p.params.Reporter,
|
||||
),
|
||||
},
|
||||
dti,
|
||||
)
|
||||
|
||||
p.UpDataTrackManager.AddPublishedDataTrack(dt)
|
||||
|
||||
p.sendPublishDataTrackResponse(dti)
|
||||
|
||||
p.setIsPublisher(true)
|
||||
p.dirty.Store(true)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) HandleUnpublishDataTrackRequest(req *livekit.UnpublishDataTrackRequest) {
|
||||
dt := p.UpDataTrackManager.GetPublishedDataTrack(uint16(req.PubHandle))
|
||||
if dt == nil {
|
||||
p.pubLogger.Warnw("unpublish data track not found", nil, "req", logger.Proto(req))
|
||||
p.sendRequestResponse(&livekit.RequestResponse{
|
||||
Reason: livekit.RequestResponse_NOT_FOUND,
|
||||
Request: &livekit.RequestResponse_UnpublishDataTrack{
|
||||
UnpublishDataTrack: utils.CloneProto(req),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.UpDataTrackManager.RemovePublishedDataTrack(dt)
|
||||
|
||||
p.sendUnpublishDataTrackResponse(dt.ToProto())
|
||||
|
||||
p.dirty.Store(true)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) HandleUpdateDataSubscription(req *livekit.UpdateDataSubscription) {
|
||||
p.listener().OnUpdateDataSubscriptions(p, req)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) onReceivedDataTrackMessage(data []byte, arrivalTime int64) {
|
||||
var packet datatrack.Packet
|
||||
if err := packet.Unmarshal(data); err != nil {
|
||||
p.params.Logger.Errorw("could not unmarshal data track message", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.UpDataTrackManager.HandleReceivedDataTrackMessage(data, &packet, arrivalTime)
|
||||
|
||||
p.listener().OnDataTrackMessage(p, data, &packet)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) GetNextSubscribedDataTrackHandle() uint16 {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
p.nextSubscribedDataTrackHandle++
|
||||
if p.nextSubscribedDataTrackHandle == 0 {
|
||||
p.nextSubscribedDataTrackHandle++
|
||||
}
|
||||
|
||||
return p.nextSubscribedDataTrackHandle
|
||||
}
|
||||
@@ -26,11 +26,14 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
|
||||
"github.com/livekit/protocol/auth"
|
||||
protoCodecs "github.com/livekit/protocol/codecs"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/observability/roomobs"
|
||||
lksdp "github.com/livekit/protocol/sdp"
|
||||
"github.com/livekit/protocol/signalling"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
|
||||
@@ -81,14 +84,14 @@ func TestTrackPublishing(t *testing.T) {
|
||||
track.IDReturns("id")
|
||||
published := false
|
||||
updated := false
|
||||
p.OnTrackUpdated(func(p types.LocalParticipant, track types.MediaTrack) {
|
||||
p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackUpdatedCalls(func(p types.Participant, track types.MediaTrack) {
|
||||
updated = true
|
||||
})
|
||||
p.OnTrackPublished(func(p types.LocalParticipant, track types.MediaTrack) {
|
||||
p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackPublishedCalls(func(p types.Participant, track types.MediaTrack) {
|
||||
published = true
|
||||
})
|
||||
p.UpTrackManager.AddPublishedTrack(track)
|
||||
p.handleTrackPublished(track)
|
||||
p.handleTrackPublished(track, false)
|
||||
require.True(t, published)
|
||||
require.False(t, updated)
|
||||
require.Len(t, p.UpTrackManager.publishedTracks, 1)
|
||||
@@ -129,7 +132,8 @@ func TestTrackPublishing(t *testing.T) {
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
})
|
||||
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
// error response on duplicate adds a message
|
||||
require.Equal(t, 2, sink.WriteMessageCallCount())
|
||||
})
|
||||
|
||||
t.Run("should queue adding of duplicate tracks if already published by client id in signalling", func(t *testing.T) {
|
||||
@@ -137,7 +141,7 @@ func TestTrackPublishing(t *testing.T) {
|
||||
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
|
||||
|
||||
track := &typesfakes.FakeLocalMediaTrack{}
|
||||
track.SignalCidReturns("cid")
|
||||
track.HasSignalCidCalls(func(s string) bool { return s == "cid" })
|
||||
track.ToProtoReturns(&livekit.TrackInfo{})
|
||||
// directly add to publishedTracks without lock - for testing purpose only
|
||||
p.UpTrackManager.publishedTracks["cid"] = track
|
||||
@@ -147,7 +151,8 @@ func TestTrackPublishing(t *testing.T) {
|
||||
Name: "webcam",
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
})
|
||||
require.Equal(t, 0, sink.WriteMessageCallCount())
|
||||
// `queued` `RequestResponse` should add a message
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))
|
||||
|
||||
// add again - it should be added to the queue
|
||||
@@ -156,7 +161,8 @@ func TestTrackPublishing(t *testing.T) {
|
||||
Name: "webcam",
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
})
|
||||
require.Equal(t, 0, sink.WriteMessageCallCount())
|
||||
// `queued` `RequestResponse`s should have been sent for duplicate additions
|
||||
require.Equal(t, 2, sink.WriteMessageCallCount())
|
||||
require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))
|
||||
|
||||
// check SID is the same
|
||||
@@ -178,7 +184,8 @@ func TestTrackPublishing(t *testing.T) {
|
||||
Name: "webcam",
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
})
|
||||
require.Equal(t, 0, sink.WriteMessageCallCount())
|
||||
// `queued` `RequestResponse` should add a message
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))
|
||||
|
||||
// add again - it should be added to the queue
|
||||
@@ -187,7 +194,8 @@ func TestTrackPublishing(t *testing.T) {
|
||||
Name: "webcam",
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
})
|
||||
require.Equal(t, 0, sink.WriteMessageCallCount())
|
||||
// `queued` `RequestResponse`s should have been sent for duplicate additions
|
||||
require.Equal(t, 2, sink.WriteMessageCallCount())
|
||||
require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))
|
||||
|
||||
// check SID is the same
|
||||
@@ -217,7 +225,8 @@ func TestTrackPublishing(t *testing.T) {
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
Source: livekit.TrackSource_MICROPHONE,
|
||||
})
|
||||
require.Equal(t, 1, sink.WriteMessageCallCount())
|
||||
// an error response for disallowed source should send a `RequestResponse`.
|
||||
require.Equal(t, 2, sink.WriteMessageCallCount())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -225,7 +234,7 @@ func TestOutOfOrderUpdates(t *testing.T) {
|
||||
p := newParticipantForTest("test")
|
||||
p.updateState(livekit.ParticipantInfo_JOINED)
|
||||
p.SetMetadata("initial metadata")
|
||||
sink := p.getResponseSink().(*routingfakes.FakeMessageSink)
|
||||
sink := p.GetResponseSink().(*routingfakes.FakeMessageSink)
|
||||
pi1 := p.ToProto()
|
||||
p.SetMetadata("second update")
|
||||
pi2 := p.ToProto()
|
||||
@@ -255,7 +264,7 @@ func TestDisconnectTiming(t *testing.T) {
|
||||
}()
|
||||
track := &typesfakes.FakeMediaTrack{}
|
||||
p.UpTrackManager.AddPublishedTrack(track)
|
||||
p.handleTrackPublished(track)
|
||||
p.handleTrackPublished(track, false)
|
||||
|
||||
// close channel and then try to Negotiate
|
||||
msg.Close()
|
||||
@@ -275,7 +284,10 @@ func TestMuteSetting(t *testing.T) {
|
||||
ti := &livekit.TrackInfo{Sid: "testTrack"}
|
||||
p.pendingTracks["cid"] = &pendingTrackInfo{trackInfos: []*livekit.TrackInfo{ti}}
|
||||
|
||||
p.SetTrackMuted(livekit.TrackID(ti.Sid), true, false)
|
||||
p.SetTrackMuted(&livekit.MuteTrackRequest{
|
||||
Sid: ti.Sid,
|
||||
Muted: true,
|
||||
}, false)
|
||||
require.True(t, p.pendingTracks["cid"].trackInfos[0].Muted)
|
||||
})
|
||||
|
||||
@@ -287,7 +299,7 @@ func TestMuteSetting(t *testing.T) {
|
||||
Muted: true,
|
||||
})
|
||||
|
||||
_, ti, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
|
||||
_, ti, _, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
|
||||
require.NotNil(t, ti)
|
||||
require.True(t, ti.Muted)
|
||||
})
|
||||
@@ -362,28 +374,36 @@ func TestDisableCodecs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
require.True(t, found264)
|
||||
offerId := uint32(42)
|
||||
|
||||
// negotiated codec should not contain h264
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SetResponseSink(sink)
|
||||
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
var answer webrtc.SessionDescription
|
||||
var answerId uint32
|
||||
var answerReceived atomic.Bool
|
||||
var answerIdReceived atomic.Uint32
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
if res.GetAnswer() != nil {
|
||||
answer = FromProtoSessionDescription(res.GetAnswer())
|
||||
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
|
||||
answerReceived.Store(true)
|
||||
answerIdReceived.Store(answerId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
participant.HandleOffer(sdp)
|
||||
participant.HandleOffer(&livekit.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer.String(),
|
||||
Sdp: sdp.SDP,
|
||||
Id: offerId,
|
||||
})
|
||||
|
||||
testutils.WithTimeout(t, func() string {
|
||||
if answerReceived.Load() {
|
||||
if answerReceived.Load() && answerIdReceived.Load() == offerId {
|
||||
return ""
|
||||
} else {
|
||||
return "answer not received"
|
||||
return "answer not received OR answer id mismatch"
|
||||
}
|
||||
})
|
||||
require.NoError(t, pc.SetRemoteDescription(answer), answer.SDP, sdp.SDP)
|
||||
@@ -415,7 +435,7 @@ func TestDisablePublishCodec(t *testing.T) {
|
||||
}
|
||||
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SetResponseSink(sink)
|
||||
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
var publishReceived atomic.Bool
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
@@ -463,89 +483,138 @@ func TestDisablePublishCodec(t *testing.T) {
|
||||
require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestPreferVideoCodecForPublisher(t *testing.T) {
|
||||
participant := newParticipantForTestWithOpts("123", &participantOpts{
|
||||
publisher: true,
|
||||
})
|
||||
participant.SetMigrateState(types.MigrateStateComplete)
|
||||
|
||||
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
||||
require.NoError(t, err)
|
||||
defer pc.Close()
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
// publish h264 track without client preferred codec
|
||||
trackCid := fmt.Sprintf("preferh264video%d", i)
|
||||
participant.AddTrack(&livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Name: "video",
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Source: livekit.TrackSource_CAMERA,
|
||||
SimulcastCodecs: []*livekit.SimulcastCodec{
|
||||
{
|
||||
Codec: "h264",
|
||||
Cid: trackCid,
|
||||
},
|
||||
func TestPreferMediaCodecForPublisher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mediaKind string
|
||||
trackBaseCid string
|
||||
preferredCodec string
|
||||
addTrack *livekit.AddTrackRequest
|
||||
mimeTypeStringChecker func(string) bool
|
||||
mimeTypeCodecStringChecker func(string) bool
|
||||
transceiverMimeType mime.MimeType
|
||||
}{
|
||||
{
|
||||
name: "video",
|
||||
mediaKind: "video",
|
||||
trackBaseCid: "preferH264Video",
|
||||
preferredCodec: "h264",
|
||||
addTrack: &livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_VIDEO,
|
||||
Name: "video",
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Source: livekit.TrackSource_CAMERA,
|
||||
},
|
||||
})
|
||||
mimeTypeStringChecker: mime.IsMimeTypeStringH264,
|
||||
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringH264,
|
||||
transceiverMimeType: mime.MimeTypeVP8,
|
||||
},
|
||||
{
|
||||
name: "audio",
|
||||
mediaKind: "audio",
|
||||
trackBaseCid: "preferPCMAAudio",
|
||||
preferredCodec: "pcma",
|
||||
addTrack: &livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
Name: "audio",
|
||||
Source: livekit.TrackSource_MICROPHONE,
|
||||
},
|
||||
mimeTypeStringChecker: mime.IsMimeTypeStringPCMA,
|
||||
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringPCMA,
|
||||
transceiverMimeType: mime.MimeTypeOpus,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
participant := newParticipantForTestWithOpts("123", &participantOpts{
|
||||
publisher: true,
|
||||
})
|
||||
participant.SetMigrateState(types.MigrateStateComplete)
|
||||
|
||||
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, trackCid, trackCid)
|
||||
require.NoError(t, err)
|
||||
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
|
||||
require.NoError(t, err)
|
||||
codecs := transceiver.Receiver().GetParameters().Codecs
|
||||
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
||||
require.NoError(t, err)
|
||||
defer pc.Close()
|
||||
|
||||
if i > 0 {
|
||||
// the negotiated codecs order could be updated by first negotiation, reorder to make h264 not preferred
|
||||
for mime.IsMimeTypeStringH264(codecs[0].MimeType) {
|
||||
codecs = append(codecs[1:], codecs[0])
|
||||
}
|
||||
}
|
||||
// h264 should not be preferred
|
||||
require.False(t, mime.IsMimeTypeStringH264(codecs[0].MimeType), "codecs", codecs)
|
||||
|
||||
sdp, err := pc.CreateOffer(nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pc.SetLocalDescription(sdp))
|
||||
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SetResponseSink(sink)
|
||||
var answer webrtc.SessionDescription
|
||||
var answerReceived atomic.Bool
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
if res.GetAnswer() != nil {
|
||||
answer = FromProtoSessionDescription(res.GetAnswer())
|
||||
pc.SetRemoteDescription(answer)
|
||||
answerReceived.Store(true)
|
||||
for i := 0; i < 2; i++ {
|
||||
// publish preferred track without client using setCodecPreferences()
|
||||
trackCid := fmt.Sprintf("%s-%d", tc.trackBaseCid, i)
|
||||
req := utils.CloneProto(tc.addTrack)
|
||||
req.SimulcastCodecs = []*livekit.SimulcastCodec{
|
||||
{
|
||||
Codec: tc.preferredCodec,
|
||||
Cid: trackCid,
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
participant.HandleOffer(sdp)
|
||||
participant.AddTrack(req)
|
||||
|
||||
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
|
||||
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: tc.transceiverMimeType.String()}, trackCid, trackCid)
|
||||
require.NoError(t, err)
|
||||
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
|
||||
require.NoError(t, err)
|
||||
codecs := transceiver.Receiver().GetParameters().Codecs
|
||||
|
||||
var h264Preferred bool
|
||||
parsed, err := answer.Unmarshal()
|
||||
require.NoError(t, err)
|
||||
var videoSectionIndex int
|
||||
for _, m := range parsed.MediaDescriptions {
|
||||
if m.MediaName.Media == "video" {
|
||||
if videoSectionIndex == i {
|
||||
codecs, err := codecsFromMediaDescription(m)
|
||||
require.NoError(t, err)
|
||||
if mime.IsMimeTypeCodecStringH264(codecs[0].Name) {
|
||||
h264Preferred = true
|
||||
break
|
||||
if i > 0 {
|
||||
// the negotiated codecs order could be updated by first negotiation,
|
||||
// reorder to make tested preferred codec not preferred
|
||||
for tc.mimeTypeStringChecker(codecs[0].MimeType) {
|
||||
codecs = append(codecs[1:], codecs[0])
|
||||
}
|
||||
}
|
||||
videoSectionIndex++
|
||||
}
|
||||
}
|
||||
// preferred codec should not be preferred in `offer`
|
||||
require.False(t, tc.mimeTypeStringChecker(codecs[0].MimeType), "codecs", codecs)
|
||||
|
||||
require.Truef(t, h264Preferred, "h264 should be preferred for video section %d, answer sdp: \n%s", i, answer.SDP)
|
||||
sdp, err := pc.CreateOffer(nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pc.SetLocalDescription(sdp))
|
||||
offerId := uint32(23)
|
||||
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
var answer webrtc.SessionDescription
|
||||
var answerId uint32
|
||||
var answerReceived atomic.Bool
|
||||
var answerIdReceived atomic.Uint32
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
if res.GetAnswer() != nil {
|
||||
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
|
||||
pc.SetRemoteDescription(answer)
|
||||
answerReceived.Store(true)
|
||||
answerIdReceived.Store(answerId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
participant.HandleOffer(&livekit.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer.String(),
|
||||
Sdp: sdp.SDP,
|
||||
Id: offerId,
|
||||
})
|
||||
|
||||
require.Eventually(t, func() bool { return answerReceived.Load() && answerIdReceived.Load() == offerId }, 5*time.Second, 10*time.Millisecond)
|
||||
|
||||
var havePreferred bool
|
||||
parsed, err := answer.Unmarshal()
|
||||
require.NoError(t, err)
|
||||
var mediaSectionIndex int
|
||||
for _, m := range parsed.MediaDescriptions {
|
||||
if m.MediaName.Media == tc.mediaKind {
|
||||
if mediaSectionIndex == i {
|
||||
codecs, err := lksdp.CodecsFromMediaDescription(m)
|
||||
require.NoError(t, err)
|
||||
if tc.mimeTypeCodecStringChecker(codecs[0].Name) {
|
||||
havePreferred = true
|
||||
break
|
||||
}
|
||||
}
|
||||
mediaSectionIndex++
|
||||
}
|
||||
}
|
||||
|
||||
require.Truef(t, havePreferred, "%s should be preferred for %s section %d, answer sdp: \n%s", tc.preferredCodec, tc.mediaKind, i, answer.SDP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,33 +625,59 @@ func TestPreferAudioCodecForRed(t *testing.T) {
|
||||
participant.SetMigrateState(types.MigrateStateComplete)
|
||||
|
||||
me := webrtc.MediaEngine{}
|
||||
me.RegisterDefaultCodecs()
|
||||
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: RedCodecCapability,
|
||||
PayloadType: 63,
|
||||
}, webrtc.RTPCodecTypeAudio))
|
||||
opusCodecParameters := protoCodecs.OpusCodecParameters
|
||||
opusCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
|
||||
require.NoError(t, me.RegisterCodec(opusCodecParameters, webrtc.RTPCodecTypeAudio))
|
||||
redCodecParameters := protoCodecs.RedCodecParameters
|
||||
redCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
|
||||
require.NoError(t, me.RegisterCodec(redCodecParameters, webrtc.RTPCodecTypeAudio))
|
||||
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(&me))
|
||||
pc, err := api.NewPeerConnection(webrtc.Configuration{})
|
||||
require.NoError(t, err)
|
||||
defer pc.Close()
|
||||
|
||||
for i, disableRed := range []bool{false, true} {
|
||||
for idx, disableRed := range []bool{false, true, false, true} {
|
||||
t.Run(fmt.Sprintf("disableRed=%v", disableRed), func(t *testing.T) {
|
||||
trackCid := fmt.Sprintf("audiotrack%d", i)
|
||||
participant.AddTrack(&livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
DisableRed: disableRed,
|
||||
Cid: trackCid,
|
||||
})
|
||||
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, trackCid, trackCid)
|
||||
trackCid := fmt.Sprintf("audiotrack%d", idx)
|
||||
req := &livekit.AddTrackRequest{
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
Cid: trackCid,
|
||||
}
|
||||
if idx < 2 {
|
||||
req.DisableRed = disableRed
|
||||
} else {
|
||||
codec := "red"
|
||||
if disableRed {
|
||||
codec = "opus"
|
||||
}
|
||||
req.SimulcastCodecs = []*livekit.SimulcastCodec{
|
||||
{
|
||||
Codec: codec,
|
||||
Cid: trackCid,
|
||||
},
|
||||
}
|
||||
}
|
||||
participant.AddTrack(req)
|
||||
|
||||
track, err := webrtc.NewTrackLocalStaticRTP(
|
||||
webrtc.RTPCodecCapability{MimeType: "audio/opus"},
|
||||
trackCid,
|
||||
trackCid,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
|
||||
|
||||
transceiver, err := pc.AddTransceiverFromTrack(
|
||||
track,
|
||||
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
codecs := transceiver.Sender().GetParameters().Codecs
|
||||
for i, c := range codecs {
|
||||
if c.MimeType == "audio/opus" && i != 0 {
|
||||
codecs[0], codecs[i] = codecs[i], codecs[0]
|
||||
if c.MimeType == "audio/opus" {
|
||||
if i != 0 {
|
||||
codecs[0], codecs[i] = codecs[i], codecs[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -592,24 +687,38 @@ func TestPreferAudioCodecForRed(t *testing.T) {
|
||||
pc.SetLocalDescription(sdp)
|
||||
// opus should be preferred
|
||||
require.Equal(t, codecs[0].MimeType, "audio/opus", sdp)
|
||||
offerId := uint32(0xffffff)
|
||||
|
||||
sink := &routingfakes.FakeMessageSink{}
|
||||
participant.SetResponseSink(sink)
|
||||
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
|
||||
var answer webrtc.SessionDescription
|
||||
var answerId uint32
|
||||
var answerReceived atomic.Bool
|
||||
var answerIdReceived atomic.Uint32
|
||||
sink.WriteMessageCalls(func(msg proto.Message) error {
|
||||
if res, ok := msg.(*livekit.SignalResponse); ok {
|
||||
if res.GetAnswer() != nil {
|
||||
answer = FromProtoSessionDescription(res.GetAnswer())
|
||||
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
|
||||
pc.SetRemoteDescription(answer)
|
||||
answerReceived.Store(true)
|
||||
answerIdReceived.Store(answerId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
participant.HandleOffer(sdp)
|
||||
|
||||
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
|
||||
participant.HandleOffer(&livekit.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer.String(),
|
||||
Sdp: sdp.SDP,
|
||||
Id: offerId,
|
||||
})
|
||||
require.Eventually(
|
||||
t,
|
||||
func() bool {
|
||||
return answerReceived.Load() && answerIdReceived.Load() == offerId
|
||||
},
|
||||
5*time.Second,
|
||||
10*time.Millisecond,
|
||||
)
|
||||
|
||||
var redPreferred bool
|
||||
parsed, err := answer.Unmarshal()
|
||||
@@ -617,8 +726,8 @@ func TestPreferAudioCodecForRed(t *testing.T) {
|
||||
var audioSectionIndex int
|
||||
for _, m := range parsed.MediaDescriptions {
|
||||
if m.MediaName.Media == "audio" {
|
||||
if audioSectionIndex == i {
|
||||
codecs, err := codecsFromMediaDescription(m)
|
||||
if audioSectionIndex == idx {
|
||||
codecs, err := lksdp.CodecsFromMediaDescription(m)
|
||||
require.NoError(t, err)
|
||||
// nack is always enabled. if red is preferred, server will not generate nack request
|
||||
var nackEnabled bool
|
||||
@@ -702,8 +811,11 @@ func newParticipantForTestWithOpts(identity livekit.ParticipantIdentity, opts *p
|
||||
ClientConf: opts.clientConf,
|
||||
ClientInfo: ClientInfo{ClientInfo: opts.clientInfo},
|
||||
Logger: LoggerWithParticipant(logger.GetLogger(), identity, sid, false),
|
||||
Telemetry: &telemetryfakes.FakeTelemetryService{},
|
||||
Reporter: roomobs.NewNoopParticipantSessionReporter(),
|
||||
TelemetryListener: &typesfakes.FakeParticipantTelemetryListener{},
|
||||
VersionGenerator: utils.NewDefaultTimedVersionGenerator(),
|
||||
ParticipantListener: &typesfakes.FakeLocalParticipantListener{},
|
||||
ParticipantHelper: &typesfakes.FakeLocalParticipantHelper{},
|
||||
})
|
||||
p.isPublisher.Store(opts.publisher)
|
||||
p.updateState(livekit.ParticipantInfo_ACTIVE)
|
||||
|
||||
+268
-134
@@ -16,44 +16,279 @@ package rtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
lksdp "github.com/livekit/protocol/sdp"
|
||||
"github.com/livekit/protocol/utils"
|
||||
)
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
|
||||
offer = p.setCodecPreferencesOpusRedForPublisher(offer)
|
||||
offer = p.setCodecPreferencesVideoForPublisher(offer)
|
||||
return offer
|
||||
}
|
||||
func (p *ParticipantImpl) populateSdpCid(parsedOffer *sdp.SessionDescription) ([]*sdp.MediaDescription, []*sdp.MediaDescription) {
|
||||
processUnmatch := func(unmatches []*sdp.MediaDescription, trackType livekit.TrackType) {
|
||||
for _, unmatch := range unmatches {
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatch)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
|
||||
parsed, unmatchAudios, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "audio")
|
||||
if err != nil || len(unmatchAudios) == 0 {
|
||||
return offer
|
||||
sdpCodecs, err := lksdp.CodecsFromMediaDescription(unmatch)
|
||||
if err != nil || len(sdpCodecs) == 0 {
|
||||
p.pubLogger.Errorw(
|
||||
"extract codecs from media section failed", err,
|
||||
"media", unmatch,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
p.pendingTracksLock.Lock()
|
||||
signalCid, info, _, migrated, _ := p.getPendingTrack(streamID, trackType, false)
|
||||
if migrated {
|
||||
p.pendingTracksLock.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
p.pendingTracksLock.Unlock()
|
||||
|
||||
// could be already published track and the unmatch could be a back up codec publish
|
||||
numUnmatchedTracks := 0
|
||||
var unmatchedTrack types.MediaTrack
|
||||
var unmatchedSdpMimeType mime.MimeType
|
||||
|
||||
found := false
|
||||
for _, sdpCodec := range sdpCodecs {
|
||||
sdpMimeType := mime.NormalizeMimeTypeCodec(sdpCodec.Name).ToMimeType()
|
||||
for _, publishedTrack := range p.GetPublishedTracks() {
|
||||
if sigCid, sdpCid := publishedTrack.(*MediaTrack).GetCidsForMimeType(sdpMimeType); sigCid != "" && sdpCid == "" {
|
||||
// a back up codec has a SDP cid match
|
||||
if sigCid == streamID {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
numUnmatchedTracks++
|
||||
unmatchedTrack = publishedTrack
|
||||
unmatchedSdpMimeType = sdpMimeType
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && unmatchedTrack != nil {
|
||||
if numUnmatchedTracks != 1 {
|
||||
p.pubLogger.Warnw(
|
||||
"too many unmatched tracks", nil,
|
||||
"media", unmatch,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
}
|
||||
unmatchedTrack.(*MediaTrack).UpdateCodecSdpCid(unmatchedSdpMimeType, streamID)
|
||||
p.pubLogger.Debugw(
|
||||
"published track SDP cid updated",
|
||||
"trackID", unmatchedTrack.ID(),
|
||||
"track", logger.Proto(unmatchedTrack.ToProto()),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(info.Codecs) == 0 {
|
||||
p.pendingTracksLock.Unlock()
|
||||
p.pubLogger.Warnw(
|
||||
"track without codecs", nil,
|
||||
"trackID", info.Sid,
|
||||
"pendingTrack", p.pendingTracks[signalCid],
|
||||
"media", unmatch,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
found := false
|
||||
updated := false
|
||||
for _, sdpCodec := range sdpCodecs {
|
||||
if mime.NormalizeMimeTypeCodec(sdpCodec.Name) == mime.GetMimeTypeCodec(info.Codecs[0].MimeType) {
|
||||
// set SdpCid only if different from SignalCid
|
||||
if streamID != info.Codecs[0].Cid {
|
||||
info.Codecs[0].SdpCid = streamID
|
||||
updated = true
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// not using SimulcastCodec, i. e. mime type not available till track publish
|
||||
if len(info.Codecs) == 1 {
|
||||
// set SdpCid only if different from SignalCid
|
||||
if streamID != info.Codecs[0].Cid {
|
||||
info.Codecs[0].SdpCid = streamID
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
p.pendingTracks[signalCid].trackInfos[0] = utils.CloneProto(info)
|
||||
p.pubLogger.Debugw(
|
||||
"pending track SDP cid updated",
|
||||
"signalCid", signalCid,
|
||||
"trackID", info.Sid,
|
||||
"pendingTrack", p.pendingTracks[signalCid],
|
||||
)
|
||||
}
|
||||
p.pendingTracksLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
unmatchAudios, err := p.TransportManager.GetUnmatchMediaForOffer(parsedOffer, "audio")
|
||||
if err != nil {
|
||||
p.pubLogger.Warnw("could not get unmatch audios", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
unmatchVideos, err := p.TransportManager.GetUnmatchMediaForOffer(parsedOffer, "video")
|
||||
if err != nil {
|
||||
p.pubLogger.Warnw("could not get unmatch videos", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
processUnmatch(unmatchAudios, livekit.TrackType_AUDIO)
|
||||
processUnmatch(unmatchVideos, livekit.TrackType_VIDEO)
|
||||
return unmatchAudios, unmatchVideos
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesForPublisher(
|
||||
parsedOffer *sdp.SessionDescription,
|
||||
unmatchAudios []*sdp.MediaDescription,
|
||||
unmatchVideos []*sdp.MediaDescription,
|
||||
) {
|
||||
unprocessedUnmatchAudios := p.setCodecPreferencesForPublisherMedia(
|
||||
parsedOffer,
|
||||
unmatchAudios,
|
||||
livekit.TrackType_AUDIO,
|
||||
)
|
||||
p.setCodecPreferencesOpusRedForPublisher(parsedOffer, unprocessedUnmatchAudios)
|
||||
_ = p.setCodecPreferencesForPublisherMedia(
|
||||
parsedOffer,
|
||||
unmatchVideos,
|
||||
livekit.TrackType_VIDEO,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesForPublisherMedia(
|
||||
parsedOffer *sdp.SessionDescription,
|
||||
unmatches []*sdp.MediaDescription,
|
||||
trackType livekit.TrackType,
|
||||
) []*sdp.MediaDescription {
|
||||
unprocessed := make([]*sdp.MediaDescription, 0, len(unmatches))
|
||||
for _, unmatch := range unmatches {
|
||||
var ti *livekit.TrackInfo
|
||||
var mimeType string
|
||||
|
||||
mid := lksdp.GetMidValue(unmatch)
|
||||
if mid == "" {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
transceiver := p.TransportManager.GetPublisherRTPTransceiver(mid)
|
||||
if transceiver == nil {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatch)
|
||||
if !ok {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
p.pendingTracksLock.RLock()
|
||||
mt := p.getPublishedTrackBySdpCid(streamID)
|
||||
if mt != nil {
|
||||
ti = mt.ToProto()
|
||||
} else {
|
||||
_, ti, _, _, _ = p.getPendingTrack(streamID, trackType, false)
|
||||
}
|
||||
p.pendingTracksLock.RUnlock()
|
||||
|
||||
if ti == nil {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range ti.Codecs {
|
||||
if c.Cid == streamID || c.SdpCid == streamID {
|
||||
mimeType = c.MimeType
|
||||
break
|
||||
}
|
||||
}
|
||||
if mimeType == "" && len(ti.Codecs) > 0 {
|
||||
mimeType = ti.Codecs[0].MimeType
|
||||
}
|
||||
|
||||
if mimeType == "" {
|
||||
unprocessed = append(unprocessed, unmatch)
|
||||
continue
|
||||
}
|
||||
|
||||
configureReceiverCodecs(
|
||||
transceiver,
|
||||
mimeType,
|
||||
p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer(),
|
||||
)
|
||||
}
|
||||
|
||||
return unprocessed
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(
|
||||
parsedOffer *sdp.SessionDescription,
|
||||
unmatchAudios []*sdp.MediaDescription,
|
||||
) {
|
||||
for _, unmatchAudio := range unmatchAudios {
|
||||
mid := lksdp.GetMidValue(unmatchAudio)
|
||||
if mid == "" {
|
||||
continue
|
||||
}
|
||||
transceiver := p.TransportManager.GetPublisherRTPTransceiver(mid)
|
||||
if transceiver == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatchAudio)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
p.pendingTracksLock.RLock()
|
||||
_, info, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
|
||||
// if RED is disabled for this track, don't prefer RED codec in offer
|
||||
disableRed := info != nil && info.DisableRed
|
||||
_, ti, _, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
|
||||
p.pendingTracksLock.RUnlock()
|
||||
if ti == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
codecs, err := codecsFromMediaDescription(unmatchAudio)
|
||||
codecs, err := lksdp.CodecsFromMediaDescription(unmatchAudio)
|
||||
if err != nil {
|
||||
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchAudio)
|
||||
p.pubLogger.Errorw(
|
||||
"extract codecs from media section failed", err,
|
||||
"media", unmatchAudio,
|
||||
"parsedOffer", parsedOffer,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -68,122 +303,17 @@ func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(offer webrtc.Se
|
||||
continue
|
||||
}
|
||||
|
||||
var preferredCodecs, leftCodecs []string
|
||||
preferRED := IsRedEnabled(ti)
|
||||
// if RED is enabled for this track, prefer RED codec in offer
|
||||
for _, codec := range codecs {
|
||||
// codec contain opus/red
|
||||
if !disableRed && mime.IsMimeTypeCodecStringRED(codec.Name) && strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
|
||||
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
|
||||
} else {
|
||||
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
|
||||
}
|
||||
}
|
||||
|
||||
// ensure nack enabled for audio in publisher offer
|
||||
var nackFound bool
|
||||
for _, attr := range unmatchAudio.Attributes {
|
||||
if attr.Key == "rtcp-fb" && strings.Contains(attr.Value, fmt.Sprintf("%d nack", opusPayload)) {
|
||||
nackFound = true
|
||||
if preferRED &&
|
||||
mime.IsMimeTypeCodecStringRED(codec.Name) &&
|
||||
strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
|
||||
configureReceiverCodecs(transceiver, "audio/red", true)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !nackFound {
|
||||
unmatchAudio.Attributes = append(unmatchAudio.Attributes, sdp.Attribute{
|
||||
Key: "rtcp-fb",
|
||||
Value: fmt.Sprintf("%d nack", opusPayload),
|
||||
})
|
||||
}
|
||||
|
||||
// no opus/red found
|
||||
if len(preferredCodecs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats[:0], preferredCodecs...)
|
||||
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats, leftCodecs...)
|
||||
}
|
||||
|
||||
bytes, err := parsed.Marshal()
|
||||
if err != nil {
|
||||
p.pubLogger.Errorw("failed to marshal offer", err)
|
||||
return offer
|
||||
}
|
||||
|
||||
return webrtc.SessionDescription{
|
||||
Type: offer.Type,
|
||||
SDP: string(bytes),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) setCodecPreferencesVideoForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
|
||||
parsed, unmatchVideos, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "video")
|
||||
if err != nil || len(unmatchVideos) == 0 {
|
||||
return offer
|
||||
}
|
||||
// unmatched video is pending for publish, set codec preference
|
||||
for _, unmatchVideo := range unmatchVideos {
|
||||
streamID, ok := lksdp.ExtractStreamID(unmatchVideo)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var info *livekit.TrackInfo
|
||||
p.pendingTracksLock.RLock()
|
||||
mt := p.getPublishedTrackBySdpCid(streamID)
|
||||
if mt != nil {
|
||||
info = mt.ToProto()
|
||||
} else {
|
||||
_, info, _, _ = p.getPendingTrack(streamID, livekit.TrackType_VIDEO, false)
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
p.pendingTracksLock.RUnlock()
|
||||
continue
|
||||
}
|
||||
var mimeType string
|
||||
for _, c := range info.Codecs {
|
||||
if c.Cid == streamID {
|
||||
mimeType = c.MimeType
|
||||
break
|
||||
}
|
||||
}
|
||||
if mimeType == "" && len(info.Codecs) > 0 {
|
||||
mimeType = info.Codecs[0].MimeType
|
||||
}
|
||||
p.pendingTracksLock.RUnlock()
|
||||
|
||||
if mimeType != "" {
|
||||
codecs, err := codecsFromMediaDescription(unmatchVideo)
|
||||
if err != nil {
|
||||
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchVideo)
|
||||
continue
|
||||
}
|
||||
|
||||
var preferredCodecs, leftCodecs []string
|
||||
for _, c := range codecs {
|
||||
if mime.GetMimeTypeCodec(mimeType) == mime.NormalizeMimeTypeCodec(c.Name) {
|
||||
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
|
||||
} else {
|
||||
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
|
||||
}
|
||||
}
|
||||
|
||||
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats[:0], preferredCodecs...)
|
||||
// if the client don't comply with codec order in SDP answer, only keep preferred codecs to force client to use it
|
||||
if p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer() {
|
||||
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats, leftCodecs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := parsed.Marshal()
|
||||
if err != nil {
|
||||
p.pubLogger.Errorw("failed to marshal offer", err)
|
||||
return offer
|
||||
}
|
||||
|
||||
return webrtc.SessionDescription{
|
||||
Type: offer.Type,
|
||||
SDP: string(bytes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,12 +325,12 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
|
||||
return answer
|
||||
}
|
||||
|
||||
parsed, err := answer.Unmarshal()
|
||||
parsedAnswer, err := answer.Unmarshal()
|
||||
if err != nil {
|
||||
return answer
|
||||
}
|
||||
|
||||
for _, m := range parsed.MediaDescriptions {
|
||||
for _, m := range parsedAnswer.MediaDescriptions {
|
||||
switch m.MediaName.Media {
|
||||
case "audio":
|
||||
_, ok := m.Attribute(sdp.AttrKeyInactive)
|
||||
@@ -227,7 +357,7 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
|
||||
track, _ := p.getPublishedTrackBySdpCid(streamID).(*MediaTrack)
|
||||
if track == nil {
|
||||
p.pendingTracksLock.RLock()
|
||||
_, ti, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
|
||||
_, ti, _, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
|
||||
p.pendingTracksLock.RUnlock()
|
||||
} else {
|
||||
ti = track.ToProto()
|
||||
@@ -236,12 +366,12 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
|
||||
}
|
||||
}
|
||||
|
||||
if ti == nil || (ti.DisableDtx && !ti.Stereo) {
|
||||
if ti == nil {
|
||||
// no need to configure
|
||||
continue
|
||||
}
|
||||
|
||||
opusPT, err := parsed.GetPayloadTypeForCodec(sdp.Codec{Name: mime.MimeTypeCodecOpus.String()})
|
||||
opusPT, err := parsedAnswer.GetPayloadTypeForCodec(sdp.Codec{Name: mime.MimeTypeCodecOpus.String()})
|
||||
if err != nil {
|
||||
p.pubLogger.Infow("failed to get opus payload type", "error", err, "trackID", ti.Sid)
|
||||
continue
|
||||
@@ -249,11 +379,15 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
|
||||
|
||||
for i, attr := range m.Attributes {
|
||||
if strings.HasPrefix(attr.String(), fmt.Sprintf("fmtp:%d", opusPT)) {
|
||||
if !ti.DisableDtx {
|
||||
if !slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_NO_DTX) {
|
||||
attr.Value += ";usedtx=1"
|
||||
} else {
|
||||
attr.Value = strings.ReplaceAll(attr.Value, ";usedtx=1", "")
|
||||
}
|
||||
if ti.Stereo {
|
||||
if slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO) {
|
||||
attr.Value += ";stereo=1;maxaveragebitrate=510000"
|
||||
} else {
|
||||
attr.Value = strings.ReplaceAll(attr.Value, ";stereo=1", "")
|
||||
}
|
||||
m.Attributes[i] = attr
|
||||
}
|
||||
@@ -264,7 +398,7 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := parsed.Marshal()
|
||||
bytes, err := parsedAnswer.Marshal()
|
||||
if err != nil {
|
||||
p.pubLogger.Infow("failed to marshal answer", "error", err)
|
||||
return answer
|
||||
|
||||
+124
-155
@@ -15,30 +15,28 @@
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/psrpc"
|
||||
protosignalling "github.com/livekit/protocol/signalling"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
func (p *ParticipantImpl) getResponseSink() routing.MessageSink {
|
||||
p.resSinkMu.Lock()
|
||||
defer p.resSinkMu.Unlock()
|
||||
return p.resSink
|
||||
func (p *ParticipantImpl) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
|
||||
p.signaller.SwapResponseSink(sink, reason)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SetResponseSink(sink routing.MessageSink) {
|
||||
p.resSinkMu.Lock()
|
||||
defer p.resSinkMu.Unlock()
|
||||
p.resSink = sink
|
||||
func (p *ParticipantImpl) GetResponseSink() routing.MessageSink {
|
||||
return p.signaller.GetResponseSink()
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) CloseSignalConnection(reason types.SignallingCloseReason) {
|
||||
p.signaller.CloseSignalConnection(reason)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendJoinResponse(joinResponse *livekit.JoinResponse) error {
|
||||
@@ -55,11 +53,7 @@ func (p *ParticipantImpl) SendJoinResponse(joinResponse *livekit.JoinResponse) e
|
||||
p.updateLock.Unlock()
|
||||
|
||||
// send Join response
|
||||
err := p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Join{
|
||||
Join: joinResponse,
|
||||
},
|
||||
})
|
||||
err := p.signaller.WriteMessage(p.signalling.SignalJoinResponse(joinResponse))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -111,7 +105,7 @@ func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
if pi.Permission != nil && pi.Permission.Hidden && pi.Sid != string(p.params.SID) {
|
||||
if pi.Permission != nil && pi.Permission.Hidden && pi.Sid != string(p.ID()) {
|
||||
p.params.Logger.Debugw("skipping hidden participant update", "otherParticipant", pi.Identity)
|
||||
isValid = false
|
||||
}
|
||||
@@ -127,17 +121,7 @@ func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.
|
||||
}
|
||||
p.updateLock.Unlock()
|
||||
|
||||
if len(validUpdates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Update{
|
||||
Update: &livekit.ParticipantUpdate{
|
||||
Participants: validUpdates,
|
||||
},
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(validUpdates))
|
||||
}
|
||||
|
||||
// SendSpeakerUpdate notifies participant changes to speakers. only send members that have changed since last update
|
||||
@@ -158,47 +142,23 @@ func (p *ParticipantImpl) SendSpeakerUpdate(speakers []*livekit.SpeakerInfo, for
|
||||
}
|
||||
}
|
||||
|
||||
if len(scopedSpeakers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_SpeakersChanged{
|
||||
SpeakersChanged: &livekit.SpeakersChanged{
|
||||
Speakers: scopedSpeakers,
|
||||
},
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSpeakerUpdate(scopedSpeakers))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendRoomUpdate(room *livekit.Room) error {
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_RoomUpdate{
|
||||
RoomUpdate: &livekit.RoomUpdate{
|
||||
Room: room,
|
||||
},
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRoomUpdate(room))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error {
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_ConnectionQuality{
|
||||
ConnectionQuality: update,
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalConnectionQualityUpdate(update))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendRefreshToken(token string) error {
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_RefreshToken{
|
||||
RefreshToken: token,
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRefreshToken(token))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendRequestResponse(requestResponse *livekit.RequestResponse) error {
|
||||
if requestResponse.RequestId == 0 || !p.params.ClientInfo.SupportErrorResponse() {
|
||||
func (p *ParticipantImpl) sendRequestResponse(requestResponse *livekit.RequestResponse) error {
|
||||
if !p.params.ClientInfo.SupportsRequestResponse() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -206,11 +166,11 @@ func (p *ParticipantImpl) SendRequestResponse(requestResponse *livekit.RequestRe
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_RequestResponse{
|
||||
RequestResponse: requestResponse,
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRequestResponse(requestResponse))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendRoomMovedResponse(roomMovedResponse *livekit.RoomMovedResponse) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalRoomMovedResponse(roomMovedResponse))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) HandleReconnectAndSendResponse(reconnectReason livekit.ReconnectReason, reconnectResponse *livekit.ReconnectResponse) error {
|
||||
@@ -219,15 +179,11 @@ func (p *ParticipantImpl) HandleReconnectAndSendResponse(reconnectReason livekit
|
||||
if !p.params.ClientInfo.CanHandleReconnectResponse() {
|
||||
return nil
|
||||
}
|
||||
if err := p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Reconnect{
|
||||
Reconnect: reconnectResponse,
|
||||
},
|
||||
}); err != nil {
|
||||
if err := p.signaller.WriteMessage(p.signalling.SignalReconnectResponse(reconnectResponse)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.params.ProtocolVersion.SupportHandlesDisconnectedUpdate() {
|
||||
if p.params.ProtocolVersion.SupportsDisconnectedUpdate() {
|
||||
return p.sendDisconnectUpdatesForReconnect()
|
||||
}
|
||||
|
||||
@@ -255,17 +211,7 @@ func (p *ParticipantImpl) sendDisconnectUpdatesForReconnect() error {
|
||||
}
|
||||
p.updateLock.Unlock()
|
||||
|
||||
if len(disconnectedParticipants) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Update{
|
||||
Update: &livekit.ParticipantUpdate{
|
||||
Participants: disconnectedParticipants,
|
||||
},
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(disconnectedParticipants))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livekit.SignalTarget) error {
|
||||
@@ -274,90 +220,43 @@ func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livek
|
||||
return nil
|
||||
}
|
||||
|
||||
trickle := ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
|
||||
trickle := protosignalling.ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
|
||||
p.params.Logger.Debugw("sending ICE candidate", "transport", target, "trickle", logger.Proto(trickle))
|
||||
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Trickle{
|
||||
Trickle: trickle,
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalICECandidate(trickle))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackMuted(trackID livekit.TrackID, muted bool) {
|
||||
_ = p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Mute{
|
||||
Mute: &livekit.MuteTrackRequest{
|
||||
Sid: string(trackID),
|
||||
Muted: muted,
|
||||
},
|
||||
},
|
||||
})
|
||||
_ = p.signaller.WriteMessage(p.signalling.SignalTrackMuted(&livekit.MuteTrackRequest{
|
||||
Sid: string(trackID),
|
||||
Muted: muted,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackPublished(cid string, ti *livekit.TrackInfo) error {
|
||||
p.pubLogger.Debugw("sending track published", "cid", cid, "trackInfo", logger.Proto(ti))
|
||||
return p.signaller.WriteMessage(p.signalling.SignalTrackPublished(&livekit.TrackPublishedResponse{
|
||||
Cid: cid,
|
||||
Track: ti,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackUnpublished(trackID livekit.TrackID) {
|
||||
_ = p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_TrackUnpublished{
|
||||
TrackUnpublished: &livekit.TrackUnpublishedResponse{
|
||||
TrackSid: string(trackID),
|
||||
},
|
||||
},
|
||||
})
|
||||
_ = p.signaller.WriteMessage(p.signalling.SignalTrackUnpublished(&livekit.TrackUnpublishedResponse{
|
||||
TrackSid: string(trackID),
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendTrackHasBeenSubscribed(trackID livekit.TrackID) {
|
||||
if !p.params.ClientInfo.SupportTrackSubscribedEvent() {
|
||||
if !p.params.ClientInfo.SupportsTrackSubscribedEvent() {
|
||||
return
|
||||
}
|
||||
_ = p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_TrackSubscribed{
|
||||
TrackSubscribed: &livekit.TrackSubscribed{
|
||||
TrackSid: string(trackID),
|
||||
},
|
||||
},
|
||||
})
|
||||
_ = p.signaller.WriteMessage(p.signalling.SignalTrackSubscribed(&livekit.TrackSubscribed{
|
||||
TrackSid: string(trackID),
|
||||
}))
|
||||
p.params.Logger.Debugw("track has been subscribed", "trackID", trackID)
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) writeMessage(msg *livekit.SignalResponse) error {
|
||||
if p.IsDisconnected() || (!p.IsReady() && msg.GetJoin() == nil) {
|
||||
return nil
|
||||
}
|
||||
|
||||
sink := p.getResponseSink()
|
||||
if sink == nil {
|
||||
p.params.Logger.Debugw("could not send message to participant", "messageType", fmt.Sprintf("%T", msg.Message))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := sink.WriteMessage(msg)
|
||||
if utils.ErrorIsOneOf(err, psrpc.Canceled, routing.ErrChannelClosed) {
|
||||
p.params.Logger.Debugw(
|
||||
"could not send message to participant",
|
||||
"error", err,
|
||||
"messageType", fmt.Sprintf("%T", msg.Message),
|
||||
)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
p.params.Logger.Warnw(
|
||||
"could not send message to participant", err,
|
||||
"messageType", fmt.Sprintf("%T", msg.Message),
|
||||
)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// closes signal connection to notify client to resume/reconnect
|
||||
func (p *ParticipantImpl) CloseSignalConnection(reason types.SignallingCloseReason) {
|
||||
sink := p.getResponseSink()
|
||||
if sink != nil {
|
||||
p.params.Logger.Debugw("closing signal connection", "reason", reason, "connID", sink.ConnectionID())
|
||||
sink.Close()
|
||||
p.SetResponseSink(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendLeaveRequest(
|
||||
reason types.ParticipantCloseReason,
|
||||
isExpectedToResume bool,
|
||||
@@ -377,9 +276,9 @@ func (p *ParticipantImpl) sendLeaveRequest(
|
||||
default:
|
||||
leave.Action = livekit.LeaveRequest_DISCONNECT
|
||||
}
|
||||
if leave.Action != livekit.LeaveRequest_DISCONNECT && p.params.GetRegionSettings != nil {
|
||||
if leave.Action != livekit.LeaveRequest_DISCONNECT {
|
||||
// sending region settings even for RESUME just in case client wants to a full reconnect despite server saying RESUME
|
||||
leave.Regions = p.params.GetRegionSettings(p.params.ClientInfo.Address)
|
||||
leave.Regions = p.helper().GetRegionSettings(p.params.ClientInfo.Address)
|
||||
}
|
||||
} else {
|
||||
if !sendOnlyIfSupportingLeaveRequestWithAction {
|
||||
@@ -390,12 +289,82 @@ func (p *ParticipantImpl) sendLeaveRequest(
|
||||
}
|
||||
}
|
||||
if leave != nil {
|
||||
return p.writeMessage(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Leave{
|
||||
Leave: leave,
|
||||
},
|
||||
})
|
||||
return p.signaller.WriteMessage(p.signalling.SignalLeaveRequest(leave))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSdpAnswer(answer webrtc.SessionDescription, answerId uint32, midToTrackID map[string]string) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSdpAnswer(protosignalling.ToProtoSessionDescription(answer, answerId, midToTrackID)))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSdpOffer(offer webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSdpOffer(protosignalling.ToProtoSessionDescription(offer, offerId, midToTrackID)))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalStreamStateUpdate(streamStateUpdate))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSubscribedQualityUpdate(subscribedQualityUpdate))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendSubscriptionResponse(trackID livekit.TrackID, subErr livekit.SubscriptionError) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalSubscriptionResponse(&livekit.SubscriptionResponse{
|
||||
TrackSid: string(trackID),
|
||||
Err: subErr,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendSubscriptionPermissionUpdate(publisherID livekit.ParticipantID, trackID livekit.TrackID, allowed bool) error {
|
||||
p.subLogger.Debugw("sending subscription permission update", "publisherID", publisherID, "trackID", trackID, "allowed", allowed)
|
||||
err := p.signaller.WriteMessage(p.signalling.SignalSubscriptionPermissionUpdate(&livekit.SubscriptionPermissionUpdate{
|
||||
ParticipantSid: string(publisherID),
|
||||
TrackSid: string(trackID),
|
||||
Allowed: allowed,
|
||||
}))
|
||||
if err != nil {
|
||||
p.subLogger.Errorw("could not send subscription permission update", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendMediaSectionsRequirement(numAudios uint32, numVideos uint32) error {
|
||||
p.pubLogger.Debugw(
|
||||
"sending media sections requirement",
|
||||
"numAudios", numAudios,
|
||||
"numVideos", numVideos,
|
||||
)
|
||||
err := p.signaller.WriteMessage(p.signalling.SignalMediaSectionsRequirement(&livekit.MediaSectionsRequirement{
|
||||
NumAudios: numAudios,
|
||||
NumVideos: numVideos,
|
||||
}))
|
||||
if err != nil {
|
||||
p.subLogger.Errorw("could not send media sections requirement", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendPublishDataTrackResponse(dti *livekit.DataTrackInfo) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalPublishDataTrackResponse(&livekit.PublishDataTrackResponse{
|
||||
Info: dti,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) sendUnpublishDataTrackResponse(dti *livekit.DataTrackInfo) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalUnpublishDataTrackResponse(&livekit.UnpublishDataTrackResponse{
|
||||
Info: dti,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *ParticipantImpl) SendDataTrackSubscriberHandles(handles map[uint32]*livekit.DataTrackSubscriberHandles_PublishedDataTrack) error {
|
||||
return p.signaller.WriteMessage(p.signalling.SignalDataTrackSubscriberHandles(&livekit.DataTrackSubscriberHandles{
|
||||
SubHandles: handles,
|
||||
}))
|
||||
}
|
||||
|
||||
+892
-429
File diff suppressed because it is too large
Load Diff
+82
-57
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/protocol/auth/authfakes"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/webhook"
|
||||
@@ -89,7 +90,7 @@ func TestJoinedState(t *testing.T) {
|
||||
func TestRoomJoin(t *testing.T) {
|
||||
t.Run("joining returns existing participant data", func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: numParticipants})
|
||||
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false)
|
||||
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
|
||||
|
||||
_ = rm.Join(pNew, nil, nil, iceServersForRoom)
|
||||
|
||||
@@ -104,14 +105,14 @@ func TestRoomJoin(t *testing.T) {
|
||||
t.Run("subscribe to existing channels upon join", func(t *testing.T) {
|
||||
numExisting := 3
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: numExisting})
|
||||
p := NewMockParticipant("new", types.CurrentProtocol, false, false)
|
||||
lpl := rm.LocalParticipantListener()
|
||||
p := NewMockParticipant("new", types.CurrentProtocol, false, false, lpl)
|
||||
|
||||
err := rm.Join(p, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
|
||||
require.NoError(t, err)
|
||||
|
||||
stateChangeCB := p.OnStateChangeArgsForCall(0)
|
||||
require.NotNil(t, stateChangeCB)
|
||||
stateChangeCB(p, livekit.ParticipantInfo_ACTIVE)
|
||||
p.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
||||
lpl.OnStateChange(p)
|
||||
|
||||
// it should become a subscriber when connectivity changes
|
||||
numTracks := 0
|
||||
@@ -128,7 +129,7 @@ func TestRoomJoin(t *testing.T) {
|
||||
t.Run("participant state change is broadcasted to others", func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: numParticipants})
|
||||
var changedParticipant types.Participant
|
||||
rm.OnParticipantChanged(func(participant types.LocalParticipant) {
|
||||
rm.OnParticipantChanged(func(participant types.Participant) {
|
||||
changedParticipant = participant
|
||||
})
|
||||
participants := rm.GetParticipants()
|
||||
@@ -159,7 +160,7 @@ func TestRoomJoin(t *testing.T) {
|
||||
rm.lock.Lock()
|
||||
rm.protoRoom.MaxParticipants = 1
|
||||
rm.lock.Unlock()
|
||||
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false)
|
||||
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false, rm.LocalParticipantListener())
|
||||
|
||||
err := rm.Join(p, nil, nil, iceServersForRoom)
|
||||
require.Equal(t, ErrMaxParticipantsExceeded, err)
|
||||
@@ -177,7 +178,7 @@ func TestParticipantUpdate(t *testing.T) {
|
||||
"track mutes are sent to everyone",
|
||||
true,
|
||||
func(p types.LocalParticipant) {
|
||||
p.SetTrackMuted("", true, false)
|
||||
p.SetTrackMuted(&livekit.MuteTrackRequest{Muted: true}, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -270,14 +271,14 @@ func TestPushAndDequeueUpdates(t *testing.T) {
|
||||
pi *livekit.ParticipantInfo
|
||||
closeReason types.ParticipantCloseReason
|
||||
immediate bool
|
||||
existing *participantUpdate
|
||||
expected []*participantUpdate
|
||||
validate func(t *testing.T, rm *Room, updates []*participantUpdate)
|
||||
existing *ParticipantUpdate
|
||||
expected []*ParticipantUpdate
|
||||
validate func(t *testing.T, rm *Room, updates []*ParticipantUpdate)
|
||||
}{
|
||||
{
|
||||
name: "publisher updates are immediate",
|
||||
pi: publisher1v1,
|
||||
expected: []*participantUpdate{{pi: publisher1v1}},
|
||||
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v1}},
|
||||
},
|
||||
{
|
||||
name: "subscriber updates are queued",
|
||||
@@ -286,20 +287,20 @@ func TestPushAndDequeueUpdates(t *testing.T) {
|
||||
{
|
||||
name: "last version is enqueued",
|
||||
pi: subscriber1v2,
|
||||
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)}, // clone the existing value since it can be modified when setting to disconnected
|
||||
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
|
||||
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)}, // clone the existing value since it can be modified when setting to disconnected
|
||||
validate: func(t *testing.T, rm *Room, _ []*ParticipantUpdate) {
|
||||
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
|
||||
require.NotNil(t, queued)
|
||||
requirePIEquals(t, subscriber1v2, queued.pi)
|
||||
requirePIEquals(t, subscriber1v2, queued.ParticipantInfo)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "latest version when immediate",
|
||||
pi: subscriber1v2,
|
||||
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
|
||||
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
|
||||
immediate: true,
|
||||
expected: []*participantUpdate{{pi: subscriber1v2}},
|
||||
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
|
||||
expected: []*ParticipantUpdate{{ParticipantInfo: subscriber1v2}},
|
||||
validate: func(t *testing.T, rm *Room, _ []*ParticipantUpdate) {
|
||||
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
|
||||
require.Nil(t, queued)
|
||||
},
|
||||
@@ -307,37 +308,37 @@ func TestPushAndDequeueUpdates(t *testing.T) {
|
||||
{
|
||||
name: "out of order updates are rejected",
|
||||
pi: subscriber1v1,
|
||||
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v2)},
|
||||
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
|
||||
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v2)},
|
||||
validate: func(t *testing.T, rm *Room, updates []*ParticipantUpdate) {
|
||||
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
|
||||
requirePIEquals(t, subscriber1v2, queued.pi)
|
||||
requirePIEquals(t, subscriber1v2, queued.ParticipantInfo)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sid change is broadcasted immediately with synthsized disconnect",
|
||||
pi: publisher2,
|
||||
closeReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant, // just to test if update contain the close reason
|
||||
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v2), closeReason: types.ParticipantCloseReasonStale},
|
||||
expected: []*participantUpdate{
|
||||
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v2), CloseReason: types.ParticipantCloseReasonStale},
|
||||
expected: []*ParticipantUpdate{
|
||||
{
|
||||
pi: &livekit.ParticipantInfo{
|
||||
ParticipantInfo: &livekit.ParticipantInfo{
|
||||
Identity: identity,
|
||||
Sid: "1",
|
||||
Version: 2,
|
||||
State: livekit.ParticipantInfo_DISCONNECTED,
|
||||
},
|
||||
isSynthesizedDisconnect: true,
|
||||
closeReason: types.ParticipantCloseReasonStale,
|
||||
IsSynthesizedDisconnect: true,
|
||||
CloseReason: types.ParticipantCloseReasonStale,
|
||||
},
|
||||
{pi: publisher2, closeReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
|
||||
{ParticipantInfo: publisher2, CloseReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when switching to publisher, queue is cleared",
|
||||
pi: publisher1v2,
|
||||
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
|
||||
expected: []*participantUpdate{{pi: publisher1v2}},
|
||||
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
|
||||
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
|
||||
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v2}},
|
||||
validate: func(t *testing.T, rm *Room, updates []*ParticipantUpdate) {
|
||||
require.Empty(t, rm.batchedUpdates)
|
||||
},
|
||||
},
|
||||
@@ -347,14 +348,22 @@ func TestPushAndDequeueUpdates(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
|
||||
if tc.existing != nil {
|
||||
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.pi.Identity)] = tc.existing
|
||||
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.ParticipantInfo.Identity)] = tc.existing
|
||||
}
|
||||
updates := rm.pushAndDequeueUpdates(tc.pi, tc.closeReason, tc.immediate)
|
||||
rm.batchedUpdatesMu.Lock()
|
||||
updates := PushAndDequeueUpdates(
|
||||
tc.pi,
|
||||
tc.closeReason,
|
||||
tc.immediate,
|
||||
rm.GetParticipant(livekit.ParticipantIdentity(tc.pi.Identity)),
|
||||
rm.batchedUpdates,
|
||||
)
|
||||
rm.batchedUpdatesMu.Unlock()
|
||||
require.Equal(t, len(tc.expected), len(updates))
|
||||
for i, item := range tc.expected {
|
||||
requirePIEquals(t, item.pi, updates[i].pi)
|
||||
require.Equal(t, item.isSynthesizedDisconnect, updates[i].isSynthesizedDisconnect)
|
||||
require.Equal(t, item.closeReason, updates[i].closeReason)
|
||||
requirePIEquals(t, item.ParticipantInfo, updates[i].ParticipantInfo)
|
||||
require.Equal(t, item.IsSynthesizedDisconnect, updates[i].IsSynthesizedDisconnect)
|
||||
require.Equal(t, item.CloseReason, updates[i].CloseReason)
|
||||
}
|
||||
|
||||
if tc.validate != nil {
|
||||
@@ -417,6 +426,8 @@ func TestRoomClosure(t *testing.T) {
|
||||
func TestNewTrack(t *testing.T) {
|
||||
t.Run("new track should be added to ready participants", func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
|
||||
lpl := rm.LocalParticipantListener()
|
||||
|
||||
participants := rm.GetParticipants()
|
||||
p0 := participants[0].(*typesfakes.FakeLocalParticipant)
|
||||
p0.StateReturns(livekit.ParticipantInfo_JOINED)
|
||||
@@ -427,9 +438,8 @@ func TestNewTrack(t *testing.T) {
|
||||
|
||||
// pub adds track
|
||||
track := NewMockTrack(livekit.TrackType_VIDEO, "webcam")
|
||||
trackCB := pub.OnTrackPublishedArgsForCall(0)
|
||||
require.NotNil(t, trackCB)
|
||||
trackCB(pub, track)
|
||||
lpl.OnTrackPublished(pub, track)
|
||||
|
||||
// only p1 should've been subscribed to
|
||||
require.Equal(t, 0, p0.SubscribeToTrackCallCount())
|
||||
require.Equal(t, 1, p1.SubscribeToTrackCallCount())
|
||||
@@ -441,7 +451,7 @@ func TestActiveSpeakers(t *testing.T) {
|
||||
getActiveSpeakerUpdates := func(p *typesfakes.FakeLocalParticipant) [][]*livekit.SpeakerInfo {
|
||||
var updates [][]*livekit.SpeakerInfo
|
||||
numCalls := p.SendSpeakerUpdateCallCount()
|
||||
for i := 0; i < numCalls; i++ {
|
||||
for i := range numCalls {
|
||||
infos, _ := p.SendSpeakerUpdateArgsForCall(i)
|
||||
updates = append(updates, infos)
|
||||
}
|
||||
@@ -518,6 +528,7 @@ func TestActiveSpeakers(t *testing.T) {
|
||||
t.Run("audio level is smoothed", func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: 3, audioSmoothIntervals: 3})
|
||||
defer rm.Close(types.ParticipantCloseReasonNone)
|
||||
|
||||
participants := rm.GetParticipants()
|
||||
p := participants[0].(*typesfakes.FakeLocalParticipant)
|
||||
op := participants[1].(*typesfakes.FakeLocalParticipant)
|
||||
@@ -611,6 +622,9 @@ func TestDataChannel(t *testing.T) {
|
||||
t.Run(modeNames[mode], func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
|
||||
defer rm.Close(types.ParticipantCloseReasonNone)
|
||||
|
||||
lpl := rm.LocalParticipantListener()
|
||||
|
||||
participants := rm.GetParticipants()
|
||||
p := participants[0].(*typesfakes.FakeLocalParticipant)
|
||||
|
||||
@@ -631,17 +645,17 @@ func TestDataChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
encoded, _ := proto.Marshal(packetExp)
|
||||
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
|
||||
lpl.OnDataMessage(p, packet.Kind, packet)
|
||||
|
||||
// ensure everyone has received the packet
|
||||
for _, op := range participants {
|
||||
fp := op.(*typesfakes.FakeLocalParticipant)
|
||||
if fp == p {
|
||||
require.Zero(t, fp.SendDataPacketCallCount())
|
||||
require.Zero(t, fp.SendDataMessageCallCount())
|
||||
continue
|
||||
}
|
||||
require.Equal(t, 1, fp.SendDataPacketCallCount())
|
||||
_, got := fp.SendDataPacketArgsForCall(0)
|
||||
require.Equal(t, 1, fp.SendDataMessageCallCount())
|
||||
_, got, _, _ := fp.SendDataMessageArgsForCall(0)
|
||||
require.Equal(t, encoded, got)
|
||||
}
|
||||
})
|
||||
@@ -654,6 +668,9 @@ func TestDataChannel(t *testing.T) {
|
||||
t.Run(modeNames[mode], func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 4})
|
||||
defer rm.Close(types.ParticipantCloseReasonNone)
|
||||
|
||||
lpl := rm.LocalParticipantListener()
|
||||
|
||||
participants := rm.GetParticipants()
|
||||
p := participants[0].(*typesfakes.FakeLocalParticipant)
|
||||
p1 := participants[1].(*typesfakes.FakeLocalParticipant)
|
||||
@@ -678,17 +695,17 @@ func TestDataChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
encoded, _ := proto.Marshal(packetExp)
|
||||
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
|
||||
lpl.OnDataMessage(p, packet.Kind, packet)
|
||||
|
||||
// only p1 should receive the data
|
||||
for _, op := range participants {
|
||||
fp := op.(*typesfakes.FakeLocalParticipant)
|
||||
if fp != p1 {
|
||||
require.Zero(t, fp.SendDataPacketCallCount())
|
||||
require.Zero(t, fp.SendDataMessageCallCount())
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, p1.SendDataPacketCallCount())
|
||||
_, got := p1.SendDataPacketArgsForCall(0)
|
||||
require.Equal(t, 1, p1.SendDataMessageCallCount())
|
||||
_, got, _, _ := p1.SendDataMessageArgsForCall(0)
|
||||
require.Equal(t, encoded, got)
|
||||
})
|
||||
}
|
||||
@@ -697,6 +714,7 @@ func TestDataChannel(t *testing.T) {
|
||||
t.Run("publishing disallowed", func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
|
||||
defer rm.Close(types.ParticipantCloseReasonNone)
|
||||
|
||||
participants := rm.GetParticipants()
|
||||
p := participants[0].(*typesfakes.FakeLocalParticipant)
|
||||
p.CanPublishDataReturns(false)
|
||||
@@ -710,13 +728,14 @@ func TestDataChannel(t *testing.T) {
|
||||
},
|
||||
}
|
||||
if p.CanPublishData() {
|
||||
p.OnDataPacketArgsForCall(0)(p, packet.Kind, &packet)
|
||||
lpl := rm.LocalParticipantListener()
|
||||
lpl.OnDataMessage(p, packet.Kind, &packet)
|
||||
}
|
||||
|
||||
// no one should've been sent packet
|
||||
for _, op := range participants {
|
||||
fp := op.(*typesfakes.FakeLocalParticipant)
|
||||
require.Zero(t, fp.SendDataPacketCallCount())
|
||||
require.Zero(t, fp.SendDataMessageCallCount())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -726,7 +745,7 @@ func TestHiddenParticipants(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, numHidden: 1})
|
||||
defer rm.Close(types.ParticipantCloseReasonNone)
|
||||
|
||||
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false)
|
||||
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
|
||||
rm.Join(pNew, nil, nil, iceServersForRoom)
|
||||
|
||||
// expect new participant to get a JoinReply
|
||||
@@ -740,14 +759,14 @@ func TestHiddenParticipants(t *testing.T) {
|
||||
|
||||
t.Run("hidden participant subscribes to tracks", func(t *testing.T) {
|
||||
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
|
||||
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false)
|
||||
lpl := rm.LocalParticipantListener()
|
||||
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false, lpl)
|
||||
|
||||
err := rm.Join(hidden, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
|
||||
require.NoError(t, err)
|
||||
|
||||
stateChangeCB := hidden.OnStateChangeArgsForCall(0)
|
||||
require.NotNil(t, stateChangeCB)
|
||||
stateChangeCB(hidden, livekit.ParticipantInfo_ACTIVE)
|
||||
hidden.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
||||
lpl.OnStateChange(hidden)
|
||||
|
||||
require.Eventually(t, func() bool { return hidden.SubscribeToTrackCallCount() == 2 }, 5*time.Second, 10*time.Millisecond)
|
||||
})
|
||||
@@ -761,7 +780,7 @@ func TestRoomUpdate(t *testing.T) {
|
||||
p1 := rm.GetParticipants()[0].(*typesfakes.FakeLocalParticipant)
|
||||
require.Equal(t, 0, p1.SendRoomUpdateCallCount())
|
||||
|
||||
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false)
|
||||
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
|
||||
require.NoError(t, rm.Join(p2, nil, nil, iceServersForRoom))
|
||||
|
||||
// p1 should have received an update
|
||||
@@ -795,6 +814,12 @@ type testRoomOpts struct {
|
||||
}
|
||||
|
||||
func newRoomWithParticipants(t *testing.T, opts testRoomOpts) *Room {
|
||||
kp := &authfakes.FakeKeyProvider{}
|
||||
kp.GetSecretReturns("testkey")
|
||||
|
||||
n, err := webhook.NewDefaultNotifier(webhook.DefaultWebHookConfig, kp)
|
||||
require.NoError(t, err)
|
||||
|
||||
rm := NewRoom(
|
||||
&livekit.Room{Name: "room"},
|
||||
nil,
|
||||
@@ -816,12 +841,12 @@ func newRoomWithParticipants(t *testing.T, opts testRoomOpts) *Room {
|
||||
NodeId: "testnode",
|
||||
Region: "testregion",
|
||||
},
|
||||
telemetry.NewTelemetryService(webhook.NewDefaultNotifier("", "", nil), &telemetryfakes.FakeAnalyticsService{}),
|
||||
telemetry.NewTelemetryService(n, &telemetryfakes.FakeAnalyticsService{}),
|
||||
nil, nil, nil,
|
||||
)
|
||||
for i := 0; i < opts.num+opts.numHidden; i++ {
|
||||
identity := livekit.ParticipantIdentity(fmt.Sprintf("p%d", i))
|
||||
participant := NewMockParticipant(identity, opts.protocol, i >= opts.num, true)
|
||||
participant := NewMockParticipant(identity, opts.protocol, i >= opts.num, true, rm.LocalParticipantListener())
|
||||
err := rm.Join(participant, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
|
||||
require.NoError(t, err)
|
||||
participant.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
||||
|
||||
+153
-15
@@ -22,14 +22,18 @@ import (
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
// RoomTrackManager holds tracks that are published to the room
|
||||
type RoomTrackManager struct {
|
||||
logger logger.Logger
|
||||
|
||||
lock sync.RWMutex
|
||||
changedNotifier *utils.ChangeNotifierManager
|
||||
removedNotifier *utils.ChangeNotifierManager
|
||||
tracks map[livekit.TrackID]*TrackInfo
|
||||
tracks map[livekit.TrackID][]*TrackInfo
|
||||
dataTracks map[livekit.TrackID][]*DataTrackInfo
|
||||
}
|
||||
|
||||
type TrackInfo struct {
|
||||
@@ -38,54 +42,101 @@ type TrackInfo struct {
|
||||
PublisherID livekit.ParticipantID
|
||||
}
|
||||
|
||||
func NewRoomTrackManager() *RoomTrackManager {
|
||||
type DataTrackInfo struct {
|
||||
DataTrack types.DataTrack
|
||||
PublisherIdentity livekit.ParticipantIdentity
|
||||
PublisherID livekit.ParticipantID
|
||||
}
|
||||
|
||||
func NewRoomTrackManager(logger logger.Logger) *RoomTrackManager {
|
||||
return &RoomTrackManager{
|
||||
tracks: make(map[livekit.TrackID]*TrackInfo),
|
||||
logger: logger,
|
||||
tracks: make(map[livekit.TrackID][]*TrackInfo),
|
||||
dataTracks: make(map[livekit.TrackID][]*DataTrackInfo),
|
||||
changedNotifier: utils.NewChangeNotifierManager(),
|
||||
removedNotifier: utils.NewChangeNotifierManager(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) AddTrack(track types.MediaTrack, publisherIdentity livekit.ParticipantIdentity, publisherID livekit.ParticipantID) {
|
||||
trackID := track.ID()
|
||||
r.lock.Lock()
|
||||
r.tracks[track.ID()] = &TrackInfo{
|
||||
infos, ok := r.tracks[trackID]
|
||||
if ok {
|
||||
for _, info := range infos {
|
||||
if info.Track == track {
|
||||
r.lock.Unlock()
|
||||
r.logger.Infow("not adding duplicate track", "trackID", trackID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
r.tracks[trackID] = append(r.tracks[trackID], &TrackInfo{
|
||||
Track: track,
|
||||
PublisherIdentity: publisherIdentity,
|
||||
PublisherID: publisherID,
|
||||
}
|
||||
})
|
||||
r.lock.Unlock()
|
||||
|
||||
r.NotifyTrackChanged(track.ID())
|
||||
r.NotifyTrackChanged(trackID)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) RemoveTrack(track types.MediaTrack) {
|
||||
trackID := track.ID()
|
||||
r.lock.Lock()
|
||||
// ensure we are removing the same track as added
|
||||
info, ok := r.tracks[track.ID()]
|
||||
if !ok || info.Track != track {
|
||||
infos, ok := r.tracks[trackID]
|
||||
if !ok {
|
||||
r.lock.Unlock()
|
||||
return
|
||||
}
|
||||
delete(r.tracks, track.ID())
|
||||
r.lock.Unlock()
|
||||
|
||||
n := r.removedNotifier.GetNotifier(string(track.ID()))
|
||||
numRemoved := 0
|
||||
idx := 0
|
||||
for _, info := range infos {
|
||||
if info.Track == track {
|
||||
numRemoved++
|
||||
} else {
|
||||
r.tracks[trackID][idx] = info
|
||||
idx++
|
||||
}
|
||||
}
|
||||
for j := idx; j < len(infos); j++ {
|
||||
r.tracks[trackID][j] = nil
|
||||
}
|
||||
r.tracks[trackID] = r.tracks[trackID][:idx]
|
||||
if len(r.tracks[trackID]) == 0 {
|
||||
delete(r.tracks, trackID)
|
||||
}
|
||||
r.lock.Unlock()
|
||||
if numRemoved == 0 {
|
||||
return
|
||||
}
|
||||
if numRemoved > 1 {
|
||||
r.logger.Warnw("removed multiple tracks", nil, "trackID", trackID, "numRemoved", numRemoved)
|
||||
}
|
||||
|
||||
n := r.removedNotifier.GetNotifier(string(trackID))
|
||||
if n != nil {
|
||||
n.NotifyChanged()
|
||||
}
|
||||
|
||||
r.changedNotifier.RemoveNotifier(string(track.ID()), true)
|
||||
r.removedNotifier.RemoveNotifier(string(track.ID()), true)
|
||||
r.changedNotifier.RemoveNotifier(string(trackID), true)
|
||||
r.removedNotifier.RemoveNotifier(string(trackID), true)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) GetTrackInfo(trackID livekit.TrackID) *TrackInfo {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
info := r.tracks[trackID]
|
||||
if info == nil {
|
||||
infos := r.tracks[trackID]
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// earliest added track is used till it is removed
|
||||
info := infos[0]
|
||||
|
||||
// when track is about to close, do not resolve
|
||||
if info.Track != nil && !info.Track.IsOpen() {
|
||||
return nil
|
||||
@@ -123,3 +174,90 @@ func (r *RoomTrackManager) GetOrCreateTrackChangeNotifier(trackID livekit.TrackI
|
||||
func (r *RoomTrackManager) GetOrCreateTrackRemoveNotifier(trackID livekit.TrackID) *utils.ChangeNotifier {
|
||||
return r.removedNotifier.GetOrCreateNotifier(string(trackID))
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) AddDataTrack(dataTrack types.DataTrack, publisherIdentity livekit.ParticipantIdentity, publisherID livekit.ParticipantID) {
|
||||
trackID := dataTrack.ID()
|
||||
r.lock.Lock()
|
||||
infos, ok := r.dataTracks[trackID]
|
||||
if ok {
|
||||
for _, info := range infos {
|
||||
if info.DataTrack == dataTrack {
|
||||
r.lock.Unlock()
|
||||
r.logger.Infow("not adding duplicate data track", "trackID", trackID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
r.dataTracks[trackID] = append(r.dataTracks[trackID], &DataTrackInfo{
|
||||
DataTrack: dataTrack,
|
||||
PublisherIdentity: publisherIdentity,
|
||||
PublisherID: publisherID,
|
||||
})
|
||||
r.lock.Unlock()
|
||||
|
||||
r.NotifyTrackChanged(trackID)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) RemoveDataTrack(dataTrack types.DataTrack) {
|
||||
trackID := dataTrack.ID()
|
||||
r.lock.Lock()
|
||||
// ensure we are removing the same track as added
|
||||
infos, ok := r.dataTracks[trackID]
|
||||
if !ok {
|
||||
r.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
numRemoved := 0
|
||||
idx := 0
|
||||
for _, info := range infos {
|
||||
if info.DataTrack == dataTrack {
|
||||
numRemoved++
|
||||
} else {
|
||||
r.dataTracks[trackID][idx] = info
|
||||
idx++
|
||||
}
|
||||
}
|
||||
for j := idx; j < len(infos); j++ {
|
||||
r.dataTracks[trackID][j] = nil
|
||||
}
|
||||
r.dataTracks[trackID] = r.dataTracks[trackID][:idx]
|
||||
if len(r.dataTracks[trackID]) == 0 {
|
||||
delete(r.dataTracks, trackID)
|
||||
}
|
||||
r.lock.Unlock()
|
||||
if numRemoved == 0 {
|
||||
return
|
||||
}
|
||||
if numRemoved > 1 {
|
||||
r.logger.Warnw("removed multiple data tracks", nil, "trackID", trackID, "numRemoved", numRemoved)
|
||||
}
|
||||
|
||||
n := r.removedNotifier.GetNotifier(string(trackID))
|
||||
if n != nil {
|
||||
n.NotifyChanged()
|
||||
}
|
||||
|
||||
r.changedNotifier.RemoveNotifier(string(trackID), true)
|
||||
r.removedNotifier.RemoveNotifier(string(trackID), true)
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) GetDataTrackInfo(trackID livekit.TrackID) *DataTrackInfo {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
infos := r.dataTracks[trackID]
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// earliest added data track is used till it is removed
|
||||
return infos[0]
|
||||
}
|
||||
|
||||
func (r *RoomTrackManager) Report() (int, int) {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
return len(r.tracks), len(r.dataTracks)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/frostbyte73/core"
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/observability/roomobs"
|
||||
"github.com/livekit/protocol/utils"
|
||||
)
|
||||
|
||||
type BytesTrackType string
|
||||
|
||||
const (
|
||||
BytesTrackTypeData BytesTrackType = "DT"
|
||||
BytesTrackTypeSignal BytesTrackType = "SG"
|
||||
)
|
||||
|
||||
// -------------------------------
|
||||
|
||||
type TrafficTotals struct {
|
||||
At time.Time
|
||||
SendBytes uint64
|
||||
SendMessages uint32
|
||||
RecvBytes uint64
|
||||
RecvMessages uint32
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
|
||||
// stats for signal and data channel
|
||||
type BytesTrackStats struct {
|
||||
country string
|
||||
pID livekit.ParticipantID
|
||||
kind livekit.ParticipantInfo_Kind
|
||||
kindDetails []livekit.ParticipantInfo_KindDetail
|
||||
trackID livekit.TrackID
|
||||
send, recv atomic.Uint64
|
||||
sendMessages, recvMessages atomic.Uint32
|
||||
totalSendBytes, totalRecvBytes atomic.Uint64
|
||||
totalSendMessages, totalRecvMessages atomic.Uint32
|
||||
telemetryListener types.ParticipantTelemetryListener
|
||||
reporter roomobs.TrackReporter
|
||||
done core.Fuse
|
||||
}
|
||||
|
||||
func NewBytesTrackStats(
|
||||
country string,
|
||||
trackID livekit.TrackID,
|
||||
pID livekit.ParticipantID,
|
||||
kind livekit.ParticipantInfo_Kind,
|
||||
kindDetails []livekit.ParticipantInfo_KindDetail,
|
||||
telemetryListener types.ParticipantTelemetryListener,
|
||||
participantReporter roomobs.ParticipantSessionReporter,
|
||||
) *BytesTrackStats {
|
||||
s := &BytesTrackStats{
|
||||
country: country,
|
||||
pID: pID,
|
||||
kind: kind,
|
||||
kindDetails: kindDetails,
|
||||
trackID: trackID,
|
||||
telemetryListener: telemetryListener,
|
||||
reporter: participantReporter.WithTrack(trackID.String()),
|
||||
}
|
||||
go s.worker()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *BytesTrackStats) AddBytes(bytes uint64, isSend bool) {
|
||||
if isSend {
|
||||
s.send.Add(bytes)
|
||||
s.sendMessages.Inc()
|
||||
s.totalSendBytes.Add(bytes)
|
||||
s.totalSendMessages.Inc()
|
||||
|
||||
s.reporter.Tx(func(tx roomobs.TrackTx) {
|
||||
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(s.kind))
|
||||
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(s.kindDetails))
|
||||
tx.ReportType(roomobs.TrackTypeData)
|
||||
tx.ReportSendBytes(uint32(bytes))
|
||||
tx.ReportSendPackets(1)
|
||||
})
|
||||
} else {
|
||||
s.recv.Add(bytes)
|
||||
s.recvMessages.Inc()
|
||||
s.totalRecvBytes.Add(bytes)
|
||||
s.totalRecvMessages.Inc()
|
||||
|
||||
s.reporter.Tx(func(tx roomobs.TrackTx) {
|
||||
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(s.kind))
|
||||
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(s.kindDetails))
|
||||
tx.ReportType(roomobs.TrackTypeData)
|
||||
tx.ReportRecvBytes(uint32(bytes))
|
||||
tx.ReportRecvPackets(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BytesTrackStats) GetTrafficTotals() *TrafficTotals {
|
||||
return &TrafficTotals{
|
||||
At: time.Now(),
|
||||
SendBytes: s.totalSendBytes.Load(),
|
||||
SendMessages: s.totalSendMessages.Load(),
|
||||
RecvBytes: s.totalRecvBytes.Load(),
|
||||
RecvMessages: s.totalRecvMessages.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BytesTrackStats) Stop() {
|
||||
s.done.Break()
|
||||
}
|
||||
|
||||
func (s *BytesTrackStats) report() {
|
||||
if recv := s.recv.Swap(0); recv > 0 {
|
||||
packets := s.recvMessages.Swap(0)
|
||||
s.telemetryListener.OnTrackStats(
|
||||
telemetry.StatsKeyForData(s.country, livekit.StreamType_UPSTREAM, s.pID, s.trackID),
|
||||
&livekit.AnalyticsStat{
|
||||
Streams: []*livekit.AnalyticsStream{
|
||||
{
|
||||
PrimaryBytes: recv,
|
||||
PrimaryPackets: packets,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if send := s.send.Swap(0); send > 0 {
|
||||
packets := s.sendMessages.Swap(0)
|
||||
s.telemetryListener.OnTrackStats(
|
||||
telemetry.StatsKeyForData(s.country, livekit.StreamType_DOWNSTREAM, s.pID, s.trackID),
|
||||
&livekit.AnalyticsStat{
|
||||
Streams: []*livekit.AnalyticsStream{
|
||||
{
|
||||
PrimaryBytes: send,
|
||||
PrimaryPackets: packets,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BytesTrackStats) worker() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
s.report()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.done.Watch():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.report()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
var _ types.ParticipantTelemetryListener = (*BytesSignalStats)(nil)
|
||||
|
||||
type BytesSignalStats struct {
|
||||
BytesTrackStats
|
||||
ctx context.Context
|
||||
|
||||
telemetry telemetry.TelemetryService
|
||||
guard *telemetry.ReferenceGuard
|
||||
|
||||
participantResolver roomobs.ParticipantReporterResolver
|
||||
trackResolver roomobs.KeyResolver
|
||||
|
||||
mu sync.Mutex
|
||||
ri *livekit.Room
|
||||
pi *livekit.ParticipantInfo
|
||||
stopped chan struct{}
|
||||
|
||||
types.NullParticipantTelemetryListener
|
||||
}
|
||||
|
||||
func NewBytesSignalStats(
|
||||
ctx context.Context,
|
||||
telemetryService telemetry.TelemetryService,
|
||||
) *BytesSignalStats {
|
||||
projectReporter := telemetryService.RoomProjectReporter(ctx)
|
||||
participantReporter, participantReporterResolver := roomobs.DeferredParticipantReporter(projectReporter)
|
||||
trackReporter, trackReporterResolver := participantReporter.WithDeferredTrack()
|
||||
b := &BytesSignalStats{
|
||||
ctx: ctx,
|
||||
telemetry: telemetryService,
|
||||
guard: &telemetry.ReferenceGuard{},
|
||||
participantResolver: participantReporterResolver,
|
||||
trackResolver: trackReporterResolver,
|
||||
}
|
||||
b.BytesTrackStats = BytesTrackStats{
|
||||
telemetryListener: b,
|
||||
reporter: trackReporter,
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (s *BytesSignalStats) ResolveRoom(ri *livekit.Room) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.ri == nil && ri.GetSid() != "" {
|
||||
s.ri = &livekit.Room{
|
||||
Sid: ri.Sid,
|
||||
Name: ri.Name,
|
||||
}
|
||||
s.maybeStart()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BytesSignalStats) ResolveParticipant(pi *livekit.ParticipantInfo) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.pi == nil && pi != nil {
|
||||
s.pi = &livekit.ParticipantInfo{
|
||||
Sid: pi.Sid,
|
||||
Identity: pi.Identity,
|
||||
Kind: pi.Kind,
|
||||
KindDetails: pi.KindDetails,
|
||||
}
|
||||
s.kind = pi.Kind
|
||||
s.kindDetails = pi.KindDetails
|
||||
s.maybeStart()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BytesSignalStats) Reset() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.stopped != nil {
|
||||
s.done.Break()
|
||||
<-s.stopped
|
||||
s.stopped = nil
|
||||
s.done = core.Fuse{}
|
||||
s.guard = &telemetry.ReferenceGuard{}
|
||||
}
|
||||
s.ri = nil
|
||||
s.pi = nil
|
||||
|
||||
s.participantResolver.Reset()
|
||||
s.trackResolver.Reset()
|
||||
}
|
||||
|
||||
func (s *BytesSignalStats) maybeStart() {
|
||||
if s.ri == nil || s.pi == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.pID = livekit.ParticipantID(s.pi.Sid)
|
||||
s.trackID = BytesTrackIDForParticipantID(BytesTrackTypeSignal, s.pID)
|
||||
|
||||
s.participantResolver.Resolve(
|
||||
livekit.RoomName(s.ri.Name),
|
||||
livekit.RoomID(s.ri.Sid),
|
||||
livekit.ParticipantIdentity(s.pi.Identity),
|
||||
livekit.ParticipantID(s.pi.Sid),
|
||||
)
|
||||
s.trackResolver.Resolve(string(s.trackID))
|
||||
|
||||
s.telemetry.ParticipantJoined(s.ctx, s.ri, s.pi, nil, nil, false, s.guard)
|
||||
s.stopped = make(chan struct{})
|
||||
go s.worker()
|
||||
}
|
||||
|
||||
func (s *BytesSignalStats) worker() {
|
||||
s.BytesTrackStats.worker()
|
||||
s.telemetry.ParticipantLeft(s.ctx, s.ri, s.pi, false, s.guard)
|
||||
close(s.stopped)
|
||||
}
|
||||
|
||||
func (s *BytesSignalStats) OnTrackStats(key telemetry.StatsKey, stat *livekit.AnalyticsStat) {
|
||||
stat.RoomId, stat.RoomName = s.ri.Sid, s.ri.Name
|
||||
s.telemetry.TrackStats(livekit.RoomID(s.ri.Sid), livekit.RoomName(s.ri.Name), key, stat)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
func BytesTrackIDForParticipantID(typ BytesTrackType, participantID livekit.ParticipantID) livekit.TrackID {
|
||||
return livekit.TrackID(fmt.Sprintf("%s%s%s", utils.TrackPrefix, typ, participantID))
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rtc
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
func HandleParticipantSignal(room types.Room, participant types.LocalParticipant, req *livekit.SignalRequest, pLogger logger.Logger) error {
|
||||
participant.UpdateLastSeenSignal()
|
||||
|
||||
switch msg := req.GetMessage().(type) {
|
||||
case *livekit.SignalRequest_Offer:
|
||||
participant.HandleOffer(FromProtoSessionDescription(msg.Offer))
|
||||
|
||||
case *livekit.SignalRequest_Answer:
|
||||
participant.HandleAnswer(FromProtoSessionDescription(msg.Answer))
|
||||
|
||||
case *livekit.SignalRequest_Trickle:
|
||||
candidateInit, err := FromProtoTrickle(msg.Trickle)
|
||||
if err != nil {
|
||||
pLogger.Warnw("could not decode trickle", err)
|
||||
return nil
|
||||
}
|
||||
participant.AddICECandidate(candidateInit, msg.Trickle.Target)
|
||||
|
||||
case *livekit.SignalRequest_AddTrack:
|
||||
pLogger.Debugw("add track request", "trackID", msg.AddTrack.Cid)
|
||||
participant.AddTrack(msg.AddTrack)
|
||||
|
||||
case *livekit.SignalRequest_Mute:
|
||||
participant.SetTrackMuted(livekit.TrackID(msg.Mute.Sid), msg.Mute.Muted, false)
|
||||
|
||||
case *livekit.SignalRequest_Subscription:
|
||||
// allow participant to indicate their interest in the subscription
|
||||
// permission check happens later in SubscriptionManager
|
||||
room.UpdateSubscriptions(
|
||||
participant,
|
||||
livekit.StringsAsIDs[livekit.TrackID](msg.Subscription.TrackSids),
|
||||
msg.Subscription.ParticipantTracks,
|
||||
msg.Subscription.Subscribe,
|
||||
)
|
||||
|
||||
case *livekit.SignalRequest_TrackSetting:
|
||||
for _, sid := range livekit.StringsAsIDs[livekit.TrackID](msg.TrackSetting.TrackSids) {
|
||||
participant.UpdateSubscribedTrackSettings(sid, msg.TrackSetting)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_Leave:
|
||||
reason := types.ParticipantCloseReasonClientRequestLeave
|
||||
switch msg.Leave.Reason {
|
||||
case livekit.DisconnectReason_CLIENT_INITIATED:
|
||||
reason = types.ParticipantCloseReasonClientRequestLeave
|
||||
case livekit.DisconnectReason_USER_UNAVAILABLE:
|
||||
reason = types.ParticipantCloseReasonUserUnavailable
|
||||
case livekit.DisconnectReason_USER_REJECTED:
|
||||
reason = types.ParticipantCloseReasonUserRejected
|
||||
}
|
||||
pLogger.Debugw("client leaving room", "reason", reason)
|
||||
room.RemoveParticipant(participant.Identity(), participant.ID(), reason)
|
||||
|
||||
case *livekit.SignalRequest_SubscriptionPermission:
|
||||
err := room.UpdateSubscriptionPermission(participant, msg.SubscriptionPermission)
|
||||
if err != nil {
|
||||
pLogger.Warnw("could not update subscription permission", err,
|
||||
"permissions", msg.SubscriptionPermission)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_SyncState:
|
||||
err := room.SyncState(participant, msg.SyncState)
|
||||
if err != nil {
|
||||
pLogger.Warnw("could not sync state", err,
|
||||
"state", msg.SyncState)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_Simulate:
|
||||
err := room.SimulateScenario(participant, msg.Simulate)
|
||||
if err != nil {
|
||||
pLogger.Warnw("could not simulate scenario", err,
|
||||
"simulate", msg.Simulate)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_PingReq:
|
||||
if msg.PingReq.Rtt > 0 {
|
||||
participant.UpdateSignalingRTT(uint32(msg.PingReq.Rtt))
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_UpdateMetadata:
|
||||
requestResponse := &livekit.RequestResponse{
|
||||
RequestId: msg.UpdateMetadata.RequestId,
|
||||
Reason: livekit.RequestResponse_OK,
|
||||
}
|
||||
if participant.ClaimGrants().Video.GetCanUpdateOwnMetadata() {
|
||||
if err := participant.CheckMetadataLimits(
|
||||
msg.UpdateMetadata.Name,
|
||||
msg.UpdateMetadata.Metadata,
|
||||
msg.UpdateMetadata.Attributes,
|
||||
); err == nil {
|
||||
if msg.UpdateMetadata.Name != "" {
|
||||
participant.SetName(msg.UpdateMetadata.Name)
|
||||
}
|
||||
if msg.UpdateMetadata.Metadata != "" {
|
||||
participant.SetMetadata(msg.UpdateMetadata.Metadata)
|
||||
}
|
||||
if msg.UpdateMetadata.Attributes != nil {
|
||||
participant.SetAttributes(msg.UpdateMetadata.Attributes)
|
||||
}
|
||||
} else {
|
||||
pLogger.Warnw("could not update metadata", err)
|
||||
|
||||
switch err {
|
||||
case ErrNameExceedsLimits:
|
||||
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
|
||||
requestResponse.Message = "exceeds name length limit"
|
||||
|
||||
case ErrMetadataExceedsLimits:
|
||||
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
|
||||
requestResponse.Message = "exceeds metadata size limit"
|
||||
|
||||
case ErrAttributesExceedsLimits:
|
||||
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
|
||||
requestResponse.Message = "exceeds attributes size limit"
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
requestResponse.Reason = livekit.RequestResponse_NOT_ALLOWED
|
||||
requestResponse.Message = "does not have permission to update own metadata"
|
||||
}
|
||||
participant.SendRequestResponse(requestResponse)
|
||||
|
||||
case *livekit.SignalRequest_UpdateAudioTrack:
|
||||
if err := participant.UpdateAudioTrack(msg.UpdateAudioTrack); err != nil {
|
||||
pLogger.Warnw("could not update audio track", err, "update", msg.UpdateAudioTrack)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_UpdateVideoTrack:
|
||||
if err := participant.UpdateVideoTrack(msg.UpdateVideoTrack); err != nil {
|
||||
pLogger.Warnw("could not update video track", err, "update", msg.UpdateVideoTrack)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidMessageType = errors.New("invalid message type")
|
||||
ErrNameExceedsLimits = errors.New("name length exceeds limits")
|
||||
ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits")
|
||||
ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits")
|
||||
ErrUpdateOwnMetadataNotAllowed = errors.New("update own metadata not allowed")
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type ParticipantSignalHandler interface {
|
||||
HandleMessage(msg proto.Message) error
|
||||
}
|
||||
|
||||
type ParticipantSignaller interface {
|
||||
SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason)
|
||||
GetResponseSink() routing.MessageSink
|
||||
CloseSignalConnection(reason types.SignallingCloseReason)
|
||||
|
||||
WriteMessage(msg proto.Message) error
|
||||
}
|
||||
|
||||
type ParticipantSignalling interface {
|
||||
SignalJoinResponse(join *livekit.JoinResponse) proto.Message
|
||||
SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message
|
||||
SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message
|
||||
SignalRoomUpdate(room *livekit.Room) proto.Message
|
||||
SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message
|
||||
SignalRefreshToken(token string) proto.Message
|
||||
SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message
|
||||
SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message
|
||||
SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message
|
||||
SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message
|
||||
SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message
|
||||
SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message
|
||||
SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message
|
||||
SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message
|
||||
SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message
|
||||
SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message
|
||||
SignalSdpOffer(offer *livekit.SessionDescription) proto.Message
|
||||
SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message
|
||||
SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message
|
||||
SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message
|
||||
SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message
|
||||
SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message
|
||||
SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message
|
||||
SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message
|
||||
SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message
|
||||
SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
var _ ParticipantSignalHandler = (*signalhandler)(nil)
|
||||
|
||||
type SignalHandlerParams struct {
|
||||
Logger logger.Logger
|
||||
Participant types.LocalParticipant
|
||||
}
|
||||
|
||||
type signalhandler struct {
|
||||
params SignalHandlerParams
|
||||
}
|
||||
|
||||
func NewSignalHandler(params SignalHandlerParams) ParticipantSignalHandler {
|
||||
return &signalhandler{
|
||||
params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalhandler) HandleMessage(msg proto.Message) error {
|
||||
req, ok := msg.(*livekit.SignalRequest)
|
||||
if !ok {
|
||||
s.params.Logger.Warnw(
|
||||
"unknown message type", nil,
|
||||
"messageType", fmt.Sprintf("%T", msg),
|
||||
)
|
||||
return ErrInvalidMessageType
|
||||
}
|
||||
s.params.Participant.UpdateLastSeenSignal()
|
||||
|
||||
s.params.Logger.Debugw("handling signal request", "request", logger.Proto(req))
|
||||
switch msg := req.GetMessage().(type) {
|
||||
case *livekit.SignalRequest_Offer:
|
||||
s.params.Participant.HandleOffer(msg.Offer)
|
||||
|
||||
case *livekit.SignalRequest_Answer:
|
||||
s.params.Participant.HandleAnswer(msg.Answer)
|
||||
|
||||
case *livekit.SignalRequest_Trickle:
|
||||
s.params.Participant.HandleICETrickle(msg.Trickle)
|
||||
|
||||
case *livekit.SignalRequest_AddTrack:
|
||||
s.params.Participant.AddTrack(msg.AddTrack)
|
||||
|
||||
case *livekit.SignalRequest_Mute:
|
||||
s.params.Participant.SetTrackMuted(msg.Mute, false)
|
||||
|
||||
case *livekit.SignalRequest_Subscription:
|
||||
// allow participant to indicate their interest in the subscription
|
||||
// permission check happens later in SubscriptionManager
|
||||
s.params.Participant.HandleUpdateSubscriptions(
|
||||
livekit.StringsAsIDs[livekit.TrackID](msg.Subscription.TrackSids),
|
||||
msg.Subscription.ParticipantTracks,
|
||||
msg.Subscription.Subscribe,
|
||||
)
|
||||
|
||||
case *livekit.SignalRequest_TrackSetting:
|
||||
for _, sid := range livekit.StringsAsIDs[livekit.TrackID](msg.TrackSetting.TrackSids) {
|
||||
s.params.Participant.UpdateSubscribedTrackSettings(sid, msg.TrackSetting)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_Leave:
|
||||
reason := types.ParticipantCloseReasonClientRequestLeave
|
||||
switch msg.Leave.Reason {
|
||||
case livekit.DisconnectReason_CLIENT_INITIATED:
|
||||
reason = types.ParticipantCloseReasonClientRequestLeave
|
||||
case livekit.DisconnectReason_USER_UNAVAILABLE:
|
||||
reason = types.ParticipantCloseReasonUserUnavailable
|
||||
case livekit.DisconnectReason_USER_REJECTED:
|
||||
reason = types.ParticipantCloseReasonUserRejected
|
||||
case livekit.DisconnectReason_AGENT_ERROR:
|
||||
reason = types.ParticipantCloseReasonAgentError
|
||||
}
|
||||
s.params.Logger.Debugw("client leaving room", "reason", reason)
|
||||
s.params.Participant.HandleLeaveRequest(reason)
|
||||
|
||||
case *livekit.SignalRequest_SubscriptionPermission:
|
||||
err := s.params.Participant.HandleUpdateSubscriptionPermission(msg.SubscriptionPermission)
|
||||
if err != nil {
|
||||
s.params.Logger.Warnw(
|
||||
"could not update subscription permission", err,
|
||||
"permissions", msg.SubscriptionPermission,
|
||||
)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_SyncState:
|
||||
err := s.params.Participant.HandleSyncState(msg.SyncState)
|
||||
if err != nil {
|
||||
s.params.Logger.Warnw(
|
||||
"could not sync state", err,
|
||||
"state", msg.SyncState,
|
||||
)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_Simulate:
|
||||
err := s.params.Participant.HandleSimulateScenario(msg.Simulate)
|
||||
if err != nil {
|
||||
s.params.Logger.Warnw(
|
||||
"could not simulate scenario", err,
|
||||
"simulate", msg.Simulate,
|
||||
)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_PingReq:
|
||||
if msg.PingReq.Rtt > 0 {
|
||||
s.params.Participant.UpdateSignalingRTT(uint32(msg.PingReq.Rtt))
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_UpdateMetadata:
|
||||
s.params.Participant.UpdateMetadata(msg.UpdateMetadata, false)
|
||||
|
||||
case *livekit.SignalRequest_UpdateAudioTrack:
|
||||
if err := s.params.Participant.UpdateAudioTrack(msg.UpdateAudioTrack); err != nil {
|
||||
s.params.Logger.Warnw("could not update audio track", err, "update", msg.UpdateAudioTrack)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_UpdateVideoTrack:
|
||||
if err := s.params.Participant.UpdateVideoTrack(msg.UpdateVideoTrack); err != nil {
|
||||
s.params.Logger.Warnw("could not update video track", err, "update", msg.UpdateVideoTrack)
|
||||
}
|
||||
|
||||
case *livekit.SignalRequest_PublishDataTrackRequest:
|
||||
s.params.Participant.HandlePublishDataTrackRequest(msg.PublishDataTrackRequest)
|
||||
|
||||
case *livekit.SignalRequest_UnpublishDataTrackRequest:
|
||||
s.params.Participant.HandleUnpublishDataTrackRequest(msg.UnpublishDataTrackRequest)
|
||||
|
||||
case *livekit.SignalRequest_UpdateDataSubscription:
|
||||
s.params.Participant.HandleUpdateDataSubscription(msg.UpdateDataSubscription)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var _ ParticipantSignalHandler = (*signalhandlerUnimplemented)(nil)
|
||||
|
||||
type signalhandlerUnimplemented struct{}
|
||||
|
||||
func (u *signalhandlerUnimplemented) HandleMessage(msg proto.Message) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/psrpc"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var _ ParticipantSignaller = (*signallerAsync)(nil)
|
||||
|
||||
type SignallerAsyncParams struct {
|
||||
Logger logger.Logger
|
||||
Participant types.LocalParticipant
|
||||
}
|
||||
|
||||
type signallerAsync struct {
|
||||
params SignallerAsyncParams
|
||||
|
||||
*signallerAsyncBase
|
||||
}
|
||||
|
||||
func NewSignallerAsync(params SignallerAsyncParams) ParticipantSignaller {
|
||||
return &signallerAsync{
|
||||
params: params,
|
||||
signallerAsyncBase: newSignallerAsyncBase(signallerAsyncBaseParams{Logger: params.Logger}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signallerAsync) WriteMessage(msg proto.Message) error {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
getMessageType := func(msg proto.Message) string {
|
||||
messageType := "unknown"
|
||||
if typed, ok := msg.(*livekit.SignalResponse); ok {
|
||||
messageType = fmt.Sprintf("%T", typed.Message)
|
||||
}
|
||||
return messageType
|
||||
}
|
||||
|
||||
if s.params.Participant.IsDisconnected() {
|
||||
s.params.Logger.Debugw(
|
||||
"could not send message to participant, participant disconnected",
|
||||
"messageType", getMessageType(msg),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !s.params.Participant.IsReady() {
|
||||
if typed, ok := msg.(*livekit.SignalResponse); !ok {
|
||||
s.params.Logger.Warnw(
|
||||
"unknown message type", nil,
|
||||
"messageType", fmt.Sprintf("%T", msg),
|
||||
)
|
||||
} else {
|
||||
if typed.GetJoin() == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sink := s.GetResponseSink()
|
||||
if sink == nil {
|
||||
s.params.Logger.Debugw(
|
||||
"could not send message to participant, no sink",
|
||||
"messageType", getMessageType(msg),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := sink.WriteMessage(msg)
|
||||
if err != nil {
|
||||
if utils.ErrorIsOneOf(err, psrpc.Canceled, routing.ErrChannelClosed) {
|
||||
s.params.Logger.Debugw(
|
||||
"could not send message to participant",
|
||||
"error", err,
|
||||
"messageType", getMessageType(msg),
|
||||
)
|
||||
return nil
|
||||
} else {
|
||||
s.params.Logger.Warnw(
|
||||
"could not send message to participant", err,
|
||||
"messageType", getMessageType(msg),
|
||||
)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s.params.Logger.Debugw("sent signal response", "response", logger.Proto(msg))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
type signallerAsyncBaseParams struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type signallerAsyncBase struct {
|
||||
signallerUnimplemented
|
||||
|
||||
params signallerAsyncBaseParams
|
||||
|
||||
resSinkMu sync.Mutex
|
||||
resSink routing.MessageSink
|
||||
}
|
||||
|
||||
func newSignallerAsyncBase(params signallerAsyncBaseParams) *signallerAsyncBase {
|
||||
return &signallerAsyncBase{
|
||||
params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signallerAsyncBase) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
|
||||
s.resSinkMu.Lock()
|
||||
oldSink := s.resSink
|
||||
s.resSink = sink
|
||||
s.resSinkMu.Unlock()
|
||||
|
||||
if oldSink != nil {
|
||||
if sink != nil {
|
||||
s.params.Logger.Debugw(
|
||||
"swapping signal connection",
|
||||
"reason", reason,
|
||||
"connID", oldSink.ConnectionID(),
|
||||
"newConnID", sink.ConnectionID(),
|
||||
)
|
||||
} else {
|
||||
s.params.Logger.Debugw(
|
||||
"closing signal connection",
|
||||
"reason", reason,
|
||||
"connID", oldSink.ConnectionID(),
|
||||
)
|
||||
}
|
||||
oldSink.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signallerAsyncBase) GetResponseSink() routing.MessageSink {
|
||||
s.resSinkMu.Lock()
|
||||
defer s.resSinkMu.Unlock()
|
||||
return s.resSink
|
||||
}
|
||||
|
||||
// closes signal connection to notify client to resume/reconnect
|
||||
func (s *signallerAsyncBase) CloseSignalConnection(reason types.SignallingCloseReason) {
|
||||
s.SwapResponseSink(nil, reason)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var _ ParticipantSignaller = (*signallerUnimplemented)(nil)
|
||||
|
||||
type signallerUnimplemented struct{}
|
||||
|
||||
func (u *signallerUnimplemented) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
|
||||
}
|
||||
|
||||
func (u *signallerUnimplemented) GetResponseSink() routing.MessageSink {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallerUnimplemented) CloseSignalConnection(reason types.SignallingCloseReason) {}
|
||||
|
||||
func (u *signallerUnimplemented) WriteMessage(msg proto.Message) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var _ ParticipantSignalling = (*signalling)(nil)
|
||||
|
||||
type SignallingParams struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type signalling struct {
|
||||
params SignallingParams
|
||||
}
|
||||
|
||||
func NewSignalling(params SignallingParams) ParticipantSignalling {
|
||||
return &signalling{
|
||||
params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalJoinResponse(join *livekit.JoinResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Join{
|
||||
Join: join,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message {
|
||||
if len(participants) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Update{
|
||||
Update: &livekit.ParticipantUpdate{
|
||||
Participants: participants,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message {
|
||||
if len(speakers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_SpeakersChanged{
|
||||
SpeakersChanged: &livekit.SpeakersChanged{
|
||||
Speakers: speakers,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalRoomUpdate(room *livekit.Room) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_RoomUpdate{
|
||||
RoomUpdate: &livekit.RoomUpdate{
|
||||
Room: room,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_ConnectionQuality{
|
||||
ConnectionQuality: connectionQuality,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalRefreshToken(token string) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_RefreshToken{
|
||||
RefreshToken: token,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_RequestResponse{
|
||||
RequestResponse: requestResponse,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_RoomMoved{
|
||||
RoomMoved: roomMoved,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Reconnect{
|
||||
Reconnect: reconnect,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Trickle{
|
||||
Trickle: trickle,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Mute{
|
||||
Mute: mute,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_TrackPublished{
|
||||
TrackPublished: trackPublished,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_TrackUnpublished{
|
||||
TrackUnpublished: trackUnpublished,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_TrackSubscribed{
|
||||
TrackSubscribed: trackSubscribed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Leave{
|
||||
Leave: leave,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Answer{
|
||||
Answer: answer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalSdpOffer(offer *livekit.SessionDescription) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Offer{
|
||||
Offer: offer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_StreamStateUpdate{
|
||||
StreamStateUpdate: streamStateUpdate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_SubscribedQualityUpdate{
|
||||
SubscribedQualityUpdate: subscribedQualityUpdate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_SubscriptionResponse{
|
||||
SubscriptionResponse: subscriptionResponse,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_SubscriptionPermissionUpdate{
|
||||
SubscriptionPermissionUpdate: subscriptionPermissionUpdate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_MediaSectionsRequirement{
|
||||
MediaSectionsRequirement: mediaSectionsRequirement,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_SubscribedAudioCodecUpdate{
|
||||
SubscribedAudioCodecUpdate: subscribedAudioCodecUpdate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_PublishDataTrackResponse{
|
||||
PublishDataTrackResponse: publishDataTrackResponse,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_UnpublishDataTrackResponse{
|
||||
UnpublishDataTrackResponse: unpublishDataTrackResponse,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *signalling) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_DataTrackSubscriberHandles{
|
||||
DataTrackSubscriberHandles: dataTrackSubscriberHandles,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package signalling
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var _ ParticipantSignalling = (*signallingUnimplemented)(nil)
|
||||
|
||||
type signallingUnimplemented struct{}
|
||||
|
||||
func (u *signallingUnimplemented) SignalJoinResponse(join *livekit.JoinResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalRoomUpdate(room *livekit.Room) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalRefreshToken(token string) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalSdpOffer(offer *livekit.SessionDescription) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *signallingUnimplemented) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message {
|
||||
return nil
|
||||
}
|
||||
+210
-24
@@ -23,9 +23,12 @@ import (
|
||||
"go.uber.org/atomic"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
sutils "github.com/livekit/livekit-server/pkg/utils"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/observability/roomobs"
|
||||
"github.com/livekit/protocol/utils"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
@@ -37,19 +40,27 @@ const (
|
||||
subscriptionDebounceInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
var _ types.SubscribedTrack = (*SubscribedTrack)(nil)
|
||||
|
||||
type SubscribedTrackParams struct {
|
||||
PublisherID livekit.ParticipantID
|
||||
PublisherIdentity livekit.ParticipantIdentity
|
||||
PublisherVersion uint32
|
||||
Subscriber types.LocalParticipant
|
||||
MediaTrack types.MediaTrack
|
||||
DownTrack *sfu.DownTrack
|
||||
AdaptiveStream bool
|
||||
ReceiverConfig ReceiverConfig
|
||||
SubscriberConfig DirectionConfig
|
||||
Subscriber types.LocalParticipant
|
||||
MediaTrack types.MediaTrack
|
||||
AdaptiveStream bool
|
||||
TelemetryListener types.ParticipantTelemetryListener
|
||||
WrappedReceiver *WrappedReceiver
|
||||
IsRelayed bool
|
||||
OnDownTrackCreated func(downTrack *sfu.DownTrack)
|
||||
OnDownTrackClosed func(subscriberID livekit.ParticipantID)
|
||||
OnSubscriberMaxQualityChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)
|
||||
OnSubscriberAudioCodecChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)
|
||||
}
|
||||
|
||||
type SubscribedTrack struct {
|
||||
params SubscribedTrackParams
|
||||
logger logger.Logger
|
||||
downTrack *sfu.DownTrack
|
||||
sender atomic.Pointer[webrtc.RTPSender]
|
||||
needsNegotiation atomic.Bool
|
||||
|
||||
@@ -65,21 +76,98 @@ type SubscribedTrack struct {
|
||||
onClose atomic.Value // func(bool)
|
||||
|
||||
debouncer func(func())
|
||||
|
||||
statsKey telemetry.StatsKey
|
||||
reporter roomobs.TrackReporter
|
||||
}
|
||||
|
||||
func NewSubscribedTrack(params SubscribedTrackParams) *SubscribedTrack {
|
||||
func NewSubscribedTrack(params SubscribedTrackParams) (*SubscribedTrack, error) {
|
||||
s := &SubscribedTrack{
|
||||
params: params,
|
||||
logger: params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub).WithValues(
|
||||
"trackID", params.DownTrack.ID(),
|
||||
"publisherID", params.PublisherID,
|
||||
"publisher", params.PublisherIdentity,
|
||||
"trackID", params.MediaTrack.ID(),
|
||||
"publisherID", params.MediaTrack.PublisherID(),
|
||||
"publisher", params.MediaTrack.PublisherIdentity(),
|
||||
),
|
||||
versionGenerator: utils.NewDefaultTimedVersionGenerator(),
|
||||
debouncer: debounce.New(subscriptionDebounceInterval),
|
||||
statsKey: telemetry.StatsKeyForTrack(
|
||||
params.Subscriber.GetCountry(),
|
||||
livekit.StreamType_DOWNSTREAM,
|
||||
params.Subscriber.ID(),
|
||||
params.MediaTrack.ID(),
|
||||
params.MediaTrack.Source(),
|
||||
params.MediaTrack.Kind(),
|
||||
),
|
||||
reporter: params.Subscriber.GetReporter().WithTrack(params.MediaTrack.ID().String()),
|
||||
}
|
||||
|
||||
return s
|
||||
var rtcpFeedback []webrtc.RTCPFeedback
|
||||
var maxTrack int
|
||||
switch params.MediaTrack.Kind() {
|
||||
case livekit.TrackType_AUDIO:
|
||||
rtcpFeedback = params.SubscriberConfig.RTCPFeedback.Audio
|
||||
maxTrack = params.ReceiverConfig.PacketBufferSizeAudio
|
||||
case livekit.TrackType_VIDEO:
|
||||
rtcpFeedback = params.SubscriberConfig.RTCPFeedback.Video
|
||||
maxTrack = params.ReceiverConfig.PacketBufferSizeVideo
|
||||
default:
|
||||
s.logger.Warnw("unexpected track type", nil, "kind", params.MediaTrack.Kind())
|
||||
}
|
||||
codecs := params.WrappedReceiver.Codecs()
|
||||
for _, c := range codecs {
|
||||
c.RTCPFeedback = rtcpFeedback
|
||||
}
|
||||
|
||||
streamID := params.WrappedReceiver.StreamID()
|
||||
if params.Subscriber.SupportsSyncStreamID() && params.MediaTrack.Stream() != "" {
|
||||
streamID = PackSyncStreamID(params.MediaTrack.PublisherID(), params.MediaTrack.Stream())
|
||||
}
|
||||
|
||||
isEncrypted := params.MediaTrack.IsEncrypted()
|
||||
var trailer []byte
|
||||
if isEncrypted {
|
||||
trailer = params.Subscriber.GetTrailer()
|
||||
}
|
||||
subClientInfo := ClientInfo{ClientInfo: params.Subscriber.GetClientInfo()}
|
||||
subSupportsPacketTrailer := subClientInfo.SupportsPacketTrailer()
|
||||
// Strip packet trailer if track has packet trailer but subscriber does not have cap
|
||||
stripPacketTrailer := params.MediaTrack.HasPacketTrailer() && !subSupportsPacketTrailer
|
||||
downTrack, err := sfu.NewDownTrack(sfu.DownTrackParams{
|
||||
Codecs: codecs,
|
||||
IsEncrypted: isEncrypted,
|
||||
Source: params.MediaTrack.Source(),
|
||||
Receiver: params.WrappedReceiver,
|
||||
BufferFactory: params.Subscriber.GetBufferFactory(),
|
||||
SubID: params.Subscriber.ID(),
|
||||
StreamID: streamID,
|
||||
MaxTrack: maxTrack,
|
||||
PlayoutDelayLimit: params.Subscriber.GetPlayoutDelayConfig(),
|
||||
Pacer: params.Subscriber.GetPacer(),
|
||||
Trailer: trailer,
|
||||
StripPacketTrailer: stripPacketTrailer,
|
||||
Logger: LoggerWithTrack(
|
||||
params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub),
|
||||
params.MediaTrack.ID(),
|
||||
params.IsRelayed,
|
||||
),
|
||||
RTCPWriter: params.Subscriber.WriteSubscriberRTCP,
|
||||
DisableSenderReportPassThrough: params.Subscriber.GetDisableSenderReportPassThrough(),
|
||||
SupportsCodecChange: params.Subscriber.SupportsCodecChange(),
|
||||
Listener: s,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params.OnDownTrackCreated != nil {
|
||||
params.OnDownTrackCreated(downTrack)
|
||||
}
|
||||
|
||||
downTrack.AddReceiverReportListener(params.Subscriber.HandleReceiverReport)
|
||||
|
||||
s.downTrack = downTrack
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) AddOnBind(f func(error)) {
|
||||
@@ -121,6 +209,7 @@ func (t *SubscribedTrack) Bound(err error) {
|
||||
if t.params.AdaptiveStream {
|
||||
// remove `disabled` flag to force a visibility update
|
||||
t.settings.Disabled = false
|
||||
t.logger.Debugw("enabling subscriber track settings on bind", "settings", logger.Proto(t.settings))
|
||||
}
|
||||
} else {
|
||||
if t.params.AdaptiveStream {
|
||||
@@ -128,6 +217,7 @@ func (t *SubscribedTrack) Bound(err error) {
|
||||
} else {
|
||||
t.settings = &livekit.UpdateTrackSettings{Quality: livekit.VideoQuality_HIGH}
|
||||
}
|
||||
t.logger.Debugw("initializing subscriber track settings on bind", "settings", logger.Proto(t.settings))
|
||||
}
|
||||
t.settingsLock.Unlock()
|
||||
t.applySettings()
|
||||
@@ -141,7 +231,7 @@ func (t *SubscribedTrack) Bound(err error) {
|
||||
// for DownTrack callback to notify us that it's closed
|
||||
func (t *SubscribedTrack) Close(isExpectedToResume bool) {
|
||||
if onClose := t.onClose.Load(); onClose != nil {
|
||||
go onClose.(func(bool))(isExpectedToResume)
|
||||
onClose.(func(bool))(isExpectedToResume)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,19 +247,19 @@ func (t *SubscribedTrack) IsBound() bool {
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) ID() livekit.TrackID {
|
||||
return livekit.TrackID(t.params.DownTrack.ID())
|
||||
return livekit.TrackID(t.downTrack.ID())
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) PublisherID() livekit.ParticipantID {
|
||||
return t.params.PublisherID
|
||||
return t.params.MediaTrack.PublisherID()
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) PublisherIdentity() livekit.ParticipantIdentity {
|
||||
return t.params.PublisherIdentity
|
||||
return t.params.MediaTrack.PublisherIdentity()
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) PublisherVersion() uint32 {
|
||||
return t.params.PublisherVersion
|
||||
return t.params.MediaTrack.PublisherVersion()
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) SubscriberID() livekit.ParticipantID {
|
||||
@@ -185,7 +275,7 @@ func (t *SubscribedTrack) Subscriber() types.LocalParticipant {
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) DownTrack() *sfu.DownTrack {
|
||||
return t.params.DownTrack
|
||||
return t.downTrack
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) MediaTrack() types.MediaTrack {
|
||||
@@ -209,18 +299,20 @@ func (t *SubscribedTrack) isMutedLocked() bool {
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) SetPublisherMuted(muted bool) {
|
||||
t.DownTrack().PubMute(muted)
|
||||
t.downTrack.PubMute(muted)
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) UpdateSubscriberSettings(settings *livekit.UpdateTrackSettings, isImmediate bool) {
|
||||
t.settingsLock.Lock()
|
||||
if proto.Equal(t.settings, settings) {
|
||||
t.logger.Debugw("skipping subscriber track settings", "settings", logger.Proto(t.settings))
|
||||
t.settingsLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
isImmediate = isImmediate || (!settings.Disabled && settings.Disabled != t.isMutedLocked())
|
||||
t.settings = utils.CloneProto(settings)
|
||||
t.logger.Debugw("saving subscriber track settings", "settings", logger.Proto(t.settings))
|
||||
t.settingsLock.Unlock()
|
||||
|
||||
if isImmediate {
|
||||
@@ -242,7 +334,6 @@ func (t *SubscribedTrack) applySettings() {
|
||||
return
|
||||
}
|
||||
|
||||
t.logger.Debugw("updating subscriber track settings", "settings", logger.Proto(t.settings))
|
||||
t.settingsVersion = t.versionGenerator.Next()
|
||||
settingsVersion := t.settingsVersion
|
||||
t.settingsLock.Unlock()
|
||||
@@ -253,23 +344,25 @@ func (t *SubscribedTrack) applySettings() {
|
||||
if dt.Kind() == webrtc.RTPCodecTypeVideo {
|
||||
mt := t.MediaTrack()
|
||||
quality := t.settings.Quality
|
||||
mimeType := dt.Mime()
|
||||
if t.settings.Width > 0 {
|
||||
quality = mt.GetQualityForDimension(t.settings.Width, t.settings.Height)
|
||||
quality = mt.GetQualityForDimension(mimeType, t.settings.Width, t.settings.Height)
|
||||
}
|
||||
|
||||
spatial = buffer.VideoQualityToSpatialLayer(quality, mt.ToProto())
|
||||
spatial = buffer.GetSpatialLayerForVideoQuality(mimeType, quality, mt.ToProto())
|
||||
if t.settings.Fps > 0 {
|
||||
temporal = mt.GetTemporalLayerForSpatialFps(spatial, t.settings.Fps, dt.Mime())
|
||||
temporal = mt.GetTemporalLayerForSpatialFps(mimeType, spatial, t.settings.Fps)
|
||||
}
|
||||
}
|
||||
|
||||
t.settingsLock.Lock()
|
||||
if settingsVersion != t.settingsVersion {
|
||||
// a newer settings has superceded this one
|
||||
// a newer settings has superseded this one
|
||||
t.settingsLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
t.logger.Debugw("applying subscriber track settings", "settings", logger.Proto(t.settings))
|
||||
if t.settings.Disabled {
|
||||
dt.Mute(true)
|
||||
t.settingsLock.Unlock()
|
||||
@@ -302,3 +395,96 @@ func (t *SubscribedTrack) RTPSender() *webrtc.RTPSender {
|
||||
func (t *SubscribedTrack) SetRTPSender(sender *webrtc.RTPSender) {
|
||||
t.sender.Store(sender)
|
||||
}
|
||||
|
||||
// DownTrackListener implementation
|
||||
var _ sfu.DownTrackListener = (*SubscribedTrack)(nil)
|
||||
|
||||
func (t *SubscribedTrack) OnBindAndConnected() {
|
||||
if t.params.Subscriber.Hidden() {
|
||||
return
|
||||
}
|
||||
|
||||
t.params.MediaTrack.OnTrackSubscribed()
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) OnStatsUpdate(stat *livekit.AnalyticsStat) {
|
||||
t.params.TelemetryListener.OnTrackStats(t.statsKey, stat)
|
||||
|
||||
if cs, ok := telemetry.CondenseStat(stat); ok {
|
||||
ti := t.params.WrappedReceiver.TrackInfo()
|
||||
t.reporter.Tx(func(tx roomobs.TrackTx) {
|
||||
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(t.params.Subscriber.Kind()))
|
||||
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(t.params.Subscriber.KindDetails()))
|
||||
tx.ReportName(ti.Name)
|
||||
tx.ReportKind(roomobs.TrackKindSub)
|
||||
tx.ReportType(roomobs.TrackTypeFromProto(ti.Type))
|
||||
tx.ReportSource(roomobs.TrackSourceFromProto(ti.Source))
|
||||
tx.ReportMime(mime.NormalizeMimeType(ti.MimeType).ReporterType())
|
||||
tx.ReportLayer(roomobs.PackTrackLayer(ti.Height, ti.Width))
|
||||
tx.ReportDuration(uint16(cs.EndTime.Sub(cs.StartTime).Milliseconds()))
|
||||
tx.ReportFrames(uint16(cs.Frames))
|
||||
tx.ReportSendBytes(uint32(cs.Bytes))
|
||||
tx.ReportSendPackets(cs.Packets)
|
||||
tx.ReportPacketsLost(cs.PacketsLost)
|
||||
tx.ReportScore(stat.Score)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) OnMaxSubscribedLayerChanged(layer int32) {
|
||||
if t.params.OnSubscriberMaxQualityChange != nil {
|
||||
t.params.OnSubscriberMaxQualityChange(t.downTrack.SubscriberID(), t.downTrack.Mime(), layer)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) OnRttUpdate(rtt uint32) {
|
||||
go t.params.Subscriber.UpdateMediaRTT(rtt)
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) OnCodecNegotiated(codec webrtc.RTPCodecCapability) {
|
||||
if isAvailable, needsPublish := t.params.WrappedReceiver.DetermineReceiver(codec); !isAvailable || !needsPublish {
|
||||
return
|
||||
}
|
||||
|
||||
if t.params.OnSubscriberMaxQualityChange != nil || t.params.OnSubscriberAudioCodecChange != nil {
|
||||
go func() {
|
||||
mimeType := mime.NormalizeMimeType(codec.MimeType)
|
||||
switch t.params.MediaTrack.Kind() {
|
||||
case livekit.TrackType_VIDEO:
|
||||
spatial := buffer.GetSpatialLayerForVideoQuality(
|
||||
mimeType,
|
||||
livekit.VideoQuality_HIGH,
|
||||
t.params.MediaTrack.ToProto(),
|
||||
)
|
||||
if t.params.OnSubscriberMaxQualityChange != nil {
|
||||
t.params.OnSubscriberMaxQualityChange(t.downTrack.SubscriberID(), mimeType, spatial)
|
||||
}
|
||||
|
||||
case livekit.TrackType_AUDIO:
|
||||
if t.params.OnSubscriberAudioCodecChange != nil {
|
||||
t.params.OnSubscriberAudioCodecChange(t.downTrack.SubscriberID(), mimeType, true)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) OnDownTrackClose(isExpectedToResume bool) {
|
||||
// Cache transceiver for potential re-use on resume.
|
||||
// To ensure subscription manager does not re-subscribe before caching,
|
||||
// delete the subscribed track only after caching.
|
||||
if isExpectedToResume {
|
||||
if tr := t.downTrack.GetTransceiver(); tr != nil {
|
||||
t.params.Subscriber.CacheDownTrack(t.ID(), tr, t.downTrack.GetState())
|
||||
}
|
||||
}
|
||||
|
||||
if t.params.OnDownTrackClosed != nil {
|
||||
t.params.OnDownTrackClosed(t.params.Subscriber.ID())
|
||||
}
|
||||
t.Close(isExpectedToResume)
|
||||
}
|
||||
|
||||
func (t *SubscribedTrack) OnStreamStarted() {
|
||||
t.params.TelemetryListener.OnTrackSubscribeStreamStarted(t.params.Subscriber.ID(), t.params.MediaTrack.ToProto())
|
||||
}
|
||||
|
||||
+723
-297
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,6 @@ import (
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
@@ -67,7 +66,7 @@ func TestSubscribe(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
sm.SubscribeToTrack("track")
|
||||
sm.SubscribeToTrack("track", false)
|
||||
s := sm.subscriptions["track"]
|
||||
require.True(t, s.isDesired())
|
||||
require.Eventually(t, func() bool {
|
||||
@@ -83,8 +82,8 @@ func TestSubscribe(t *testing.T) {
|
||||
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
|
||||
|
||||
// ensure telemetry events are sent
|
||||
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
|
||||
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
|
||||
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
|
||||
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
|
||||
|
||||
// ensure bound
|
||||
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
|
||||
@@ -93,7 +92,7 @@ func TestSubscribe(t *testing.T) {
|
||||
}, subSettleTimeout, subCheckInterval, "track was not bound")
|
||||
|
||||
// telemetry event should have been sent
|
||||
require.Equal(t, 1, tm.TrackSubscribedCallCount())
|
||||
require.Equal(t, 1, tl.OnTrackSubscribedCallCount())
|
||||
|
||||
time.Sleep(notFoundTimeout)
|
||||
require.False(t, failed.Load())
|
||||
@@ -127,7 +126,7 @@ func TestSubscribe(t *testing.T) {
|
||||
failed.Store(true)
|
||||
}
|
||||
|
||||
sm.SubscribeToTrack("track")
|
||||
sm.SubscribeToTrack("track", false)
|
||||
s := sm.subscriptions["track"]
|
||||
require.Eventually(t, func() bool {
|
||||
return !s.getHasPermission()
|
||||
@@ -142,9 +141,9 @@ func TestSubscribe(t *testing.T) {
|
||||
require.Len(t, sm.GetSubscribedTracks(), 0)
|
||||
|
||||
// trackSubscribed telemetry not sent
|
||||
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
|
||||
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
|
||||
require.Equal(t, 0, tm.TrackSubscribedCallCount())
|
||||
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
|
||||
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
|
||||
require.Equal(t, 0, tl.OnTrackSubscribedCallCount())
|
||||
|
||||
// give permissions now
|
||||
resolver.lock.Lock()
|
||||
@@ -168,7 +167,7 @@ func TestSubscribe(t *testing.T) {
|
||||
failed.Store(true)
|
||||
}
|
||||
|
||||
sm.SubscribeToTrack("track")
|
||||
sm.SubscribeToTrack("track", false)
|
||||
s := sm.subscriptions["track"]
|
||||
require.Eventually(t, func() bool {
|
||||
return !s.needsSubscribe()
|
||||
@@ -197,15 +196,17 @@ func TestUnsubscribe(t *testing.T) {
|
||||
|
||||
resolver := newTestResolver(true, true, "pub", "pubID")
|
||||
|
||||
s := &trackSubscription{
|
||||
trackID: "track",
|
||||
desired: true,
|
||||
subscriberID: sm.params.Participant.ID(),
|
||||
publisherID: "pubID",
|
||||
publisherIdentity: "pub",
|
||||
hasPermission: true,
|
||||
bound: true,
|
||||
logger: logger.GetLogger(),
|
||||
s := &mediaTrackSubscription{
|
||||
trackSubscription: trackSubscription{
|
||||
trackID: "track",
|
||||
desired: true,
|
||||
subscriberID: sm.params.Participant.ID(),
|
||||
publisherID: "pubID",
|
||||
publisherIdentity: "pub",
|
||||
logger: logger.GetLogger(),
|
||||
},
|
||||
hasPermission: true,
|
||||
bound: true,
|
||||
}
|
||||
// a bunch of unfortunate manual wiring
|
||||
res := resolver.Resolve(nil, s.trackID)
|
||||
@@ -242,18 +243,15 @@ func TestUnsubscribe(t *testing.T) {
|
||||
sm.lock.RLock()
|
||||
subLen := len(sm.subscriptions)
|
||||
sm.lock.RUnlock()
|
||||
if subLen != 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return subLen == 0
|
||||
}, subSettleTimeout, subCheckInterval, "Track was not unsubscribed")
|
||||
|
||||
// no traces should be left
|
||||
require.Len(t, sm.GetSubscribedTracks(), 0)
|
||||
require.False(t, res.TrackChangedNotifier.HasObservers())
|
||||
|
||||
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
|
||||
require.Equal(t, 1, tm.TrackUnsubscribedCallCount())
|
||||
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
|
||||
require.Equal(t, 1, tl.OnTrackUnsubscribedCallCount())
|
||||
}
|
||||
|
||||
func TestSubscribeStatusChanged(t *testing.T) {
|
||||
@@ -271,8 +269,8 @@ func TestSubscribeStatusChanged(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
sm.SubscribeToTrack("track1")
|
||||
sm.SubscribeToTrack("track2")
|
||||
sm.SubscribeToTrack("track1", false)
|
||||
sm.SubscribeToTrack("track2", false)
|
||||
s1 := sm.subscriptions["track1"]
|
||||
s2 := sm.subscriptions["track2"]
|
||||
require.Eventually(t, func() bool {
|
||||
@@ -332,7 +330,7 @@ func TestUpdateSettingsBeforeSubscription(t *testing.T) {
|
||||
}
|
||||
sm.UpdateSubscribedTrackSettings("track", settings)
|
||||
|
||||
sm.SubscribeToTrack("track")
|
||||
sm.SubscribeToTrack("track", false)
|
||||
|
||||
s := sm.subscriptions["track"]
|
||||
require.Eventually(t, func() bool {
|
||||
@@ -376,7 +374,7 @@ func TestSubscriptionLimits(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
sm.SubscribeToTrack("track")
|
||||
sm.SubscribeToTrack("track", false)
|
||||
s := sm.subscriptions["track"]
|
||||
require.True(t, s.isDesired())
|
||||
require.Eventually(t, func() bool {
|
||||
@@ -392,8 +390,8 @@ func TestSubscriptionLimits(t *testing.T) {
|
||||
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
|
||||
|
||||
// ensure telemetry events are sent
|
||||
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
|
||||
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
|
||||
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
|
||||
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
|
||||
|
||||
// ensure bound
|
||||
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
|
||||
@@ -402,15 +400,15 @@ func TestSubscriptionLimits(t *testing.T) {
|
||||
}, subSettleTimeout, subCheckInterval, "track was not bound")
|
||||
|
||||
// telemetry event should have been sent
|
||||
require.Equal(t, 1, tm.TrackSubscribedCallCount())
|
||||
require.Equal(t, 1, tl.OnTrackSubscribedCallCount())
|
||||
|
||||
// reach subscription limit, subscribe pending
|
||||
sm.SubscribeToTrack("track2")
|
||||
sm.SubscribeToTrack("track2", false)
|
||||
s2 := sm.subscriptions["track2"]
|
||||
time.Sleep(subscriptionTimeout * 2)
|
||||
require.True(t, s2.needsSubscribe())
|
||||
require.Equal(t, 2, tm.TrackSubscribeRequestedCallCount())
|
||||
require.Equal(t, 1, tm.TrackSubscribeFailedCallCount())
|
||||
require.Equal(t, 2, tl.OnTrackSubscribeRequestedCallCount())
|
||||
require.Equal(t, 1, tl.OnTrackSubscribeFailedCallCount())
|
||||
require.Len(t, sm.GetSubscribedTracks(), 1)
|
||||
|
||||
// unsubscribe track1, then track2 should be subscribed
|
||||
@@ -427,7 +425,7 @@ func TestSubscriptionLimits(t *testing.T) {
|
||||
require.False(t, s2.needsSubscribe())
|
||||
require.EqualValues(t, 2, subCount.Load())
|
||||
require.NotNil(t, s2.getSubscribedTrack())
|
||||
require.Equal(t, 2, tm.TrackSubscribeRequestedCallCount())
|
||||
require.Equal(t, 2, tl.OnTrackSubscribeRequestedCallCount())
|
||||
require.Len(t, sm.GetSubscribedTracks(), 1)
|
||||
|
||||
// ensure bound
|
||||
@@ -437,13 +435,13 @@ func TestSubscriptionLimits(t *testing.T) {
|
||||
}, subSettleTimeout, subCheckInterval, "track was not bound")
|
||||
|
||||
// subscribe to track1 again, which should pending
|
||||
sm.SubscribeToTrack("track")
|
||||
sm.SubscribeToTrack("track", false)
|
||||
s = sm.subscriptions["track"]
|
||||
require.True(t, s.isDesired())
|
||||
time.Sleep(subscriptionTimeout * 2)
|
||||
require.True(t, s.needsSubscribe())
|
||||
require.Equal(t, 3, tm.TrackSubscribeRequestedCallCount())
|
||||
require.Equal(t, 2, tm.TrackSubscribeFailedCallCount())
|
||||
require.Equal(t, 3, tl.OnTrackSubscribeRequestedCallCount())
|
||||
require.Equal(t, 2, tl.OnTrackSubscribeFailedCallCount())
|
||||
require.Len(t, sm.GetSubscribedTracks(), 1)
|
||||
}
|
||||
|
||||
@@ -471,7 +469,7 @@ func newTestSubscriptionManagerWithParams(params testSubscriptionParams) *Subscr
|
||||
TrackResolver: func(sub types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
|
||||
return types.MediaResolverResult{}
|
||||
},
|
||||
Telemetry: &telemetryfakes.FakeTelemetryService{},
|
||||
TelemetryListener: &typesfakes.FakeParticipantTelemetryListener{},
|
||||
SubscriptionLimitAudio: params.SubscriptionLimitAudio,
|
||||
SubscriptionLimitVideo: params.SubscriptionLimitVideo,
|
||||
})
|
||||
|
||||
+12
-16
@@ -17,6 +17,7 @@ package rtc
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/observability/roomobs"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
|
||||
@@ -24,7 +25,13 @@ import (
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
|
||||
)
|
||||
|
||||
func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.ProtocolVersion, hidden bool, publisher bool) *typesfakes.FakeLocalParticipant {
|
||||
func NewMockParticipant(
|
||||
identity livekit.ParticipantIdentity,
|
||||
protocol types.ProtocolVersion,
|
||||
hidden bool,
|
||||
publisher bool,
|
||||
participantListener types.LocalParticipantListener,
|
||||
) *typesfakes.FakeLocalParticipant {
|
||||
p := &typesfakes.FakeLocalParticipant{}
|
||||
sid := guid.New(utils.ParticipantPrefix)
|
||||
p.IDReturns(livekit.ParticipantID(sid))
|
||||
@@ -49,25 +56,13 @@ func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.Pro
|
||||
}, utils.TimedVersion(0))
|
||||
|
||||
p.SetMetadataCalls(func(m string) {
|
||||
var f func(participant types.LocalParticipant)
|
||||
if p.OnParticipantUpdateCallCount() > 0 {
|
||||
f = p.OnParticipantUpdateArgsForCall(p.OnParticipantUpdateCallCount() - 1)
|
||||
}
|
||||
if f != nil {
|
||||
f(p)
|
||||
}
|
||||
participantListener.OnParticipantUpdate(p)
|
||||
})
|
||||
updateTrack := func() {
|
||||
var f func(participant types.LocalParticipant, track types.MediaTrack)
|
||||
if p.OnTrackUpdatedCallCount() > 0 {
|
||||
f = p.OnTrackUpdatedArgsForCall(p.OnTrackUpdatedCallCount() - 1)
|
||||
}
|
||||
if f != nil {
|
||||
f(p, NewMockTrack(livekit.TrackType_VIDEO, "testcam"))
|
||||
}
|
||||
participantListener.OnTrackUpdated(p, NewMockTrack(livekit.TrackType_VIDEO, "testcam"))
|
||||
}
|
||||
|
||||
p.SetTrackMutedCalls(func(sid livekit.TrackID, muted bool, fromServer bool) *livekit.TrackInfo {
|
||||
p.SetTrackMutedCalls(func(mute *livekit.MuteTrackRequest, fromServer bool) *livekit.TrackInfo {
|
||||
updateTrack()
|
||||
return nil
|
||||
})
|
||||
@@ -75,6 +70,7 @@ func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.Pro
|
||||
updateTrack()
|
||||
})
|
||||
p.GetLoggerReturns(logger.GetLogger())
|
||||
p.GetReporterReturns(roomobs.NewNoopParticipantSessionReporter())
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
+1241
-219
File diff suppressed because it is too large
Load Diff
@@ -39,13 +39,17 @@ type Handler interface {
|
||||
OnFullyEstablished()
|
||||
OnFailed(isShortLived bool, iceConnectionInfo *types.ICEConnectionInfo)
|
||||
OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver)
|
||||
OnDataPacket(kind livekit.DataPacket_Kind, data []byte)
|
||||
OnDataMessage(kind livekit.DataPacket_Kind, data []byte)
|
||||
OnDataMessageUnlabeled(data []byte)
|
||||
OnDataTrackMessage(data []byte, arrivalTime int64)
|
||||
OnDataSendError(err error)
|
||||
OnOffer(sd webrtc.SessionDescription) error
|
||||
OnAnswer(sd webrtc.SessionDescription) error
|
||||
OnOffer(sd webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error
|
||||
OnSetRemoteDescriptionOffer()
|
||||
OnAnswer(sd webrtc.SessionDescription, answerId uint32, midToTrackID map[string]string) error
|
||||
OnNegotiationStateChanged(state NegotiationState)
|
||||
OnNegotiationFailed()
|
||||
OnStreamStateChange(update *streamallocator.StreamStateUpdate) error
|
||||
OnUnmatchedMedia(numAudios uint32, numVideos uint32) error
|
||||
}
|
||||
|
||||
type UnimplementedHandler struct{}
|
||||
@@ -57,12 +61,15 @@ func (h UnimplementedHandler) OnInitialConnected()
|
||||
func (h UnimplementedHandler) OnFullyEstablished() {}
|
||||
func (h UnimplementedHandler) OnFailed(isShortLived bool) {}
|
||||
func (h UnimplementedHandler) OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {}
|
||||
func (h UnimplementedHandler) OnDataPacket(kind livekit.DataPacket_Kind, data []byte) {}
|
||||
func (h UnimplementedHandler) OnDataMessage(kind livekit.DataPacket_Kind, data []byte) {}
|
||||
func (h UnimplementedHandler) OnDataMessageUnlabeled(data []byte) {}
|
||||
func (h UnimplementedHandler) OnDataTrackMessage(data []byte, arrivalTime int64) {}
|
||||
func (h UnimplementedHandler) OnDataSendError(err error) {}
|
||||
func (h UnimplementedHandler) OnOffer(sd webrtc.SessionDescription) error {
|
||||
func (h UnimplementedHandler) OnOffer(sd webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error {
|
||||
return ErrNoOfferHandler
|
||||
}
|
||||
func (h UnimplementedHandler) OnAnswer(sd webrtc.SessionDescription) error {
|
||||
func (h UnimplementedHandler) OnSetRemoteDescriptionOffer() {}
|
||||
func (h UnimplementedHandler) OnAnswer(sd webrtc.SessionDescription, answerId uint32, midToTrackID map[string]string) error {
|
||||
return ErrNoAnswerHandler
|
||||
}
|
||||
func (h UnimplementedHandler) OnNegotiationStateChanged(state NegotiationState) {}
|
||||
@@ -70,3 +77,6 @@ func (h UnimplementedHandler) OnNegotiationFailed()
|
||||
func (h UnimplementedHandler) OnStreamStateChange(update *streamallocator.StreamStateUpdate) error {
|
||||
return nil
|
||||
}
|
||||
func (h UnimplementedHandler) OnUnmatchedMedia(numAudios uint32, numVideos uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@ import (
|
||||
)
|
||||
|
||||
type FakeHandler struct {
|
||||
OnAnswerStub func(webrtc.SessionDescription) error
|
||||
OnAnswerStub func(webrtc.SessionDescription, uint32, map[string]string) error
|
||||
onAnswerMutex sync.RWMutex
|
||||
onAnswerArgsForCall []struct {
|
||||
arg1 webrtc.SessionDescription
|
||||
arg2 uint32
|
||||
arg3 map[string]string
|
||||
}
|
||||
onAnswerReturns struct {
|
||||
result1 error
|
||||
@@ -23,17 +25,28 @@ type FakeHandler struct {
|
||||
onAnswerReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
OnDataPacketStub func(livekit.DataPacket_Kind, []byte)
|
||||
onDataPacketMutex sync.RWMutex
|
||||
onDataPacketArgsForCall []struct {
|
||||
OnDataMessageStub func(livekit.DataPacket_Kind, []byte)
|
||||
onDataMessageMutex sync.RWMutex
|
||||
onDataMessageArgsForCall []struct {
|
||||
arg1 livekit.DataPacket_Kind
|
||||
arg2 []byte
|
||||
}
|
||||
OnDataMessageUnlabeledStub func([]byte)
|
||||
onDataMessageUnlabeledMutex sync.RWMutex
|
||||
onDataMessageUnlabeledArgsForCall []struct {
|
||||
arg1 []byte
|
||||
}
|
||||
OnDataSendErrorStub func(error)
|
||||
onDataSendErrorMutex sync.RWMutex
|
||||
onDataSendErrorArgsForCall []struct {
|
||||
arg1 error
|
||||
}
|
||||
OnDataTrackMessageStub func([]byte, int64)
|
||||
onDataTrackMessageMutex sync.RWMutex
|
||||
onDataTrackMessageArgsForCall []struct {
|
||||
arg1 []byte
|
||||
arg2 int64
|
||||
}
|
||||
OnFailedStub func(bool, *types.ICEConnectionInfo)
|
||||
onFailedMutex sync.RWMutex
|
||||
onFailedArgsForCall []struct {
|
||||
@@ -69,10 +82,12 @@ type FakeHandler struct {
|
||||
onNegotiationStateChangedArgsForCall []struct {
|
||||
arg1 transport.NegotiationState
|
||||
}
|
||||
OnOfferStub func(webrtc.SessionDescription) error
|
||||
OnOfferStub func(webrtc.SessionDescription, uint32, map[string]string) error
|
||||
onOfferMutex sync.RWMutex
|
||||
onOfferArgsForCall []struct {
|
||||
arg1 webrtc.SessionDescription
|
||||
arg2 uint32
|
||||
arg3 map[string]string
|
||||
}
|
||||
onOfferReturns struct {
|
||||
result1 error
|
||||
@@ -80,6 +95,10 @@ type FakeHandler struct {
|
||||
onOfferReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
OnSetRemoteDescriptionOfferStub func()
|
||||
onSetRemoteDescriptionOfferMutex sync.RWMutex
|
||||
onSetRemoteDescriptionOfferArgsForCall []struct {
|
||||
}
|
||||
OnStreamStateChangeStub func(*streamallocator.StreamStateUpdate) error
|
||||
onStreamStateChangeMutex sync.RWMutex
|
||||
onStreamStateChangeArgsForCall []struct {
|
||||
@@ -97,22 +116,36 @@ type FakeHandler struct {
|
||||
arg1 *webrtc.TrackRemote
|
||||
arg2 *webrtc.RTPReceiver
|
||||
}
|
||||
OnUnmatchedMediaStub func(uint32, uint32) error
|
||||
onUnmatchedMediaMutex sync.RWMutex
|
||||
onUnmatchedMediaArgsForCall []struct {
|
||||
arg1 uint32
|
||||
arg2 uint32
|
||||
}
|
||||
onUnmatchedMediaReturns struct {
|
||||
result1 error
|
||||
}
|
||||
onUnmatchedMediaReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnAnswer(arg1 webrtc.SessionDescription) error {
|
||||
func (fake *FakeHandler) OnAnswer(arg1 webrtc.SessionDescription, arg2 uint32, arg3 map[string]string) error {
|
||||
fake.onAnswerMutex.Lock()
|
||||
ret, specificReturn := fake.onAnswerReturnsOnCall[len(fake.onAnswerArgsForCall)]
|
||||
fake.onAnswerArgsForCall = append(fake.onAnswerArgsForCall, struct {
|
||||
arg1 webrtc.SessionDescription
|
||||
}{arg1})
|
||||
arg2 uint32
|
||||
arg3 map[string]string
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.OnAnswerStub
|
||||
fakeReturns := fake.onAnswerReturns
|
||||
fake.recordInvocation("OnAnswer", []interface{}{arg1})
|
||||
fake.recordInvocation("OnAnswer", []interface{}{arg1, arg2, arg3})
|
||||
fake.onAnswerMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
@@ -126,17 +159,17 @@ func (fake *FakeHandler) OnAnswerCallCount() int {
|
||||
return len(fake.onAnswerArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnAnswerCalls(stub func(webrtc.SessionDescription) error) {
|
||||
func (fake *FakeHandler) OnAnswerCalls(stub func(webrtc.SessionDescription, uint32, map[string]string) error) {
|
||||
fake.onAnswerMutex.Lock()
|
||||
defer fake.onAnswerMutex.Unlock()
|
||||
fake.OnAnswerStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnAnswerArgsForCall(i int) webrtc.SessionDescription {
|
||||
func (fake *FakeHandler) OnAnswerArgsForCall(i int) (webrtc.SessionDescription, uint32, map[string]string) {
|
||||
fake.onAnswerMutex.RLock()
|
||||
defer fake.onAnswerMutex.RUnlock()
|
||||
argsForCall := fake.onAnswerArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnAnswerReturns(result1 error) {
|
||||
@@ -162,44 +195,81 @@ func (fake *FakeHandler) OnAnswerReturnsOnCall(i int, result1 error) {
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataPacket(arg1 livekit.DataPacket_Kind, arg2 []byte) {
|
||||
func (fake *FakeHandler) OnDataMessage(arg1 livekit.DataPacket_Kind, arg2 []byte) {
|
||||
var arg2Copy []byte
|
||||
if arg2 != nil {
|
||||
arg2Copy = make([]byte, len(arg2))
|
||||
copy(arg2Copy, arg2)
|
||||
}
|
||||
fake.onDataPacketMutex.Lock()
|
||||
fake.onDataPacketArgsForCall = append(fake.onDataPacketArgsForCall, struct {
|
||||
fake.onDataMessageMutex.Lock()
|
||||
fake.onDataMessageArgsForCall = append(fake.onDataMessageArgsForCall, struct {
|
||||
arg1 livekit.DataPacket_Kind
|
||||
arg2 []byte
|
||||
}{arg1, arg2Copy})
|
||||
stub := fake.OnDataPacketStub
|
||||
fake.recordInvocation("OnDataPacket", []interface{}{arg1, arg2Copy})
|
||||
fake.onDataPacketMutex.Unlock()
|
||||
stub := fake.OnDataMessageStub
|
||||
fake.recordInvocation("OnDataMessage", []interface{}{arg1, arg2Copy})
|
||||
fake.onDataMessageMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.OnDataPacketStub(arg1, arg2)
|
||||
fake.OnDataMessageStub(arg1, arg2)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataPacketCallCount() int {
|
||||
fake.onDataPacketMutex.RLock()
|
||||
defer fake.onDataPacketMutex.RUnlock()
|
||||
return len(fake.onDataPacketArgsForCall)
|
||||
func (fake *FakeHandler) OnDataMessageCallCount() int {
|
||||
fake.onDataMessageMutex.RLock()
|
||||
defer fake.onDataMessageMutex.RUnlock()
|
||||
return len(fake.onDataMessageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataPacketCalls(stub func(livekit.DataPacket_Kind, []byte)) {
|
||||
fake.onDataPacketMutex.Lock()
|
||||
defer fake.onDataPacketMutex.Unlock()
|
||||
fake.OnDataPacketStub = stub
|
||||
func (fake *FakeHandler) OnDataMessageCalls(stub func(livekit.DataPacket_Kind, []byte)) {
|
||||
fake.onDataMessageMutex.Lock()
|
||||
defer fake.onDataMessageMutex.Unlock()
|
||||
fake.OnDataMessageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataPacketArgsForCall(i int) (livekit.DataPacket_Kind, []byte) {
|
||||
fake.onDataPacketMutex.RLock()
|
||||
defer fake.onDataPacketMutex.RUnlock()
|
||||
argsForCall := fake.onDataPacketArgsForCall[i]
|
||||
func (fake *FakeHandler) OnDataMessageArgsForCall(i int) (livekit.DataPacket_Kind, []byte) {
|
||||
fake.onDataMessageMutex.RLock()
|
||||
defer fake.onDataMessageMutex.RUnlock()
|
||||
argsForCall := fake.onDataMessageArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataMessageUnlabeled(arg1 []byte) {
|
||||
var arg1Copy []byte
|
||||
if arg1 != nil {
|
||||
arg1Copy = make([]byte, len(arg1))
|
||||
copy(arg1Copy, arg1)
|
||||
}
|
||||
fake.onDataMessageUnlabeledMutex.Lock()
|
||||
fake.onDataMessageUnlabeledArgsForCall = append(fake.onDataMessageUnlabeledArgsForCall, struct {
|
||||
arg1 []byte
|
||||
}{arg1Copy})
|
||||
stub := fake.OnDataMessageUnlabeledStub
|
||||
fake.recordInvocation("OnDataMessageUnlabeled", []interface{}{arg1Copy})
|
||||
fake.onDataMessageUnlabeledMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.OnDataMessageUnlabeledStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataMessageUnlabeledCallCount() int {
|
||||
fake.onDataMessageUnlabeledMutex.RLock()
|
||||
defer fake.onDataMessageUnlabeledMutex.RUnlock()
|
||||
return len(fake.onDataMessageUnlabeledArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataMessageUnlabeledCalls(stub func([]byte)) {
|
||||
fake.onDataMessageUnlabeledMutex.Lock()
|
||||
defer fake.onDataMessageUnlabeledMutex.Unlock()
|
||||
fake.OnDataMessageUnlabeledStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataMessageUnlabeledArgsForCall(i int) []byte {
|
||||
fake.onDataMessageUnlabeledMutex.RLock()
|
||||
defer fake.onDataMessageUnlabeledMutex.RUnlock()
|
||||
argsForCall := fake.onDataMessageUnlabeledArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataSendError(arg1 error) {
|
||||
fake.onDataSendErrorMutex.Lock()
|
||||
fake.onDataSendErrorArgsForCall = append(fake.onDataSendErrorArgsForCall, struct {
|
||||
@@ -232,6 +302,44 @@ func (fake *FakeHandler) OnDataSendErrorArgsForCall(i int) error {
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataTrackMessage(arg1 []byte, arg2 int64) {
|
||||
var arg1Copy []byte
|
||||
if arg1 != nil {
|
||||
arg1Copy = make([]byte, len(arg1))
|
||||
copy(arg1Copy, arg1)
|
||||
}
|
||||
fake.onDataTrackMessageMutex.Lock()
|
||||
fake.onDataTrackMessageArgsForCall = append(fake.onDataTrackMessageArgsForCall, struct {
|
||||
arg1 []byte
|
||||
arg2 int64
|
||||
}{arg1Copy, arg2})
|
||||
stub := fake.OnDataTrackMessageStub
|
||||
fake.recordInvocation("OnDataTrackMessage", []interface{}{arg1Copy, arg2})
|
||||
fake.onDataTrackMessageMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.OnDataTrackMessageStub(arg1, arg2)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataTrackMessageCallCount() int {
|
||||
fake.onDataTrackMessageMutex.RLock()
|
||||
defer fake.onDataTrackMessageMutex.RUnlock()
|
||||
return len(fake.onDataTrackMessageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataTrackMessageCalls(stub func([]byte, int64)) {
|
||||
fake.onDataTrackMessageMutex.Lock()
|
||||
defer fake.onDataTrackMessageMutex.Unlock()
|
||||
fake.OnDataTrackMessageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnDataTrackMessageArgsForCall(i int) ([]byte, int64) {
|
||||
fake.onDataTrackMessageMutex.RLock()
|
||||
defer fake.onDataTrackMessageMutex.RUnlock()
|
||||
argsForCall := fake.onDataTrackMessageArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnFailed(arg1 bool, arg2 *types.ICEConnectionInfo) {
|
||||
fake.onFailedMutex.Lock()
|
||||
fake.onFailedArgsForCall = append(fake.onFailedArgsForCall, struct {
|
||||
@@ -431,18 +539,20 @@ func (fake *FakeHandler) OnNegotiationStateChangedArgsForCall(i int) transport.N
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnOffer(arg1 webrtc.SessionDescription) error {
|
||||
func (fake *FakeHandler) OnOffer(arg1 webrtc.SessionDescription, arg2 uint32, arg3 map[string]string) error {
|
||||
fake.onOfferMutex.Lock()
|
||||
ret, specificReturn := fake.onOfferReturnsOnCall[len(fake.onOfferArgsForCall)]
|
||||
fake.onOfferArgsForCall = append(fake.onOfferArgsForCall, struct {
|
||||
arg1 webrtc.SessionDescription
|
||||
}{arg1})
|
||||
arg2 uint32
|
||||
arg3 map[string]string
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.OnOfferStub
|
||||
fakeReturns := fake.onOfferReturns
|
||||
fake.recordInvocation("OnOffer", []interface{}{arg1})
|
||||
fake.recordInvocation("OnOffer", []interface{}{arg1, arg2, arg3})
|
||||
fake.onOfferMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
@@ -456,17 +566,17 @@ func (fake *FakeHandler) OnOfferCallCount() int {
|
||||
return len(fake.onOfferArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnOfferCalls(stub func(webrtc.SessionDescription) error) {
|
||||
func (fake *FakeHandler) OnOfferCalls(stub func(webrtc.SessionDescription, uint32, map[string]string) error) {
|
||||
fake.onOfferMutex.Lock()
|
||||
defer fake.onOfferMutex.Unlock()
|
||||
fake.OnOfferStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnOfferArgsForCall(i int) webrtc.SessionDescription {
|
||||
func (fake *FakeHandler) OnOfferArgsForCall(i int) (webrtc.SessionDescription, uint32, map[string]string) {
|
||||
fake.onOfferMutex.RLock()
|
||||
defer fake.onOfferMutex.RUnlock()
|
||||
argsForCall := fake.onOfferArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnOfferReturns(result1 error) {
|
||||
@@ -492,6 +602,30 @@ func (fake *FakeHandler) OnOfferReturnsOnCall(i int, result1 error) {
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnSetRemoteDescriptionOffer() {
|
||||
fake.onSetRemoteDescriptionOfferMutex.Lock()
|
||||
fake.onSetRemoteDescriptionOfferArgsForCall = append(fake.onSetRemoteDescriptionOfferArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.OnSetRemoteDescriptionOfferStub
|
||||
fake.recordInvocation("OnSetRemoteDescriptionOffer", []interface{}{})
|
||||
fake.onSetRemoteDescriptionOfferMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.OnSetRemoteDescriptionOfferStub()
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnSetRemoteDescriptionOfferCallCount() int {
|
||||
fake.onSetRemoteDescriptionOfferMutex.RLock()
|
||||
defer fake.onSetRemoteDescriptionOfferMutex.RUnlock()
|
||||
return len(fake.onSetRemoteDescriptionOfferArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnSetRemoteDescriptionOfferCalls(stub func()) {
|
||||
fake.onSetRemoteDescriptionOfferMutex.Lock()
|
||||
defer fake.onSetRemoteDescriptionOfferMutex.Unlock()
|
||||
fake.OnSetRemoteDescriptionOfferStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnStreamStateChange(arg1 *streamallocator.StreamStateUpdate) error {
|
||||
fake.onStreamStateChangeMutex.Lock()
|
||||
ret, specificReturn := fake.onStreamStateChangeReturnsOnCall[len(fake.onStreamStateChangeArgsForCall)]
|
||||
@@ -586,33 +720,71 @@ func (fake *FakeHandler) OnTrackArgsForCall(i int) (*webrtc.TrackRemote, *webrtc
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMedia(arg1 uint32, arg2 uint32) error {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
ret, specificReturn := fake.onUnmatchedMediaReturnsOnCall[len(fake.onUnmatchedMediaArgsForCall)]
|
||||
fake.onUnmatchedMediaArgsForCall = append(fake.onUnmatchedMediaArgsForCall, struct {
|
||||
arg1 uint32
|
||||
arg2 uint32
|
||||
}{arg1, arg2})
|
||||
stub := fake.OnUnmatchedMediaStub
|
||||
fakeReturns := fake.onUnmatchedMediaReturns
|
||||
fake.recordInvocation("OnUnmatchedMedia", []interface{}{arg1, arg2})
|
||||
fake.onUnmatchedMediaMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaCallCount() int {
|
||||
fake.onUnmatchedMediaMutex.RLock()
|
||||
defer fake.onUnmatchedMediaMutex.RUnlock()
|
||||
return len(fake.onUnmatchedMediaArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaCalls(stub func(uint32, uint32) error) {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
defer fake.onUnmatchedMediaMutex.Unlock()
|
||||
fake.OnUnmatchedMediaStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaArgsForCall(i int) (uint32, uint32) {
|
||||
fake.onUnmatchedMediaMutex.RLock()
|
||||
defer fake.onUnmatchedMediaMutex.RUnlock()
|
||||
argsForCall := fake.onUnmatchedMediaArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaReturns(result1 error) {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
defer fake.onUnmatchedMediaMutex.Unlock()
|
||||
fake.OnUnmatchedMediaStub = nil
|
||||
fake.onUnmatchedMediaReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) OnUnmatchedMediaReturnsOnCall(i int, result1 error) {
|
||||
fake.onUnmatchedMediaMutex.Lock()
|
||||
defer fake.onUnmatchedMediaMutex.Unlock()
|
||||
fake.OnUnmatchedMediaStub = nil
|
||||
if fake.onUnmatchedMediaReturnsOnCall == nil {
|
||||
fake.onUnmatchedMediaReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.onUnmatchedMediaReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeHandler) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.onAnswerMutex.RLock()
|
||||
defer fake.onAnswerMutex.RUnlock()
|
||||
fake.onDataPacketMutex.RLock()
|
||||
defer fake.onDataPacketMutex.RUnlock()
|
||||
fake.onDataSendErrorMutex.RLock()
|
||||
defer fake.onDataSendErrorMutex.RUnlock()
|
||||
fake.onFailedMutex.RLock()
|
||||
defer fake.onFailedMutex.RUnlock()
|
||||
fake.onFullyEstablishedMutex.RLock()
|
||||
defer fake.onFullyEstablishedMutex.RUnlock()
|
||||
fake.onICECandidateMutex.RLock()
|
||||
defer fake.onICECandidateMutex.RUnlock()
|
||||
fake.onInitialConnectedMutex.RLock()
|
||||
defer fake.onInitialConnectedMutex.RUnlock()
|
||||
fake.onNegotiationFailedMutex.RLock()
|
||||
defer fake.onNegotiationFailedMutex.RUnlock()
|
||||
fake.onNegotiationStateChangedMutex.RLock()
|
||||
defer fake.onNegotiationStateChangedMutex.RUnlock()
|
||||
fake.onOfferMutex.RLock()
|
||||
defer fake.onOfferMutex.RUnlock()
|
||||
fake.onStreamStateChangeMutex.RLock()
|
||||
defer fake.onStreamStateChangeMutex.RUnlock()
|
||||
fake.onTrackMutex.RLock()
|
||||
defer fake.onTrackMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
||||
+209
-56
@@ -28,17 +28,15 @@ import (
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/transport"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/transport/transportfakes"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/testutils"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
func TestMissingAnswerDuringICERestart(t *testing.T) {
|
||||
params := TransportParams{
|
||||
ParticipantID: "id",
|
||||
ParticipantIdentity: "identity",
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
}
|
||||
|
||||
paramsA := params
|
||||
@@ -70,7 +68,7 @@ func TestMissingAnswerDuringICERestart(t *testing.T) {
|
||||
|
||||
// offer again, but missed
|
||||
var offerReceived atomic.Bool
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, _offerId uint32, _midToTrackID map[string]string) error {
|
||||
require.Equal(t, webrtc.SignalingStateHaveLocalOffer, transportA.pc.SignalingState())
|
||||
require.Equal(t, transport.NegotiationStateRemote, negotiationState.Load().(transport.NegotiationState))
|
||||
offerReceived.Store(true)
|
||||
@@ -91,10 +89,8 @@ func TestMissingAnswerDuringICERestart(t *testing.T) {
|
||||
|
||||
func TestNegotiationTiming(t *testing.T) {
|
||||
params := TransportParams{
|
||||
ParticipantID: "id",
|
||||
ParticipantIdentity: "identity",
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
}
|
||||
|
||||
paramsA := params
|
||||
@@ -116,9 +112,16 @@ func TestNegotiationTiming(t *testing.T) {
|
||||
require.False(t, transportB.IsEstablished())
|
||||
|
||||
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
|
||||
offer := atomic.Value{}
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
|
||||
offer.Store(&sd)
|
||||
firstOffer := atomic.Value{}
|
||||
firstOfferId := atomic.Uint32{}
|
||||
secondOffer := atomic.Value{}
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
|
||||
if _, ok := firstOffer.Load().(*webrtc.SessionDescription); !ok {
|
||||
firstOffer.Store(&sd)
|
||||
firstOfferId.Store(offerId)
|
||||
} else {
|
||||
secondOffer.Store(&sd)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -161,15 +164,22 @@ func TestNegotiationTiming(t *testing.T) {
|
||||
return state == transport.NegotiationStateRetry
|
||||
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRetry")
|
||||
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
actualOffer, ok := offer.Load().(*webrtc.SessionDescription)
|
||||
require.True(t, ok)
|
||||
require.Eventually(t, func() bool {
|
||||
_, ok := firstOffer.Load().(*webrtc.SessionDescription)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if firstOfferId.Load() == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, 10*time.Second, 10*time.Millisecond, "first offer not received yet")
|
||||
|
||||
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription) error {
|
||||
transportA.HandleRemoteDescription(answer)
|
||||
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
|
||||
transportA.HandleRemoteDescription(answer, answerId)
|
||||
return nil
|
||||
})
|
||||
transportB.HandleRemoteDescription(*actualOffer)
|
||||
transportB.HandleRemoteDescription(*firstOffer.Load().(*webrtc.SessionDescription), firstOfferId.Load())
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return transportA.IsEstablished()
|
||||
@@ -178,11 +188,18 @@ func TestNegotiationTiming(t *testing.T) {
|
||||
return transportB.IsEstablished()
|
||||
}, 10*time.Second, time.Millisecond*10, "transportB is not established")
|
||||
|
||||
// it should still be negotiating again
|
||||
require.Equal(t, transport.NegotiationStateRemote, negotiationState.Load().(transport.NegotiationState))
|
||||
offer2, ok := offer.Load().(*webrtc.SessionDescription)
|
||||
// offerer should send another offer after processing the answer
|
||||
// as there were forced negotiations a couple of time above
|
||||
require.Eventually(t, func() bool {
|
||||
state, ok := negotiationState.Load().(transport.NegotiationState)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return state == transport.NegotiationStateRemote
|
||||
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRemote")
|
||||
_, ok := secondOffer.Load().(*webrtc.SessionDescription)
|
||||
require.True(t, ok)
|
||||
require.False(t, offer2 == actualOffer)
|
||||
|
||||
transportA.Close()
|
||||
transportB.Close()
|
||||
@@ -190,10 +207,8 @@ func TestNegotiationTiming(t *testing.T) {
|
||||
|
||||
func TestFirstOfferMissedDuringICERestart(t *testing.T) {
|
||||
params := TransportParams{
|
||||
ParticipantID: "id",
|
||||
ParticipantIdentity: "identity",
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
}
|
||||
|
||||
paramsA := params
|
||||
@@ -216,7 +231,7 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
|
||||
|
||||
// first offer missed
|
||||
var firstOfferReceived atomic.Bool
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, _offerId uint32, _midToTrackID map[string]string) error {
|
||||
firstOfferReceived.Store(true)
|
||||
return nil
|
||||
})
|
||||
@@ -228,13 +243,13 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
|
||||
// set offer/answer with restart ICE, will negotiate twice,
|
||||
// first one is recover from missed offer
|
||||
// second one is restartICE
|
||||
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription) error {
|
||||
transportA.HandleRemoteDescription(answer)
|
||||
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
|
||||
transportA.HandleRemoteDescription(answer, answerId)
|
||||
return nil
|
||||
})
|
||||
|
||||
var offerCount atomic.Int32
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
|
||||
offerCount.Inc()
|
||||
|
||||
// the second offer is a ice restart offer, so we wait transportB complete the ice gathering
|
||||
@@ -244,7 +259,7 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
|
||||
}, 10*time.Second, time.Millisecond*10)
|
||||
}
|
||||
|
||||
transportB.HandleRemoteDescription(sd)
|
||||
transportB.HandleRemoteDescription(sd, offerId)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -264,10 +279,8 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
|
||||
|
||||
func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
|
||||
params := TransportParams{
|
||||
ParticipantID: "id",
|
||||
ParticipantIdentity: "identity",
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
}
|
||||
|
||||
paramsA := params
|
||||
@@ -290,17 +303,17 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
|
||||
|
||||
// first answer missed
|
||||
var firstAnswerReceived atomic.Bool
|
||||
handlerB.OnAnswerCalls(func(sd webrtc.SessionDescription) error {
|
||||
handlerB.OnAnswerCalls(func(sd webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
|
||||
if firstAnswerReceived.Load() {
|
||||
transportA.HandleRemoteDescription(sd)
|
||||
transportA.HandleRemoteDescription(sd, answerId)
|
||||
} else {
|
||||
// do not send first answer so that remote misses the first answer
|
||||
firstAnswerReceived.Store(true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
|
||||
transportB.HandleRemoteDescription(sd)
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
|
||||
transportB.HandleRemoteDescription(sd, offerId)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -313,7 +326,7 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
|
||||
// first one is recover from missed offer
|
||||
// second one is restartICE
|
||||
var offerCount atomic.Int32
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
|
||||
offerCount.Inc()
|
||||
|
||||
// the second offer is a ice restart offer, so we wait for transportB to complete ICE gathering
|
||||
@@ -323,7 +336,7 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
|
||||
}, 10*time.Second, time.Millisecond*10)
|
||||
}
|
||||
|
||||
transportB.HandleRemoteDescription(sd)
|
||||
transportB.HandleRemoteDescription(sd, offerId)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -343,10 +356,8 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
|
||||
|
||||
func TestNegotiationFailed(t *testing.T) {
|
||||
params := TransportParams{
|
||||
ParticipantID: "id",
|
||||
ParticipantIdentity: "identity",
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
Config: &WebRTCConfig{},
|
||||
IsOfferer: true,
|
||||
}
|
||||
|
||||
paramsA := params
|
||||
@@ -371,7 +382,9 @@ func TestNegotiationFailed(t *testing.T) {
|
||||
connectTransports(t, transportA, transportB, handlerA, handlerB, false, 1, 1)
|
||||
|
||||
// reset OnOffer to force a negotiation failure
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error { return nil })
|
||||
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
|
||||
return nil
|
||||
})
|
||||
var failed atomic.Int32
|
||||
handlerA.OnNegotiationFailedCalls(func() {
|
||||
failed.Inc()
|
||||
@@ -386,10 +399,8 @@ func TestNegotiationFailed(t *testing.T) {
|
||||
|
||||
func TestFilteringCandidates(t *testing.T) {
|
||||
params := TransportParams{
|
||||
ParticipantID: "id",
|
||||
ParticipantIdentity: "identity",
|
||||
Config: &WebRTCConfig{},
|
||||
EnabledCodecs: []*livekit.Codec{
|
||||
Config: &WebRTCConfig{},
|
||||
EnabledPublishCodecs: []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeOpus.String()},
|
||||
{Mime: mime.MimeTypeVP8.String()},
|
||||
{Mime: mime.MimeTypeH264.String()},
|
||||
@@ -527,15 +538,15 @@ func handleICEExchange(t *testing.T, a, b *PCTransport, ah, bh *transportfakes.F
|
||||
func connectTransports(t *testing.T, offerer, answerer *PCTransport, offererHandler, answererHandler *transportfakes.FakeHandler, isICERestart bool, expectedOfferCount int32, expectedAnswerCount int32) {
|
||||
var offerCount atomic.Int32
|
||||
var answerCount atomic.Int32
|
||||
answererHandler.OnAnswerCalls(func(answer webrtc.SessionDescription) error {
|
||||
answererHandler.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
|
||||
answerCount.Inc()
|
||||
offerer.HandleRemoteDescription(answer)
|
||||
offerer.HandleRemoteDescription(answer, answerId)
|
||||
return nil
|
||||
})
|
||||
|
||||
offererHandler.OnOfferCalls(func(offer webrtc.SessionDescription) error {
|
||||
offererHandler.OnOfferCalls(func(offer webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
|
||||
offerCount.Inc()
|
||||
answerer.HandleRemoteDescription(offer)
|
||||
answerer.HandleRemoteDescription(offer, offerId)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -606,7 +617,7 @@ func TestConfigureAudioTransceiver(t *testing.T) {
|
||||
tr, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
|
||||
require.NoError(t, err)
|
||||
|
||||
configureAudioTransceiver(tr, testcase.stereo, testcase.nack)
|
||||
configureSenderAudio(tr, testcase.stereo, testcase.nack)
|
||||
codecs := tr.Sender().GetParameters().Codecs
|
||||
for _, codec := range codecs {
|
||||
if mime.IsMimeTypeStringOpus(codec.MimeType) {
|
||||
@@ -624,3 +635,145 @@ func TestConfigureAudioTransceiver(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// In single-PC mode the publisher PC carries both publish and subscribe
|
||||
// directions. If the MediaEngine were built only from the publish codec list,
|
||||
// the SDP offer would not advertise some codecs in the m-section even though
|
||||
// the subscribe direction is supposed to support it. This regression-tests
|
||||
// the union behavior in newPeerConnection: build the MediaEngine from publish +
|
||||
// subscribe codec lists.
|
||||
func TestSinglePCMediaEngineUnionsCodecs(t *testing.T) {
|
||||
videoMSectionCodecs := func(transport *PCTransport) []string {
|
||||
_, err := transport.pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
|
||||
require.NoError(t, err)
|
||||
offer, err := transport.pc.CreateOffer(nil)
|
||||
require.NoError(t, err)
|
||||
parsed, err := offer.Unmarshal()
|
||||
require.NoError(t, err)
|
||||
var rtpmaps []string
|
||||
for _, m := range parsed.MediaDescriptions {
|
||||
if m.MediaName.Media != "video" {
|
||||
continue
|
||||
}
|
||||
for _, a := range m.Attributes {
|
||||
if a.Key == "rtpmap" {
|
||||
rtpmaps = append(rtpmaps, a.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rtpmaps
|
||||
}
|
||||
|
||||
sdpHasH264 := func(rtpmaps []string) bool {
|
||||
for _, r := range rtpmaps {
|
||||
if strings.Contains(r, "H264/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
publishOnly := []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeOpus.String()},
|
||||
{Mime: mime.MimeTypeVP8.String()},
|
||||
}
|
||||
subscribeOnly := []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeOpus.String()},
|
||||
{Mime: mime.MimeTypeVP8.String()},
|
||||
{Mime: mime.MimeTypeH264.String()},
|
||||
}
|
||||
|
||||
// Control: only publish codecs set (dual-PC publisher PC). H.264 absent.
|
||||
dualPC, err := NewPCTransport(TransportParams{
|
||||
Config: &WebRTCConfig{},
|
||||
EnabledPublishCodecs: publishOnly,
|
||||
Handler: &transportfakes.FakeHandler{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, sdpHasH264(videoMSectionCodecs(dualPC)),
|
||||
"dual-PC publisher must not advertise H.264 when it's stripped from the publish list")
|
||||
|
||||
// Single-PC publisher PC: both lists set. H.264 must appear.
|
||||
singlePC, err := NewPCTransport(TransportParams{
|
||||
Config: &WebRTCConfig{},
|
||||
EnabledPublishCodecs: publishOnly,
|
||||
EnabledSubscribeCodecs: subscribeOnly,
|
||||
IsSendSide: true,
|
||||
Handler: &transportfakes.FakeHandler{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, sdpHasH264(videoMSectionCodecs(singlePC)),
|
||||
"single-PC publisher must advertise H.264 from the subscribe list even when it's stripped from the publish list")
|
||||
}
|
||||
|
||||
// Regression test for restrictReceiverCodecsToPublishList: subscribe-only
|
||||
// codecs (e.g., H.264) registered for subscriptions must not leak into the
|
||||
// recv-side m-section of an answer, or the peer could publish them.
|
||||
func TestSinglePCAnswerStripsSubscribeOnlyCodecsFromRecvSide(t *testing.T) {
|
||||
publishCodecs := []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeOpus.String()},
|
||||
{Mime: mime.MimeTypeVP8.String()},
|
||||
}
|
||||
subscribeCodecs := []*livekit.Codec{
|
||||
{Mime: mime.MimeTypeOpus.String()},
|
||||
{Mime: mime.MimeTypeVP8.String()},
|
||||
{Mime: mime.MimeTypeH264.String()},
|
||||
}
|
||||
|
||||
handler := &transportfakes.FakeHandler{}
|
||||
server, err := NewPCTransport(TransportParams{
|
||||
Config: &WebRTCConfig{},
|
||||
EnabledPublishCodecs: publishCodecs,
|
||||
EnabledSubscribeCodecs: subscribeCodecs,
|
||||
IsSendSide: true,
|
||||
Handler: handler,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer server.Close()
|
||||
|
||||
var clientME webrtc.MediaEngine
|
||||
require.NoError(t, registerCodecs(&clientME, subscribeCodecs, RTCPFeedbackConfig{}, false))
|
||||
client, err := webrtc.NewAPI(webrtc.WithMediaEngine(&clientME)).NewPeerConnection(webrtc.Configuration{})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
offer, err := client.CreateOffer(nil)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, offer.SDP, "H264/", "offer must advertise H.264")
|
||||
require.NoError(t, client.SetLocalDescription(offer))
|
||||
|
||||
var answer atomic.Pointer[webrtc.SessionDescription]
|
||||
handler.OnAnswerCalls(func(sd webrtc.SessionDescription, _ uint32, _ map[string]string) error {
|
||||
answer.Store(&sd)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, server.HandleRemoteDescription(*client.LocalDescription(), 1))
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return answer.Load() != nil
|
||||
}, 5*time.Second, 10*time.Millisecond, "server did not produce answer")
|
||||
|
||||
parsed, err := answer.Load().Unmarshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
var videoSection *sdp.MediaDescription
|
||||
for _, m := range parsed.MediaDescriptions {
|
||||
if m.MediaName.Media == "video" {
|
||||
videoSection = m
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, videoSection, "answer missing video m-section")
|
||||
|
||||
for _, a := range videoSection.Attributes {
|
||||
if a.Key != "rtpmap" {
|
||||
continue
|
||||
}
|
||||
require.NotContains(t, a.Value, "H264/",
|
||||
"answer must not advertise H.264 in recv-side m-section: %s", a.Value)
|
||||
}
|
||||
}
|
||||
|
||||
+398
-179
@@ -39,8 +39,8 @@ import (
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/datachannel"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/interceptor"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/pacer"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -71,43 +71,33 @@ func (h TransportManagerTransportHandler) OnFailed(isShortLived bool, iceConnect
|
||||
|
||||
// -------------------------------
|
||||
|
||||
type TransportManagerPublisherTransportHandler struct {
|
||||
TransportManagerTransportHandler
|
||||
}
|
||||
|
||||
func (h TransportManagerPublisherTransportHandler) OnAnswer(sd webrtc.SessionDescription) error {
|
||||
h.t.lastPublisherAnswer.Store(sd)
|
||||
return h.Handler.OnAnswer(sd)
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
|
||||
type TransportManagerParams struct {
|
||||
Identity livekit.ParticipantIdentity
|
||||
SID livekit.ParticipantID
|
||||
SubscriberAsPrimary bool
|
||||
Config *WebRTCConfig
|
||||
Twcc *twcc.Responder
|
||||
ProtocolVersion types.ProtocolVersion
|
||||
CongestionControlConfig config.CongestionControlConfig
|
||||
EnabledSubscribeCodecs []*livekit.Codec
|
||||
EnabledPublishCodecs []*livekit.Codec
|
||||
SimTracks map[uint32]SimulcastTrackInfo
|
||||
ClientInfo ClientInfo
|
||||
Migration bool
|
||||
AllowTCPFallback bool
|
||||
TCPFallbackRTTThreshold int
|
||||
AllowUDPUnstableFallback bool
|
||||
TURNSEnabled bool
|
||||
AllowPlayoutDelay bool
|
||||
DataChannelMaxBufferedAmount uint64
|
||||
DatachannelSlowThreshold int
|
||||
Logger logger.Logger
|
||||
PublisherHandler transport.Handler
|
||||
SubscriberHandler transport.Handler
|
||||
DataChannelStats *telemetry.BytesTrackStats
|
||||
UseOneShotSignallingMode bool
|
||||
FireOnTrackBySdp bool
|
||||
SubscriberAsPrimary bool
|
||||
UseSinglePeerConnection bool
|
||||
Config *WebRTCConfig
|
||||
Twcc *twcc.Responder
|
||||
ProtocolVersion types.ProtocolVersion
|
||||
CongestionControlConfig config.CongestionControlConfig
|
||||
EnabledSubscribeCodecs []*livekit.Codec
|
||||
EnabledPublishCodecs []*livekit.Codec
|
||||
SimTracks map[uint32]interceptor.SimulcastTrackInfo
|
||||
ClientInfo ClientInfo
|
||||
Migration bool
|
||||
AllowTCPFallback bool
|
||||
TCPFallbackRTTThreshold int
|
||||
AllowUDPUnstableFallback bool
|
||||
TURNSEnabled bool
|
||||
AllowPlayoutDelay bool
|
||||
DataChannelMaxBufferedAmount uint64
|
||||
DatachannelSlowThreshold int
|
||||
DatachannelLossyTargetLatency time.Duration
|
||||
Logger logger.Logger
|
||||
PublisherHandler transport.Handler
|
||||
SubscriberHandler transport.Handler
|
||||
DataChannelStats *BytesTrackStats
|
||||
UseOneShotSignallingMode bool
|
||||
FireOnTrackBySdp bool
|
||||
EnableDataTracks bool
|
||||
}
|
||||
|
||||
type TransportManager struct {
|
||||
@@ -124,9 +114,8 @@ type TransportManager struct {
|
||||
signalSourceValid atomic.Bool
|
||||
|
||||
pendingOfferPublisher *webrtc.SessionDescription
|
||||
pendingOfferIdPublisher uint32
|
||||
pendingDataChannelsPublisher []*livekit.DataChannelInfo
|
||||
lastPublisherAnswer atomic.Value
|
||||
lastPublisherOffer atomic.Value
|
||||
iceConfig *livekit.ICEConfig
|
||||
|
||||
mediaLossProxy *MediaLossProxy
|
||||
@@ -135,7 +124,8 @@ type TransportManager struct {
|
||||
|
||||
onICEConfigChanged func(iceConfig *livekit.ICEConfig)
|
||||
|
||||
droppedBySlowReaderCount atomic.Uint32
|
||||
dataChannelSendErrorDroppedBySlowReaderCount atomic.Uint32
|
||||
dataChannelSendErrorCount atomic.Uint32
|
||||
}
|
||||
|
||||
func NewTransportManager(params TransportManagerParams) (*TransportManager, error) {
|
||||
@@ -150,53 +140,67 @@ func NewTransportManager(params TransportManagerParams) (*TransportManager, erro
|
||||
t.mediaLossProxy.OnMediaLossUpdate(t.onMediaLossUpdate)
|
||||
|
||||
lgr := LoggerWithPCTarget(params.Logger, livekit.SignalTarget_PUBLISHER)
|
||||
// In single-PC mode the one PC carries both directions, so it needs both
|
||||
// codec lists registered on its MediaEngine. In dual-PC mode the publisher
|
||||
// PC is recvonly, so subscribe codecs are left nil.
|
||||
var publisherSubscribeCodecs []*livekit.Codec
|
||||
if params.UseSinglePeerConnection || params.UseOneShotSignallingMode {
|
||||
publisherSubscribeCodecs = params.EnabledSubscribeCodecs
|
||||
}
|
||||
publisher, err := NewPCTransport(TransportParams{
|
||||
ParticipantID: params.SID,
|
||||
ParticipantIdentity: params.Identity,
|
||||
ProtocolVersion: params.ProtocolVersion,
|
||||
Config: params.Config,
|
||||
Twcc: params.Twcc,
|
||||
DirectionConfig: params.Config.Publisher,
|
||||
CongestionControlConfig: params.CongestionControlConfig,
|
||||
EnabledCodecs: params.EnabledPublishCodecs,
|
||||
Logger: lgr,
|
||||
SimTracks: params.SimTracks,
|
||||
ClientInfo: params.ClientInfo,
|
||||
Transport: livekit.SignalTarget_PUBLISHER,
|
||||
Handler: TransportManagerPublisherTransportHandler{TransportManagerTransportHandler{params.PublisherHandler, t, lgr}},
|
||||
UseOneShotSignallingMode: params.UseOneShotSignallingMode,
|
||||
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
|
||||
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
|
||||
FireOnTrackBySdp: params.FireOnTrackBySdp,
|
||||
ProtocolVersion: params.ProtocolVersion,
|
||||
Config: params.Config,
|
||||
Twcc: params.Twcc,
|
||||
DirectionConfig: params.Config.Publisher,
|
||||
CongestionControlConfig: params.CongestionControlConfig,
|
||||
EnabledPublishCodecs: params.EnabledPublishCodecs,
|
||||
EnabledSubscribeCodecs: publisherSubscribeCodecs,
|
||||
Logger: lgr,
|
||||
SimTracks: params.SimTracks,
|
||||
ClientInfo: params.ClientInfo,
|
||||
IsSendSide: params.UseOneShotSignallingMode || params.UseSinglePeerConnection,
|
||||
AllowPlayoutDelay: params.AllowPlayoutDelay,
|
||||
Transport: livekit.SignalTarget_PUBLISHER,
|
||||
Handler: TransportManagerTransportHandler{params.PublisherHandler, t, lgr},
|
||||
UseOneShotSignallingMode: params.UseOneShotSignallingMode,
|
||||
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
|
||||
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
|
||||
DatachannelLossyTargetLatency: params.DatachannelLossyTargetLatency,
|
||||
FireOnTrackBySdp: params.FireOnTrackBySdp,
|
||||
EnableDataTracks: params.EnableDataTracks,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.publisher = publisher
|
||||
|
||||
lgr = LoggerWithPCTarget(params.Logger, livekit.SignalTarget_SUBSCRIBER)
|
||||
subscriber, err := NewPCTransport(TransportParams{
|
||||
ParticipantID: params.SID,
|
||||
ParticipantIdentity: params.Identity,
|
||||
ProtocolVersion: params.ProtocolVersion,
|
||||
Config: params.Config,
|
||||
DirectionConfig: params.Config.Subscriber,
|
||||
CongestionControlConfig: params.CongestionControlConfig,
|
||||
EnabledCodecs: params.EnabledSubscribeCodecs,
|
||||
Logger: lgr,
|
||||
ClientInfo: params.ClientInfo,
|
||||
IsOfferer: true,
|
||||
IsSendSide: true,
|
||||
AllowPlayoutDelay: params.AllowPlayoutDelay,
|
||||
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
|
||||
Transport: livekit.SignalTarget_SUBSCRIBER,
|
||||
Handler: TransportManagerTransportHandler{params.SubscriberHandler, t, lgr},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !t.params.UseOneShotSignallingMode && !t.params.UseSinglePeerConnection {
|
||||
lgr := LoggerWithPCTarget(params.Logger, livekit.SignalTarget_SUBSCRIBER)
|
||||
subscriber, err := NewPCTransport(TransportParams{
|
||||
ProtocolVersion: params.ProtocolVersion,
|
||||
Config: params.Config,
|
||||
DirectionConfig: params.Config.Subscriber,
|
||||
CongestionControlConfig: params.CongestionControlConfig,
|
||||
EnabledSubscribeCodecs: params.EnabledSubscribeCodecs,
|
||||
Logger: lgr,
|
||||
ClientInfo: params.ClientInfo,
|
||||
IsOfferer: true,
|
||||
IsSendSide: true,
|
||||
AllowPlayoutDelay: params.AllowPlayoutDelay,
|
||||
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
|
||||
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
|
||||
DatachannelLossyTargetLatency: params.DatachannelLossyTargetLatency,
|
||||
Transport: livekit.SignalTarget_SUBSCRIBER,
|
||||
Handler: TransportManagerTransportHandler{params.SubscriberHandler, t, lgr},
|
||||
FireOnTrackBySdp: params.FireOnTrackBySdp,
|
||||
EnableDataTracks: params.EnableDataTracks,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.subscriber = subscriber
|
||||
}
|
||||
t.subscriber = subscriber
|
||||
if !t.params.Migration {
|
||||
if !t.params.Migration && t.params.SubscriberAsPrimary {
|
||||
if err := t.createDataChannelsForSubscriber(nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -207,8 +211,12 @@ func NewTransportManager(params TransportManagerParams) (*TransportManager, erro
|
||||
}
|
||||
|
||||
func (t *TransportManager) Close() {
|
||||
t.publisher.Close()
|
||||
t.subscriber.Close()
|
||||
if t.publisher != nil {
|
||||
t.publisher.Close()
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) SubscriberClose() {
|
||||
@@ -231,6 +239,10 @@ func (t *TransportManager) GetPublisherMid(rtpReceiver *webrtc.RTPReceiver) stri
|
||||
return t.publisher.GetMid(rtpReceiver)
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetPublisherRTPTransceiver(mid string) *webrtc.RTPTransceiver {
|
||||
return t.publisher.GetRTPTransceiver(mid)
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetPublisherRTPReceiver(mid string) *webrtc.RTPReceiver {
|
||||
return t.publisher.GetRTPReceiver(mid)
|
||||
}
|
||||
@@ -240,37 +252,49 @@ func (t *TransportManager) WritePublisherRTCP(pkts []rtcp.Packet) error {
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetSubscriberRTT() (float64, bool) {
|
||||
return t.subscriber.GetRTT()
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.GetRTT()
|
||||
} else {
|
||||
return t.subscriber.GetRTT()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) HasSubscriberEverConnected() bool {
|
||||
return t.subscriber.HasEverConnected()
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.HasEverConnected()
|
||||
} else {
|
||||
return t.subscriber.HasEverConnected()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) AddTrackLocal(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
return t.publisher.AddTrack(trackLocal, params)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.AddTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
} else {
|
||||
return t.subscriber.AddTrack(trackLocal, params)
|
||||
return t.subscriber.AddTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) AddTransceiverFromTrackLocal(
|
||||
trackLocal webrtc.TrackLocal,
|
||||
params types.AddTrackParams,
|
||||
enabledCodecs []*livekit.Codec,
|
||||
rtcpFeedbackConfig RTCPFeedbackConfig,
|
||||
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
return t.publisher.AddTransceiverFromTrack(trackLocal, params)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
} else {
|
||||
return t.subscriber.AddTransceiverFromTrack(trackLocal, params)
|
||||
return t.subscriber.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) RemoveTrackLocal(sender *webrtc.RTPSender) error {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.RemoveTrack(sender)
|
||||
} else {
|
||||
return t.subscriber.RemoveTrack(sender)
|
||||
@@ -278,7 +302,7 @@ func (t *TransportManager) RemoveTrackLocal(sender *webrtc.RTPSender) error {
|
||||
}
|
||||
|
||||
func (t *TransportManager) WriteSubscriberRTCP(pkts []rtcp.Packet) error {
|
||||
if t.params.UseOneShotSignallingMode {
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.WriteRTCP(pkts)
|
||||
} else {
|
||||
return t.subscriber.WriteRTCP(pkts)
|
||||
@@ -286,36 +310,85 @@ func (t *TransportManager) WriteSubscriberRTCP(pkts []rtcp.Packet) error {
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetSubscriberPacer() pacer.Pacer {
|
||||
return t.subscriber.GetPacer()
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
return t.publisher.GetPacer()
|
||||
} else {
|
||||
return t.subscriber.GetPacer()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) AddSubscribedTrack(subTrack types.SubscribedTrack) {
|
||||
t.subscriber.AddTrackToStreamAllocator(subTrack)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.AddTrackToStreamAllocator(subTrack)
|
||||
} else {
|
||||
t.subscriber.AddTrackToStreamAllocator(subTrack)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) RemoveSubscribedTrack(subTrack types.SubscribedTrack) {
|
||||
t.subscriber.RemoveTrackFromStreamAllocator(subTrack)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.RemoveTrackFromStreamAllocator(subTrack)
|
||||
} else {
|
||||
t.subscriber.RemoveTrackFromStreamAllocator(subTrack)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) SendDataPacket(kind livekit.DataPacket_Kind, encoded []byte) error {
|
||||
func (t *TransportManager) SendDataMessage(kind livekit.DataPacket_Kind, data []byte) error {
|
||||
// downstream data is sent via primary peer connection
|
||||
err := t.getTransport(true).SendDataPacket(kind, encoded)
|
||||
return t.handleSendDataResult(t.getTransport(true).SendDataMessage(kind, data), kind.String(), len(data))
|
||||
}
|
||||
|
||||
func (t *TransportManager) SendDataMessageUnlabeled(data []byte, useRaw bool, sender livekit.ParticipantIdentity) error {
|
||||
// downstream data is sent via primary peer connection
|
||||
return t.handleSendDataResult(
|
||||
t.getTransport(true).SendDataMessageUnlabeled(data, useRaw, sender),
|
||||
"unlabeled",
|
||||
len(data),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *TransportManager) handleSendDataResult(err error, kind string, size int) error {
|
||||
if err != nil {
|
||||
if !utils.ErrorIsOneOf(err, io.ErrClosedPipe, sctp.ErrStreamClosed, ErrTransportFailure, ErrDataChannelBufferFull, context.DeadlineExceeded) {
|
||||
if !utils.ErrorIsOneOf(
|
||||
err,
|
||||
io.ErrClosedPipe,
|
||||
sctp.ErrStreamClosed,
|
||||
ErrTransportFailure,
|
||||
ErrDataChannelUnavailable,
|
||||
context.DeadlineExceeded,
|
||||
datachannel.ErrDataDroppedByHighBufferedAmount,
|
||||
) {
|
||||
if errors.Is(err, datachannel.ErrDataDroppedBySlowReader) {
|
||||
droppedBySlowReaderCount := t.droppedBySlowReaderCount.Inc()
|
||||
droppedBySlowReaderCount := t.dataChannelSendErrorDroppedBySlowReaderCount.Inc()
|
||||
if (droppedBySlowReaderCount-1)%100 == 0 {
|
||||
t.params.Logger.Infow("drop data packet by slow reader", "error", err, "kind", kind, "count", droppedBySlowReaderCount)
|
||||
t.params.Logger.Infow(
|
||||
"drop data message by slow reader",
|
||||
"error", err,
|
||||
"kind", kind,
|
||||
"count", droppedBySlowReaderCount,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
t.params.Logger.Warnw("send data packet error", err)
|
||||
count := t.dataChannelSendErrorCount.Inc()
|
||||
if (count-1)%100 == 0 {
|
||||
t.params.Logger.Infow(
|
||||
"send data message error",
|
||||
"error", err,
|
||||
"kind", kind,
|
||||
"count", count,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if utils.ErrorIsOneOf(err, sctp.ErrStreamClosed, io.ErrClosedPipe) {
|
||||
t.params.SubscriberHandler.OnDataSendError(err)
|
||||
if t.params.SubscriberAsPrimary {
|
||||
t.params.SubscriberHandler.OnDataSendError(err)
|
||||
} else {
|
||||
t.params.PublisherHandler.OnDataSendError(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.params.DataChannelStats.AddBytes(uint64(len(encoded)), true)
|
||||
t.params.DataChannelStats.AddBytes(uint64(size), true)
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -323,8 +396,8 @@ func (t *TransportManager) SendDataPacket(kind livekit.DataPacket_Kind, encoded
|
||||
|
||||
func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels []*livekit.DataChannelInfo) error {
|
||||
var (
|
||||
reliableID, lossyID uint16
|
||||
reliableIDPtr, lossyIDPtr *uint16
|
||||
reliableID, lossyID, dataTrackID uint16
|
||||
reliableIDPtr, lossyIDPtr, dataTrackIDPtr *uint16
|
||||
)
|
||||
|
||||
//
|
||||
@@ -335,13 +408,17 @@ func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels [
|
||||
// For new version migration clients, we create data channels with new ID and negotiate with client
|
||||
//
|
||||
for _, dc := range pendingDataChannels {
|
||||
if dc.Label == ReliableDataChannel {
|
||||
// pion use step 2 for auto generated ID, so we need to add 4 to avoid conflict
|
||||
reliableID = uint16(dc.Id) + 4
|
||||
switch dc.Label {
|
||||
case ReliableDataChannel:
|
||||
// pion use step 2 for auto generated ID, so we need to add 6 to avoid conflict
|
||||
reliableID = uint16(dc.Id) + 6
|
||||
reliableIDPtr = &reliableID
|
||||
} else if dc.Label == LossyDataChannel {
|
||||
lossyID = uint16(dc.Id) + 4
|
||||
case LossyDataChannel:
|
||||
lossyID = uint16(dc.Id) + 6
|
||||
lossyIDPtr = &lossyID
|
||||
case DataTrackDataChannel:
|
||||
dataTrackID = uint16(dc.Id) + 6
|
||||
dataTrackIDPtr = &dataTrackID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +432,7 @@ func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels [
|
||||
return err
|
||||
}
|
||||
|
||||
ordered = false
|
||||
retransmits := uint16(0)
|
||||
negotiated = t.params.Migration && lossyIDPtr == nil
|
||||
if err := t.subscriber.CreateDataChannel(LossyDataChannel, &webrtc.DataChannelInit{
|
||||
@@ -365,26 +443,28 @@ func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels [
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
negotiated = t.params.Migration && dataTrackIDPtr == nil
|
||||
if err := t.subscriber.CreateDataChannel(DataTrackDataChannel, &webrtc.DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
MaxRetransmits: &retransmits,
|
||||
ID: dataTrackIDPtr,
|
||||
Negotiated: &negotiated,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetUnmatchMediaForOffer(offer webrtc.SessionDescription, mediaType string) (parsed *sdp.SessionDescription, unmatched []*sdp.MediaDescription, err error) {
|
||||
// prefer codec from offer for clients that don't support setCodecPreferences
|
||||
parsed, err = offer.Unmarshal()
|
||||
if err != nil {
|
||||
t.params.Logger.Errorw("failed to parse offer for codec preference", err)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetUnmatchMediaForOffer(parsedOffer *sdp.SessionDescription, mediaType string) (unmatched []*sdp.MediaDescription, err error) {
|
||||
var lastMatchedMid string
|
||||
lastAnswer := t.lastPublisherAnswer.Load()
|
||||
if lastAnswer != nil {
|
||||
answer := lastAnswer.(webrtc.SessionDescription)
|
||||
parsedAnswer, err1 := answer.Unmarshal()
|
||||
if lastAnswer := t.publisher.CurrentLocalDescription(); lastAnswer != nil {
|
||||
parsedAnswer, err1 := lastAnswer.Unmarshal()
|
||||
if err1 != nil {
|
||||
// should not happen
|
||||
t.params.Logger.Errorw("failed to parse last answer", err)
|
||||
return
|
||||
t.params.Logger.Errorw("failed to parse last answer", err1)
|
||||
return unmatched, err1
|
||||
}
|
||||
|
||||
for i := len(parsedAnswer.MediaDescriptions) - 1; i >= 0; i-- {
|
||||
@@ -396,8 +476,8 @@ func (t *TransportManager) GetUnmatchMediaForOffer(offer webrtc.SessionDescripti
|
||||
}
|
||||
}
|
||||
|
||||
for i := len(parsed.MediaDescriptions) - 1; i >= 0; i-- {
|
||||
media := parsed.MediaDescriptions[i]
|
||||
for i := len(parsedOffer.MediaDescriptions) - 1; i >= 0; i-- {
|
||||
media := parsedOffer.MediaDescriptions[i]
|
||||
if media.MediaName.Media == mediaType {
|
||||
mid, _ := media.Attribute(sdp.AttrKeyMID)
|
||||
if mid == lastMatchedMid {
|
||||
@@ -410,56 +490,76 @@ func (t *TransportManager) GetUnmatchMediaForOffer(offer webrtc.SessionDescripti
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TransportManager) LastPublisherOffer() webrtc.SessionDescription {
|
||||
if sd := t.lastPublisherOffer.Load(); sd != nil {
|
||||
return sd.(webrtc.SessionDescription)
|
||||
}
|
||||
return webrtc.SessionDescription{}
|
||||
func (t *TransportManager) LastPublisherOffer() *webrtc.SessionDescription {
|
||||
return t.publisher.CurrentRemoteDescription()
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleOffer(offer webrtc.SessionDescription, shouldPend bool) error {
|
||||
func (t *TransportManager) LastPublisherOfferPending() *webrtc.SessionDescription {
|
||||
return t.publisher.PendingRemoteDescription()
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleOffer(offer webrtc.SessionDescription, offerId uint32, shouldPend bool) error {
|
||||
t.lock.Lock()
|
||||
if shouldPend {
|
||||
t.pendingOfferPublisher = &offer
|
||||
t.pendingOfferIdPublisher = offerId
|
||||
t.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
t.lock.Unlock()
|
||||
t.lastPublisherOffer.Store(offer)
|
||||
|
||||
return t.publisher.HandleRemoteDescription(offer)
|
||||
return t.publisher.HandleRemoteDescription(offer, offerId)
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetAnswer() (webrtc.SessionDescription, error) {
|
||||
answer, err := t.publisher.GetAnswer()
|
||||
if err == nil {
|
||||
t.lastPublisherAnswer.Store(answer)
|
||||
}
|
||||
return answer, err
|
||||
func (t *TransportManager) GetAnswer() (webrtc.SessionDescription, uint32, error) {
|
||||
return t.publisher.GetAnswer()
|
||||
}
|
||||
|
||||
func (t *TransportManager) GetPublisherICESessionUfrag() (string, error) {
|
||||
return t.publisher.GetICESessionUfrag()
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleICETrickleSDPFragment(sdpFragment string) error {
|
||||
return t.publisher.HandleICETrickleSDPFragment(sdpFragment)
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleICERestartSDPFragment(sdpFragment string) (string, error) {
|
||||
return t.publisher.HandleICERestartSDPFragment(sdpFragment)
|
||||
}
|
||||
|
||||
func (t *TransportManager) ProcessPendingPublisherOffer() {
|
||||
t.lock.Lock()
|
||||
pendingOffer := t.pendingOfferPublisher
|
||||
t.pendingOfferPublisher = nil
|
||||
|
||||
pendingOfferId := t.pendingOfferIdPublisher
|
||||
t.pendingOfferIdPublisher = 0
|
||||
t.lock.Unlock()
|
||||
|
||||
if pendingOffer != nil {
|
||||
t.HandleOffer(*pendingOffer, false)
|
||||
t.HandleOffer(*pendingOffer, pendingOfferId, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleAnswer(answer webrtc.SessionDescription) {
|
||||
t.subscriber.HandleRemoteDescription(answer)
|
||||
func (t *TransportManager) HandleAnswer(answer webrtc.SessionDescription, answerId uint32) {
|
||||
t.subscriber.HandleRemoteDescription(answer, answerId)
|
||||
}
|
||||
|
||||
// AddICECandidate adds candidates for remote peer
|
||||
func (t *TransportManager) AddICECandidate(candidate webrtc.ICECandidateInit, target livekit.SignalTarget) {
|
||||
switch target {
|
||||
case livekit.SignalTarget_PUBLISHER:
|
||||
t.publisher.AddICECandidate(candidate)
|
||||
if t.publisher != nil {
|
||||
t.publisher.AddICECandidate(candidate)
|
||||
} else {
|
||||
t.params.Logger.Warnw("ice candidate for publisher, but no peer connection", nil, "candidate", candidate)
|
||||
}
|
||||
case livekit.SignalTarget_SUBSCRIBER:
|
||||
t.subscriber.AddICECandidate(candidate)
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.AddICECandidate(candidate)
|
||||
} else {
|
||||
t.params.Logger.Warnw("ice candidate for subscriber, but no peer connection", nil, "candidate", candidate)
|
||||
}
|
||||
default:
|
||||
err := errors.New("unknown signal target")
|
||||
t.params.Logger.Errorw("ice candidate for unknown signal target", err, "target", target)
|
||||
@@ -467,7 +567,11 @@ func (t *TransportManager) AddICECandidate(candidate webrtc.ICECandidateInit, ta
|
||||
}
|
||||
|
||||
func (t *TransportManager) NegotiateSubscriber(force bool) {
|
||||
t.subscriber.Negotiate(force)
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.Negotiate(force)
|
||||
} else {
|
||||
t.publisher.Negotiate(force)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason) {
|
||||
@@ -478,12 +582,16 @@ func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason)
|
||||
)
|
||||
switch reason {
|
||||
case livekit.ReconnectReason_RR_PUBLISHER_FAILED:
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.publisher.IsShortConnection(time.Now())
|
||||
if t.publisher != nil {
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.publisher.IsShortConnection(time.Now())
|
||||
}
|
||||
|
||||
case livekit.ReconnectReason_RR_SUBSCRIBER_FAILED:
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.subscriber.IsShortConnection(time.Now())
|
||||
if t.subscriber != nil {
|
||||
resetShortConnection = true
|
||||
isShort, duration = t.subscriber.IsShortConnection(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
if isShort {
|
||||
@@ -495,15 +603,23 @@ func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason)
|
||||
}
|
||||
|
||||
if resetShortConnection {
|
||||
t.publisher.ResetShortConnOnICERestart()
|
||||
t.subscriber.ResetShortConnOnICERestart()
|
||||
if t.publisher != nil {
|
||||
t.publisher.ResetShortConnOnICERestart()
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.ResetShortConnOnICERestart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) ICERestart(iceConfig *livekit.ICEConfig) error {
|
||||
t.SetICEConfig(iceConfig)
|
||||
|
||||
return t.subscriber.ICERestart()
|
||||
if t.subscriber != nil {
|
||||
return t.subscriber.ICERestart()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TransportManager) OnICEConfigChanged(f func(iceConfig *livekit.ICEConfig)) {
|
||||
@@ -555,8 +671,12 @@ func (t *TransportManager) configureICE(iceConfig *livekit.ICEConfig, reset bool
|
||||
t.mediaLossProxy.OnMediaLossUpdate(nil)
|
||||
}
|
||||
|
||||
t.publisher.SetPreferTCP(iceConfig.PreferencePublisher == livekit.ICECandidateType_ICT_TCP)
|
||||
t.subscriber.SetPreferTCP(iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TCP)
|
||||
if t.publisher != nil {
|
||||
t.publisher.SetPreferTCP(iceConfig.PreferencePublisher == livekit.ICECandidateType_ICT_TCP)
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.SetPreferTCP(iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TCP)
|
||||
}
|
||||
|
||||
if onICEConfigChanged != nil {
|
||||
onICEConfigChanged(iceConfig)
|
||||
@@ -570,6 +690,10 @@ func (t *TransportManager) SubscriberAsPrimary() bool {
|
||||
func (t *TransportManager) GetICEConnectionInfo() []*types.ICEConnectionInfo {
|
||||
infos := make([]*types.ICEConnectionInfo, 0, 2)
|
||||
for _, pc := range []*PCTransport{t.publisher, t.subscriber} {
|
||||
if pc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := pc.GetICEConnectionInfo()
|
||||
if info.HasCandidates() {
|
||||
infos = append(infos, info)
|
||||
@@ -578,13 +702,43 @@ func (t *TransportManager) GetICEConnectionInfo() []*types.ICEConnectionInfo {
|
||||
return infos
|
||||
}
|
||||
|
||||
func (t *TransportManager) getTransport(isPrimary bool) *PCTransport {
|
||||
pcTransport := t.publisher
|
||||
if (isPrimary && t.params.SubscriberAsPrimary) || (!isPrimary && !t.params.SubscriberAsPrimary) {
|
||||
pcTransport = t.subscriber
|
||||
}
|
||||
func (t *TransportManager) GetDataTrackTransport() types.DataTrackTransport {
|
||||
return t.getTransport(true)
|
||||
}
|
||||
|
||||
return pcTransport
|
||||
func (t *TransportManager) getTransport(isPrimary bool) *PCTransport {
|
||||
switch {
|
||||
case t.publisher == nil:
|
||||
return t.subscriber
|
||||
|
||||
case t.subscriber == nil:
|
||||
return t.publisher
|
||||
|
||||
default:
|
||||
pcTransport := t.publisher
|
||||
if (isPrimary && t.params.SubscriberAsPrimary) || (!isPrimary && !t.params.SubscriberAsPrimary) {
|
||||
pcTransport = t.subscriber
|
||||
}
|
||||
|
||||
return pcTransport
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) getLowestPriorityConnectionType() types.ICEConnectionType {
|
||||
switch {
|
||||
case t.publisher == nil:
|
||||
return t.subscriber.GetICEConnectionType()
|
||||
|
||||
case t.subscriber == nil:
|
||||
return t.publisher.GetICEConnectionType()
|
||||
|
||||
default:
|
||||
ctype := t.publisher.GetICEConnectionType()
|
||||
if stype := t.subscriber.GetICEConnectionType(); stype > ctype {
|
||||
ctype = stype
|
||||
}
|
||||
return ctype
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
|
||||
@@ -614,6 +768,8 @@ func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
|
||||
return
|
||||
}
|
||||
|
||||
lowestPriorityConnectionType := t.getLowestPriorityConnectionType()
|
||||
|
||||
//
|
||||
// Checking only `PreferenceSubscriber` field although any connection failure (PUBLISHER OR SUBSCRIBER) will
|
||||
// flow through here.
|
||||
@@ -621,13 +777,36 @@ func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
|
||||
// As both transports are switched to the same type on any failure, checking just subscriber should be fine.
|
||||
//
|
||||
getNext := func(ic *livekit.ICEConfig) livekit.ICECandidateType {
|
||||
if ic.PreferenceSubscriber == livekit.ICECandidateType_ICT_NONE && t.params.ClientInfo.SupportsICETCP() && t.canUseICETCP() {
|
||||
return livekit.ICECandidateType_ICT_TCP
|
||||
} else if ic.PreferenceSubscriber != livekit.ICECandidateType_ICT_TLS && t.params.TURNSEnabled {
|
||||
return livekit.ICECandidateType_ICT_TLS
|
||||
} else {
|
||||
return livekit.ICECandidateType_ICT_NONE
|
||||
switch lowestPriorityConnectionType {
|
||||
case types.ICEConnectionTypeUDP:
|
||||
// try ICE/TCP if ICE/UDP failed
|
||||
if ic.PreferenceSubscriber == livekit.ICECandidateType_ICT_NONE {
|
||||
if t.params.ClientInfo.SupportsICETCP() && t.canUseICETCP() {
|
||||
return livekit.ICECandidateType_ICT_TCP
|
||||
} else if t.params.TURNSEnabled {
|
||||
// fallback to TURN/TLS if TCP is not supported
|
||||
return livekit.ICECandidateType_ICT_TLS
|
||||
}
|
||||
}
|
||||
|
||||
case types.ICEConnectionTypeTCP:
|
||||
// try TURN/TLS if ICE/TCP failed,
|
||||
// the configuration could have been ICT_NONE or ICT_TCP,
|
||||
// in either case, fallback to TURN/TLS
|
||||
if t.params.TURNSEnabled {
|
||||
return livekit.ICECandidateType_ICT_TLS
|
||||
} else {
|
||||
// keep the current config
|
||||
return ic.PreferenceSubscriber
|
||||
}
|
||||
|
||||
case types.ICEConnectionTypeTURN:
|
||||
// TURN/TLS is the most permissive option, if that fails there is nowhere to go to
|
||||
// the configuration could have been ICT_NONE or ICT_TLS,
|
||||
// keep the current config
|
||||
return ic.PreferenceSubscriber
|
||||
}
|
||||
return livekit.ICECandidateType_ICT_NONE
|
||||
}
|
||||
|
||||
var preferNext livekit.ICECandidateType
|
||||
@@ -672,7 +851,11 @@ func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
|
||||
}, false)
|
||||
}
|
||||
|
||||
func (t *TransportManager) SetMigrateInfo(previousOffer, previousAnswer *webrtc.SessionDescription, dataChannels []*livekit.DataChannelInfo) {
|
||||
func (t *TransportManager) SetMigrateInfo(
|
||||
previousOffer *webrtc.SessionDescription,
|
||||
previousAnswer *webrtc.SessionDescription,
|
||||
dataChannels []*livekit.DataChannelInfo,
|
||||
) {
|
||||
t.lock.Lock()
|
||||
t.pendingDataChannelsPublisher = make([]*livekit.DataChannelInfo, 0, len(dataChannels))
|
||||
pendingDataChannelsSubscriber := make([]*livekit.DataChannelInfo, 0, len(dataChannels))
|
||||
@@ -691,7 +874,11 @@ func (t *TransportManager) SetMigrateInfo(previousOffer, previousAnswer *webrtc.
|
||||
}
|
||||
}
|
||||
|
||||
t.subscriber.SetPreviousSdp(previousOffer, previousAnswer)
|
||||
if t.params.UseSinglePeerConnection {
|
||||
t.publisher.SetPreviousSdp(previousAnswer, previousOffer)
|
||||
} else {
|
||||
t.subscriber.SetPreviousSdp(previousOffer, previousAnswer)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) ProcessPendingPublisherDataChannels() {
|
||||
@@ -710,7 +897,16 @@ func (t *TransportManager) ProcessPendingPublisherDataChannels() {
|
||||
dcExisting bool
|
||||
err error
|
||||
)
|
||||
if ci.Label == LossyDataChannel {
|
||||
switch ci.Label {
|
||||
case ReliableDataChannel:
|
||||
id := uint16(ci.GetId())
|
||||
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(ReliableDataChannel, &webrtc.DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
Negotiated: &negotiated,
|
||||
ID: &id,
|
||||
})
|
||||
case LossyDataChannel:
|
||||
ordered = false
|
||||
retransmits := uint16(0)
|
||||
id := uint16(ci.GetId())
|
||||
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(LossyDataChannel, &webrtc.DataChannelInit{
|
||||
@@ -719,12 +915,15 @@ func (t *TransportManager) ProcessPendingPublisherDataChannels() {
|
||||
Negotiated: &negotiated,
|
||||
ID: &id,
|
||||
})
|
||||
} else if ci.Label == ReliableDataChannel {
|
||||
case DataTrackDataChannel:
|
||||
ordered = false
|
||||
retransmits := uint16(0)
|
||||
id := uint16(ci.GetId())
|
||||
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(ReliableDataChannel, &webrtc.DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
Negotiated: &negotiated,
|
||||
ID: &id,
|
||||
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(DataTrackDataChannel, &webrtc.DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
MaxRetransmits: &retransmits,
|
||||
Negotiated: &negotiated,
|
||||
ID: &id,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
@@ -755,7 +954,11 @@ func (t *TransportManager) onMediaLossUpdate(loss uint8) {
|
||||
t.lock.Unlock()
|
||||
|
||||
t.params.Logger.Infow("udp connection unstable, switch to tcp", "signalingRTT", t.signalingRTT)
|
||||
t.params.SubscriberHandler.OnFailed(true, t.subscriber.GetICEConnectionInfo())
|
||||
if t.params.UseSinglePeerConnection {
|
||||
t.params.PublisherHandler.OnFailed(true, t.publisher.GetICEConnectionInfo())
|
||||
} else {
|
||||
t.params.SubscriberHandler.OnFailed(true, t.subscriber.GetICEConnectionInfo())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -767,8 +970,12 @@ func (t *TransportManager) UpdateSignalingRTT(rtt uint32) {
|
||||
t.lock.Lock()
|
||||
t.signalingRTT = rtt
|
||||
t.lock.Unlock()
|
||||
t.publisher.SetSignalingRTT(rtt)
|
||||
t.subscriber.SetSignalingRTT(rtt)
|
||||
if t.publisher != nil {
|
||||
t.publisher.SetSignalingRTT(rtt)
|
||||
}
|
||||
if t.subscriber != nil {
|
||||
t.subscriber.SetSignalingRTT(rtt)
|
||||
}
|
||||
|
||||
// TODO: considering using tcp rtt to calculate ice connection cost, if ice connection can't be established
|
||||
// within 5 * tcp rtt(at least 5s), means udp traffic might be block/dropped, switch to tcp.
|
||||
@@ -813,13 +1020,25 @@ func (t *TransportManager) SetSignalSourceValid(valid bool) {
|
||||
}
|
||||
|
||||
func (t *TransportManager) SetSubscriberAllowPause(allowPause bool) {
|
||||
t.subscriber.SetAllowPauseOfStreamAllocator(allowPause)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.SetAllowPauseOfStreamAllocator(allowPause)
|
||||
} else {
|
||||
t.subscriber.SetAllowPauseOfStreamAllocator(allowPause)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) SetSubscriberChannelCapacity(channelCapacity int64) {
|
||||
t.subscriber.SetChannelCapacityOfStreamAllocator(channelCapacity)
|
||||
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
|
||||
t.publisher.SetChannelCapacityOfStreamAllocator(channelCapacity)
|
||||
} else {
|
||||
t.subscriber.SetChannelCapacityOfStreamAllocator(channelCapacity)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TransportManager) hasRecentSignalLocked() bool {
|
||||
return time.Since(t.lastSignalAt) < PingTimeoutSeconds*time.Second
|
||||
}
|
||||
|
||||
func (t *TransportManager) RTPStreamPublished(ssrc uint32, mid, rid string) {
|
||||
t.publisher.RTPStreamPublished(ssrc, mid, rid)
|
||||
}
|
||||
|
||||
+149
-103
@@ -18,30 +18,63 @@ package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/observability/roomobs"
|
||||
)
|
||||
|
||||
type ICEConnectionType string
|
||||
type ICEConnectionType int
|
||||
|
||||
const (
|
||||
ICEConnectionTypeUDP ICEConnectionType = "udp"
|
||||
ICEConnectionTypeTCP ICEConnectionType = "tcp"
|
||||
ICEConnectionTypeTURN ICEConnectionType = "turn"
|
||||
ICEConnectionTypeUnknown ICEConnectionType = "unknown"
|
||||
// this is in ICE priority highest -> lowest ordering
|
||||
// WARNING: Keep this ordering as it is used to find lowest priority connection type.
|
||||
ICEConnectionTypeUnknown ICEConnectionType = iota
|
||||
ICEConnectionTypeUDP
|
||||
ICEConnectionTypeTCP
|
||||
ICEConnectionTypeTURN
|
||||
)
|
||||
|
||||
func (i ICEConnectionType) String() string {
|
||||
switch i {
|
||||
case ICEConnectionTypeUnknown:
|
||||
return "unknown"
|
||||
case ICEConnectionTypeUDP:
|
||||
return "udp"
|
||||
case ICEConnectionTypeTCP:
|
||||
return "tcp"
|
||||
case ICEConnectionTypeTURN:
|
||||
return "turn"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (i ICEConnectionType) ReporterType() roomobs.ConnectionType {
|
||||
switch i {
|
||||
case ICEConnectionTypeUnknown:
|
||||
return roomobs.ConnectionTypeUndefined
|
||||
case ICEConnectionTypeUDP:
|
||||
return roomobs.ConnectionTypeUDP
|
||||
case ICEConnectionTypeTCP:
|
||||
return roomobs.ConnectionTypeTCP
|
||||
case ICEConnectionTypeTURN:
|
||||
return roomobs.ConnectionTypeTurn
|
||||
default:
|
||||
return roomobs.ConnectionTypeUndefined
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
|
||||
type ICECandidateExtended struct {
|
||||
// only one of local or remote is set. This is due to type foo in Pion
|
||||
Local *webrtc.ICECandidate
|
||||
Remote ice.Candidate
|
||||
Candidate *webrtc.ICECandidate
|
||||
SelectedOrder int
|
||||
Filtered bool
|
||||
Trickle bool
|
||||
@@ -60,6 +93,15 @@ func (i *ICEConnectionInfo) HasCandidates() bool {
|
||||
return len(i.Local) > 0 || len(i.Remote) > 0
|
||||
}
|
||||
|
||||
func ICEConnectionInfosType(infos []*ICEConnectionInfo) ICEConnectionType {
|
||||
for _, info := range infos {
|
||||
if info.Type != ICEConnectionTypeUnknown {
|
||||
return info.Type
|
||||
}
|
||||
}
|
||||
return ICEConnectionTypeUnknown
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
|
||||
type ICEConnectionDetails struct {
|
||||
@@ -91,7 +133,7 @@ func (d *ICEConnectionDetails) GetInfo() *ICEConnectionInfo {
|
||||
}
|
||||
for _, c := range d.Local {
|
||||
info.Local = append(info.Local, &ICECandidateExtended{
|
||||
Local: c.Local,
|
||||
Candidate: c.Candidate,
|
||||
Filtered: c.Filtered,
|
||||
SelectedOrder: c.SelectedOrder,
|
||||
Trickle: c.Trickle,
|
||||
@@ -99,7 +141,7 @@ func (d *ICEConnectionDetails) GetInfo() *ICEConnectionInfo {
|
||||
}
|
||||
for _, c := range d.Remote {
|
||||
info.Remote = append(info.Remote, &ICECandidateExtended{
|
||||
Remote: c.Remote,
|
||||
Candidate: c.Candidate,
|
||||
Filtered: c.Filtered,
|
||||
SelectedOrder: c.SelectedOrder,
|
||||
Trickle: c.Trickle,
|
||||
@@ -108,19 +150,26 @@ func (d *ICEConnectionDetails) GetInfo() *ICEConnectionInfo {
|
||||
return info
|
||||
}
|
||||
|
||||
func (d *ICEConnectionDetails) GetConnectionType() ICEConnectionType {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
return d.Type
|
||||
}
|
||||
|
||||
func (d *ICEConnectionDetails) AddLocalCandidate(c *webrtc.ICECandidate, filtered, trickle bool) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
compFn := func(e *ICECandidateExtended) bool {
|
||||
return isCandidateEqualTo(e.Local, c)
|
||||
return isCandidateEqualTo(e.Candidate, c)
|
||||
}
|
||||
if slices.ContainsFunc(d.Local, compFn) {
|
||||
return
|
||||
}
|
||||
d.Local = append(d.Local, &ICECandidateExtended{
|
||||
Local: c,
|
||||
Filtered: filtered,
|
||||
Trickle: trickle,
|
||||
Candidate: c,
|
||||
Filtered: filtered,
|
||||
Trickle: trickle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,24 +184,30 @@ func (d *ICEConnectionDetails) AddLocalICECandidate(c ice.Candidate, filtered, t
|
||||
}
|
||||
|
||||
func (d *ICEConnectionDetails) AddRemoteCandidate(c webrtc.ICECandidateInit, filtered, trickle, canUpdate bool) {
|
||||
candidate, err := unmarshalICECandidate(c)
|
||||
iceCandidate, err := unmarshalICECandidate(c)
|
||||
if err != nil {
|
||||
d.logger.Errorw("could not unmarshal candidate", err, "candidate", c)
|
||||
return
|
||||
}
|
||||
d.AddRemoteICECandidate(candidate, filtered, trickle, canUpdate)
|
||||
d.AddRemoteICECandidate(iceCandidate, filtered, trickle, canUpdate)
|
||||
}
|
||||
|
||||
func (d *ICEConnectionDetails) AddRemoteICECandidate(candidate ice.Candidate, filtered, trickle, canUpdate bool) {
|
||||
if candidate == nil {
|
||||
func (d *ICEConnectionDetails) AddRemoteICECandidate(iceCandidate ice.Candidate, filtered, trickle, canUpdate bool) {
|
||||
if iceCandidate == nil {
|
||||
// end-of-candidates candidate
|
||||
return
|
||||
}
|
||||
|
||||
candidate, err := unmarshalCandidate(iceCandidate)
|
||||
if err != nil {
|
||||
d.logger.Errorw("could not unmarshal ice candidate", err, "candidate", iceCandidate)
|
||||
return
|
||||
}
|
||||
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
indexFn := func(e *ICECandidateExtended) bool {
|
||||
return isICECandidateEqualTo(e.Remote, candidate)
|
||||
return isCandidateEqualTo(e.Candidate, candidate)
|
||||
}
|
||||
if idx := slices.IndexFunc(d.Remote, indexFn); idx != -1 {
|
||||
if canUpdate {
|
||||
@@ -162,10 +217,11 @@ func (d *ICEConnectionDetails) AddRemoteICECandidate(candidate ice.Candidate, fi
|
||||
return
|
||||
}
|
||||
d.Remote = append(d.Remote, &ICECandidateExtended{
|
||||
Remote: candidate,
|
||||
Filtered: filtered,
|
||||
Trickle: trickle,
|
||||
Candidate: candidate,
|
||||
Filtered: filtered,
|
||||
Trickle: trickle,
|
||||
})
|
||||
d.updateConnectionTypeLocked()
|
||||
}
|
||||
|
||||
func (d *ICEConnectionDetails) Clear() {
|
||||
@@ -183,56 +239,78 @@ func (d *ICEConnectionDetails) SetSelectedPair(pair *webrtc.ICECandidatePair) {
|
||||
d.selectedCount++
|
||||
|
||||
remoteIdx := slices.IndexFunc(d.Remote, func(e *ICECandidateExtended) bool {
|
||||
return isICECandidateEqualToCandidate(e.Remote, pair.Remote)
|
||||
return isCandidateEqualTo(e.Candidate, pair.Remote)
|
||||
})
|
||||
if remoteIdx < 0 {
|
||||
// it's possible for prflx candidates to be generated by Pion, we'll add them
|
||||
candidate, err := unmarshalICECandidate(pair.Remote.ToJSON())
|
||||
if err != nil {
|
||||
d.logger.Errorw("could not unmarshal remote candidate", err, "candidate", pair.Remote)
|
||||
return
|
||||
}
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
d.Remote = append(d.Remote, &ICECandidateExtended{
|
||||
Remote: candidate,
|
||||
Filtered: false,
|
||||
Trickle: true,
|
||||
Candidate: pair.Remote,
|
||||
Filtered: false,
|
||||
Trickle: false,
|
||||
})
|
||||
remoteIdx = len(d.Remote) - 1
|
||||
}
|
||||
remote := d.Remote[remoteIdx]
|
||||
remote.SelectedOrder = d.selectedCount
|
||||
d.Remote[remoteIdx].SelectedOrder = d.selectedCount
|
||||
d.updateConnectionTypeLocked()
|
||||
|
||||
localIdx := slices.IndexFunc(d.Local, func(e *ICECandidateExtended) bool {
|
||||
return isCandidateEqualTo(e.Local, pair.Local)
|
||||
return isCandidateEqualTo(e.Candidate, pair.Local)
|
||||
})
|
||||
if localIdx < 0 {
|
||||
d.logger.Errorw("could not match local candidate", nil, "local", pair.Local)
|
||||
// should not happen
|
||||
return
|
||||
}
|
||||
local := d.Local[localIdx]
|
||||
local.SelectedOrder = d.selectedCount
|
||||
d.Local[localIdx].SelectedOrder = d.selectedCount
|
||||
}
|
||||
|
||||
d.Type = ICEConnectionTypeUDP
|
||||
if pair.Remote.Protocol == webrtc.ICEProtocolTCP {
|
||||
func (d *ICEConnectionDetails) updateConnectionTypeLocked() {
|
||||
highestSelectedOrder := -1
|
||||
var selectedRemoteCandidate *ICECandidateExtended
|
||||
for _, remote := range d.Remote {
|
||||
if remote.SelectedOrder == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if remote.SelectedOrder > highestSelectedOrder {
|
||||
highestSelectedOrder = remote.SelectedOrder
|
||||
selectedRemoteCandidate = remote
|
||||
}
|
||||
}
|
||||
|
||||
if selectedRemoteCandidate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
remoteCandidate := selectedRemoteCandidate.Candidate
|
||||
switch remoteCandidate.Protocol {
|
||||
case webrtc.ICEProtocolUDP:
|
||||
d.Type = ICEConnectionTypeUDP
|
||||
|
||||
case webrtc.ICEProtocolTCP:
|
||||
d.Type = ICEConnectionTypeTCP
|
||||
}
|
||||
if pair.Remote.Typ == webrtc.ICECandidateTypeRelay {
|
||||
|
||||
switch remoteCandidate.Typ {
|
||||
case webrtc.ICECandidateTypeRelay:
|
||||
d.Type = ICEConnectionTypeTURN
|
||||
} else if pair.Remote.Typ == webrtc.ICECandidateTypePrflx {
|
||||
|
||||
case webrtc.ICECandidateTypePrflx:
|
||||
// if the remote relay candidate pings us *before* we get a relay candidate,
|
||||
// Pion would have created a prflx candidate with the same address as the relay candidate.
|
||||
// to report an accurate connection type, we'll compare to see if existing relay candidates match
|
||||
for _, other := range d.Remote {
|
||||
or := other.Remote
|
||||
if or.Type() == ice.CandidateTypeRelay &&
|
||||
pair.Remote.Address == or.Address() &&
|
||||
pair.Remote.Port == uint16(or.Port()) &&
|
||||
pair.Remote.Protocol.String() == or.NetworkType().NetworkShort() {
|
||||
or := other.Candidate
|
||||
if or.Typ == webrtc.ICECandidateTypeRelay &&
|
||||
remoteCandidate.Address == or.Address &&
|
||||
// NOTE: port is not compared as relayed address reported by TURN ALLOCATE from
|
||||
// pion/turn server -> client and later sent from client -> server via ICE Trickle does not
|
||||
// match port of `prflx` candidate learnt via TURN path. TODO-INVESTIGATE: how and why doesn't
|
||||
// port match?
|
||||
// remoteCandidate.Port == or.Port &&
|
||||
remoteCandidate.Protocol == or.Protocol {
|
||||
d.Type = ICEConnectionTypeTURN
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,39 +336,6 @@ func isCandidateEqualTo(c1, c2 *webrtc.ICECandidate) bool {
|
||||
c1.TCPType == c2.TCPType
|
||||
}
|
||||
|
||||
func isICECandidateEqualTo(c1, c2 ice.Candidate) bool {
|
||||
if c1 == nil && c2 == nil {
|
||||
return true
|
||||
}
|
||||
if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) {
|
||||
return false
|
||||
}
|
||||
return c1.Type() == c2.Type() &&
|
||||
c1.NetworkType() == c2.NetworkType() &&
|
||||
c1.Address() == c2.Address() &&
|
||||
c1.Port() == c2.Port() &&
|
||||
c1.Foundation() == c2.Foundation() &&
|
||||
c1.Priority() == c2.Priority() &&
|
||||
c1.RelatedAddress().Equal(c2.RelatedAddress()) &&
|
||||
c1.TCPType() == c2.TCPType()
|
||||
}
|
||||
|
||||
func isICECandidateEqualToCandidate(c1 ice.Candidate, c2 *webrtc.ICECandidate) bool {
|
||||
if c1 == nil && c2 == nil {
|
||||
return true
|
||||
}
|
||||
if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) {
|
||||
return false
|
||||
}
|
||||
return c1.Type().String() == c2.Typ.String() &&
|
||||
c1.NetworkType().NetworkShort() == c2.Protocol.String() &&
|
||||
c1.Address() == c2.Address &&
|
||||
c1.Port() == int(c2.Port) &&
|
||||
c1.Foundation() == c2.Foundation &&
|
||||
c1.Priority() == c2.Priority &&
|
||||
c1.TCPType().String() == c2.TCPType
|
||||
}
|
||||
|
||||
func unmarshalICECandidate(c webrtc.ICECandidateInit) (ice.Candidate, error) {
|
||||
candidateValue := strings.TrimPrefix(c.Candidate, "candidate:")
|
||||
if candidateValue == "" {
|
||||
@@ -306,28 +351,14 @@ func unmarshalICECandidate(c webrtc.ICECandidateInit) (ice.Candidate, error) {
|
||||
}
|
||||
|
||||
func unmarshalCandidate(i ice.Candidate) (*webrtc.ICECandidate, error) {
|
||||
var typ webrtc.ICECandidateType
|
||||
switch i.Type() {
|
||||
case ice.CandidateTypeHost:
|
||||
typ = webrtc.ICECandidateTypeHost
|
||||
case ice.CandidateTypeServerReflexive:
|
||||
typ = webrtc.ICECandidateTypeSrflx
|
||||
case ice.CandidateTypePeerReflexive:
|
||||
typ = webrtc.ICECandidateTypePrflx
|
||||
case ice.CandidateTypeRelay:
|
||||
typ = webrtc.ICECandidateTypeRelay
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown candidate type: %s", i.Type())
|
||||
typ, err := convertTypeFromICE(i.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var protocol webrtc.ICEProtocol
|
||||
switch strings.ToLower(i.NetworkType().NetworkShort()) {
|
||||
case "udp":
|
||||
protocol = webrtc.ICEProtocolUDP
|
||||
case "tcp":
|
||||
protocol = webrtc.ICEProtocolTCP
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown network type: %s", i.NetworkType())
|
||||
protocol, err := webrtc.NewICEProtocol(i.NetworkType().NetworkShort())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := webrtc.ICECandidate{
|
||||
@@ -349,6 +380,21 @@ func unmarshalCandidate(i ice.Candidate) (*webrtc.ICECandidate, error) {
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func convertTypeFromICE(t ice.CandidateType) (webrtc.ICECandidateType, error) {
|
||||
switch t {
|
||||
case ice.CandidateTypeHost:
|
||||
return webrtc.ICECandidateTypeHost, nil
|
||||
case ice.CandidateTypeServerReflexive:
|
||||
return webrtc.ICECandidateTypeSrflx, nil
|
||||
case ice.CandidateTypePeerReflexive:
|
||||
return webrtc.ICECandidateTypePrflx, nil
|
||||
case ice.CandidateTypeRelay:
|
||||
return webrtc.ICECandidateTypeRelay, nil
|
||||
default:
|
||||
return webrtc.ICECandidateType(t), fmt.Errorf("unknown ice candidate type: %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
func IsCandidateMDNS(candidate webrtc.ICECandidateInit) bool {
|
||||
c, err := unmarshalICECandidate(candidate)
|
||||
if err != nil {
|
||||
@@ -364,5 +410,5 @@ func IsICECandidateMDNS(candidate ice.Candidate) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.HasSuffix(candidate.Address(), ".local")
|
||||
return strings.HasSuffix(candidate.Address(), ".local") || strings.HasSuffix(candidate.Address(), ".invalid")
|
||||
}
|
||||
|
||||
+361
-48
@@ -22,15 +22,20 @@ import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"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/roomobs"
|
||||
"github.com/livekit/protocol/utils"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/pacer"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
|
||||
@@ -111,6 +116,8 @@ const (
|
||||
ParticipantCloseReasonRoomClosed
|
||||
ParticipantCloseReasonUserUnavailable
|
||||
ParticipantCloseReasonUserRejected
|
||||
ParticipantCloseReasonMoveFailed
|
||||
ParticipantCloseReasonAgentError
|
||||
)
|
||||
|
||||
func (p ParticipantCloseReason) String() string {
|
||||
@@ -169,6 +176,10 @@ func (p ParticipantCloseReason) String() string {
|
||||
return "USER_UNAVAILABLE"
|
||||
case ParticipantCloseReasonUserRejected:
|
||||
return "USER_REJECTED"
|
||||
case ParticipantCloseReasonMoveFailed:
|
||||
return "MOVE_FAILED"
|
||||
case ParticipantCloseReasonAgentError:
|
||||
return "AGENT_ERROR"
|
||||
default:
|
||||
return fmt.Sprintf("%d", int(p))
|
||||
}
|
||||
@@ -184,7 +195,7 @@ func (p ParticipantCloseReason) ToDisconnectReason() livekit.DisconnectReason {
|
||||
// expected to be connected but is not
|
||||
return livekit.DisconnectReason_JOIN_FAILURE
|
||||
case ParticipantCloseReasonPeerConnectionDisconnected:
|
||||
return livekit.DisconnectReason_STATE_MISMATCH
|
||||
return livekit.DisconnectReason_CONNECTION_TIMEOUT
|
||||
case ParticipantCloseReasonDuplicateIdentity, ParticipantCloseReasonStale:
|
||||
return livekit.DisconnectReason_DUPLICATE_IDENTITY
|
||||
case ParticipantCloseReasonMigrationRequested, ParticipantCloseReasonMigrationComplete, ParticipantCloseReasonSimulateMigration:
|
||||
@@ -195,7 +206,8 @@ func (p ParticipantCloseReason) ToDisconnectReason() livekit.DisconnectReason {
|
||||
return livekit.DisconnectReason_ROOM_DELETED
|
||||
case ParticipantCloseReasonSimulateNodeFailure, ParticipantCloseReasonSimulateServerLeave:
|
||||
return livekit.DisconnectReason_SERVER_SHUTDOWN
|
||||
case ParticipantCloseReasonNegotiateFailed, ParticipantCloseReasonPublicationError, ParticipantCloseReasonSubscriptionError, ParticipantCloseReasonDataChannelError, ParticipantCloseReasonMigrateCodecMismatch:
|
||||
case ParticipantCloseReasonNegotiateFailed, ParticipantCloseReasonPublicationError, ParticipantCloseReasonSubscriptionError,
|
||||
ParticipantCloseReasonDataChannelError, ParticipantCloseReasonMigrateCodecMismatch, ParticipantCloseReasonMoveFailed:
|
||||
return livekit.DisconnectReason_STATE_MISMATCH
|
||||
case ParticipantCloseReasonSignalSourceClose:
|
||||
return livekit.DisconnectReason_SIGNAL_CLOSE
|
||||
@@ -205,12 +217,31 @@ func (p ParticipantCloseReason) ToDisconnectReason() livekit.DisconnectReason {
|
||||
return livekit.DisconnectReason_USER_UNAVAILABLE
|
||||
case ParticipantCloseReasonUserRejected:
|
||||
return livekit.DisconnectReason_USER_REJECTED
|
||||
case ParticipantCloseReasonAgentError:
|
||||
return livekit.DisconnectReason_AGENT_ERROR
|
||||
default:
|
||||
// the other types will map to unknown reason
|
||||
return livekit.DisconnectReason_UNKNOWN_REASON
|
||||
}
|
||||
}
|
||||
|
||||
// IsIntentionalDisconnect reports whether a disconnect reason represents an
|
||||
// intentional/expected closure (client leaving, admin action, room teardown,
|
||||
// migration, etc.) as opposed to a connection failure.
|
||||
func IsIntentionalDisconnect(reason livekit.DisconnectReason) bool {
|
||||
switch reason {
|
||||
case livekit.DisconnectReason_CLIENT_INITIATED,
|
||||
livekit.DisconnectReason_SERVER_SHUTDOWN,
|
||||
livekit.DisconnectReason_DUPLICATE_IDENTITY,
|
||||
livekit.DisconnectReason_MIGRATION,
|
||||
livekit.DisconnectReason_PARTICIPANT_REMOVED,
|
||||
livekit.DisconnectReason_ROOM_DELETED,
|
||||
livekit.DisconnectReason_ROOM_CLOSED:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
type SignallingCloseReason int
|
||||
@@ -258,6 +289,12 @@ func (s SignallingCloseReason) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
const (
|
||||
ParticipantCloseKeyNormal = "normal"
|
||||
ParticipantCloseKeyWHIP = "whip"
|
||||
)
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
//counterfeiter:generate . Participant
|
||||
@@ -268,18 +305,26 @@ type Participant interface {
|
||||
ConnectedAt() time.Time
|
||||
CloseReason() ParticipantCloseReason
|
||||
Kind() livekit.ParticipantInfo_Kind
|
||||
KindDetails() []livekit.ParticipantInfo_KindDetail
|
||||
IsRecorder() bool
|
||||
IsDependent() bool
|
||||
IsAgent() bool
|
||||
|
||||
GetLogger() logger.Logger
|
||||
|
||||
CanSkipBroadcast() bool
|
||||
Version() utils.TimedVersion
|
||||
ToProto() *livekit.ParticipantInfo
|
||||
ToProtoWithVersion() (*livekit.ParticipantInfo, utils.TimedVersion)
|
||||
|
||||
IsPublisher() bool
|
||||
GetPublishedTrack(trackID livekit.TrackID) MediaTrack
|
||||
GetPublishedTracks() []MediaTrack
|
||||
RemovePublishedTrack(track MediaTrack, isExpectedToResume bool, shouldClose bool)
|
||||
RemovePublishedTrack(track MediaTrack, isExpectedToResume bool)
|
||||
|
||||
GetPublishedDataTracks() []DataTrack
|
||||
GetPublishedDataTrack(handle uint16) DataTrack
|
||||
RemovePublishedDataTrack(track DataTrack)
|
||||
|
||||
GetAudioLevel() (smoothedLevel float64, active bool)
|
||||
|
||||
@@ -290,7 +335,11 @@ type Participant interface {
|
||||
// permissions
|
||||
Hidden() bool
|
||||
|
||||
MigrateState() MigrateState
|
||||
|
||||
Close(sendLeave bool, reason ParticipantCloseReason, isExpectedToResume bool) error
|
||||
IsClosed() bool
|
||||
IsDisconnected() bool
|
||||
|
||||
SubscriptionPermission() (*livekit.SubscriptionPermission, utils.TimedVersion)
|
||||
|
||||
@@ -301,9 +350,11 @@ type Participant interface {
|
||||
resolverBySid func(participantID livekit.ParticipantID) LocalParticipant,
|
||||
) error
|
||||
|
||||
DebugInfo() map[string]interface{}
|
||||
DebugInfo() map[string]any
|
||||
|
||||
OnMetrics(callback func(Participant, *livekit.DataPacket))
|
||||
HandleReceivedDataTrackMessage([]byte, *datatrack.Packet, int64)
|
||||
|
||||
GetParticipantListener() ParticipantListener
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
@@ -313,22 +364,51 @@ type AddTrackParams struct {
|
||||
Red bool
|
||||
}
|
||||
|
||||
type MoveToRoomParams struct {
|
||||
RoomName livekit.RoomName
|
||||
ParticipantID livekit.ParticipantID
|
||||
Listener LocalParticipantListener
|
||||
Helper LocalParticipantHelper
|
||||
}
|
||||
|
||||
type DataMessageCache struct {
|
||||
Data []byte
|
||||
SenderID livekit.ParticipantID
|
||||
Seq uint32
|
||||
DestIdentities []livekit.ParticipantIdentity
|
||||
}
|
||||
|
||||
//counterfeiter:generate . LocalParticipantHelper
|
||||
type LocalParticipantHelper interface {
|
||||
ResolveMediaTrack(LocalParticipant, livekit.TrackID) MediaResolverResult
|
||||
ResolveDataTrack(LocalParticipant, livekit.TrackID) DataResolverResult
|
||||
GetParticipantInfo(pID livekit.ParticipantID) *livekit.ParticipantInfo
|
||||
GetRegionSettings(ip string) *livekit.RegionSettings
|
||||
GetSubscriberForwarderState(p LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error)
|
||||
ShouldRegressCodec() bool
|
||||
GetCachedReliableDataMessage(seqs map[livekit.ParticipantID]uint32) []*DataMessageCache
|
||||
}
|
||||
|
||||
//counterfeiter:generate . LocalParticipant
|
||||
type LocalParticipant interface {
|
||||
Participant
|
||||
|
||||
ToProtoWithVersion() (*livekit.ParticipantInfo, utils.TimedVersion)
|
||||
TelemetryGuard() *telemetry.ReferenceGuard
|
||||
GetTelemetryListener() ParticipantTelemetryListener
|
||||
|
||||
// getters
|
||||
GetCountry() string
|
||||
GetTrailer() []byte
|
||||
GetLogger() logger.Logger
|
||||
GetLoggerResolver() logger.DeferredFieldResolver
|
||||
GetReporter() roomobs.ParticipantSessionReporter
|
||||
GetReporterResolver() roomobs.ParticipantReporterResolver
|
||||
GetAdaptiveStream() bool
|
||||
ProtocolVersion() ProtocolVersion
|
||||
SupportsSyncStreamID() bool
|
||||
SupportsTransceiverReuse() bool
|
||||
IsClosed() bool
|
||||
SupportsTransceiverReuse(mt MediaTrack) bool
|
||||
IsUsingSinglePeerConnection() bool
|
||||
IsReady() bool
|
||||
IsDisconnected() bool
|
||||
ActiveAt() time.Time
|
||||
Disconnected() <-chan struct{}
|
||||
IsIdle() bool
|
||||
SubscriberAsPrimary() bool
|
||||
@@ -340,15 +420,19 @@ type LocalParticipant interface {
|
||||
GetICEConnectionInfo() []*ICEConnectionInfo
|
||||
HasConnected() bool
|
||||
GetEnabledPublishCodecs() []*livekit.Codec
|
||||
GetPublisherICESessionUfrag() (string, error)
|
||||
SupportsMoving() error
|
||||
GetLastReliableSequence(migrateOut bool) uint32
|
||||
|
||||
SetResponseSink(sink routing.MessageSink)
|
||||
SwapResponseSink(sink routing.MessageSink, reason SignallingCloseReason)
|
||||
GetResponseSink() routing.MessageSink
|
||||
CloseSignalConnection(reason SignallingCloseReason)
|
||||
UpdateLastSeenSignal()
|
||||
SetSignalSourceValid(valid bool)
|
||||
HandleSignalSourceClose()
|
||||
|
||||
// updates
|
||||
CheckMetadataLimits(name string, metadata string, attributes map[string]string) error
|
||||
UpdateMetadata(update *livekit.UpdateParticipantMetadata, fromAdmin bool) error
|
||||
SetName(name string)
|
||||
SetMetadata(metadata string)
|
||||
SetAttributes(attributes map[string]string)
|
||||
@@ -364,13 +448,15 @@ type LocalParticipant interface {
|
||||
CanPublishData() bool
|
||||
|
||||
// PeerConnection
|
||||
AddICECandidate(candidate webrtc.ICECandidateInit, target livekit.SignalTarget)
|
||||
HandleOffer(sdp webrtc.SessionDescription) error
|
||||
GetAnswer() (webrtc.SessionDescription, error)
|
||||
HandleICETrickle(trickleRequest *livekit.TrickleRequest)
|
||||
HandleOffer(sd *livekit.SessionDescription) error
|
||||
GetAnswer() (webrtc.SessionDescription, uint32, error)
|
||||
HandleICETrickleSDPFragment(sdpFragment string) error
|
||||
HandleICERestartSDPFragment(sdpFragment string) (string, error)
|
||||
AddTrack(req *livekit.AddTrackRequest)
|
||||
SetTrackMuted(trackID livekit.TrackID, muted bool, fromAdmin bool) *livekit.TrackInfo
|
||||
SetTrackMuted(mute *livekit.MuteTrackRequest, fromAdmin bool) *livekit.TrackInfo
|
||||
|
||||
HandleAnswer(sdp webrtc.SessionDescription)
|
||||
HandleAnswer(sd *livekit.SessionDescription)
|
||||
Negotiate(force bool)
|
||||
ICERestart(iceConfig *livekit.ICEConfig)
|
||||
AddTrackLocal(trackLocal webrtc.TrackLocal, params AddTrackParams) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error)
|
||||
@@ -380,11 +466,14 @@ type LocalParticipant interface {
|
||||
WriteSubscriberRTCP(pkts []rtcp.Packet) error
|
||||
|
||||
// subscriptions
|
||||
SubscribeToTrack(trackID livekit.TrackID)
|
||||
SubscribeToTrack(trackID livekit.TrackID, isSync bool)
|
||||
UnsubscribeFromTrack(trackID livekit.TrackID)
|
||||
UpdateSubscribedTrackSettings(trackID livekit.TrackID, settings *livekit.UpdateTrackSettings)
|
||||
GetSubscribedTracks() []SubscribedTrack
|
||||
IsTrackNameSubscribed(publisherIdentity livekit.ParticipantIdentity, trackName string) bool
|
||||
SubscribeToDataTrack(trackID livekit.TrackID)
|
||||
UnsubscribeFromDataTrack(trackID livekit.TrackID)
|
||||
UpdateDataTrackSubscriptionOptions(trackID livekit.TrackID, subscriptionOptions *livekit.DataTrackSubscriptionOptions)
|
||||
Verify() bool
|
||||
VerifySubscribeParticipantInfo(pID livekit.ParticipantID, version uint32)
|
||||
// WaitUntilSubscribed waits until all subscriptions have been settled, or if the timeout
|
||||
@@ -403,29 +492,18 @@ type LocalParticipant interface {
|
||||
SendJoinResponse(joinResponse *livekit.JoinResponse) error
|
||||
SendParticipantUpdate(participants []*livekit.ParticipantInfo) error
|
||||
SendSpeakerUpdate(speakers []*livekit.SpeakerInfo, force bool) error
|
||||
SendDataPacket(kind livekit.DataPacket_Kind, encoded []byte) error
|
||||
SendDataMessage(kind livekit.DataPacket_Kind, data []byte, senderID livekit.ParticipantID, seq uint32) error
|
||||
SendDataMessageUnlabeled(data []byte, useRaw bool, sender livekit.ParticipantIdentity) error
|
||||
SendRoomUpdate(room *livekit.Room) error
|
||||
SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error
|
||||
SubscriptionPermissionUpdate(publisherID livekit.ParticipantID, trackID livekit.TrackID, allowed bool)
|
||||
SendSubscriptionPermissionUpdate(publisherID livekit.ParticipantID, trackID livekit.TrackID, allowed bool) error
|
||||
SendRefreshToken(token string) error
|
||||
SendRequestResponse(requestResponse *livekit.RequestResponse) error
|
||||
HandleReconnectAndSendResponse(reconnectReason livekit.ReconnectReason, reconnectResponse *livekit.ReconnectResponse) error
|
||||
IssueFullReconnect(reason ParticipantCloseReason)
|
||||
SendRoomMovedResponse(moved *livekit.RoomMovedResponse) error
|
||||
SendDataTrackSubscriberHandles(handles map[uint32]*livekit.DataTrackSubscriberHandles_PublishedDataTrack) error
|
||||
|
||||
// callbacks
|
||||
OnStateChange(func(p LocalParticipant, state livekit.ParticipantInfo_State))
|
||||
OnMigrateStateChange(func(p LocalParticipant, migrateState MigrateState))
|
||||
// OnTrackPublished - remote added a track
|
||||
OnTrackPublished(func(LocalParticipant, MediaTrack))
|
||||
// OnTrackUpdated - one of its publishedTracks changed in status
|
||||
OnTrackUpdated(callback func(LocalParticipant, MediaTrack))
|
||||
// OnTrackUnpublished - a track was unpublished
|
||||
OnTrackUnpublished(callback func(LocalParticipant, MediaTrack))
|
||||
// OnParticipantUpdate - metadata or permission is updated
|
||||
OnParticipantUpdate(callback func(LocalParticipant))
|
||||
OnDataPacket(callback func(LocalParticipant, livekit.DataPacket_Kind, *livekit.DataPacket))
|
||||
OnSubscribeStatusChanged(fn func(publisherID livekit.ParticipantID, subscribed bool))
|
||||
OnClose(callback func(LocalParticipant))
|
||||
AddOnClose(key string, callback func(LocalParticipant))
|
||||
OnClaimsChanged(callback func(LocalParticipant))
|
||||
|
||||
HandleReceiverReport(dt *sfu.DownTrack, report *rtcp.ReceiverReport)
|
||||
@@ -434,13 +512,16 @@ type LocalParticipant interface {
|
||||
MaybeStartMigration(force bool, onStart func()) bool
|
||||
NotifyMigration()
|
||||
SetMigrateState(s MigrateState)
|
||||
MigrateState() MigrateState
|
||||
SetMigrateInfo(
|
||||
previousOffer, previousAnswer *webrtc.SessionDescription,
|
||||
previousOffer *webrtc.SessionDescription,
|
||||
previousAnswer *webrtc.SessionDescription,
|
||||
mediaTracks []*livekit.TrackPublishedResponse,
|
||||
dataChannels []*livekit.DataChannelInfo,
|
||||
dataChannelReceiveState []*livekit.DataChannelReceiveState,
|
||||
dataTracks []*livekit.PublishDataTrackResponse,
|
||||
)
|
||||
IsReconnect() bool
|
||||
MoveToRoom(params MoveToRoomParams)
|
||||
|
||||
UpdateMediaRTT(rtt uint32)
|
||||
UpdateSignalingRTT(rtt uint32)
|
||||
@@ -454,6 +535,7 @@ type LocalParticipant interface {
|
||||
OnICEConfigChanged(callback func(participant LocalParticipant, iceConfig *livekit.ICEConfig))
|
||||
|
||||
UpdateSubscribedQuality(nodeID livekit.NodeID, trackID livekit.TrackID, maxQualities []SubscribedCodecQuality) error
|
||||
UpdateSubscribedAudioCodecs(nodeID livekit.NodeID, trackID livekit.TrackID, codecs []*livekit.SubscribedAudioCodec) error
|
||||
UpdateMediaLoss(nodeID livekit.NodeID, trackID livekit.TrackID, fractionalLoss uint32) error
|
||||
|
||||
// down stream bandwidth management
|
||||
@@ -465,8 +547,177 @@ type LocalParticipant interface {
|
||||
GetDisableSenderReportPassThrough() bool
|
||||
|
||||
HandleMetrics(senderParticipantID livekit.ParticipantID, batch *livekit.MetricsBatch) error
|
||||
HandleUpdateSubscriptions(
|
||||
[]livekit.TrackID,
|
||||
[]*livekit.ParticipantTracks,
|
||||
bool,
|
||||
)
|
||||
HandleUpdateSubscriptionPermission(*livekit.SubscriptionPermission) error
|
||||
HandleSyncState(*livekit.SyncState) error
|
||||
HandleSimulateScenario(*livekit.SimulateScenario) error
|
||||
HandleLeaveRequest(reason ParticipantCloseReason)
|
||||
|
||||
HandlePublishDataTrackRequest(*livekit.PublishDataTrackRequest)
|
||||
HandleUnpublishDataTrackRequest(*livekit.UnpublishDataTrackRequest)
|
||||
HandleUpdateDataSubscription(*livekit.UpdateDataSubscription)
|
||||
|
||||
HandleSignalMessage(msg proto.Message) error
|
||||
|
||||
PerformRpc(req *livekit.PerformRpcRequest, resultCh chan string, errorCh chan error)
|
||||
|
||||
GetDataTrackTransport() DataTrackTransport
|
||||
|
||||
ClearParticipantListener()
|
||||
|
||||
GetNextSubscribedDataTrackHandle() uint16
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
//counterfeiter:generate . ParticipantListener
|
||||
type ParticipantListener interface {
|
||||
OnParticipantUpdate(Participant)
|
||||
OnTrackPublished(Participant, MediaTrack)
|
||||
OnTrackUpdated(Participant, MediaTrack)
|
||||
OnTrackUnpublished(Participant, MediaTrack)
|
||||
OnDataTrackPublished(Participant, DataTrack)
|
||||
OnDataTrackUnpublished(Participant, DataTrack)
|
||||
OnDataTrackMessage(Participant, []byte, *datatrack.Packet)
|
||||
OnMetrics(Participant, *livekit.DataPacket)
|
||||
}
|
||||
|
||||
var _ ParticipantListener = (*NullParticipantListener)(nil)
|
||||
|
||||
type NullParticipantListener struct{}
|
||||
|
||||
func (*NullParticipantListener) OnParticipantUpdate(Participant) {}
|
||||
func (*NullParticipantListener) OnTrackPublished(Participant, MediaTrack) {}
|
||||
func (*NullParticipantListener) OnTrackUpdated(Participant, MediaTrack) {}
|
||||
func (*NullParticipantListener) OnTrackUnpublished(Participant, MediaTrack) {}
|
||||
func (*NullParticipantListener) OnDataTrackPublished(Participant, DataTrack) {}
|
||||
func (*NullParticipantListener) OnDataTrackUnpublished(Participant, DataTrack) {}
|
||||
func (*NullParticipantListener) OnDataTrackMessage(Participant, []byte, *datatrack.Packet) {}
|
||||
func (*NullParticipantListener) OnMetrics(Participant, *livekit.DataPacket) {}
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
//counterfeiter:generate . LocalParticipantListener
|
||||
type LocalParticipantListener interface {
|
||||
ParticipantListener
|
||||
|
||||
OnStateChange(LocalParticipant)
|
||||
OnSubscriberReady(LocalParticipant)
|
||||
OnMigrateStateChange(LocalParticipant, MigrateState)
|
||||
OnDataMessage(LocalParticipant, livekit.DataPacket_Kind, *livekit.DataPacket)
|
||||
OnDataMessageUnlabeled(LocalParticipant, []byte)
|
||||
OnSubscribeStatusChanged(LocalParticipant, livekit.ParticipantID, bool)
|
||||
OnUpdateSubscriptions(
|
||||
LocalParticipant,
|
||||
[]livekit.TrackID,
|
||||
[]*livekit.ParticipantTracks,
|
||||
bool,
|
||||
)
|
||||
OnUpdateSubscriptionPermission(LocalParticipant, *livekit.SubscriptionPermission) error
|
||||
OnUpdateDataSubscriptions(LocalParticipant, *livekit.UpdateDataSubscription)
|
||||
OnSyncState(LocalParticipant, *livekit.SyncState) error
|
||||
OnSimulateScenario(LocalParticipant, *livekit.SimulateScenario) error
|
||||
OnLeave(LocalParticipant, ParticipantCloseReason)
|
||||
}
|
||||
|
||||
var _ LocalParticipantListener = (*NullLocalParticipantListener)(nil)
|
||||
|
||||
type NullLocalParticipantListener struct {
|
||||
NullParticipantListener
|
||||
}
|
||||
|
||||
func (*NullLocalParticipantListener) OnStateChange(LocalParticipant) {}
|
||||
func (*NullLocalParticipantListener) OnSubscriberReady(LocalParticipant) {}
|
||||
func (*NullLocalParticipantListener) OnMigrateStateChange(LocalParticipant, MigrateState) {}
|
||||
func (*NullLocalParticipantListener) OnDataMessage(LocalParticipant, livekit.DataPacket_Kind, *livekit.DataPacket) {
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnDataMessageUnlabeled(LocalParticipant, []byte) {}
|
||||
func (*NullLocalParticipantListener) OnSubscribeStatusChanged(LocalParticipant, livekit.ParticipantID, bool) {
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnUpdateSubscriptions(
|
||||
LocalParticipant,
|
||||
[]livekit.TrackID,
|
||||
[]*livekit.ParticipantTracks,
|
||||
bool,
|
||||
) {
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnUpdateSubscriptionPermission(LocalParticipant, *livekit.SubscriptionPermission) error {
|
||||
return nil
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnUpdateDataSubscriptions(LocalParticipant, *livekit.UpdateDataSubscription) {
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnSyncState(LocalParticipant, *livekit.SyncState) error {
|
||||
return nil
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnSimulateScenario(LocalParticipant, *livekit.SimulateScenario) error {
|
||||
return nil
|
||||
}
|
||||
func (*NullLocalParticipantListener) OnLeave(LocalParticipant, ParticipantCloseReason) {}
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
//counterfeiter:generate . ParticipantTelemetryListener
|
||||
type ParticipantTelemetryListener interface {
|
||||
OnTrackPublishRequested(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo)
|
||||
OnTrackPublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool)
|
||||
OnTrackUnpublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool)
|
||||
OnTrackSubscribeRequested(pID livekit.ParticipantID, ti *livekit.TrackInfo)
|
||||
OnTrackSubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, publisherInfo *livekit.ParticipantInfo, shouldSendEvent bool)
|
||||
OnTrackUnsubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, shouldSendEvent bool)
|
||||
OnTrackSubscribeFailed(pID livekit.ParticipantID, trackID livekit.TrackID, err error, isUserError bool)
|
||||
OnTrackSubscribeStreamStarted(pID livekit.ParticipantID, ti *livekit.TrackInfo)
|
||||
OnTrackMuted(pID livekit.ParticipantID, ti *livekit.TrackInfo)
|
||||
OnTrackUnmuted(pID livekit.ParticipantID, ti *livekit.TrackInfo)
|
||||
OnTrackPublishedUpdate(pID livekit.ParticipantID, ti *livekit.TrackInfo)
|
||||
OnTrackMaxSubscribedVideoQuality(pID livekit.ParticipantID, ti *livekit.TrackInfo, mime mime.MimeType, maxQuality livekit.VideoQuality)
|
||||
OnTrackPublishRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, layer int, stats *livekit.RTPStats)
|
||||
OnTrackSubscribeRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, stats *livekit.RTPStats)
|
||||
|
||||
OnTrackStats(key telemetry.StatsKey, stat *livekit.AnalyticsStat)
|
||||
}
|
||||
|
||||
var _ ParticipantTelemetryListener = (*NullParticipantTelemetryListener)(nil)
|
||||
|
||||
type NullParticipantTelemetryListener struct{}
|
||||
|
||||
func (NullParticipantTelemetryListener) OnTrackPublishRequested(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackPublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackUnpublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackSubscribeRequested(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackSubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, publisherInfo *livekit.ParticipantInfo, shouldSendEvent bool) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackUnsubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, shouldSendEvent bool) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackSubscribeFailed(pID livekit.ParticipantID, trackID livekit.TrackID, err error, isUserError bool) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackSubscribeStreamStarted(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackMuted(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackUnmuted(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackPublishedUpdate(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackMaxSubscribedVideoQuality(pID livekit.ParticipantID, ti *livekit.TrackInfo, mime mime.MimeType, maxQuality livekit.VideoQuality) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackPublishRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, layer int, stats *livekit.RTPStats) {
|
||||
}
|
||||
func (NullParticipantTelemetryListener) OnTrackSubscribeRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, stats *livekit.RTPStats) {
|
||||
}
|
||||
|
||||
func (NullParticipantTelemetryListener) OnTrackStats(key telemetry.StatsKey, stat *livekit.AnalyticsStat) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
// Room is a container of participants, and can provide room-level actions
|
||||
//
|
||||
//counterfeiter:generate . Room
|
||||
@@ -474,11 +725,14 @@ type Room interface {
|
||||
Name() livekit.RoomName
|
||||
ID() livekit.RoomID
|
||||
RemoveParticipant(identity livekit.ParticipantIdentity, pID livekit.ParticipantID, reason ParticipantCloseReason)
|
||||
UpdateSubscriptions(participant LocalParticipant, trackIDs []livekit.TrackID, participantTracks []*livekit.ParticipantTracks, subscribe bool)
|
||||
UpdateSubscriptionPermission(participant LocalParticipant, permissions *livekit.SubscriptionPermission) error
|
||||
SyncState(participant LocalParticipant, state *livekit.SyncState) error
|
||||
SimulateScenario(participant LocalParticipant, scenario *livekit.SimulateScenario) error
|
||||
UpdateSubscriptions(
|
||||
participant LocalParticipant,
|
||||
trackIDs []livekit.TrackID,
|
||||
participantTracks []*livekit.ParticipantTracks,
|
||||
subscribe bool,
|
||||
)
|
||||
ResolveMediaTrackForSubscriber(sub LocalParticipant, trackID livekit.TrackID) MediaResolverResult
|
||||
ResolveDataTrackForSubscriber(sub LocalParticipant, trackID livekit.TrackID) DataResolverResult
|
||||
GetLocalParticipants() []LocalParticipant
|
||||
IsDataMessageUserPacketDuplicate(ip *livekit.UserPacket) bool
|
||||
}
|
||||
@@ -501,12 +755,11 @@ type MediaTrack interface {
|
||||
PublisherID() livekit.ParticipantID
|
||||
PublisherIdentity() livekit.ParticipantIdentity
|
||||
PublisherVersion() uint32
|
||||
Logger() logger.Logger
|
||||
|
||||
IsMuted() bool
|
||||
SetMuted(muted bool)
|
||||
|
||||
IsSimulcast() bool
|
||||
|
||||
GetAudioLevel() (level float64, active bool)
|
||||
|
||||
Close(isExpectedToResume bool)
|
||||
@@ -525,15 +778,16 @@ type MediaTrack interface {
|
||||
OnTrackSubscribed()
|
||||
|
||||
// returns quality information that's appropriate for width & height
|
||||
GetQualityForDimension(width, height uint32) livekit.VideoQuality
|
||||
GetQualityForDimension(mimeType mime.MimeType, width, height uint32) livekit.VideoQuality
|
||||
|
||||
// returns temporal layer that's appropriate for fps
|
||||
GetTemporalLayerForSpatialFps(spatial int32, fps uint32, mime mime.MimeType) int32
|
||||
GetTemporalLayerForSpatialFps(mimeType mime.MimeType, spatial int32, fps uint32) int32
|
||||
|
||||
Receivers() []sfu.TrackReceiver
|
||||
ClearAllReceivers(isExpectedToResume bool)
|
||||
|
||||
IsEncrypted() bool
|
||||
HasPacketTrailer() bool
|
||||
}
|
||||
|
||||
//counterfeiter:generate . LocalMediaTrack
|
||||
@@ -542,7 +796,7 @@ type LocalMediaTrack interface {
|
||||
|
||||
Restart()
|
||||
|
||||
SignalCid() string
|
||||
HasSignalCid(cid string) bool
|
||||
HasSdpCid(cid string) bool
|
||||
|
||||
GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality)
|
||||
@@ -551,9 +805,57 @@ type LocalMediaTrack interface {
|
||||
SetRTT(rtt uint32)
|
||||
|
||||
NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []SubscribedCodecQuality)
|
||||
NotifySubscriptionNode(nodeID livekit.NodeID, codecs []*livekit.SubscribedAudioCodec)
|
||||
ClearSubscriberNodes()
|
||||
NotifySubscriberNodeMediaLoss(nodeID livekit.NodeID, fractionalLoss uint8)
|
||||
}
|
||||
|
||||
// DataTrack represents a data track
|
||||
//
|
||||
//counterfeiter:generate . DataTrack
|
||||
type DataTrack interface {
|
||||
ID() livekit.TrackID
|
||||
PubHandle() uint16
|
||||
Name() string
|
||||
ToProto() *livekit.DataTrackInfo
|
||||
|
||||
PublisherID() livekit.ParticipantID
|
||||
PublisherIdentity() livekit.ParticipantIdentity
|
||||
|
||||
AddSubscriber(sub LocalParticipant) (DataDownTrack, error)
|
||||
RemoveSubscriber(participantID livekit.ParticipantID)
|
||||
IsSubscriber(subID livekit.ParticipantID) bool
|
||||
|
||||
AddDataDownTrack(sender DataTrackSender) error
|
||||
DeleteDataDownTrack(subscriberID livekit.ParticipantID)
|
||||
|
||||
HandlePacket(data []byte, packet *datatrack.Packet, arrivalTime int64)
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
//counterfeiter:generate . DataDownTrack
|
||||
type DataDownTrack interface {
|
||||
Close()
|
||||
|
||||
Handle() uint16
|
||||
PublishDataTrack() DataTrack
|
||||
|
||||
UpdateSubscriptionOptions(subscriptionOptions *livekit.DataTrackSubscriptionOptions)
|
||||
}
|
||||
|
||||
//counterfeiter:generate . DataTrackSender
|
||||
type DataTrackSender interface {
|
||||
SubscriberID() livekit.ParticipantID
|
||||
|
||||
WritePacket(data []byte, packet *datatrack.Packet, arrivalTime int64)
|
||||
}
|
||||
|
||||
//counterfeiter:generate . DataTrackTransport
|
||||
type DataTrackTransport interface {
|
||||
SendDataTrackMessage(data []byte) error
|
||||
}
|
||||
|
||||
//counterfeiter:generate . SubscribedTrack
|
||||
type SubscribedTrack interface {
|
||||
AddOnBind(f func(error))
|
||||
@@ -595,9 +897,20 @@ type MediaResolverResult struct {
|
||||
PublisherIdentity livekit.ParticipantIdentity
|
||||
}
|
||||
|
||||
type DataResolverResult struct {
|
||||
TrackChangedNotifier ChangeNotifier
|
||||
TrackRemovedNotifier ChangeNotifier
|
||||
DataTrack DataTrack
|
||||
PublisherID livekit.ParticipantID
|
||||
PublisherIdentity livekit.ParticipantIdentity
|
||||
}
|
||||
|
||||
// MediaTrackResolver locates a specific media track for a subscriber
|
||||
type MediaTrackResolver func(LocalParticipant, livekit.TrackID) MediaResolverResult
|
||||
|
||||
// DataTrackResolver locates a specific data track for a subscriber
|
||||
type DataTrackResolver func(LocalParticipant, livekit.TrackID) DataResolverResult
|
||||
|
||||
// Supervisor/operation monitor related definitions
|
||||
type OperationMonitorEvent int
|
||||
|
||||
@@ -626,7 +939,7 @@ func (o OperationMonitorEvent) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
type OperationMonitorData interface{}
|
||||
type OperationMonitorData any
|
||||
|
||||
type OperationMonitor interface {
|
||||
PostEvent(ome OperationMonitorEvent, omd OperationMonitorData)
|
||||
|
||||
@@ -16,7 +16,7 @@ package types
|
||||
|
||||
type ProtocolVersion int
|
||||
|
||||
const CurrentProtocol = 15
|
||||
const CurrentProtocol = 17
|
||||
|
||||
func (v ProtocolVersion) SupportsPackedStreamId() bool {
|
||||
return v > 0
|
||||
@@ -68,11 +68,11 @@ func (v ProtocolVersion) SupportFastStart() bool {
|
||||
return v > 7
|
||||
}
|
||||
|
||||
func (v ProtocolVersion) SupportHandlesDisconnectedUpdate() bool {
|
||||
func (v ProtocolVersion) SupportsDisconnectedUpdate() bool {
|
||||
return v > 8
|
||||
}
|
||||
|
||||
func (v ProtocolVersion) SupportSyncStreamID() bool {
|
||||
func (v ProtocolVersion) SupportsSyncStreamID() bool {
|
||||
return v > 9
|
||||
}
|
||||
|
||||
@@ -95,3 +95,11 @@ func (v ProtocolVersion) SupportsRegionsInLeaveRequest() bool {
|
||||
func (v ProtocolVersion) SupportsNonErrorSignalResponse() bool {
|
||||
return v > 14
|
||||
}
|
||||
|
||||
func (v ProtocolVersion) SupportsMoving() bool {
|
||||
return v > 15
|
||||
}
|
||||
|
||||
func (v ProtocolVersion) SupportsPacketTrailer() bool {
|
||||
return v > 16
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package typesfakes
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeDataDownTrack struct {
|
||||
CloseStub func()
|
||||
closeMutex sync.RWMutex
|
||||
closeArgsForCall []struct {
|
||||
}
|
||||
HandleStub func() uint16
|
||||
handleMutex sync.RWMutex
|
||||
handleArgsForCall []struct {
|
||||
}
|
||||
handleReturns struct {
|
||||
result1 uint16
|
||||
}
|
||||
handleReturnsOnCall map[int]struct {
|
||||
result1 uint16
|
||||
}
|
||||
PublishDataTrackStub func() types.DataTrack
|
||||
publishDataTrackMutex sync.RWMutex
|
||||
publishDataTrackArgsForCall []struct {
|
||||
}
|
||||
publishDataTrackReturns struct {
|
||||
result1 types.DataTrack
|
||||
}
|
||||
publishDataTrackReturnsOnCall map[int]struct {
|
||||
result1 types.DataTrack
|
||||
}
|
||||
UpdateSubscriptionOptionsStub func(*livekit.DataTrackSubscriptionOptions)
|
||||
updateSubscriptionOptionsMutex sync.RWMutex
|
||||
updateSubscriptionOptionsArgsForCall []struct {
|
||||
arg1 *livekit.DataTrackSubscriptionOptions
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) Close() {
|
||||
fake.closeMutex.Lock()
|
||||
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.CloseStub
|
||||
fake.recordInvocation("Close", []interface{}{})
|
||||
fake.closeMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.CloseStub()
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) CloseCallCount() int {
|
||||
fake.closeMutex.RLock()
|
||||
defer fake.closeMutex.RUnlock()
|
||||
return len(fake.closeArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) CloseCalls(stub func()) {
|
||||
fake.closeMutex.Lock()
|
||||
defer fake.closeMutex.Unlock()
|
||||
fake.CloseStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) Handle() uint16 {
|
||||
fake.handleMutex.Lock()
|
||||
ret, specificReturn := fake.handleReturnsOnCall[len(fake.handleArgsForCall)]
|
||||
fake.handleArgsForCall = append(fake.handleArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.HandleStub
|
||||
fakeReturns := fake.handleReturns
|
||||
fake.recordInvocation("Handle", []interface{}{})
|
||||
fake.handleMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) HandleCallCount() int {
|
||||
fake.handleMutex.RLock()
|
||||
defer fake.handleMutex.RUnlock()
|
||||
return len(fake.handleArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) HandleCalls(stub func() uint16) {
|
||||
fake.handleMutex.Lock()
|
||||
defer fake.handleMutex.Unlock()
|
||||
fake.HandleStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) HandleReturns(result1 uint16) {
|
||||
fake.handleMutex.Lock()
|
||||
defer fake.handleMutex.Unlock()
|
||||
fake.HandleStub = nil
|
||||
fake.handleReturns = struct {
|
||||
result1 uint16
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) HandleReturnsOnCall(i int, result1 uint16) {
|
||||
fake.handleMutex.Lock()
|
||||
defer fake.handleMutex.Unlock()
|
||||
fake.HandleStub = nil
|
||||
if fake.handleReturnsOnCall == nil {
|
||||
fake.handleReturnsOnCall = make(map[int]struct {
|
||||
result1 uint16
|
||||
})
|
||||
}
|
||||
fake.handleReturnsOnCall[i] = struct {
|
||||
result1 uint16
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) PublishDataTrack() types.DataTrack {
|
||||
fake.publishDataTrackMutex.Lock()
|
||||
ret, specificReturn := fake.publishDataTrackReturnsOnCall[len(fake.publishDataTrackArgsForCall)]
|
||||
fake.publishDataTrackArgsForCall = append(fake.publishDataTrackArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.PublishDataTrackStub
|
||||
fakeReturns := fake.publishDataTrackReturns
|
||||
fake.recordInvocation("PublishDataTrack", []interface{}{})
|
||||
fake.publishDataTrackMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) PublishDataTrackCallCount() int {
|
||||
fake.publishDataTrackMutex.RLock()
|
||||
defer fake.publishDataTrackMutex.RUnlock()
|
||||
return len(fake.publishDataTrackArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) PublishDataTrackCalls(stub func() types.DataTrack) {
|
||||
fake.publishDataTrackMutex.Lock()
|
||||
defer fake.publishDataTrackMutex.Unlock()
|
||||
fake.PublishDataTrackStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) PublishDataTrackReturns(result1 types.DataTrack) {
|
||||
fake.publishDataTrackMutex.Lock()
|
||||
defer fake.publishDataTrackMutex.Unlock()
|
||||
fake.PublishDataTrackStub = nil
|
||||
fake.publishDataTrackReturns = struct {
|
||||
result1 types.DataTrack
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) PublishDataTrackReturnsOnCall(i int, result1 types.DataTrack) {
|
||||
fake.publishDataTrackMutex.Lock()
|
||||
defer fake.publishDataTrackMutex.Unlock()
|
||||
fake.PublishDataTrackStub = nil
|
||||
if fake.publishDataTrackReturnsOnCall == nil {
|
||||
fake.publishDataTrackReturnsOnCall = make(map[int]struct {
|
||||
result1 types.DataTrack
|
||||
})
|
||||
}
|
||||
fake.publishDataTrackReturnsOnCall[i] = struct {
|
||||
result1 types.DataTrack
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) UpdateSubscriptionOptions(arg1 *livekit.DataTrackSubscriptionOptions) {
|
||||
fake.updateSubscriptionOptionsMutex.Lock()
|
||||
fake.updateSubscriptionOptionsArgsForCall = append(fake.updateSubscriptionOptionsArgsForCall, struct {
|
||||
arg1 *livekit.DataTrackSubscriptionOptions
|
||||
}{arg1})
|
||||
stub := fake.UpdateSubscriptionOptionsStub
|
||||
fake.recordInvocation("UpdateSubscriptionOptions", []interface{}{arg1})
|
||||
fake.updateSubscriptionOptionsMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.UpdateSubscriptionOptionsStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) UpdateSubscriptionOptionsCallCount() int {
|
||||
fake.updateSubscriptionOptionsMutex.RLock()
|
||||
defer fake.updateSubscriptionOptionsMutex.RUnlock()
|
||||
return len(fake.updateSubscriptionOptionsArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) UpdateSubscriptionOptionsCalls(stub func(*livekit.DataTrackSubscriptionOptions)) {
|
||||
fake.updateSubscriptionOptionsMutex.Lock()
|
||||
defer fake.updateSubscriptionOptionsMutex.Unlock()
|
||||
fake.UpdateSubscriptionOptionsStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) UpdateSubscriptionOptionsArgsForCall(i int) *livekit.DataTrackSubscriptionOptions {
|
||||
fake.updateSubscriptionOptionsMutex.RLock()
|
||||
defer fake.updateSubscriptionOptionsMutex.RUnlock()
|
||||
argsForCall := fake.updateSubscriptionOptionsArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeDataDownTrack) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ types.DataDownTrack = new(FakeDataDownTrack)
|
||||
@@ -0,0 +1,786 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package typesfakes
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeDataTrack struct {
|
||||
AddDataDownTrackStub func(types.DataTrackSender) error
|
||||
addDataDownTrackMutex sync.RWMutex
|
||||
addDataDownTrackArgsForCall []struct {
|
||||
arg1 types.DataTrackSender
|
||||
}
|
||||
addDataDownTrackReturns struct {
|
||||
result1 error
|
||||
}
|
||||
addDataDownTrackReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
AddSubscriberStub func(types.LocalParticipant) (types.DataDownTrack, error)
|
||||
addSubscriberMutex sync.RWMutex
|
||||
addSubscriberArgsForCall []struct {
|
||||
arg1 types.LocalParticipant
|
||||
}
|
||||
addSubscriberReturns struct {
|
||||
result1 types.DataDownTrack
|
||||
result2 error
|
||||
}
|
||||
addSubscriberReturnsOnCall map[int]struct {
|
||||
result1 types.DataDownTrack
|
||||
result2 error
|
||||
}
|
||||
CloseStub func()
|
||||
closeMutex sync.RWMutex
|
||||
closeArgsForCall []struct {
|
||||
}
|
||||
DeleteDataDownTrackStub func(livekit.ParticipantID)
|
||||
deleteDataDownTrackMutex sync.RWMutex
|
||||
deleteDataDownTrackArgsForCall []struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}
|
||||
HandlePacketStub func([]byte, *datatrack.Packet, int64)
|
||||
handlePacketMutex sync.RWMutex
|
||||
handlePacketArgsForCall []struct {
|
||||
arg1 []byte
|
||||
arg2 *datatrack.Packet
|
||||
arg3 int64
|
||||
}
|
||||
IDStub func() livekit.TrackID
|
||||
iDMutex sync.RWMutex
|
||||
iDArgsForCall []struct {
|
||||
}
|
||||
iDReturns struct {
|
||||
result1 livekit.TrackID
|
||||
}
|
||||
iDReturnsOnCall map[int]struct {
|
||||
result1 livekit.TrackID
|
||||
}
|
||||
IsSubscriberStub func(livekit.ParticipantID) bool
|
||||
isSubscriberMutex sync.RWMutex
|
||||
isSubscriberArgsForCall []struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}
|
||||
isSubscriberReturns struct {
|
||||
result1 bool
|
||||
}
|
||||
isSubscriberReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
NameStub func() string
|
||||
nameMutex sync.RWMutex
|
||||
nameArgsForCall []struct {
|
||||
}
|
||||
nameReturns struct {
|
||||
result1 string
|
||||
}
|
||||
nameReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
}
|
||||
PubHandleStub func() uint16
|
||||
pubHandleMutex sync.RWMutex
|
||||
pubHandleArgsForCall []struct {
|
||||
}
|
||||
pubHandleReturns struct {
|
||||
result1 uint16
|
||||
}
|
||||
pubHandleReturnsOnCall map[int]struct {
|
||||
result1 uint16
|
||||
}
|
||||
PublisherIDStub func() livekit.ParticipantID
|
||||
publisherIDMutex sync.RWMutex
|
||||
publisherIDArgsForCall []struct {
|
||||
}
|
||||
publisherIDReturns struct {
|
||||
result1 livekit.ParticipantID
|
||||
}
|
||||
publisherIDReturnsOnCall map[int]struct {
|
||||
result1 livekit.ParticipantID
|
||||
}
|
||||
PublisherIdentityStub func() livekit.ParticipantIdentity
|
||||
publisherIdentityMutex sync.RWMutex
|
||||
publisherIdentityArgsForCall []struct {
|
||||
}
|
||||
publisherIdentityReturns struct {
|
||||
result1 livekit.ParticipantIdentity
|
||||
}
|
||||
publisherIdentityReturnsOnCall map[int]struct {
|
||||
result1 livekit.ParticipantIdentity
|
||||
}
|
||||
RemoveSubscriberStub func(livekit.ParticipantID)
|
||||
removeSubscriberMutex sync.RWMutex
|
||||
removeSubscriberArgsForCall []struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}
|
||||
ToProtoStub func() *livekit.DataTrackInfo
|
||||
toProtoMutex sync.RWMutex
|
||||
toProtoArgsForCall []struct {
|
||||
}
|
||||
toProtoReturns struct {
|
||||
result1 *livekit.DataTrackInfo
|
||||
}
|
||||
toProtoReturnsOnCall map[int]struct {
|
||||
result1 *livekit.DataTrackInfo
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddDataDownTrack(arg1 types.DataTrackSender) error {
|
||||
fake.addDataDownTrackMutex.Lock()
|
||||
ret, specificReturn := fake.addDataDownTrackReturnsOnCall[len(fake.addDataDownTrackArgsForCall)]
|
||||
fake.addDataDownTrackArgsForCall = append(fake.addDataDownTrackArgsForCall, struct {
|
||||
arg1 types.DataTrackSender
|
||||
}{arg1})
|
||||
stub := fake.AddDataDownTrackStub
|
||||
fakeReturns := fake.addDataDownTrackReturns
|
||||
fake.recordInvocation("AddDataDownTrack", []interface{}{arg1})
|
||||
fake.addDataDownTrackMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddDataDownTrackCallCount() int {
|
||||
fake.addDataDownTrackMutex.RLock()
|
||||
defer fake.addDataDownTrackMutex.RUnlock()
|
||||
return len(fake.addDataDownTrackArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddDataDownTrackCalls(stub func(types.DataTrackSender) error) {
|
||||
fake.addDataDownTrackMutex.Lock()
|
||||
defer fake.addDataDownTrackMutex.Unlock()
|
||||
fake.AddDataDownTrackStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddDataDownTrackArgsForCall(i int) types.DataTrackSender {
|
||||
fake.addDataDownTrackMutex.RLock()
|
||||
defer fake.addDataDownTrackMutex.RUnlock()
|
||||
argsForCall := fake.addDataDownTrackArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddDataDownTrackReturns(result1 error) {
|
||||
fake.addDataDownTrackMutex.Lock()
|
||||
defer fake.addDataDownTrackMutex.Unlock()
|
||||
fake.AddDataDownTrackStub = nil
|
||||
fake.addDataDownTrackReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddDataDownTrackReturnsOnCall(i int, result1 error) {
|
||||
fake.addDataDownTrackMutex.Lock()
|
||||
defer fake.addDataDownTrackMutex.Unlock()
|
||||
fake.AddDataDownTrackStub = nil
|
||||
if fake.addDataDownTrackReturnsOnCall == nil {
|
||||
fake.addDataDownTrackReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.addDataDownTrackReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddSubscriber(arg1 types.LocalParticipant) (types.DataDownTrack, error) {
|
||||
fake.addSubscriberMutex.Lock()
|
||||
ret, specificReturn := fake.addSubscriberReturnsOnCall[len(fake.addSubscriberArgsForCall)]
|
||||
fake.addSubscriberArgsForCall = append(fake.addSubscriberArgsForCall, struct {
|
||||
arg1 types.LocalParticipant
|
||||
}{arg1})
|
||||
stub := fake.AddSubscriberStub
|
||||
fakeReturns := fake.addSubscriberReturns
|
||||
fake.recordInvocation("AddSubscriber", []interface{}{arg1})
|
||||
fake.addSubscriberMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddSubscriberCallCount() int {
|
||||
fake.addSubscriberMutex.RLock()
|
||||
defer fake.addSubscriberMutex.RUnlock()
|
||||
return len(fake.addSubscriberArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddSubscriberCalls(stub func(types.LocalParticipant) (types.DataDownTrack, error)) {
|
||||
fake.addSubscriberMutex.Lock()
|
||||
defer fake.addSubscriberMutex.Unlock()
|
||||
fake.AddSubscriberStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddSubscriberArgsForCall(i int) types.LocalParticipant {
|
||||
fake.addSubscriberMutex.RLock()
|
||||
defer fake.addSubscriberMutex.RUnlock()
|
||||
argsForCall := fake.addSubscriberArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddSubscriberReturns(result1 types.DataDownTrack, result2 error) {
|
||||
fake.addSubscriberMutex.Lock()
|
||||
defer fake.addSubscriberMutex.Unlock()
|
||||
fake.AddSubscriberStub = nil
|
||||
fake.addSubscriberReturns = struct {
|
||||
result1 types.DataDownTrack
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) AddSubscriberReturnsOnCall(i int, result1 types.DataDownTrack, result2 error) {
|
||||
fake.addSubscriberMutex.Lock()
|
||||
defer fake.addSubscriberMutex.Unlock()
|
||||
fake.AddSubscriberStub = nil
|
||||
if fake.addSubscriberReturnsOnCall == nil {
|
||||
fake.addSubscriberReturnsOnCall = make(map[int]struct {
|
||||
result1 types.DataDownTrack
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.addSubscriberReturnsOnCall[i] = struct {
|
||||
result1 types.DataDownTrack
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) Close() {
|
||||
fake.closeMutex.Lock()
|
||||
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.CloseStub
|
||||
fake.recordInvocation("Close", []interface{}{})
|
||||
fake.closeMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.CloseStub()
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) CloseCallCount() int {
|
||||
fake.closeMutex.RLock()
|
||||
defer fake.closeMutex.RUnlock()
|
||||
return len(fake.closeArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) CloseCalls(stub func()) {
|
||||
fake.closeMutex.Lock()
|
||||
defer fake.closeMutex.Unlock()
|
||||
fake.CloseStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) DeleteDataDownTrack(arg1 livekit.ParticipantID) {
|
||||
fake.deleteDataDownTrackMutex.Lock()
|
||||
fake.deleteDataDownTrackArgsForCall = append(fake.deleteDataDownTrackArgsForCall, struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}{arg1})
|
||||
stub := fake.DeleteDataDownTrackStub
|
||||
fake.recordInvocation("DeleteDataDownTrack", []interface{}{arg1})
|
||||
fake.deleteDataDownTrackMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.DeleteDataDownTrackStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) DeleteDataDownTrackCallCount() int {
|
||||
fake.deleteDataDownTrackMutex.RLock()
|
||||
defer fake.deleteDataDownTrackMutex.RUnlock()
|
||||
return len(fake.deleteDataDownTrackArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) DeleteDataDownTrackCalls(stub func(livekit.ParticipantID)) {
|
||||
fake.deleteDataDownTrackMutex.Lock()
|
||||
defer fake.deleteDataDownTrackMutex.Unlock()
|
||||
fake.DeleteDataDownTrackStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) DeleteDataDownTrackArgsForCall(i int) livekit.ParticipantID {
|
||||
fake.deleteDataDownTrackMutex.RLock()
|
||||
defer fake.deleteDataDownTrackMutex.RUnlock()
|
||||
argsForCall := fake.deleteDataDownTrackArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) HandlePacket(arg1 []byte, arg2 *datatrack.Packet, arg3 int64) {
|
||||
var arg1Copy []byte
|
||||
if arg1 != nil {
|
||||
arg1Copy = make([]byte, len(arg1))
|
||||
copy(arg1Copy, arg1)
|
||||
}
|
||||
fake.handlePacketMutex.Lock()
|
||||
fake.handlePacketArgsForCall = append(fake.handlePacketArgsForCall, struct {
|
||||
arg1 []byte
|
||||
arg2 *datatrack.Packet
|
||||
arg3 int64
|
||||
}{arg1Copy, arg2, arg3})
|
||||
stub := fake.HandlePacketStub
|
||||
fake.recordInvocation("HandlePacket", []interface{}{arg1Copy, arg2, arg3})
|
||||
fake.handlePacketMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.HandlePacketStub(arg1, arg2, arg3)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) HandlePacketCallCount() int {
|
||||
fake.handlePacketMutex.RLock()
|
||||
defer fake.handlePacketMutex.RUnlock()
|
||||
return len(fake.handlePacketArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) HandlePacketCalls(stub func([]byte, *datatrack.Packet, int64)) {
|
||||
fake.handlePacketMutex.Lock()
|
||||
defer fake.handlePacketMutex.Unlock()
|
||||
fake.HandlePacketStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) HandlePacketArgsForCall(i int) ([]byte, *datatrack.Packet, int64) {
|
||||
fake.handlePacketMutex.RLock()
|
||||
defer fake.handlePacketMutex.RUnlock()
|
||||
argsForCall := fake.handlePacketArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) ID() livekit.TrackID {
|
||||
fake.iDMutex.Lock()
|
||||
ret, specificReturn := fake.iDReturnsOnCall[len(fake.iDArgsForCall)]
|
||||
fake.iDArgsForCall = append(fake.iDArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.IDStub
|
||||
fakeReturns := fake.iDReturns
|
||||
fake.recordInvocation("ID", []interface{}{})
|
||||
fake.iDMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IDCallCount() int {
|
||||
fake.iDMutex.RLock()
|
||||
defer fake.iDMutex.RUnlock()
|
||||
return len(fake.iDArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IDCalls(stub func() livekit.TrackID) {
|
||||
fake.iDMutex.Lock()
|
||||
defer fake.iDMutex.Unlock()
|
||||
fake.IDStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IDReturns(result1 livekit.TrackID) {
|
||||
fake.iDMutex.Lock()
|
||||
defer fake.iDMutex.Unlock()
|
||||
fake.IDStub = nil
|
||||
fake.iDReturns = struct {
|
||||
result1 livekit.TrackID
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IDReturnsOnCall(i int, result1 livekit.TrackID) {
|
||||
fake.iDMutex.Lock()
|
||||
defer fake.iDMutex.Unlock()
|
||||
fake.IDStub = nil
|
||||
if fake.iDReturnsOnCall == nil {
|
||||
fake.iDReturnsOnCall = make(map[int]struct {
|
||||
result1 livekit.TrackID
|
||||
})
|
||||
}
|
||||
fake.iDReturnsOnCall[i] = struct {
|
||||
result1 livekit.TrackID
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IsSubscriber(arg1 livekit.ParticipantID) bool {
|
||||
fake.isSubscriberMutex.Lock()
|
||||
ret, specificReturn := fake.isSubscriberReturnsOnCall[len(fake.isSubscriberArgsForCall)]
|
||||
fake.isSubscriberArgsForCall = append(fake.isSubscriberArgsForCall, struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}{arg1})
|
||||
stub := fake.IsSubscriberStub
|
||||
fakeReturns := fake.isSubscriberReturns
|
||||
fake.recordInvocation("IsSubscriber", []interface{}{arg1})
|
||||
fake.isSubscriberMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IsSubscriberCallCount() int {
|
||||
fake.isSubscriberMutex.RLock()
|
||||
defer fake.isSubscriberMutex.RUnlock()
|
||||
return len(fake.isSubscriberArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IsSubscriberCalls(stub func(livekit.ParticipantID) bool) {
|
||||
fake.isSubscriberMutex.Lock()
|
||||
defer fake.isSubscriberMutex.Unlock()
|
||||
fake.IsSubscriberStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IsSubscriberArgsForCall(i int) livekit.ParticipantID {
|
||||
fake.isSubscriberMutex.RLock()
|
||||
defer fake.isSubscriberMutex.RUnlock()
|
||||
argsForCall := fake.isSubscriberArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IsSubscriberReturns(result1 bool) {
|
||||
fake.isSubscriberMutex.Lock()
|
||||
defer fake.isSubscriberMutex.Unlock()
|
||||
fake.IsSubscriberStub = nil
|
||||
fake.isSubscriberReturns = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) IsSubscriberReturnsOnCall(i int, result1 bool) {
|
||||
fake.isSubscriberMutex.Lock()
|
||||
defer fake.isSubscriberMutex.Unlock()
|
||||
fake.IsSubscriberStub = nil
|
||||
if fake.isSubscriberReturnsOnCall == nil {
|
||||
fake.isSubscriberReturnsOnCall = make(map[int]struct {
|
||||
result1 bool
|
||||
})
|
||||
}
|
||||
fake.isSubscriberReturnsOnCall[i] = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) Name() string {
|
||||
fake.nameMutex.Lock()
|
||||
ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)]
|
||||
fake.nameArgsForCall = append(fake.nameArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.NameStub
|
||||
fakeReturns := fake.nameReturns
|
||||
fake.recordInvocation("Name", []interface{}{})
|
||||
fake.nameMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) NameCallCount() int {
|
||||
fake.nameMutex.RLock()
|
||||
defer fake.nameMutex.RUnlock()
|
||||
return len(fake.nameArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) NameCalls(stub func() string) {
|
||||
fake.nameMutex.Lock()
|
||||
defer fake.nameMutex.Unlock()
|
||||
fake.NameStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) NameReturns(result1 string) {
|
||||
fake.nameMutex.Lock()
|
||||
defer fake.nameMutex.Unlock()
|
||||
fake.NameStub = nil
|
||||
fake.nameReturns = struct {
|
||||
result1 string
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) NameReturnsOnCall(i int, result1 string) {
|
||||
fake.nameMutex.Lock()
|
||||
defer fake.nameMutex.Unlock()
|
||||
fake.NameStub = nil
|
||||
if fake.nameReturnsOnCall == nil {
|
||||
fake.nameReturnsOnCall = make(map[int]struct {
|
||||
result1 string
|
||||
})
|
||||
}
|
||||
fake.nameReturnsOnCall[i] = struct {
|
||||
result1 string
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PubHandle() uint16 {
|
||||
fake.pubHandleMutex.Lock()
|
||||
ret, specificReturn := fake.pubHandleReturnsOnCall[len(fake.pubHandleArgsForCall)]
|
||||
fake.pubHandleArgsForCall = append(fake.pubHandleArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.PubHandleStub
|
||||
fakeReturns := fake.pubHandleReturns
|
||||
fake.recordInvocation("PubHandle", []interface{}{})
|
||||
fake.pubHandleMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PubHandleCallCount() int {
|
||||
fake.pubHandleMutex.RLock()
|
||||
defer fake.pubHandleMutex.RUnlock()
|
||||
return len(fake.pubHandleArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PubHandleCalls(stub func() uint16) {
|
||||
fake.pubHandleMutex.Lock()
|
||||
defer fake.pubHandleMutex.Unlock()
|
||||
fake.PubHandleStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PubHandleReturns(result1 uint16) {
|
||||
fake.pubHandleMutex.Lock()
|
||||
defer fake.pubHandleMutex.Unlock()
|
||||
fake.PubHandleStub = nil
|
||||
fake.pubHandleReturns = struct {
|
||||
result1 uint16
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PubHandleReturnsOnCall(i int, result1 uint16) {
|
||||
fake.pubHandleMutex.Lock()
|
||||
defer fake.pubHandleMutex.Unlock()
|
||||
fake.PubHandleStub = nil
|
||||
if fake.pubHandleReturnsOnCall == nil {
|
||||
fake.pubHandleReturnsOnCall = make(map[int]struct {
|
||||
result1 uint16
|
||||
})
|
||||
}
|
||||
fake.pubHandleReturnsOnCall[i] = struct {
|
||||
result1 uint16
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherID() livekit.ParticipantID {
|
||||
fake.publisherIDMutex.Lock()
|
||||
ret, specificReturn := fake.publisherIDReturnsOnCall[len(fake.publisherIDArgsForCall)]
|
||||
fake.publisherIDArgsForCall = append(fake.publisherIDArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.PublisherIDStub
|
||||
fakeReturns := fake.publisherIDReturns
|
||||
fake.recordInvocation("PublisherID", []interface{}{})
|
||||
fake.publisherIDMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIDCallCount() int {
|
||||
fake.publisherIDMutex.RLock()
|
||||
defer fake.publisherIDMutex.RUnlock()
|
||||
return len(fake.publisherIDArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIDCalls(stub func() livekit.ParticipantID) {
|
||||
fake.publisherIDMutex.Lock()
|
||||
defer fake.publisherIDMutex.Unlock()
|
||||
fake.PublisherIDStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIDReturns(result1 livekit.ParticipantID) {
|
||||
fake.publisherIDMutex.Lock()
|
||||
defer fake.publisherIDMutex.Unlock()
|
||||
fake.PublisherIDStub = nil
|
||||
fake.publisherIDReturns = struct {
|
||||
result1 livekit.ParticipantID
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIDReturnsOnCall(i int, result1 livekit.ParticipantID) {
|
||||
fake.publisherIDMutex.Lock()
|
||||
defer fake.publisherIDMutex.Unlock()
|
||||
fake.PublisherIDStub = nil
|
||||
if fake.publisherIDReturnsOnCall == nil {
|
||||
fake.publisherIDReturnsOnCall = make(map[int]struct {
|
||||
result1 livekit.ParticipantID
|
||||
})
|
||||
}
|
||||
fake.publisherIDReturnsOnCall[i] = struct {
|
||||
result1 livekit.ParticipantID
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIdentity() livekit.ParticipantIdentity {
|
||||
fake.publisherIdentityMutex.Lock()
|
||||
ret, specificReturn := fake.publisherIdentityReturnsOnCall[len(fake.publisherIdentityArgsForCall)]
|
||||
fake.publisherIdentityArgsForCall = append(fake.publisherIdentityArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.PublisherIdentityStub
|
||||
fakeReturns := fake.publisherIdentityReturns
|
||||
fake.recordInvocation("PublisherIdentity", []interface{}{})
|
||||
fake.publisherIdentityMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIdentityCallCount() int {
|
||||
fake.publisherIdentityMutex.RLock()
|
||||
defer fake.publisherIdentityMutex.RUnlock()
|
||||
return len(fake.publisherIdentityArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIdentityCalls(stub func() livekit.ParticipantIdentity) {
|
||||
fake.publisherIdentityMutex.Lock()
|
||||
defer fake.publisherIdentityMutex.Unlock()
|
||||
fake.PublisherIdentityStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIdentityReturns(result1 livekit.ParticipantIdentity) {
|
||||
fake.publisherIdentityMutex.Lock()
|
||||
defer fake.publisherIdentityMutex.Unlock()
|
||||
fake.PublisherIdentityStub = nil
|
||||
fake.publisherIdentityReturns = struct {
|
||||
result1 livekit.ParticipantIdentity
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) PublisherIdentityReturnsOnCall(i int, result1 livekit.ParticipantIdentity) {
|
||||
fake.publisherIdentityMutex.Lock()
|
||||
defer fake.publisherIdentityMutex.Unlock()
|
||||
fake.PublisherIdentityStub = nil
|
||||
if fake.publisherIdentityReturnsOnCall == nil {
|
||||
fake.publisherIdentityReturnsOnCall = make(map[int]struct {
|
||||
result1 livekit.ParticipantIdentity
|
||||
})
|
||||
}
|
||||
fake.publisherIdentityReturnsOnCall[i] = struct {
|
||||
result1 livekit.ParticipantIdentity
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) RemoveSubscriber(arg1 livekit.ParticipantID) {
|
||||
fake.removeSubscriberMutex.Lock()
|
||||
fake.removeSubscriberArgsForCall = append(fake.removeSubscriberArgsForCall, struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}{arg1})
|
||||
stub := fake.RemoveSubscriberStub
|
||||
fake.recordInvocation("RemoveSubscriber", []interface{}{arg1})
|
||||
fake.removeSubscriberMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.RemoveSubscriberStub(arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) RemoveSubscriberCallCount() int {
|
||||
fake.removeSubscriberMutex.RLock()
|
||||
defer fake.removeSubscriberMutex.RUnlock()
|
||||
return len(fake.removeSubscriberArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) RemoveSubscriberCalls(stub func(livekit.ParticipantID)) {
|
||||
fake.removeSubscriberMutex.Lock()
|
||||
defer fake.removeSubscriberMutex.Unlock()
|
||||
fake.RemoveSubscriberStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) RemoveSubscriberArgsForCall(i int) livekit.ParticipantID {
|
||||
fake.removeSubscriberMutex.RLock()
|
||||
defer fake.removeSubscriberMutex.RUnlock()
|
||||
argsForCall := fake.removeSubscriberArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) ToProto() *livekit.DataTrackInfo {
|
||||
fake.toProtoMutex.Lock()
|
||||
ret, specificReturn := fake.toProtoReturnsOnCall[len(fake.toProtoArgsForCall)]
|
||||
fake.toProtoArgsForCall = append(fake.toProtoArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.ToProtoStub
|
||||
fakeReturns := fake.toProtoReturns
|
||||
fake.recordInvocation("ToProto", []interface{}{})
|
||||
fake.toProtoMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) ToProtoCallCount() int {
|
||||
fake.toProtoMutex.RLock()
|
||||
defer fake.toProtoMutex.RUnlock()
|
||||
return len(fake.toProtoArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) ToProtoCalls(stub func() *livekit.DataTrackInfo) {
|
||||
fake.toProtoMutex.Lock()
|
||||
defer fake.toProtoMutex.Unlock()
|
||||
fake.ToProtoStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) ToProtoReturns(result1 *livekit.DataTrackInfo) {
|
||||
fake.toProtoMutex.Lock()
|
||||
defer fake.toProtoMutex.Unlock()
|
||||
fake.ToProtoStub = nil
|
||||
fake.toProtoReturns = struct {
|
||||
result1 *livekit.DataTrackInfo
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) ToProtoReturnsOnCall(i int, result1 *livekit.DataTrackInfo) {
|
||||
fake.toProtoMutex.Lock()
|
||||
defer fake.toProtoMutex.Unlock()
|
||||
fake.ToProtoStub = nil
|
||||
if fake.toProtoReturnsOnCall == nil {
|
||||
fake.toProtoReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.DataTrackInfo
|
||||
})
|
||||
}
|
||||
fake.toProtoReturnsOnCall[i] = struct {
|
||||
result1 *livekit.DataTrackInfo
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrack) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ types.DataTrack = new(FakeDataTrack)
|
||||
@@ -0,0 +1,148 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package typesfakes
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeDataTrackSender struct {
|
||||
SubscriberIDStub func() livekit.ParticipantID
|
||||
subscriberIDMutex sync.RWMutex
|
||||
subscriberIDArgsForCall []struct {
|
||||
}
|
||||
subscriberIDReturns struct {
|
||||
result1 livekit.ParticipantID
|
||||
}
|
||||
subscriberIDReturnsOnCall map[int]struct {
|
||||
result1 livekit.ParticipantID
|
||||
}
|
||||
WritePacketStub func([]byte, *datatrack.Packet, int64)
|
||||
writePacketMutex sync.RWMutex
|
||||
writePacketArgsForCall []struct {
|
||||
arg1 []byte
|
||||
arg2 *datatrack.Packet
|
||||
arg3 int64
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) SubscriberID() livekit.ParticipantID {
|
||||
fake.subscriberIDMutex.Lock()
|
||||
ret, specificReturn := fake.subscriberIDReturnsOnCall[len(fake.subscriberIDArgsForCall)]
|
||||
fake.subscriberIDArgsForCall = append(fake.subscriberIDArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.SubscriberIDStub
|
||||
fakeReturns := fake.subscriberIDReturns
|
||||
fake.recordInvocation("SubscriberID", []interface{}{})
|
||||
fake.subscriberIDMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) SubscriberIDCallCount() int {
|
||||
fake.subscriberIDMutex.RLock()
|
||||
defer fake.subscriberIDMutex.RUnlock()
|
||||
return len(fake.subscriberIDArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) SubscriberIDCalls(stub func() livekit.ParticipantID) {
|
||||
fake.subscriberIDMutex.Lock()
|
||||
defer fake.subscriberIDMutex.Unlock()
|
||||
fake.SubscriberIDStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) SubscriberIDReturns(result1 livekit.ParticipantID) {
|
||||
fake.subscriberIDMutex.Lock()
|
||||
defer fake.subscriberIDMutex.Unlock()
|
||||
fake.SubscriberIDStub = nil
|
||||
fake.subscriberIDReturns = struct {
|
||||
result1 livekit.ParticipantID
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) SubscriberIDReturnsOnCall(i int, result1 livekit.ParticipantID) {
|
||||
fake.subscriberIDMutex.Lock()
|
||||
defer fake.subscriberIDMutex.Unlock()
|
||||
fake.SubscriberIDStub = nil
|
||||
if fake.subscriberIDReturnsOnCall == nil {
|
||||
fake.subscriberIDReturnsOnCall = make(map[int]struct {
|
||||
result1 livekit.ParticipantID
|
||||
})
|
||||
}
|
||||
fake.subscriberIDReturnsOnCall[i] = struct {
|
||||
result1 livekit.ParticipantID
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) WritePacket(arg1 []byte, arg2 *datatrack.Packet, arg3 int64) {
|
||||
var arg1Copy []byte
|
||||
if arg1 != nil {
|
||||
arg1Copy = make([]byte, len(arg1))
|
||||
copy(arg1Copy, arg1)
|
||||
}
|
||||
fake.writePacketMutex.Lock()
|
||||
fake.writePacketArgsForCall = append(fake.writePacketArgsForCall, struct {
|
||||
arg1 []byte
|
||||
arg2 *datatrack.Packet
|
||||
arg3 int64
|
||||
}{arg1Copy, arg2, arg3})
|
||||
stub := fake.WritePacketStub
|
||||
fake.recordInvocation("WritePacket", []interface{}{arg1Copy, arg2, arg3})
|
||||
fake.writePacketMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.WritePacketStub(arg1, arg2, arg3)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) WritePacketCallCount() int {
|
||||
fake.writePacketMutex.RLock()
|
||||
defer fake.writePacketMutex.RUnlock()
|
||||
return len(fake.writePacketArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) WritePacketCalls(stub func([]byte, *datatrack.Packet, int64)) {
|
||||
fake.writePacketMutex.Lock()
|
||||
defer fake.writePacketMutex.Unlock()
|
||||
fake.WritePacketStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) WritePacketArgsForCall(i int) ([]byte, *datatrack.Packet, int64) {
|
||||
fake.writePacketMutex.RLock()
|
||||
defer fake.writePacketMutex.RUnlock()
|
||||
argsForCall := fake.writePacketArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackSender) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ types.DataTrackSender = new(FakeDataTrackSender)
|
||||
@@ -0,0 +1,114 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package typesfakes
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
type FakeDataTrackTransport struct {
|
||||
SendDataTrackMessageStub func([]byte) error
|
||||
sendDataTrackMessageMutex sync.RWMutex
|
||||
sendDataTrackMessageArgsForCall []struct {
|
||||
arg1 []byte
|
||||
}
|
||||
sendDataTrackMessageReturns struct {
|
||||
result1 error
|
||||
}
|
||||
sendDataTrackMessageReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) SendDataTrackMessage(arg1 []byte) error {
|
||||
var arg1Copy []byte
|
||||
if arg1 != nil {
|
||||
arg1Copy = make([]byte, len(arg1))
|
||||
copy(arg1Copy, arg1)
|
||||
}
|
||||
fake.sendDataTrackMessageMutex.Lock()
|
||||
ret, specificReturn := fake.sendDataTrackMessageReturnsOnCall[len(fake.sendDataTrackMessageArgsForCall)]
|
||||
fake.sendDataTrackMessageArgsForCall = append(fake.sendDataTrackMessageArgsForCall, struct {
|
||||
arg1 []byte
|
||||
}{arg1Copy})
|
||||
stub := fake.SendDataTrackMessageStub
|
||||
fakeReturns := fake.sendDataTrackMessageReturns
|
||||
fake.recordInvocation("SendDataTrackMessage", []interface{}{arg1Copy})
|
||||
fake.sendDataTrackMessageMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) SendDataTrackMessageCallCount() int {
|
||||
fake.sendDataTrackMessageMutex.RLock()
|
||||
defer fake.sendDataTrackMessageMutex.RUnlock()
|
||||
return len(fake.sendDataTrackMessageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) SendDataTrackMessageCalls(stub func([]byte) error) {
|
||||
fake.sendDataTrackMessageMutex.Lock()
|
||||
defer fake.sendDataTrackMessageMutex.Unlock()
|
||||
fake.SendDataTrackMessageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) SendDataTrackMessageArgsForCall(i int) []byte {
|
||||
fake.sendDataTrackMessageMutex.RLock()
|
||||
defer fake.sendDataTrackMessageMutex.RUnlock()
|
||||
argsForCall := fake.sendDataTrackMessageArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) SendDataTrackMessageReturns(result1 error) {
|
||||
fake.sendDataTrackMessageMutex.Lock()
|
||||
defer fake.sendDataTrackMessageMutex.Unlock()
|
||||
fake.SendDataTrackMessageStub = nil
|
||||
fake.sendDataTrackMessageReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) SendDataTrackMessageReturnsOnCall(i int, result1 error) {
|
||||
fake.sendDataTrackMessageMutex.Lock()
|
||||
defer fake.sendDataTrackMessageMutex.Unlock()
|
||||
fake.SendDataTrackMessageStub = nil
|
||||
if fake.sendDataTrackMessageReturnsOnCall == nil {
|
||||
fake.sendDataTrackMessageReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.sendDataTrackMessageReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeDataTrackTransport) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ types.DataTrackTransport = new(FakeDataTrackTransport)
|
||||
@@ -6,8 +6,9 @@ import (
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/mime"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
type FakeLocalMediaTrack struct {
|
||||
@@ -34,6 +35,10 @@ type FakeLocalMediaTrack struct {
|
||||
clearAllReceiversArgsForCall []struct {
|
||||
arg1 bool
|
||||
}
|
||||
ClearSubscriberNodesStub func()
|
||||
clearSubscriberNodesMutex sync.RWMutex
|
||||
clearSubscriberNodesArgsForCall []struct {
|
||||
}
|
||||
CloseStub func(bool)
|
||||
closeMutex sync.RWMutex
|
||||
closeArgsForCall []struct {
|
||||
@@ -83,11 +88,12 @@ type FakeLocalMediaTrack struct {
|
||||
getNumSubscribersReturnsOnCall map[int]struct {
|
||||
result1 int
|
||||
}
|
||||
GetQualityForDimensionStub func(uint32, uint32) livekit.VideoQuality
|
||||
GetQualityForDimensionStub func(mime.MimeType, uint32, uint32) livekit.VideoQuality
|
||||
getQualityForDimensionMutex sync.RWMutex
|
||||
getQualityForDimensionArgsForCall []struct {
|
||||
arg1 uint32
|
||||
arg1 mime.MimeType
|
||||
arg2 uint32
|
||||
arg3 uint32
|
||||
}
|
||||
getQualityForDimensionReturns struct {
|
||||
result1 livekit.VideoQuality
|
||||
@@ -95,12 +101,12 @@ type FakeLocalMediaTrack struct {
|
||||
getQualityForDimensionReturnsOnCall map[int]struct {
|
||||
result1 livekit.VideoQuality
|
||||
}
|
||||
GetTemporalLayerForSpatialFpsStub func(int32, uint32, mime.MimeType) int32
|
||||
GetTemporalLayerForSpatialFpsStub func(mime.MimeType, int32, uint32) int32
|
||||
getTemporalLayerForSpatialFpsMutex sync.RWMutex
|
||||
getTemporalLayerForSpatialFpsArgsForCall []struct {
|
||||
arg1 int32
|
||||
arg2 uint32
|
||||
arg3 mime.MimeType
|
||||
arg1 mime.MimeType
|
||||
arg2 int32
|
||||
arg3 uint32
|
||||
}
|
||||
getTemporalLayerForSpatialFpsReturns struct {
|
||||
result1 int32
|
||||
@@ -118,6 +124,16 @@ type FakeLocalMediaTrack struct {
|
||||
getTrackStatsReturnsOnCall map[int]struct {
|
||||
result1 *livekit.RTPStats
|
||||
}
|
||||
HasPacketTrailerStub func() bool
|
||||
hasPacketTrailerMutex sync.RWMutex
|
||||
hasPacketTrailerArgsForCall []struct {
|
||||
}
|
||||
hasPacketTrailerReturns struct {
|
||||
result1 bool
|
||||
}
|
||||
hasPacketTrailerReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
HasSdpCidStub func(string) bool
|
||||
hasSdpCidMutex sync.RWMutex
|
||||
hasSdpCidArgsForCall []struct {
|
||||
@@ -129,6 +145,17 @@ type FakeLocalMediaTrack struct {
|
||||
hasSdpCidReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
HasSignalCidStub func(string) bool
|
||||
hasSignalCidMutex sync.RWMutex
|
||||
hasSignalCidArgsForCall []struct {
|
||||
arg1 string
|
||||
}
|
||||
hasSignalCidReturns struct {
|
||||
result1 bool
|
||||
}
|
||||
hasSignalCidReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
IDStub func() livekit.TrackID
|
||||
iDMutex sync.RWMutex
|
||||
iDArgsForCall []struct {
|
||||
@@ -169,16 +196,6 @@ type FakeLocalMediaTrack struct {
|
||||
isOpenReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
IsSimulcastStub func() bool
|
||||
isSimulcastMutex sync.RWMutex
|
||||
isSimulcastArgsForCall []struct {
|
||||
}
|
||||
isSimulcastReturns struct {
|
||||
result1 bool
|
||||
}
|
||||
isSimulcastReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
IsSubscriberStub func(livekit.ParticipantID) bool
|
||||
isSubscriberMutex sync.RWMutex
|
||||
isSubscriberArgsForCall []struct {
|
||||
@@ -200,6 +217,16 @@ type FakeLocalMediaTrack struct {
|
||||
kindReturnsOnCall map[int]struct {
|
||||
result1 livekit.TrackType
|
||||
}
|
||||
LoggerStub func() logger.Logger
|
||||
loggerMutex sync.RWMutex
|
||||
loggerArgsForCall []struct {
|
||||
}
|
||||
loggerReturns struct {
|
||||
result1 logger.Logger
|
||||
}
|
||||
loggerReturnsOnCall map[int]struct {
|
||||
result1 logger.Logger
|
||||
}
|
||||
NameStub func() string
|
||||
nameMutex sync.RWMutex
|
||||
nameArgsForCall []struct {
|
||||
@@ -222,6 +249,12 @@ type FakeLocalMediaTrack struct {
|
||||
arg1 livekit.NodeID
|
||||
arg2 uint8
|
||||
}
|
||||
NotifySubscriptionNodeStub func(livekit.NodeID, []*livekit.SubscribedAudioCodec)
|
||||
notifySubscriptionNodeMutex sync.RWMutex
|
||||
notifySubscriptionNodeArgsForCall []struct {
|
||||
arg1 livekit.NodeID
|
||||
arg2 []*livekit.SubscribedAudioCodec
|
||||
}
|
||||
OnTrackSubscribedStub func()
|
||||
onTrackSubscribedMutex sync.RWMutex
|
||||
onTrackSubscribedArgsForCall []struct {
|
||||
@@ -297,16 +330,6 @@ type FakeLocalMediaTrack struct {
|
||||
setRTTArgsForCall []struct {
|
||||
arg1 uint32
|
||||
}
|
||||
SignalCidStub func() string
|
||||
signalCidMutex sync.RWMutex
|
||||
signalCidArgsForCall []struct {
|
||||
}
|
||||
signalCidReturns struct {
|
||||
result1 string
|
||||
}
|
||||
signalCidReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
}
|
||||
SourceStub func() livekit.TrackSource
|
||||
sourceMutex sync.RWMutex
|
||||
sourceArgsForCall []struct {
|
||||
@@ -484,6 +507,30 @@ func (fake *FakeLocalMediaTrack) ClearAllReceiversArgsForCall(i int) bool {
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) ClearSubscriberNodes() {
|
||||
fake.clearSubscriberNodesMutex.Lock()
|
||||
fake.clearSubscriberNodesArgsForCall = append(fake.clearSubscriberNodesArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.ClearSubscriberNodesStub
|
||||
fake.recordInvocation("ClearSubscriberNodes", []interface{}{})
|
||||
fake.clearSubscriberNodesMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.ClearSubscriberNodesStub()
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) ClearSubscriberNodesCallCount() int {
|
||||
fake.clearSubscriberNodesMutex.RLock()
|
||||
defer fake.clearSubscriberNodesMutex.RUnlock()
|
||||
return len(fake.clearSubscriberNodesArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) ClearSubscriberNodesCalls(stub func()) {
|
||||
fake.clearSubscriberNodesMutex.Lock()
|
||||
defer fake.clearSubscriberNodesMutex.Unlock()
|
||||
fake.ClearSubscriberNodesStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) Close(arg1 bool) {
|
||||
fake.closeMutex.Lock()
|
||||
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
|
||||
@@ -734,19 +781,20 @@ func (fake *FakeLocalMediaTrack) GetNumSubscribersReturnsOnCall(i int, result1 i
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) GetQualityForDimension(arg1 uint32, arg2 uint32) livekit.VideoQuality {
|
||||
func (fake *FakeLocalMediaTrack) GetQualityForDimension(arg1 mime.MimeType, arg2 uint32, arg3 uint32) livekit.VideoQuality {
|
||||
fake.getQualityForDimensionMutex.Lock()
|
||||
ret, specificReturn := fake.getQualityForDimensionReturnsOnCall[len(fake.getQualityForDimensionArgsForCall)]
|
||||
fake.getQualityForDimensionArgsForCall = append(fake.getQualityForDimensionArgsForCall, struct {
|
||||
arg1 uint32
|
||||
arg1 mime.MimeType
|
||||
arg2 uint32
|
||||
}{arg1, arg2})
|
||||
arg3 uint32
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.GetQualityForDimensionStub
|
||||
fakeReturns := fake.getQualityForDimensionReturns
|
||||
fake.recordInvocation("GetQualityForDimension", []interface{}{arg1, arg2})
|
||||
fake.recordInvocation("GetQualityForDimension", []interface{}{arg1, arg2, arg3})
|
||||
fake.getQualityForDimensionMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
@@ -760,17 +808,17 @@ func (fake *FakeLocalMediaTrack) GetQualityForDimensionCallCount() int {
|
||||
return len(fake.getQualityForDimensionArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) GetQualityForDimensionCalls(stub func(uint32, uint32) livekit.VideoQuality) {
|
||||
func (fake *FakeLocalMediaTrack) GetQualityForDimensionCalls(stub func(mime.MimeType, uint32, uint32) livekit.VideoQuality) {
|
||||
fake.getQualityForDimensionMutex.Lock()
|
||||
defer fake.getQualityForDimensionMutex.Unlock()
|
||||
fake.GetQualityForDimensionStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) GetQualityForDimensionArgsForCall(i int) (uint32, uint32) {
|
||||
func (fake *FakeLocalMediaTrack) GetQualityForDimensionArgsForCall(i int) (mime.MimeType, uint32, uint32) {
|
||||
fake.getQualityForDimensionMutex.RLock()
|
||||
defer fake.getQualityForDimensionMutex.RUnlock()
|
||||
argsForCall := fake.getQualityForDimensionArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) GetQualityForDimensionReturns(result1 livekit.VideoQuality) {
|
||||
@@ -796,13 +844,13 @@ func (fake *FakeLocalMediaTrack) GetQualityForDimensionReturnsOnCall(i int, resu
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFps(arg1 int32, arg2 uint32, arg3 mime.MimeType) int32 {
|
||||
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFps(arg1 mime.MimeType, arg2 int32, arg3 uint32) int32 {
|
||||
fake.getTemporalLayerForSpatialFpsMutex.Lock()
|
||||
ret, specificReturn := fake.getTemporalLayerForSpatialFpsReturnsOnCall[len(fake.getTemporalLayerForSpatialFpsArgsForCall)]
|
||||
fake.getTemporalLayerForSpatialFpsArgsForCall = append(fake.getTemporalLayerForSpatialFpsArgsForCall, struct {
|
||||
arg1 int32
|
||||
arg2 uint32
|
||||
arg3 mime.MimeType
|
||||
arg1 mime.MimeType
|
||||
arg2 int32
|
||||
arg3 uint32
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.GetTemporalLayerForSpatialFpsStub
|
||||
fakeReturns := fake.getTemporalLayerForSpatialFpsReturns
|
||||
@@ -823,13 +871,13 @@ func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsCallCount() int {
|
||||
return len(fake.getTemporalLayerForSpatialFpsArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsCalls(stub func(int32, uint32, mime.MimeType) int32) {
|
||||
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsCalls(stub func(mime.MimeType, int32, uint32) int32) {
|
||||
fake.getTemporalLayerForSpatialFpsMutex.Lock()
|
||||
defer fake.getTemporalLayerForSpatialFpsMutex.Unlock()
|
||||
fake.GetTemporalLayerForSpatialFpsStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsArgsForCall(i int) (int32, uint32, mime.MimeType) {
|
||||
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsArgsForCall(i int) (mime.MimeType, int32, uint32) {
|
||||
fake.getTemporalLayerForSpatialFpsMutex.RLock()
|
||||
defer fake.getTemporalLayerForSpatialFpsMutex.RUnlock()
|
||||
argsForCall := fake.getTemporalLayerForSpatialFpsArgsForCall[i]
|
||||
@@ -912,6 +960,59 @@ func (fake *FakeLocalMediaTrack) GetTrackStatsReturnsOnCall(i int, result1 *live
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasPacketTrailer() bool {
|
||||
fake.hasPacketTrailerMutex.Lock()
|
||||
ret, specificReturn := fake.hasPacketTrailerReturnsOnCall[len(fake.hasPacketTrailerArgsForCall)]
|
||||
fake.hasPacketTrailerArgsForCall = append(fake.hasPacketTrailerArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.HasPacketTrailerStub
|
||||
fakeReturns := fake.hasPacketTrailerReturns
|
||||
fake.recordInvocation("HasPacketTrailer", []interface{}{})
|
||||
fake.hasPacketTrailerMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasPacketTrailerCallCount() int {
|
||||
fake.hasPacketTrailerMutex.RLock()
|
||||
defer fake.hasPacketTrailerMutex.RUnlock()
|
||||
return len(fake.hasPacketTrailerArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasPacketTrailerCalls(stub func() bool) {
|
||||
fake.hasPacketTrailerMutex.Lock()
|
||||
defer fake.hasPacketTrailerMutex.Unlock()
|
||||
fake.HasPacketTrailerStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasPacketTrailerReturns(result1 bool) {
|
||||
fake.hasPacketTrailerMutex.Lock()
|
||||
defer fake.hasPacketTrailerMutex.Unlock()
|
||||
fake.HasPacketTrailerStub = nil
|
||||
fake.hasPacketTrailerReturns = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasPacketTrailerReturnsOnCall(i int, result1 bool) {
|
||||
fake.hasPacketTrailerMutex.Lock()
|
||||
defer fake.hasPacketTrailerMutex.Unlock()
|
||||
fake.HasPacketTrailerStub = nil
|
||||
if fake.hasPacketTrailerReturnsOnCall == nil {
|
||||
fake.hasPacketTrailerReturnsOnCall = make(map[int]struct {
|
||||
result1 bool
|
||||
})
|
||||
}
|
||||
fake.hasPacketTrailerReturnsOnCall[i] = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasSdpCid(arg1 string) bool {
|
||||
fake.hasSdpCidMutex.Lock()
|
||||
ret, specificReturn := fake.hasSdpCidReturnsOnCall[len(fake.hasSdpCidArgsForCall)]
|
||||
@@ -973,6 +1074,67 @@ func (fake *FakeLocalMediaTrack) HasSdpCidReturnsOnCall(i int, result1 bool) {
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasSignalCid(arg1 string) bool {
|
||||
fake.hasSignalCidMutex.Lock()
|
||||
ret, specificReturn := fake.hasSignalCidReturnsOnCall[len(fake.hasSignalCidArgsForCall)]
|
||||
fake.hasSignalCidArgsForCall = append(fake.hasSignalCidArgsForCall, struct {
|
||||
arg1 string
|
||||
}{arg1})
|
||||
stub := fake.HasSignalCidStub
|
||||
fakeReturns := fake.hasSignalCidReturns
|
||||
fake.recordInvocation("HasSignalCid", []interface{}{arg1})
|
||||
fake.hasSignalCidMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasSignalCidCallCount() int {
|
||||
fake.hasSignalCidMutex.RLock()
|
||||
defer fake.hasSignalCidMutex.RUnlock()
|
||||
return len(fake.hasSignalCidArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasSignalCidCalls(stub func(string) bool) {
|
||||
fake.hasSignalCidMutex.Lock()
|
||||
defer fake.hasSignalCidMutex.Unlock()
|
||||
fake.HasSignalCidStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasSignalCidArgsForCall(i int) string {
|
||||
fake.hasSignalCidMutex.RLock()
|
||||
defer fake.hasSignalCidMutex.RUnlock()
|
||||
argsForCall := fake.hasSignalCidArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasSignalCidReturns(result1 bool) {
|
||||
fake.hasSignalCidMutex.Lock()
|
||||
defer fake.hasSignalCidMutex.Unlock()
|
||||
fake.HasSignalCidStub = nil
|
||||
fake.hasSignalCidReturns = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) HasSignalCidReturnsOnCall(i int, result1 bool) {
|
||||
fake.hasSignalCidMutex.Lock()
|
||||
defer fake.hasSignalCidMutex.Unlock()
|
||||
fake.HasSignalCidStub = nil
|
||||
if fake.hasSignalCidReturnsOnCall == nil {
|
||||
fake.hasSignalCidReturnsOnCall = make(map[int]struct {
|
||||
result1 bool
|
||||
})
|
||||
}
|
||||
fake.hasSignalCidReturnsOnCall[i] = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) ID() livekit.TrackID {
|
||||
fake.iDMutex.Lock()
|
||||
ret, specificReturn := fake.iDReturnsOnCall[len(fake.iDArgsForCall)]
|
||||
@@ -1185,59 +1347,6 @@ func (fake *FakeLocalMediaTrack) IsOpenReturnsOnCall(i int, result1 bool) {
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) IsSimulcast() bool {
|
||||
fake.isSimulcastMutex.Lock()
|
||||
ret, specificReturn := fake.isSimulcastReturnsOnCall[len(fake.isSimulcastArgsForCall)]
|
||||
fake.isSimulcastArgsForCall = append(fake.isSimulcastArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.IsSimulcastStub
|
||||
fakeReturns := fake.isSimulcastReturns
|
||||
fake.recordInvocation("IsSimulcast", []interface{}{})
|
||||
fake.isSimulcastMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) IsSimulcastCallCount() int {
|
||||
fake.isSimulcastMutex.RLock()
|
||||
defer fake.isSimulcastMutex.RUnlock()
|
||||
return len(fake.isSimulcastArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) IsSimulcastCalls(stub func() bool) {
|
||||
fake.isSimulcastMutex.Lock()
|
||||
defer fake.isSimulcastMutex.Unlock()
|
||||
fake.IsSimulcastStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) IsSimulcastReturns(result1 bool) {
|
||||
fake.isSimulcastMutex.Lock()
|
||||
defer fake.isSimulcastMutex.Unlock()
|
||||
fake.IsSimulcastStub = nil
|
||||
fake.isSimulcastReturns = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) IsSimulcastReturnsOnCall(i int, result1 bool) {
|
||||
fake.isSimulcastMutex.Lock()
|
||||
defer fake.isSimulcastMutex.Unlock()
|
||||
fake.IsSimulcastStub = nil
|
||||
if fake.isSimulcastReturnsOnCall == nil {
|
||||
fake.isSimulcastReturnsOnCall = make(map[int]struct {
|
||||
result1 bool
|
||||
})
|
||||
}
|
||||
fake.isSimulcastReturnsOnCall[i] = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) IsSubscriber(arg1 livekit.ParticipantID) bool {
|
||||
fake.isSubscriberMutex.Lock()
|
||||
ret, specificReturn := fake.isSubscriberReturnsOnCall[len(fake.isSubscriberArgsForCall)]
|
||||
@@ -1352,6 +1461,59 @@ func (fake *FakeLocalMediaTrack) KindReturnsOnCall(i int, result1 livekit.TrackT
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) Logger() logger.Logger {
|
||||
fake.loggerMutex.Lock()
|
||||
ret, specificReturn := fake.loggerReturnsOnCall[len(fake.loggerArgsForCall)]
|
||||
fake.loggerArgsForCall = append(fake.loggerArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.LoggerStub
|
||||
fakeReturns := fake.loggerReturns
|
||||
fake.recordInvocation("Logger", []interface{}{})
|
||||
fake.loggerMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) LoggerCallCount() int {
|
||||
fake.loggerMutex.RLock()
|
||||
defer fake.loggerMutex.RUnlock()
|
||||
return len(fake.loggerArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) LoggerCalls(stub func() logger.Logger) {
|
||||
fake.loggerMutex.Lock()
|
||||
defer fake.loggerMutex.Unlock()
|
||||
fake.LoggerStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) LoggerReturns(result1 logger.Logger) {
|
||||
fake.loggerMutex.Lock()
|
||||
defer fake.loggerMutex.Unlock()
|
||||
fake.LoggerStub = nil
|
||||
fake.loggerReturns = struct {
|
||||
result1 logger.Logger
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) LoggerReturnsOnCall(i int, result1 logger.Logger) {
|
||||
fake.loggerMutex.Lock()
|
||||
defer fake.loggerMutex.Unlock()
|
||||
fake.LoggerStub = nil
|
||||
if fake.loggerReturnsOnCall == nil {
|
||||
fake.loggerReturnsOnCall = make(map[int]struct {
|
||||
result1 logger.Logger
|
||||
})
|
||||
}
|
||||
fake.loggerReturnsOnCall[i] = struct {
|
||||
result1 logger.Logger
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) Name() string {
|
||||
fake.nameMutex.Lock()
|
||||
ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)]
|
||||
@@ -1476,6 +1638,44 @@ func (fake *FakeLocalMediaTrack) NotifySubscriberNodeMediaLossArgsForCall(i int)
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) NotifySubscriptionNode(arg1 livekit.NodeID, arg2 []*livekit.SubscribedAudioCodec) {
|
||||
var arg2Copy []*livekit.SubscribedAudioCodec
|
||||
if arg2 != nil {
|
||||
arg2Copy = make([]*livekit.SubscribedAudioCodec, len(arg2))
|
||||
copy(arg2Copy, arg2)
|
||||
}
|
||||
fake.notifySubscriptionNodeMutex.Lock()
|
||||
fake.notifySubscriptionNodeArgsForCall = append(fake.notifySubscriptionNodeArgsForCall, struct {
|
||||
arg1 livekit.NodeID
|
||||
arg2 []*livekit.SubscribedAudioCodec
|
||||
}{arg1, arg2Copy})
|
||||
stub := fake.NotifySubscriptionNodeStub
|
||||
fake.recordInvocation("NotifySubscriptionNode", []interface{}{arg1, arg2Copy})
|
||||
fake.notifySubscriptionNodeMutex.Unlock()
|
||||
if stub != nil {
|
||||
fake.NotifySubscriptionNodeStub(arg1, arg2)
|
||||
}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) NotifySubscriptionNodeCallCount() int {
|
||||
fake.notifySubscriptionNodeMutex.RLock()
|
||||
defer fake.notifySubscriptionNodeMutex.RUnlock()
|
||||
return len(fake.notifySubscriptionNodeArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) NotifySubscriptionNodeCalls(stub func(livekit.NodeID, []*livekit.SubscribedAudioCodec)) {
|
||||
fake.notifySubscriptionNodeMutex.Lock()
|
||||
defer fake.notifySubscriptionNodeMutex.Unlock()
|
||||
fake.NotifySubscriptionNodeStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) NotifySubscriptionNodeArgsForCall(i int) (livekit.NodeID, []*livekit.SubscribedAudioCodec) {
|
||||
fake.notifySubscriptionNodeMutex.RLock()
|
||||
defer fake.notifySubscriptionNodeMutex.RUnlock()
|
||||
argsForCall := fake.notifySubscriptionNodeArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) OnTrackSubscribed() {
|
||||
fake.onTrackSubscribedMutex.Lock()
|
||||
fake.onTrackSubscribedArgsForCall = append(fake.onTrackSubscribedArgsForCall, struct {
|
||||
@@ -1899,59 +2099,6 @@ func (fake *FakeLocalMediaTrack) SetRTTArgsForCall(i int) uint32 {
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) SignalCid() string {
|
||||
fake.signalCidMutex.Lock()
|
||||
ret, specificReturn := fake.signalCidReturnsOnCall[len(fake.signalCidArgsForCall)]
|
||||
fake.signalCidArgsForCall = append(fake.signalCidArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.SignalCidStub
|
||||
fakeReturns := fake.signalCidReturns
|
||||
fake.recordInvocation("SignalCid", []interface{}{})
|
||||
fake.signalCidMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) SignalCidCallCount() int {
|
||||
fake.signalCidMutex.RLock()
|
||||
defer fake.signalCidMutex.RUnlock()
|
||||
return len(fake.signalCidArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) SignalCidCalls(stub func() string) {
|
||||
fake.signalCidMutex.Lock()
|
||||
defer fake.signalCidMutex.Unlock()
|
||||
fake.SignalCidStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) SignalCidReturns(result1 string) {
|
||||
fake.signalCidMutex.Lock()
|
||||
defer fake.signalCidMutex.Unlock()
|
||||
fake.SignalCidStub = nil
|
||||
fake.signalCidReturns = struct {
|
||||
result1 string
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) SignalCidReturnsOnCall(i int, result1 string) {
|
||||
fake.signalCidMutex.Lock()
|
||||
defer fake.signalCidMutex.Unlock()
|
||||
fake.SignalCidStub = nil
|
||||
if fake.signalCidReturnsOnCall == nil {
|
||||
fake.signalCidReturnsOnCall = make(map[int]struct {
|
||||
result1 string
|
||||
})
|
||||
}
|
||||
fake.signalCidReturnsOnCall[i] = struct {
|
||||
result1 string
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalMediaTrack) Source() livekit.TrackSource {
|
||||
fake.sourceMutex.Lock()
|
||||
ret, specificReturn := fake.sourceReturnsOnCall[len(fake.sourceArgsForCall)]
|
||||
@@ -2210,84 +2357,6 @@ func (fake *FakeLocalMediaTrack) UpdateVideoTrackArgsForCall(i int) *livekit.Upd
|
||||
func (fake *FakeLocalMediaTrack) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.addOnCloseMutex.RLock()
|
||||
defer fake.addOnCloseMutex.RUnlock()
|
||||
fake.addSubscriberMutex.RLock()
|
||||
defer fake.addSubscriberMutex.RUnlock()
|
||||
fake.clearAllReceiversMutex.RLock()
|
||||
defer fake.clearAllReceiversMutex.RUnlock()
|
||||
fake.closeMutex.RLock()
|
||||
defer fake.closeMutex.RUnlock()
|
||||
fake.getAllSubscribersMutex.RLock()
|
||||
defer fake.getAllSubscribersMutex.RUnlock()
|
||||
fake.getAudioLevelMutex.RLock()
|
||||
defer fake.getAudioLevelMutex.RUnlock()
|
||||
fake.getConnectionScoreAndQualityMutex.RLock()
|
||||
defer fake.getConnectionScoreAndQualityMutex.RUnlock()
|
||||
fake.getNumSubscribersMutex.RLock()
|
||||
defer fake.getNumSubscribersMutex.RUnlock()
|
||||
fake.getQualityForDimensionMutex.RLock()
|
||||
defer fake.getQualityForDimensionMutex.RUnlock()
|
||||
fake.getTemporalLayerForSpatialFpsMutex.RLock()
|
||||
defer fake.getTemporalLayerForSpatialFpsMutex.RUnlock()
|
||||
fake.getTrackStatsMutex.RLock()
|
||||
defer fake.getTrackStatsMutex.RUnlock()
|
||||
fake.hasSdpCidMutex.RLock()
|
||||
defer fake.hasSdpCidMutex.RUnlock()
|
||||
fake.iDMutex.RLock()
|
||||
defer fake.iDMutex.RUnlock()
|
||||
fake.isEncryptedMutex.RLock()
|
||||
defer fake.isEncryptedMutex.RUnlock()
|
||||
fake.isMutedMutex.RLock()
|
||||
defer fake.isMutedMutex.RUnlock()
|
||||
fake.isOpenMutex.RLock()
|
||||
defer fake.isOpenMutex.RUnlock()
|
||||
fake.isSimulcastMutex.RLock()
|
||||
defer fake.isSimulcastMutex.RUnlock()
|
||||
fake.isSubscriberMutex.RLock()
|
||||
defer fake.isSubscriberMutex.RUnlock()
|
||||
fake.kindMutex.RLock()
|
||||
defer fake.kindMutex.RUnlock()
|
||||
fake.nameMutex.RLock()
|
||||
defer fake.nameMutex.RUnlock()
|
||||
fake.notifySubscriberNodeMaxQualityMutex.RLock()
|
||||
defer fake.notifySubscriberNodeMaxQualityMutex.RUnlock()
|
||||
fake.notifySubscriberNodeMediaLossMutex.RLock()
|
||||
defer fake.notifySubscriberNodeMediaLossMutex.RUnlock()
|
||||
fake.onTrackSubscribedMutex.RLock()
|
||||
defer fake.onTrackSubscribedMutex.RUnlock()
|
||||
fake.publisherIDMutex.RLock()
|
||||
defer fake.publisherIDMutex.RUnlock()
|
||||
fake.publisherIdentityMutex.RLock()
|
||||
defer fake.publisherIdentityMutex.RUnlock()
|
||||
fake.publisherVersionMutex.RLock()
|
||||
defer fake.publisherVersionMutex.RUnlock()
|
||||
fake.receiversMutex.RLock()
|
||||
defer fake.receiversMutex.RUnlock()
|
||||
fake.removeSubscriberMutex.RLock()
|
||||
defer fake.removeSubscriberMutex.RUnlock()
|
||||
fake.restartMutex.RLock()
|
||||
defer fake.restartMutex.RUnlock()
|
||||
fake.revokeDisallowedSubscribersMutex.RLock()
|
||||
defer fake.revokeDisallowedSubscribersMutex.RUnlock()
|
||||
fake.setMutedMutex.RLock()
|
||||
defer fake.setMutedMutex.RUnlock()
|
||||
fake.setRTTMutex.RLock()
|
||||
defer fake.setRTTMutex.RUnlock()
|
||||
fake.signalCidMutex.RLock()
|
||||
defer fake.signalCidMutex.RUnlock()
|
||||
fake.sourceMutex.RLock()
|
||||
defer fake.sourceMutex.RUnlock()
|
||||
fake.streamMutex.RLock()
|
||||
defer fake.streamMutex.RUnlock()
|
||||
fake.toProtoMutex.RLock()
|
||||
defer fake.toProtoMutex.RUnlock()
|
||||
fake.updateAudioTrackMutex.RLock()
|
||||
defer fake.updateAudioTrackMutex.RUnlock()
|
||||
fake.updateTrackInfoMutex.RLock()
|
||||
defer fake.updateTrackInfoMutex.RUnlock()
|
||||
fake.updateVideoTrackMutex.RLock()
|
||||
defer fake.updateVideoTrackMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,542 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package typesfakes
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeLocalParticipantHelper struct {
|
||||
GetCachedReliableDataMessageStub func(map[livekit.ParticipantID]uint32) []*types.DataMessageCache
|
||||
getCachedReliableDataMessageMutex sync.RWMutex
|
||||
getCachedReliableDataMessageArgsForCall []struct {
|
||||
arg1 map[livekit.ParticipantID]uint32
|
||||
}
|
||||
getCachedReliableDataMessageReturns struct {
|
||||
result1 []*types.DataMessageCache
|
||||
}
|
||||
getCachedReliableDataMessageReturnsOnCall map[int]struct {
|
||||
result1 []*types.DataMessageCache
|
||||
}
|
||||
GetParticipantInfoStub func(livekit.ParticipantID) *livekit.ParticipantInfo
|
||||
getParticipantInfoMutex sync.RWMutex
|
||||
getParticipantInfoArgsForCall []struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}
|
||||
getParticipantInfoReturns struct {
|
||||
result1 *livekit.ParticipantInfo
|
||||
}
|
||||
getParticipantInfoReturnsOnCall map[int]struct {
|
||||
result1 *livekit.ParticipantInfo
|
||||
}
|
||||
GetRegionSettingsStub func(string) *livekit.RegionSettings
|
||||
getRegionSettingsMutex sync.RWMutex
|
||||
getRegionSettingsArgsForCall []struct {
|
||||
arg1 string
|
||||
}
|
||||
getRegionSettingsReturns struct {
|
||||
result1 *livekit.RegionSettings
|
||||
}
|
||||
getRegionSettingsReturnsOnCall map[int]struct {
|
||||
result1 *livekit.RegionSettings
|
||||
}
|
||||
GetSubscriberForwarderStateStub func(types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error)
|
||||
getSubscriberForwarderStateMutex sync.RWMutex
|
||||
getSubscriberForwarderStateArgsForCall []struct {
|
||||
arg1 types.LocalParticipant
|
||||
}
|
||||
getSubscriberForwarderStateReturns struct {
|
||||
result1 map[livekit.TrackID]*livekit.RTPForwarderState
|
||||
result2 error
|
||||
}
|
||||
getSubscriberForwarderStateReturnsOnCall map[int]struct {
|
||||
result1 map[livekit.TrackID]*livekit.RTPForwarderState
|
||||
result2 error
|
||||
}
|
||||
ResolveDataTrackStub func(types.LocalParticipant, livekit.TrackID) types.DataResolverResult
|
||||
resolveDataTrackMutex sync.RWMutex
|
||||
resolveDataTrackArgsForCall []struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 livekit.TrackID
|
||||
}
|
||||
resolveDataTrackReturns struct {
|
||||
result1 types.DataResolverResult
|
||||
}
|
||||
resolveDataTrackReturnsOnCall map[int]struct {
|
||||
result1 types.DataResolverResult
|
||||
}
|
||||
ResolveMediaTrackStub func(types.LocalParticipant, livekit.TrackID) types.MediaResolverResult
|
||||
resolveMediaTrackMutex sync.RWMutex
|
||||
resolveMediaTrackArgsForCall []struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 livekit.TrackID
|
||||
}
|
||||
resolveMediaTrackReturns struct {
|
||||
result1 types.MediaResolverResult
|
||||
}
|
||||
resolveMediaTrackReturnsOnCall map[int]struct {
|
||||
result1 types.MediaResolverResult
|
||||
}
|
||||
ShouldRegressCodecStub func() bool
|
||||
shouldRegressCodecMutex sync.RWMutex
|
||||
shouldRegressCodecArgsForCall []struct {
|
||||
}
|
||||
shouldRegressCodecReturns struct {
|
||||
result1 bool
|
||||
}
|
||||
shouldRegressCodecReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessage(arg1 map[livekit.ParticipantID]uint32) []*types.DataMessageCache {
|
||||
fake.getCachedReliableDataMessageMutex.Lock()
|
||||
ret, specificReturn := fake.getCachedReliableDataMessageReturnsOnCall[len(fake.getCachedReliableDataMessageArgsForCall)]
|
||||
fake.getCachedReliableDataMessageArgsForCall = append(fake.getCachedReliableDataMessageArgsForCall, struct {
|
||||
arg1 map[livekit.ParticipantID]uint32
|
||||
}{arg1})
|
||||
stub := fake.GetCachedReliableDataMessageStub
|
||||
fakeReturns := fake.getCachedReliableDataMessageReturns
|
||||
fake.recordInvocation("GetCachedReliableDataMessage", []interface{}{arg1})
|
||||
fake.getCachedReliableDataMessageMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageCallCount() int {
|
||||
fake.getCachedReliableDataMessageMutex.RLock()
|
||||
defer fake.getCachedReliableDataMessageMutex.RUnlock()
|
||||
return len(fake.getCachedReliableDataMessageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageCalls(stub func(map[livekit.ParticipantID]uint32) []*types.DataMessageCache) {
|
||||
fake.getCachedReliableDataMessageMutex.Lock()
|
||||
defer fake.getCachedReliableDataMessageMutex.Unlock()
|
||||
fake.GetCachedReliableDataMessageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageArgsForCall(i int) map[livekit.ParticipantID]uint32 {
|
||||
fake.getCachedReliableDataMessageMutex.RLock()
|
||||
defer fake.getCachedReliableDataMessageMutex.RUnlock()
|
||||
argsForCall := fake.getCachedReliableDataMessageArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageReturns(result1 []*types.DataMessageCache) {
|
||||
fake.getCachedReliableDataMessageMutex.Lock()
|
||||
defer fake.getCachedReliableDataMessageMutex.Unlock()
|
||||
fake.GetCachedReliableDataMessageStub = nil
|
||||
fake.getCachedReliableDataMessageReturns = struct {
|
||||
result1 []*types.DataMessageCache
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageReturnsOnCall(i int, result1 []*types.DataMessageCache) {
|
||||
fake.getCachedReliableDataMessageMutex.Lock()
|
||||
defer fake.getCachedReliableDataMessageMutex.Unlock()
|
||||
fake.GetCachedReliableDataMessageStub = nil
|
||||
if fake.getCachedReliableDataMessageReturnsOnCall == nil {
|
||||
fake.getCachedReliableDataMessageReturnsOnCall = make(map[int]struct {
|
||||
result1 []*types.DataMessageCache
|
||||
})
|
||||
}
|
||||
fake.getCachedReliableDataMessageReturnsOnCall[i] = struct {
|
||||
result1 []*types.DataMessageCache
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetParticipantInfo(arg1 livekit.ParticipantID) *livekit.ParticipantInfo {
|
||||
fake.getParticipantInfoMutex.Lock()
|
||||
ret, specificReturn := fake.getParticipantInfoReturnsOnCall[len(fake.getParticipantInfoArgsForCall)]
|
||||
fake.getParticipantInfoArgsForCall = append(fake.getParticipantInfoArgsForCall, struct {
|
||||
arg1 livekit.ParticipantID
|
||||
}{arg1})
|
||||
stub := fake.GetParticipantInfoStub
|
||||
fakeReturns := fake.getParticipantInfoReturns
|
||||
fake.recordInvocation("GetParticipantInfo", []interface{}{arg1})
|
||||
fake.getParticipantInfoMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetParticipantInfoCallCount() int {
|
||||
fake.getParticipantInfoMutex.RLock()
|
||||
defer fake.getParticipantInfoMutex.RUnlock()
|
||||
return len(fake.getParticipantInfoArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetParticipantInfoCalls(stub func(livekit.ParticipantID) *livekit.ParticipantInfo) {
|
||||
fake.getParticipantInfoMutex.Lock()
|
||||
defer fake.getParticipantInfoMutex.Unlock()
|
||||
fake.GetParticipantInfoStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetParticipantInfoArgsForCall(i int) livekit.ParticipantID {
|
||||
fake.getParticipantInfoMutex.RLock()
|
||||
defer fake.getParticipantInfoMutex.RUnlock()
|
||||
argsForCall := fake.getParticipantInfoArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetParticipantInfoReturns(result1 *livekit.ParticipantInfo) {
|
||||
fake.getParticipantInfoMutex.Lock()
|
||||
defer fake.getParticipantInfoMutex.Unlock()
|
||||
fake.GetParticipantInfoStub = nil
|
||||
fake.getParticipantInfoReturns = struct {
|
||||
result1 *livekit.ParticipantInfo
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetParticipantInfoReturnsOnCall(i int, result1 *livekit.ParticipantInfo) {
|
||||
fake.getParticipantInfoMutex.Lock()
|
||||
defer fake.getParticipantInfoMutex.Unlock()
|
||||
fake.GetParticipantInfoStub = nil
|
||||
if fake.getParticipantInfoReturnsOnCall == nil {
|
||||
fake.getParticipantInfoReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.ParticipantInfo
|
||||
})
|
||||
}
|
||||
fake.getParticipantInfoReturnsOnCall[i] = struct {
|
||||
result1 *livekit.ParticipantInfo
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetRegionSettings(arg1 string) *livekit.RegionSettings {
|
||||
fake.getRegionSettingsMutex.Lock()
|
||||
ret, specificReturn := fake.getRegionSettingsReturnsOnCall[len(fake.getRegionSettingsArgsForCall)]
|
||||
fake.getRegionSettingsArgsForCall = append(fake.getRegionSettingsArgsForCall, struct {
|
||||
arg1 string
|
||||
}{arg1})
|
||||
stub := fake.GetRegionSettingsStub
|
||||
fakeReturns := fake.getRegionSettingsReturns
|
||||
fake.recordInvocation("GetRegionSettings", []interface{}{arg1})
|
||||
fake.getRegionSettingsMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetRegionSettingsCallCount() int {
|
||||
fake.getRegionSettingsMutex.RLock()
|
||||
defer fake.getRegionSettingsMutex.RUnlock()
|
||||
return len(fake.getRegionSettingsArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetRegionSettingsCalls(stub func(string) *livekit.RegionSettings) {
|
||||
fake.getRegionSettingsMutex.Lock()
|
||||
defer fake.getRegionSettingsMutex.Unlock()
|
||||
fake.GetRegionSettingsStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetRegionSettingsArgsForCall(i int) string {
|
||||
fake.getRegionSettingsMutex.RLock()
|
||||
defer fake.getRegionSettingsMutex.RUnlock()
|
||||
argsForCall := fake.getRegionSettingsArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetRegionSettingsReturns(result1 *livekit.RegionSettings) {
|
||||
fake.getRegionSettingsMutex.Lock()
|
||||
defer fake.getRegionSettingsMutex.Unlock()
|
||||
fake.GetRegionSettingsStub = nil
|
||||
fake.getRegionSettingsReturns = struct {
|
||||
result1 *livekit.RegionSettings
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetRegionSettingsReturnsOnCall(i int, result1 *livekit.RegionSettings) {
|
||||
fake.getRegionSettingsMutex.Lock()
|
||||
defer fake.getRegionSettingsMutex.Unlock()
|
||||
fake.GetRegionSettingsStub = nil
|
||||
if fake.getRegionSettingsReturnsOnCall == nil {
|
||||
fake.getRegionSettingsReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.RegionSettings
|
||||
})
|
||||
}
|
||||
fake.getRegionSettingsReturnsOnCall[i] = struct {
|
||||
result1 *livekit.RegionSettings
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderState(arg1 types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error) {
|
||||
fake.getSubscriberForwarderStateMutex.Lock()
|
||||
ret, specificReturn := fake.getSubscriberForwarderStateReturnsOnCall[len(fake.getSubscriberForwarderStateArgsForCall)]
|
||||
fake.getSubscriberForwarderStateArgsForCall = append(fake.getSubscriberForwarderStateArgsForCall, struct {
|
||||
arg1 types.LocalParticipant
|
||||
}{arg1})
|
||||
stub := fake.GetSubscriberForwarderStateStub
|
||||
fakeReturns := fake.getSubscriberForwarderStateReturns
|
||||
fake.recordInvocation("GetSubscriberForwarderState", []interface{}{arg1})
|
||||
fake.getSubscriberForwarderStateMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateCallCount() int {
|
||||
fake.getSubscriberForwarderStateMutex.RLock()
|
||||
defer fake.getSubscriberForwarderStateMutex.RUnlock()
|
||||
return len(fake.getSubscriberForwarderStateArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateCalls(stub func(types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error)) {
|
||||
fake.getSubscriberForwarderStateMutex.Lock()
|
||||
defer fake.getSubscriberForwarderStateMutex.Unlock()
|
||||
fake.GetSubscriberForwarderStateStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateArgsForCall(i int) types.LocalParticipant {
|
||||
fake.getSubscriberForwarderStateMutex.RLock()
|
||||
defer fake.getSubscriberForwarderStateMutex.RUnlock()
|
||||
argsForCall := fake.getSubscriberForwarderStateArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateReturns(result1 map[livekit.TrackID]*livekit.RTPForwarderState, result2 error) {
|
||||
fake.getSubscriberForwarderStateMutex.Lock()
|
||||
defer fake.getSubscriberForwarderStateMutex.Unlock()
|
||||
fake.GetSubscriberForwarderStateStub = nil
|
||||
fake.getSubscriberForwarderStateReturns = struct {
|
||||
result1 map[livekit.TrackID]*livekit.RTPForwarderState
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateReturnsOnCall(i int, result1 map[livekit.TrackID]*livekit.RTPForwarderState, result2 error) {
|
||||
fake.getSubscriberForwarderStateMutex.Lock()
|
||||
defer fake.getSubscriberForwarderStateMutex.Unlock()
|
||||
fake.GetSubscriberForwarderStateStub = nil
|
||||
if fake.getSubscriberForwarderStateReturnsOnCall == nil {
|
||||
fake.getSubscriberForwarderStateReturnsOnCall = make(map[int]struct {
|
||||
result1 map[livekit.TrackID]*livekit.RTPForwarderState
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.getSubscriberForwarderStateReturnsOnCall[i] = struct {
|
||||
result1 map[livekit.TrackID]*livekit.RTPForwarderState
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveDataTrack(arg1 types.LocalParticipant, arg2 livekit.TrackID) types.DataResolverResult {
|
||||
fake.resolveDataTrackMutex.Lock()
|
||||
ret, specificReturn := fake.resolveDataTrackReturnsOnCall[len(fake.resolveDataTrackArgsForCall)]
|
||||
fake.resolveDataTrackArgsForCall = append(fake.resolveDataTrackArgsForCall, struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 livekit.TrackID
|
||||
}{arg1, arg2})
|
||||
stub := fake.ResolveDataTrackStub
|
||||
fakeReturns := fake.resolveDataTrackReturns
|
||||
fake.recordInvocation("ResolveDataTrack", []interface{}{arg1, arg2})
|
||||
fake.resolveDataTrackMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveDataTrackCallCount() int {
|
||||
fake.resolveDataTrackMutex.RLock()
|
||||
defer fake.resolveDataTrackMutex.RUnlock()
|
||||
return len(fake.resolveDataTrackArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveDataTrackCalls(stub func(types.LocalParticipant, livekit.TrackID) types.DataResolverResult) {
|
||||
fake.resolveDataTrackMutex.Lock()
|
||||
defer fake.resolveDataTrackMutex.Unlock()
|
||||
fake.ResolveDataTrackStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveDataTrackArgsForCall(i int) (types.LocalParticipant, livekit.TrackID) {
|
||||
fake.resolveDataTrackMutex.RLock()
|
||||
defer fake.resolveDataTrackMutex.RUnlock()
|
||||
argsForCall := fake.resolveDataTrackArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveDataTrackReturns(result1 types.DataResolverResult) {
|
||||
fake.resolveDataTrackMutex.Lock()
|
||||
defer fake.resolveDataTrackMutex.Unlock()
|
||||
fake.ResolveDataTrackStub = nil
|
||||
fake.resolveDataTrackReturns = struct {
|
||||
result1 types.DataResolverResult
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveDataTrackReturnsOnCall(i int, result1 types.DataResolverResult) {
|
||||
fake.resolveDataTrackMutex.Lock()
|
||||
defer fake.resolveDataTrackMutex.Unlock()
|
||||
fake.ResolveDataTrackStub = nil
|
||||
if fake.resolveDataTrackReturnsOnCall == nil {
|
||||
fake.resolveDataTrackReturnsOnCall = make(map[int]struct {
|
||||
result1 types.DataResolverResult
|
||||
})
|
||||
}
|
||||
fake.resolveDataTrackReturnsOnCall[i] = struct {
|
||||
result1 types.DataResolverResult
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveMediaTrack(arg1 types.LocalParticipant, arg2 livekit.TrackID) types.MediaResolverResult {
|
||||
fake.resolveMediaTrackMutex.Lock()
|
||||
ret, specificReturn := fake.resolveMediaTrackReturnsOnCall[len(fake.resolveMediaTrackArgsForCall)]
|
||||
fake.resolveMediaTrackArgsForCall = append(fake.resolveMediaTrackArgsForCall, struct {
|
||||
arg1 types.LocalParticipant
|
||||
arg2 livekit.TrackID
|
||||
}{arg1, arg2})
|
||||
stub := fake.ResolveMediaTrackStub
|
||||
fakeReturns := fake.resolveMediaTrackReturns
|
||||
fake.recordInvocation("ResolveMediaTrack", []interface{}{arg1, arg2})
|
||||
fake.resolveMediaTrackMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackCallCount() int {
|
||||
fake.resolveMediaTrackMutex.RLock()
|
||||
defer fake.resolveMediaTrackMutex.RUnlock()
|
||||
return len(fake.resolveMediaTrackArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackCalls(stub func(types.LocalParticipant, livekit.TrackID) types.MediaResolverResult) {
|
||||
fake.resolveMediaTrackMutex.Lock()
|
||||
defer fake.resolveMediaTrackMutex.Unlock()
|
||||
fake.ResolveMediaTrackStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackArgsForCall(i int) (types.LocalParticipant, livekit.TrackID) {
|
||||
fake.resolveMediaTrackMutex.RLock()
|
||||
defer fake.resolveMediaTrackMutex.RUnlock()
|
||||
argsForCall := fake.resolveMediaTrackArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackReturns(result1 types.MediaResolverResult) {
|
||||
fake.resolveMediaTrackMutex.Lock()
|
||||
defer fake.resolveMediaTrackMutex.Unlock()
|
||||
fake.ResolveMediaTrackStub = nil
|
||||
fake.resolveMediaTrackReturns = struct {
|
||||
result1 types.MediaResolverResult
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackReturnsOnCall(i int, result1 types.MediaResolverResult) {
|
||||
fake.resolveMediaTrackMutex.Lock()
|
||||
defer fake.resolveMediaTrackMutex.Unlock()
|
||||
fake.ResolveMediaTrackStub = nil
|
||||
if fake.resolveMediaTrackReturnsOnCall == nil {
|
||||
fake.resolveMediaTrackReturnsOnCall = make(map[int]struct {
|
||||
result1 types.MediaResolverResult
|
||||
})
|
||||
}
|
||||
fake.resolveMediaTrackReturnsOnCall[i] = struct {
|
||||
result1 types.MediaResolverResult
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ShouldRegressCodec() bool {
|
||||
fake.shouldRegressCodecMutex.Lock()
|
||||
ret, specificReturn := fake.shouldRegressCodecReturnsOnCall[len(fake.shouldRegressCodecArgsForCall)]
|
||||
fake.shouldRegressCodecArgsForCall = append(fake.shouldRegressCodecArgsForCall, struct {
|
||||
}{})
|
||||
stub := fake.ShouldRegressCodecStub
|
||||
fakeReturns := fake.shouldRegressCodecReturns
|
||||
fake.recordInvocation("ShouldRegressCodec", []interface{}{})
|
||||
fake.shouldRegressCodecMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub()
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecCallCount() int {
|
||||
fake.shouldRegressCodecMutex.RLock()
|
||||
defer fake.shouldRegressCodecMutex.RUnlock()
|
||||
return len(fake.shouldRegressCodecArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecCalls(stub func() bool) {
|
||||
fake.shouldRegressCodecMutex.Lock()
|
||||
defer fake.shouldRegressCodecMutex.Unlock()
|
||||
fake.ShouldRegressCodecStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecReturns(result1 bool) {
|
||||
fake.shouldRegressCodecMutex.Lock()
|
||||
defer fake.shouldRegressCodecMutex.Unlock()
|
||||
fake.ShouldRegressCodecStub = nil
|
||||
fake.shouldRegressCodecReturns = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecReturnsOnCall(i int, result1 bool) {
|
||||
fake.shouldRegressCodecMutex.Lock()
|
||||
defer fake.shouldRegressCodecMutex.Unlock()
|
||||
fake.ShouldRegressCodecStub = nil
|
||||
if fake.shouldRegressCodecReturnsOnCall == nil {
|
||||
fake.shouldRegressCodecReturnsOnCall = make(map[int]struct {
|
||||
result1 bool
|
||||
})
|
||||
}
|
||||
fake.shouldRegressCodecReturnsOnCall[i] = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeLocalParticipantHelper) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ types.LocalParticipantHelper = new(FakeLocalParticipantHelper)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user