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:
+150
-918
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -82,7 +83,7 @@ func TestNack(t *testing.T) {
|
||||
}, vp8Codec.RTPCodecCapability, 0)
|
||||
rtt := uint32(20)
|
||||
buff.nacker.SetRTT(rtt)
|
||||
for i := 0; i < 15; i++ {
|
||||
for i := range 15 {
|
||||
if i == 1 {
|
||||
continue
|
||||
}
|
||||
@@ -147,7 +148,7 @@ func TestNack(t *testing.T) {
|
||||
}, vp8Codec.RTPCodecCapability, 0)
|
||||
rtt := uint32(30)
|
||||
buff.nacker.SetRTT(rtt)
|
||||
for i := 0; i < 15; i++ {
|
||||
for i := range 15 {
|
||||
if i > 0 && i < 5 {
|
||||
continue
|
||||
}
|
||||
@@ -265,7 +266,7 @@ func TestFractionLostReport(t *testing.T) {
|
||||
HeaderExtensions: nil,
|
||||
Codecs: []webrtc.RTPCodecParameters{opusCodec},
|
||||
}, opusCodec.RTPCodecCapability, 0)
|
||||
for i := 0; i < 15; i++ {
|
||||
for i := range 15 {
|
||||
pkt := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
@@ -303,7 +304,7 @@ func TestFractionLostReport(t *testing.T) {
|
||||
HeaderExtensions: nil,
|
||||
Codecs: []webrtc.RTPCodecParameters{opusCodec},
|
||||
}, opusCodec.RTPCodecCapability, 0)
|
||||
for i := 0; i < 15; i++ {
|
||||
for i := range 15 {
|
||||
pkt := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
@@ -337,6 +338,15 @@ func TestCodecChange(t *testing.T) {
|
||||
t.Fatalf("codec change not consumed")
|
||||
}
|
||||
})
|
||||
buff.OnStreamRestart(func(reason string) {
|
||||
require.Equal(t, "codec-change", reason)
|
||||
|
||||
// read once to clear pending restart
|
||||
var buf [1500]byte
|
||||
extPkt, err := buff.ReadExtended(buf[:])
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, extPkt)
|
||||
})
|
||||
|
||||
h265Pkt := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
@@ -359,20 +369,27 @@ func TestCodecChange(t *testing.T) {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
buff.Bind(webrtc.RTPParameters{
|
||||
HeaderExtensions: nil,
|
||||
Codecs: []webrtc.RTPCodecParameters{vp8Codec, h265Codec},
|
||||
}, vp8Codec.RTPCodecCapability, 0)
|
||||
// Bind sets up VP8 as expected codec,
|
||||
// packet written to the buffer above before Bind is H.265,
|
||||
// that should trigger a codec change and a stream restart
|
||||
// when the queued packets from Write before Bind get flushed
|
||||
buff.Bind(
|
||||
webrtc.RTPParameters{
|
||||
HeaderExtensions: nil,
|
||||
Codecs: []webrtc.RTPCodecParameters{vp8Codec, h265Codec},
|
||||
},
|
||||
vp8Codec.RTPCodecCapability,
|
||||
0,
|
||||
)
|
||||
|
||||
select {
|
||||
case c := <-changedCodec:
|
||||
require.Equal(t, h265Codec, c)
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("expected codec change")
|
||||
}
|
||||
|
||||
// codec change after bind
|
||||
// second codec change - writing VP8 packet after Bind should trigger another codec change
|
||||
vp8Pkt := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
@@ -391,12 +408,16 @@ func TestCodecChange(t *testing.T) {
|
||||
select {
|
||||
case c := <-changedCodec:
|
||||
require.Equal(t, vp8Codec, c)
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("expected codec change")
|
||||
}
|
||||
fmt.Printf("done second codec change\n") // REMOVE
|
||||
|
||||
// out of order pkts can't cause codec change
|
||||
// rewrite the VP8 packet to start the sequence after a stream restart
|
||||
_, err = buff.Write(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
h265Pkt.SequenceNumber = 2
|
||||
h265Pkt.Timestamp = 2
|
||||
buf, err = h265Pkt.Marshal()
|
||||
@@ -409,7 +430,7 @@ func TestCodecChange(t *testing.T) {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
// unknown codec should not cause change
|
||||
// unknown codec should not cause change even if it is in order
|
||||
h265Pkt.SequenceNumber = 4
|
||||
h265Pkt.Timestamp = 4
|
||||
h265Pkt.PayloadType = 117
|
||||
@@ -422,14 +443,37 @@ func TestCodecChange(t *testing.T) {
|
||||
t.Fatalf("unexpected codec change")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
// an in-order packet should change codec again
|
||||
h265Pkt.SequenceNumber = 5
|
||||
h265Pkt.Timestamp = 5
|
||||
h265Pkt.PayloadType = 116
|
||||
buf, err = h265Pkt.Marshal()
|
||||
require.NoError(t, err)
|
||||
_, err = buff.Write(buf)
|
||||
require.NoError(t, err)
|
||||
select {
|
||||
case c := <-changedCodec:
|
||||
require.Equal(t, h265Codec, c)
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("expected codec change")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMemcpu(b *testing.B) {
|
||||
buf := make([]byte, 1500*1500*10)
|
||||
buf2 := make([]byte, 1500*1500*20)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
copy(buf2, buf)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExtPacketFactory(b *testing.B) {
|
||||
|
||||
for b.Loop() {
|
||||
extPkt := ExtPacketFactory.Get().(*ExtPacket)
|
||||
*extPkt = ExtPacket{}
|
||||
ExtPacketFactory.Put(extPkt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,41 @@ package buffer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
"go.uber.org/atomic"
|
||||
|
||||
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/utils"
|
||||
|
||||
"github.com/livekit/mediatransportutil/pkg/codec"
|
||||
"github.com/livekit/mediatransportutil/pkg/utils"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ExtDependencyDescriptorFactory = &sync.Pool{
|
||||
New: func() any {
|
||||
return &ExtDependencyDescriptor{}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// --------------------------------------
|
||||
|
||||
const (
|
||||
ddRestartThreshold = 30 * time.Second
|
||||
|
||||
// frame integrity check 2 seconds for L3T3 30fps video
|
||||
integrityCheckFrame = 180
|
||||
integrityCheckPkt = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFrameEarlierThanKeyFrame = fmt.Errorf("frame is earlier than current keyframe")
|
||||
ErrDDStructureAttachedToNonFirstPacket = fmt.Errorf("dependency descriptor structure is attached to non-first packet of a frame")
|
||||
ErrDDExtentionNotFound = fmt.Errorf("dependency descriptor extension not found")
|
||||
)
|
||||
|
||||
type DependencyDescriptorParser struct {
|
||||
@@ -47,16 +68,22 @@ type DependencyDescriptorParser struct {
|
||||
frameChecker *FrameIntegrityChecker
|
||||
|
||||
ddNotFoundCount atomic.Uint32
|
||||
|
||||
// restart detection
|
||||
restartGeneration int
|
||||
enableRestart bool
|
||||
lastPacketAt time.Time
|
||||
}
|
||||
|
||||
func NewDependencyDescriptorParser(ddExtID uint8, logger logger.Logger, onMaxLayerChanged func(int32, int32)) *DependencyDescriptorParser {
|
||||
func NewDependencyDescriptorParser(ddExtID uint8, logger logger.Logger, onMaxLayerChanged func(int32, int32), enableRestart bool) *DependencyDescriptorParser {
|
||||
return &DependencyDescriptorParser{
|
||||
ddExtID: ddExtID,
|
||||
logger: logger,
|
||||
onMaxLayerChanged: onMaxLayerChanged,
|
||||
seqWrapAround: utils.NewWrapAround[uint16, uint64](utils.WrapAroundParams{IsRestartAllowed: false}),
|
||||
frameWrapAround: utils.NewWrapAround[uint16, uint64](utils.WrapAroundParams{IsRestartAllowed: false}),
|
||||
frameChecker: NewFrameIntegrityChecker(180, 1024), // 2seconds for L3T3 30fps video
|
||||
frameChecker: NewFrameIntegrityChecker(integrityCheckFrame, integrityCheckPkt),
|
||||
enableRestart: enableRestart,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +97,10 @@ type ExtDependencyDescriptor struct {
|
||||
ExtFrameNum uint64
|
||||
// the frame number of the keyframe which the current frame depends on
|
||||
ExtKeyFrameNum uint64
|
||||
|
||||
// increase when the stream restarts, clear and reinitialize all dd state includes
|
||||
// attached structure, frame chain, decode target.
|
||||
RestartGeneration int
|
||||
}
|
||||
|
||||
func (r *DependencyDescriptorParser) Parse(pkt *rtp.Packet) (*ExtDependencyDescriptor, VideoLayer, error) {
|
||||
@@ -80,7 +111,7 @@ func (r *DependencyDescriptorParser) Parse(pkt *rtp.Packet) (*ExtDependencyDescr
|
||||
if ddNotFoundCount%100 == 0 {
|
||||
r.logger.Warnw("dependency descriptor extension is not present", nil, "seq", pkt.SequenceNumber, "count", ddNotFoundCount)
|
||||
}
|
||||
return nil, videoLayer, nil
|
||||
return nil, videoLayer, ErrDDExtentionNotFound
|
||||
}
|
||||
|
||||
var ddVal dd.DependencyDescriptor
|
||||
@@ -96,41 +127,92 @@ func (r *DependencyDescriptorParser) Parse(pkt *rtp.Packet) (*ExtDependencyDescr
|
||||
return nil, videoLayer, err
|
||||
}
|
||||
|
||||
var restart bool
|
||||
if r.enableRestart {
|
||||
if !r.lastPacketAt.IsZero() && time.Since(r.lastPacketAt) > ddRestartThreshold {
|
||||
r.restart()
|
||||
restart = true
|
||||
r.logger.Debugw(
|
||||
"dependency descriptor parser restart stream",
|
||||
"generation", r.restartGeneration,
|
||||
"lastPacketAt", r.lastPacketAt,
|
||||
"sinceLast", time.Since(r.lastPacketAt),
|
||||
"frameWrapAround", r.frameWrapAround,
|
||||
)
|
||||
}
|
||||
r.lastPacketAt = time.Now()
|
||||
}
|
||||
|
||||
extSeq := r.seqWrapAround.Update(pkt.SequenceNumber).ExtendedVal
|
||||
|
||||
if ddVal.FrameDependencies != nil {
|
||||
videoLayer.Spatial, videoLayer.Temporal = int32(ddVal.FrameDependencies.SpatialId), int32(ddVal.FrameDependencies.TemporalId)
|
||||
}
|
||||
|
||||
unwrapped := r.frameWrapAround.Update(ddVal.FrameNumber)
|
||||
// assume the packet is in-order when stream restarting
|
||||
unwrapped := r.frameWrapAround.UpdateWithOrderKnown(ddVal.FrameNumber, restart)
|
||||
extFN := unwrapped.ExtendedVal
|
||||
|
||||
if extFN < r.structureExtFrameNum {
|
||||
r.logger.Debugw("drop frame which is earlier than current structure", "frameNum", extFN, "structureFrameNum", r.structureExtFrameNum)
|
||||
r.logger.Debugw(
|
||||
"drop frame which is earlier than current structure",
|
||||
"fn", ddVal.FrameNumber,
|
||||
"extFN", extFN,
|
||||
"structureExtFrameNum", r.structureExtFrameNum,
|
||||
"unwrappedFN", unwrapped,
|
||||
"frameWrapAround", r.frameWrapAround,
|
||||
)
|
||||
return nil, videoLayer, ErrFrameEarlierThanKeyFrame
|
||||
}
|
||||
|
||||
r.frameChecker.AddPacket(extSeq, extFN, &ddVal)
|
||||
|
||||
extDD := &ExtDependencyDescriptor{
|
||||
Descriptor: &ddVal,
|
||||
ExtFrameNum: extFN,
|
||||
Integrity: r.frameChecker.FrameIntegrity(extFN),
|
||||
extDD := ExtDependencyDescriptorFactory.Get().(*ExtDependencyDescriptor)
|
||||
*extDD = ExtDependencyDescriptor{
|
||||
Descriptor: &ddVal,
|
||||
ExtFrameNum: extFN,
|
||||
Integrity: r.frameChecker.FrameIntegrity(extFN),
|
||||
RestartGeneration: r.restartGeneration,
|
||||
}
|
||||
|
||||
if ddVal.AttachedStructure != nil {
|
||||
if !ddVal.FirstPacketInFrame {
|
||||
r.logger.Warnw("attached structure is not the first packet in frame", nil, "extSeq", extSeq, "extFN", extFN)
|
||||
r.logger.Warnw(
|
||||
"attached structure is not the first packet in frame", nil,
|
||||
"sn", pkt.SequenceNumber,
|
||||
"extSeq", extSeq,
|
||||
"fn", ddVal.FrameNumber,
|
||||
"extFN", extFN,
|
||||
)
|
||||
ReleaseExtDependencyDescriptor(extDD)
|
||||
return nil, videoLayer, ErrDDStructureAttachedToNonFirstPacket
|
||||
}
|
||||
|
||||
if r.structure == nil || ddVal.AttachedStructure.StructureId != r.structure.StructureId {
|
||||
r.logger.Debugw("structure updated", "structureID", ddVal.AttachedStructure.StructureId, "extSeq", extSeq, "extFN", extFN, "descriptor", ddVal.String())
|
||||
r.logger.Debugw(
|
||||
"structure updated",
|
||||
"structureID", ddVal.AttachedStructure.StructureId,
|
||||
"sn", pkt.SequenceNumber,
|
||||
"extSeq", extSeq,
|
||||
"fn", ddVal.FrameNumber,
|
||||
"extFN", extFN,
|
||||
"descriptor", ddVal.String(),
|
||||
"unwrappedFN", unwrapped,
|
||||
"frameWrapAround", r.frameWrapAround,
|
||||
)
|
||||
}
|
||||
r.structure = ddVal.AttachedStructure
|
||||
r.decodeTargets = ProcessFrameDependencyStructure(ddVal.AttachedStructure)
|
||||
if extFN > unwrapped.PreExtendedHighest && extFN-unwrapped.PreExtendedHighest > 1000 {
|
||||
r.logger.Debugw("large frame number jump on structure updating", "extFN", extFN, "preExtendedHighest", unwrapped.PreExtendedHighest, "structureExtFrameNum", r.structureExtFrameNum)
|
||||
r.logger.Debugw(
|
||||
"large frame number jump on structure updating",
|
||||
"fn", ddVal.FrameNumber,
|
||||
"extFN", extFN,
|
||||
"preExtendedHighest", unwrapped.PreExtendedHighest,
|
||||
"structureExtFrameNum", r.structureExtFrameNum,
|
||||
"unwrappedFN", unwrapped,
|
||||
"frameWrapAround", r.frameWrapAround,
|
||||
)
|
||||
}
|
||||
r.structureExtFrameNum = extFN
|
||||
extDD.StructureUpdated = true
|
||||
@@ -166,6 +248,16 @@ func (r *DependencyDescriptorParser) Parse(pkt *rtp.Packet) (*ExtDependencyDescr
|
||||
return extDD, videoLayer, nil
|
||||
}
|
||||
|
||||
func (r *DependencyDescriptorParser) restart() {
|
||||
r.frameChecker = NewFrameIntegrityChecker(integrityCheckFrame, integrityCheckPkt)
|
||||
r.structure = nil
|
||||
r.structureExtFrameNum = 0
|
||||
r.activeDecodeTargetsExtSeq = 0
|
||||
r.activeDecodeTargetsMask = 0
|
||||
r.decodeTargets = r.decodeTargets[:0]
|
||||
r.restartGeneration++
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
type DependencyDescriptorDecodeTarget struct {
|
||||
@@ -197,8 +289,14 @@ func ProcessFrameDependencyStructure(structure *dd.FrameDependencyStructure) []D
|
||||
}
|
||||
|
||||
// sort decode target layer by spatial and temporal from high to low
|
||||
sort.Slice(decodeTargets, func(i, j int) bool {
|
||||
return decodeTargets[i].Layer.GreaterThan(decodeTargets[j].Layer)
|
||||
slices.SortFunc(decodeTargets, func(a, b DependencyDescriptorDecodeTarget) int {
|
||||
if a.Layer.GreaterThan(b.Layer) {
|
||||
return -1
|
||||
}
|
||||
if b.Layer.GreaterThan(a.Layer) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return decodeTargets
|
||||
@@ -216,3 +314,27 @@ func GetActiveDecodeTargetBitmask(layer VideoLayer, decodeTargets []DependencyDe
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
func ExtractDependencyDescriptorVideoSize(dd *dd.DependencyDescriptor) []codec.VideoSize {
|
||||
if dd.AttachedStructure == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
videoSizes := make([]codec.VideoSize, 0, len(dd.AttachedStructure.Resolutions))
|
||||
for _, res := range dd.AttachedStructure.Resolutions {
|
||||
videoSizes = append(videoSizes, codec.VideoSize{Width: uint32(res.Width), Height: uint32(res.Height)})
|
||||
}
|
||||
|
||||
return videoSizes
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
func ReleaseExtDependencyDescriptor(extDD *ExtDependencyDescriptor) {
|
||||
if extDD == nil {
|
||||
return
|
||||
}
|
||||
|
||||
*extDD = ExtDependencyDescriptor{}
|
||||
ExtDependencyDescriptorFactory.Put(extDD)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/transport/v3/packetio"
|
||||
"github.com/pion/transport/v4/packetio"
|
||||
)
|
||||
|
||||
type FactoryOfBufferFactory struct {
|
||||
@@ -118,7 +118,7 @@ func (f *Factory) GetRTCPReader(ssrc uint32) *RTCPReader {
|
||||
return f.rtcpReaders[ssrc]
|
||||
}
|
||||
|
||||
func (f *Factory) SetRTXPair(repair, base uint32) {
|
||||
func (f *Factory) SetRTXPair(repair, base uint32, rsid string) {
|
||||
f.Lock()
|
||||
repairBuffer, baseBuffer := f.rtpBuffers[repair], f.rtpBuffers[base]
|
||||
if repairBuffer == nil || baseBuffer == nil {
|
||||
@@ -127,5 +127,8 @@ func (f *Factory) SetRTXPair(repair, base uint32) {
|
||||
f.Unlock()
|
||||
if repairBuffer != nil && baseBuffer != nil {
|
||||
repairBuffer.SetPrimaryBufferForRTX(baseBuffer)
|
||||
if rsid != "" {
|
||||
baseBuffer.NotifyRTX(base, repair, rsid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/pion/rtp/codecs"
|
||||
|
||||
"github.com/livekit/mediatransportutil/pkg/codec"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
@@ -209,7 +210,7 @@ func (f *FrameRateCalculatorVP8) RecvPacket(ep *ExtPacket) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
vp8, ok := ep.Payload.(VP8)
|
||||
vp8, ok := ep.Payload.(codec.VP8)
|
||||
if !ok {
|
||||
f.logger.Debugw("no vp8 payload", "sn", ep.Packet.SequenceNumber)
|
||||
return false
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
|
||||
"github.com/livekit/mediatransportutil/pkg/codec"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ type testFrameInfo struct {
|
||||
func (f *testFrameInfo) toVP8() *ExtPacket {
|
||||
return &ExtPacket{
|
||||
Packet: &rtp.Packet{Header: f.header},
|
||||
Payload: VP8{
|
||||
Payload: codec.VP8{
|
||||
PictureID: f.framenumber,
|
||||
},
|
||||
VideoLayer: VideoLayer{Spatial: InvalidLayerSpatial, Temporal: int32(f.temporal)},
|
||||
@@ -75,7 +76,7 @@ func createFrames(startFrameNumber uint16, startTs uint32, startSeq uint16, tota
|
||||
|
||||
nextTs := make([][]uint32, spatials)
|
||||
tsStep := make([][]uint32, spatials)
|
||||
for i := 0; i < spatials; i++ {
|
||||
for i := range spatials {
|
||||
nextTs[i] = make([]uint32, temporals)
|
||||
tsStep[i] = make([]uint32, temporals)
|
||||
for j := 0; j < temporals; j++ {
|
||||
@@ -85,11 +86,11 @@ func createFrames(startFrameNumber uint16, startTs uint32, startSeq uint16, tota
|
||||
}
|
||||
|
||||
currentTs := make([]uint32, spatials)
|
||||
for i := 0; i < spatials; i++ {
|
||||
for i := range spatials {
|
||||
currentTs[i] = startTs
|
||||
}
|
||||
for i := 0; i < totalFramesPerSpatial; i++ {
|
||||
for s := 0; s < spatials; s++ {
|
||||
for range totalFramesPerSpatial {
|
||||
for s := range spatials {
|
||||
frame := &testFrameInfo{
|
||||
header: rtp.Header{Timestamp: currentTs[s], SequenceNumber: startSeq},
|
||||
framenumber: fn,
|
||||
@@ -134,7 +135,7 @@ func createFrames(startFrameNumber uint16, startTs uint32, startSeq uint16, tota
|
||||
|
||||
func verifyFps(t *testing.T, expect, got []float32) {
|
||||
require.Equal(t, len(expect), len(got))
|
||||
for i := 0; i < len(expect); i++ {
|
||||
for i := range expect {
|
||||
require.GreaterOrEqual(t, got[i], expect[i]*0.9, "expect %v, got %v", expect, got)
|
||||
require.LessOrEqual(t, got[i], expect[i]*1.1, "expect %v, got %v", expect, got)
|
||||
}
|
||||
|
||||
@@ -1,455 +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 buffer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
|
||||
"github.com/pion/rtp/codecs"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
errShortPacket = errors.New("packet is not large enough")
|
||||
errNilPacket = errors.New("invalid nil packet")
|
||||
errInvalidPacket = errors.New("invalid packet")
|
||||
)
|
||||
|
||||
// VP8 is a helper to get temporal data from VP8 packet header
|
||||
/*
|
||||
VP8 Payload Descriptor
|
||||
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
|X|R|N|S|R| PID | (REQUIRED) |X|R|N|S|R| PID | (REQUIRED)
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
X: |I|L|T|K| RSV | (OPTIONAL) X: |I|L|T|K| RSV | (OPTIONAL)
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
I: |M| PictureID | (OPTIONAL) I: |M| PictureID | (OPTIONAL)
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
L: | TL0PICIDX | (OPTIONAL) | PictureID |
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
T/K:|TID|Y| KEYIDX | (OPTIONAL) L: | TL0PICIDX | (OPTIONAL)
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
T/K:|TID|Y| KEYIDX | (OPTIONAL)
|
||||
+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
type VP8 struct {
|
||||
FirstByte byte
|
||||
S bool
|
||||
|
||||
I bool
|
||||
M bool
|
||||
PictureID uint16 /* 7 or 15 bits, picture ID */
|
||||
|
||||
L bool
|
||||
TL0PICIDX uint8 /* 8 bits temporal level zero index */
|
||||
|
||||
// Optional Header If either of the T or K bits are set to 1,
|
||||
// the TID/Y/KEYIDX extension field MUST be present.
|
||||
T bool
|
||||
TID uint8 /* 2 bits temporal layer idx */
|
||||
Y bool
|
||||
|
||||
K bool
|
||||
KEYIDX uint8 /* 5 bits of key frame idx */
|
||||
|
||||
HeaderSize int
|
||||
|
||||
// IsKeyFrame is a helper to detect if current packet is a keyframe
|
||||
IsKeyFrame bool
|
||||
}
|
||||
|
||||
// Unmarshal parses the passed byte slice and stores the result in the VP8 this method is called upon
|
||||
func (v *VP8) Unmarshal(payload []byte) error {
|
||||
if payload == nil {
|
||||
return errNilPacket
|
||||
}
|
||||
|
||||
payloadLen := len(payload)
|
||||
if payloadLen < 1 {
|
||||
return errShortPacket
|
||||
}
|
||||
|
||||
idx := 0
|
||||
v.FirstByte = payload[idx]
|
||||
v.S = payload[idx]&0x10 > 0
|
||||
// Check for extended bit control
|
||||
if payload[idx]&0x80 > 0 {
|
||||
idx++
|
||||
if payloadLen < idx+1 {
|
||||
return errShortPacket
|
||||
}
|
||||
v.I = payload[idx]&0x80 > 0
|
||||
v.L = payload[idx]&0x40 > 0
|
||||
v.T = payload[idx]&0x20 > 0
|
||||
v.K = payload[idx]&0x10 > 0
|
||||
if v.L && !v.T {
|
||||
return errInvalidPacket
|
||||
}
|
||||
|
||||
if v.I {
|
||||
idx++
|
||||
if payloadLen < idx+1 {
|
||||
return errShortPacket
|
||||
}
|
||||
pid := payload[idx] & 0x7f
|
||||
// if m is 1, then Picture ID is 15 bits
|
||||
v.M = payload[idx]&0x80 > 0
|
||||
if v.M {
|
||||
idx++
|
||||
if payloadLen < idx+1 {
|
||||
return errShortPacket
|
||||
}
|
||||
v.PictureID = binary.BigEndian.Uint16([]byte{pid, payload[idx]})
|
||||
} else {
|
||||
v.PictureID = uint16(pid)
|
||||
}
|
||||
}
|
||||
|
||||
if v.L {
|
||||
idx++
|
||||
if payloadLen < idx+1 {
|
||||
return errShortPacket
|
||||
}
|
||||
v.TL0PICIDX = payload[idx]
|
||||
}
|
||||
|
||||
if v.T || v.K {
|
||||
idx++
|
||||
if payloadLen < idx+1 {
|
||||
return errShortPacket
|
||||
}
|
||||
|
||||
if v.T {
|
||||
v.TID = (payload[idx] & 0xc0) >> 6
|
||||
v.Y = (payload[idx] & 0x20) > 0
|
||||
}
|
||||
|
||||
if v.K {
|
||||
v.KEYIDX = payload[idx] & 0x1f
|
||||
}
|
||||
}
|
||||
idx++
|
||||
if payloadLen < idx+1 {
|
||||
return errShortPacket
|
||||
}
|
||||
|
||||
// Check is packet is a keyframe by looking at P bit in vp8 payload
|
||||
v.IsKeyFrame = payload[idx]&0x01 == 0 && v.S
|
||||
} else {
|
||||
idx++
|
||||
if payloadLen < idx+1 {
|
||||
return errShortPacket
|
||||
}
|
||||
// Check is packet is a keyframe by looking at P bit in vp8 payload
|
||||
v.IsKeyFrame = payload[idx]&0x01 == 0 && v.S
|
||||
}
|
||||
v.HeaderSize = idx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VP8) Marshal() ([]byte, error) {
|
||||
buf := make([]byte, v.HeaderSize)
|
||||
n, err := v.MarshalTo(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], err
|
||||
}
|
||||
|
||||
func (v *VP8) MarshalTo(buf []byte) (int, error) {
|
||||
if len(buf) < v.HeaderSize {
|
||||
return 0, errShortPacket
|
||||
}
|
||||
|
||||
idx := 0
|
||||
buf[idx] = v.FirstByte
|
||||
if v.I || v.L || v.T || v.K {
|
||||
buf[idx] |= 0x80 // X bit
|
||||
idx++
|
||||
|
||||
xpos := idx
|
||||
xval := byte(0)
|
||||
|
||||
idx++
|
||||
if v.I {
|
||||
xval |= (1 << 7)
|
||||
if v.M {
|
||||
buf[idx] = 0x80 | byte((v.PictureID>>8)&0x7f)
|
||||
buf[idx+1] = byte(v.PictureID & 0xff)
|
||||
idx += 2
|
||||
} else {
|
||||
buf[idx] = byte(v.PictureID)
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
if v.L {
|
||||
xval |= (1 << 6)
|
||||
buf[idx] = v.TL0PICIDX
|
||||
idx++
|
||||
}
|
||||
|
||||
if v.T || v.K {
|
||||
buf[idx] = 0
|
||||
if v.T {
|
||||
xval |= (1 << 5)
|
||||
buf[idx] = v.TID << 6
|
||||
if v.Y {
|
||||
buf[idx] |= (1 << 5)
|
||||
}
|
||||
}
|
||||
|
||||
if v.K {
|
||||
xval |= (1 << 4)
|
||||
buf[idx] |= v.KEYIDX & 0x1f
|
||||
}
|
||||
idx++
|
||||
}
|
||||
|
||||
buf[xpos] = xval
|
||||
} else {
|
||||
buf[idx] &^= 0x80 // X bit
|
||||
idx++
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
func VPxPictureIdSizeDiff(mBit1 bool, mBit2 bool) int {
|
||||
if mBit1 == mBit2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if mBit1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
// IsH264KeyFrame detects if h264 payload is a keyframe
|
||||
// this code was taken from https://github.com/jech/galene/blob/codecs/rtpconn/rtpreader.go#L45
|
||||
// all credits belongs to Juliusz Chroboczek @jech and the awesome Galene SFU
|
||||
func IsH264KeyFrame(payload []byte) bool {
|
||||
if len(payload) < 1 {
|
||||
return false
|
||||
}
|
||||
nalu := payload[0] & 0x1F
|
||||
if nalu == 0 {
|
||||
// reserved
|
||||
return false
|
||||
} else if nalu <= 23 {
|
||||
// simple NALU
|
||||
return nalu == 7
|
||||
} else if nalu == 24 || nalu == 25 || nalu == 26 || nalu == 27 {
|
||||
// STAP-A, STAP-B, MTAP16 or MTAP24
|
||||
i := 1
|
||||
if nalu == 25 || nalu == 26 || nalu == 27 {
|
||||
// skip DON
|
||||
i += 2
|
||||
}
|
||||
for i < len(payload) {
|
||||
if i+2 > len(payload) {
|
||||
return false
|
||||
}
|
||||
length := uint16(payload[i])<<8 |
|
||||
uint16(payload[i+1])
|
||||
i += 2
|
||||
if i+int(length) > len(payload) {
|
||||
return false
|
||||
}
|
||||
offset := 0
|
||||
if nalu == 26 {
|
||||
offset = 3
|
||||
} else if nalu == 27 {
|
||||
offset = 4
|
||||
}
|
||||
if offset >= int(length) {
|
||||
return false
|
||||
}
|
||||
n := payload[i+offset] & 0x1F
|
||||
if n == 7 {
|
||||
return true
|
||||
} else if n >= 24 {
|
||||
// is this legal?
|
||||
logger.Debugw("Non-simple NALU within a STAP")
|
||||
}
|
||||
i += int(length)
|
||||
}
|
||||
if i == len(payload) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
} else if nalu == 28 || nalu == 29 {
|
||||
// FU-A or FU-B
|
||||
if len(payload) < 2 {
|
||||
return false
|
||||
}
|
||||
if (payload[1] & 0x80) == 0 {
|
||||
// not a starting fragment
|
||||
return false
|
||||
}
|
||||
return payload[1]&0x1F == 7
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
// IsVP9KeyFrame detects if vp9 payload is a keyframe
|
||||
// taken from https://github.com/jech/galene/blob/master/codecs/codecs.go
|
||||
// all credits belongs to Juliusz Chroboczek @jech and the awesome Galene SFU
|
||||
func IsVP9KeyFrame(payload []byte) bool {
|
||||
var vp9 codecs.VP9Packet
|
||||
_, err := vp9.Unmarshal(payload)
|
||||
if err != nil || len(vp9.Payload) < 1 {
|
||||
return false
|
||||
}
|
||||
if !vp9.B {
|
||||
return false
|
||||
}
|
||||
|
||||
if (vp9.Payload[0] & 0xc0) != 0x80 {
|
||||
return false
|
||||
}
|
||||
|
||||
profile := (vp9.Payload[0] >> 4) & 0x3
|
||||
if profile != 3 {
|
||||
return (vp9.Payload[0] & 0xC) == 0
|
||||
}
|
||||
return (vp9.Payload[0] & 0x6) == 0
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
// IsAV1KeyFrame detects if av1 payload is a keyframe
|
||||
// taken from https://github.com/jech/galene/blob/master/codecs/codecs.go
|
||||
// all credits belongs to Juliusz Chroboczek @jech and the awesome Galene SFU
|
||||
func IsAV1KeyFrame(payload []byte) bool {
|
||||
if len(payload) < 2 {
|
||||
return false
|
||||
}
|
||||
// Z=0, N=1
|
||||
if (payload[0] & 0x88) != 0x08 {
|
||||
return false
|
||||
}
|
||||
w := (payload[0] & 0x30) >> 4
|
||||
|
||||
getObu := func(data []byte, last bool) ([]byte, int, bool) {
|
||||
if last {
|
||||
return data, len(data), false
|
||||
}
|
||||
offset := 0
|
||||
length := 0
|
||||
for {
|
||||
if len(data) <= offset {
|
||||
return nil, offset, offset > 0
|
||||
}
|
||||
l := data[offset]
|
||||
length |= int(l&0x7f) << (offset * 7)
|
||||
offset++
|
||||
if (l & 0x80) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(data) < offset+length {
|
||||
return data[offset:], len(data), true
|
||||
}
|
||||
return data[offset : offset+length],
|
||||
offset + length, false
|
||||
}
|
||||
offset := 1
|
||||
i := 0
|
||||
for {
|
||||
obu, length, truncated :=
|
||||
getObu(payload[offset:], int(w) == i+1)
|
||||
if len(obu) < 1 {
|
||||
return false
|
||||
}
|
||||
tpe := (obu[0] & 0x38) >> 3
|
||||
switch i {
|
||||
case 0:
|
||||
// OBU_SEQUENCE_HEADER
|
||||
if tpe != 1 {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
// OBU_FRAME_HEADER or OBU_FRAME
|
||||
if tpe == 3 || tpe == 6 {
|
||||
if len(obu) < 2 {
|
||||
return false
|
||||
}
|
||||
// show_existing_frame == 0
|
||||
if (obu[1] & 0x80) != 0 {
|
||||
return false
|
||||
}
|
||||
// frame_type == KEY_FRAME
|
||||
return (obu[1] & 0x60) == 0
|
||||
}
|
||||
}
|
||||
if truncated || i >= int(w) {
|
||||
// the first frame header is in a second
|
||||
// packet, give up.
|
||||
return false
|
||||
}
|
||||
offset += length
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func IsH265KeyFrame(payload []byte) (kf bool) {
|
||||
if len(payload) < 2 {
|
||||
return false
|
||||
}
|
||||
naluType := (payload[0] & 0x7E) >> 1
|
||||
switch {
|
||||
case naluType == 33 || naluType == 34:
|
||||
return true
|
||||
case naluType == 48: // AP
|
||||
idx := 2
|
||||
for idx < len(payload)-2 {
|
||||
// TODO: check the DONL field (controled by sprop-max-don-diff)
|
||||
size := binary.BigEndian.Uint16(payload[idx:])
|
||||
idx += 2
|
||||
if idx >= len(payload) {
|
||||
return false
|
||||
}
|
||||
naluType = (payload[idx] & 0x7E) >> 1
|
||||
if naluType == 33 || naluType == 34 {
|
||||
return true
|
||||
}
|
||||
idx += int(size)
|
||||
}
|
||||
return false
|
||||
|
||||
case naluType == 49: // FU
|
||||
if len(payload) < 3 {
|
||||
return false
|
||||
}
|
||||
naluType = (payload[2] & 0x7E) >> 1
|
||||
return naluType == 33 || naluType == 34
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
@@ -1,110 +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 buffer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVP8Helper_Unmarshal(t *testing.T) {
|
||||
type args struct {
|
||||
payload []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
checkTemporal bool
|
||||
temporalSupport bool
|
||||
checkKeyFrame bool
|
||||
keyFrame bool
|
||||
checkPictureID bool
|
||||
pictureID uint16
|
||||
checkTlzIdx bool
|
||||
tlzIdx uint8
|
||||
checkTempID bool
|
||||
temporalID uint8
|
||||
}{
|
||||
{
|
||||
name: "Empty or nil payload must return error",
|
||||
args: args{payload: []byte{}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Temporal must be supported by setting T bit to 1",
|
||||
args: args{payload: []byte{0xff, 0x20, 0x1, 0x2, 0x3, 0x4}},
|
||||
checkTemporal: true,
|
||||
temporalSupport: true,
|
||||
},
|
||||
{
|
||||
name: "Picture must be ID 7 bits by setting M bit to 0 and present by I bit set to 1",
|
||||
args: args{payload: []byte{0xff, 0xff, 0x11, 0x2, 0x3, 0x4}},
|
||||
checkPictureID: true,
|
||||
pictureID: 17,
|
||||
},
|
||||
{
|
||||
name: "Picture ID must be 15 bits by setting M bit to 1 and present by I bit set to 1",
|
||||
args: args{payload: []byte{0xff, 0xff, 0x92, 0x67, 0x3, 0x4, 0x5}},
|
||||
checkPictureID: true,
|
||||
pictureID: 4711,
|
||||
},
|
||||
{
|
||||
name: "Temporal level zero index must be present if L set to 1",
|
||||
args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x4, 0x5}},
|
||||
checkTlzIdx: true,
|
||||
tlzIdx: 180,
|
||||
},
|
||||
{
|
||||
name: "Temporal index must be present and used if T bit set to 1",
|
||||
args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x5, 0x6}},
|
||||
checkTempID: true,
|
||||
temporalID: 2,
|
||||
},
|
||||
{
|
||||
name: "Check if packet is a keyframe by looking at P bit set to 0",
|
||||
args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}},
|
||||
checkKeyFrame: true,
|
||||
keyFrame: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &VP8{}
|
||||
if err := p.Unmarshal(tt.args.payload); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.checkTemporal {
|
||||
require.Equal(t, tt.temporalSupport, p.T)
|
||||
}
|
||||
if tt.checkKeyFrame {
|
||||
require.Equal(t, tt.keyFrame, p.IsKeyFrame)
|
||||
}
|
||||
if tt.checkPictureID {
|
||||
require.Equal(t, tt.pictureID, p.PictureID)
|
||||
}
|
||||
if tt.checkTlzIdx {
|
||||
require.Equal(t, tt.tlzIdx, p.TL0PICIDX)
|
||||
}
|
||||
if tt.checkTempID {
|
||||
require.Equal(t, tt.temporalID, p.TID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
@@ -31,6 +31,7 @@ func NewRTCPReader(ssrc uint32) *RTCPReader {
|
||||
return &RTCPReader{ssrc: ssrc}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func (r *RTCPReader) Write(p []byte) (n int, err error) {
|
||||
if r.closed.Load() {
|
||||
err = io.EOF
|
||||
|
||||
@@ -15,24 +15,43 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
QuarterResolution = "q"
|
||||
HalfResolution = "h"
|
||||
FullResolution = "f"
|
||||
quarterResolutionQ = "q"
|
||||
halfResolutionH = "h"
|
||||
fullResolutionF = "f"
|
||||
|
||||
quarterResolution2 = "2"
|
||||
halfResolution1 = "1"
|
||||
fullResolution0 = "0"
|
||||
)
|
||||
|
||||
// SIMULCAST-CODEC-TODO: these need to be codec mime aware if and when each codec suppports different layers
|
||||
func LayerPresenceFromTrackInfo(trackInfo *livekit.TrackInfo) *[livekit.VideoQuality_HIGH + 1]bool {
|
||||
if trackInfo == nil || len(trackInfo.Layers) == 0 {
|
||||
type VideoLayersRid [DefaultMaxLayerSpatial + 1]string
|
||||
|
||||
var (
|
||||
videoLayersRidQHF = VideoLayersRid{quarterResolutionQ, halfResolutionH, fullResolutionF}
|
||||
videoLayersRid210 = VideoLayersRid{quarterResolution2, halfResolution1, fullResolution0}
|
||||
DefaultVideoLayersRid = videoLayersRidQHF
|
||||
)
|
||||
|
||||
func LayerPresenceFromTrackInfo(mimeType mime.MimeType, trackInfo *livekit.TrackInfo) *[livekit.VideoQuality_HIGH + 1]bool {
|
||||
if trackInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
layers := GetVideoLayersForMimeType(mimeType, trackInfo)
|
||||
if len(layers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var layerPresence [livekit.VideoQuality_HIGH + 1]bool
|
||||
for _, layer := range trackInfo.Layers {
|
||||
for _, layer := range layers {
|
||||
// WARNING: comparing protobuf enum
|
||||
if layer.Quality <= livekit.VideoQuality_HIGH {
|
||||
layerPresence[layer.Quality] = true
|
||||
@@ -44,15 +63,15 @@ func LayerPresenceFromTrackInfo(trackInfo *livekit.TrackInfo) *[livekit.VideoQua
|
||||
return &layerPresence
|
||||
}
|
||||
|
||||
func RidToSpatialLayer(rid string, trackInfo *livekit.TrackInfo) int32 {
|
||||
lp := LayerPresenceFromTrackInfo(trackInfo)
|
||||
func RidToSpatialLayer(mimeType mime.MimeType, rid string, trackInfo *livekit.TrackInfo, ridSpace VideoLayersRid) int32 {
|
||||
lp := LayerPresenceFromTrackInfo(mimeType, trackInfo)
|
||||
if lp == nil {
|
||||
switch rid {
|
||||
case QuarterResolution:
|
||||
case quarterResolutionQ:
|
||||
return 0
|
||||
case HalfResolution:
|
||||
case halfResolutionH:
|
||||
return 1
|
||||
case FullResolution:
|
||||
case fullResolutionF:
|
||||
return 2
|
||||
default:
|
||||
return 0
|
||||
@@ -60,7 +79,7 @@ func RidToSpatialLayer(rid string, trackInfo *livekit.TrackInfo) int32 {
|
||||
}
|
||||
|
||||
switch rid {
|
||||
case QuarterResolution:
|
||||
case ridSpace[0]:
|
||||
switch {
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
fallthrough
|
||||
@@ -76,7 +95,7 @@ func RidToSpatialLayer(rid string, trackInfo *livekit.TrackInfo) int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
case HalfResolution:
|
||||
case ridSpace[1]:
|
||||
switch {
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
fallthrough
|
||||
@@ -92,19 +111,19 @@ func RidToSpatialLayer(rid string, trackInfo *livekit.TrackInfo) int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
case FullResolution:
|
||||
case ridSpace[2]:
|
||||
switch {
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
return 2
|
||||
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]:
|
||||
logger.Warnw("unexpected rid f with only two qualities, low and medium", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
|
||||
logger.Warnw("unexpected rid with only two qualities, low and medium", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo), "rid", ridSpace[2])
|
||||
return 1
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]:
|
||||
logger.Warnw("unexpected rid f with only two qualities, low and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
|
||||
logger.Warnw("unexpected rid with only two qualities, low and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo), "rid", ridSpace[2])
|
||||
return 1
|
||||
case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
logger.Warnw("unexpected rid f with only two qualities, medium and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
|
||||
logger.Warnw("unexpected rid with only two qualities, medium and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo), "rid", ridSpace[2])
|
||||
return 1
|
||||
|
||||
default:
|
||||
@@ -118,18 +137,18 @@ func RidToSpatialLayer(rid string, trackInfo *livekit.TrackInfo) int32 {
|
||||
}
|
||||
}
|
||||
|
||||
func SpatialLayerToRid(layer int32, trackInfo *livekit.TrackInfo) string {
|
||||
lp := LayerPresenceFromTrackInfo(trackInfo)
|
||||
func SpatialLayerToRid(mimeType mime.MimeType, layer int32, trackInfo *livekit.TrackInfo, ridSpace VideoLayersRid) string {
|
||||
lp := LayerPresenceFromTrackInfo(mimeType, trackInfo)
|
||||
if lp == nil {
|
||||
switch layer {
|
||||
case 0:
|
||||
return QuarterResolution
|
||||
return quarterResolutionQ
|
||||
case 1:
|
||||
return HalfResolution
|
||||
return halfResolutionH
|
||||
case 2:
|
||||
return FullResolution
|
||||
return fullResolutionF
|
||||
default:
|
||||
return QuarterResolution
|
||||
return quarterResolutionQ
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,10 +162,10 @@ func SpatialLayerToRid(layer int32, trackInfo *livekit.TrackInfo) string {
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]:
|
||||
fallthrough
|
||||
case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
return QuarterResolution
|
||||
return ridSpace[0]
|
||||
|
||||
default:
|
||||
return QuarterResolution
|
||||
return ridSpace[0]
|
||||
}
|
||||
|
||||
case 1:
|
||||
@@ -158,42 +177,42 @@ func SpatialLayerToRid(layer int32, trackInfo *livekit.TrackInfo) string {
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]:
|
||||
fallthrough
|
||||
case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
return HalfResolution
|
||||
return ridSpace[1]
|
||||
|
||||
default:
|
||||
return QuarterResolution
|
||||
return ridSpace[0]
|
||||
}
|
||||
|
||||
case 2:
|
||||
switch {
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
return FullResolution
|
||||
return ridSpace[2]
|
||||
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]:
|
||||
logger.Warnw("unexpected layer 2 with only two qualities, low and medium", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
|
||||
return HalfResolution
|
||||
return ridSpace[1]
|
||||
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]:
|
||||
logger.Warnw("unexpected layer 2 with only two qualities, low and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
|
||||
return HalfResolution
|
||||
return ridSpace[1]
|
||||
case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
|
||||
logger.Warnw("unexpected layer 2 with only two qualities, medium and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
|
||||
return HalfResolution
|
||||
return ridSpace[1]
|
||||
|
||||
default:
|
||||
return QuarterResolution
|
||||
return ridSpace[0]
|
||||
}
|
||||
|
||||
default:
|
||||
return QuarterResolution
|
||||
return ridSpace[0]
|
||||
}
|
||||
}
|
||||
|
||||
func VideoQualityToRid(quality livekit.VideoQuality, trackInfo *livekit.TrackInfo) string {
|
||||
return SpatialLayerToRid(VideoQualityToSpatialLayer(quality, trackInfo), trackInfo)
|
||||
func VideoQualityToRid(mimeType mime.MimeType, quality livekit.VideoQuality, trackInfo *livekit.TrackInfo, ridSpace VideoLayersRid) string {
|
||||
return SpatialLayerToRid(mimeType, VideoQualityToSpatialLayer(mimeType, quality, trackInfo), trackInfo, ridSpace)
|
||||
}
|
||||
|
||||
func SpatialLayerToVideoQuality(layer int32, trackInfo *livekit.TrackInfo) livekit.VideoQuality {
|
||||
lp := LayerPresenceFromTrackInfo(trackInfo)
|
||||
func SpatialLayerToVideoQuality(mimeType mime.MimeType, layer int32, trackInfo *livekit.TrackInfo) livekit.VideoQuality {
|
||||
lp := LayerPresenceFromTrackInfo(mimeType, trackInfo)
|
||||
if lp == nil {
|
||||
switch layer {
|
||||
case 0:
|
||||
@@ -259,8 +278,8 @@ func SpatialLayerToVideoQuality(layer int32, trackInfo *livekit.TrackInfo) livek
|
||||
return livekit.VideoQuality_OFF
|
||||
}
|
||||
|
||||
func VideoQualityToSpatialLayer(quality livekit.VideoQuality, trackInfo *livekit.TrackInfo) int32 {
|
||||
lp := LayerPresenceFromTrackInfo(trackInfo)
|
||||
func VideoQualityToSpatialLayer(mimeType mime.MimeType, quality livekit.VideoQuality, trackInfo *livekit.TrackInfo) int32 {
|
||||
lp := LayerPresenceFromTrackInfo(mimeType, trackInfo)
|
||||
if lp == nil {
|
||||
switch quality {
|
||||
case livekit.VideoQuality_LOW:
|
||||
@@ -324,3 +343,178 @@ func VideoQualityToSpatialLayer(quality livekit.VideoQuality, trackInfo *livekit
|
||||
|
||||
return InvalidLayerSpatial
|
||||
}
|
||||
|
||||
func GetVideoLayerModeForMimeType(mimeType mime.MimeType, ti *livekit.TrackInfo) livekit.VideoLayer_Mode {
|
||||
if ti != nil {
|
||||
for _, codec := range ti.Codecs {
|
||||
if mime.NormalizeMimeType(codec.MimeType) == mimeType {
|
||||
return codec.VideoLayerMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return livekit.VideoLayer_MODE_UNUSED
|
||||
}
|
||||
|
||||
func GetVideoLayersForMimeType(mimeType mime.MimeType, ti *livekit.TrackInfo) []*livekit.VideoLayer {
|
||||
var layers []*livekit.VideoLayer
|
||||
if ti != nil {
|
||||
for _, codec := range ti.Codecs {
|
||||
if mime.NormalizeMimeType(codec.MimeType) == mimeType {
|
||||
layers = codec.Layers
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(layers) == 0 {
|
||||
layers = ti.Layers
|
||||
}
|
||||
}
|
||||
return layers
|
||||
}
|
||||
|
||||
func GetSpatialLayerForRid(mimeType mime.MimeType, rid string, ti *livekit.TrackInfo) int32 {
|
||||
if ti == nil {
|
||||
return InvalidLayerSpatial
|
||||
}
|
||||
|
||||
if rid == "" {
|
||||
// single layer without RID
|
||||
return 0
|
||||
}
|
||||
|
||||
layers := GetVideoLayersForMimeType(mimeType, ti)
|
||||
for _, layer := range layers {
|
||||
if layer.Rid == rid {
|
||||
return layer.SpatialLayer
|
||||
}
|
||||
}
|
||||
|
||||
if len(layers) != 0 {
|
||||
// RID present in codec, but may not be specified via signalling
|
||||
// (happens with older browsers setting a rid for SVC codecs)
|
||||
hasRid := false
|
||||
for _, layer := range layers {
|
||||
if layer.Rid != "" {
|
||||
hasRid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRid {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// SIMULCAST-CODEC-TODO - ideally should return invalid, but there are
|
||||
// VP9 publishers using rid = f, if there are only two layers
|
||||
// in TrackInfo, that will be q;h and f will become invalid.
|
||||
//
|
||||
// Actually, there should be no rids for VP9 in SDP and hence
|
||||
// the above check should take effect. However, as simulcast
|
||||
// codec/back up codec does not update rids from SDP,
|
||||
// the default rids are used when vp9 (primary codec)
|
||||
// is published. Due to that the above check gets bypassed.
|
||||
//
|
||||
// The full proper sequence would be
|
||||
// 1. For primary codec using SVC, there will be no rids.
|
||||
// The above check should take effect and it should
|
||||
// return 0 even if some publisher uses a rid like `f`.
|
||||
// 2. When secondary codec is published, rids for the codec
|
||||
// corresponding to the back up codec mime type should
|
||||
// be updated in `TrackInfo`. This is a bit tricky
|
||||
// for a couple of cases
|
||||
// a. Browsers like Firefox use a different CID everytime.
|
||||
// So, it cannot be matched between `AddTrack` and SDP.
|
||||
// One option is to look for a published track with
|
||||
// back up codec and apply it there. But, that becomes
|
||||
// a challenge if there are multiple published tracks
|
||||
// with pending back up codec.
|
||||
// b. The back up codec publish SDP will have the full
|
||||
// codec list. It should be okay to assume that the
|
||||
// codec that will be published is the back up codec,
|
||||
// but just something to be aware of.
|
||||
// 3. Use of this function with proper mime so that proper
|
||||
// codec section can be looked up in `TrackInfo`.
|
||||
// return InvalidLayerSpatial
|
||||
logger.Infow(
|
||||
"invalid layer for rid, returning default",
|
||||
"trackID", ti.Sid,
|
||||
"rid", rid,
|
||||
"mimeType", mimeType,
|
||||
"trackInfo", logger.Proto(ti),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
func GetSpatialLayerForVideoQuality(mimeType mime.MimeType, quality livekit.VideoQuality, ti *livekit.TrackInfo) int32 {
|
||||
if ti == nil || quality == livekit.VideoQuality_OFF {
|
||||
return InvalidLayerSpatial
|
||||
}
|
||||
|
||||
layers := GetVideoLayersForMimeType(mimeType, ti)
|
||||
for _, layer := range layers {
|
||||
if layer.Quality == quality {
|
||||
return layer.SpatialLayer
|
||||
}
|
||||
}
|
||||
|
||||
if len(layers) == 0 {
|
||||
// single layer
|
||||
return 0
|
||||
}
|
||||
|
||||
// requested quality is higher than available layers, return the highest available layer
|
||||
return VideoQualityToSpatialLayer(mimeType, quality, ti)
|
||||
}
|
||||
|
||||
func GetVideoQualityForSpatialLayer(mimeType mime.MimeType, spatialLayer int32, ti *livekit.TrackInfo) livekit.VideoQuality {
|
||||
if spatialLayer == InvalidLayerSpatial || ti == nil {
|
||||
return livekit.VideoQuality_OFF
|
||||
}
|
||||
|
||||
layers := GetVideoLayersForMimeType(mimeType, ti)
|
||||
for _, layer := range layers {
|
||||
if layer.SpatialLayer == spatialLayer {
|
||||
return layer.Quality
|
||||
}
|
||||
}
|
||||
|
||||
return livekit.VideoQuality_OFF
|
||||
}
|
||||
|
||||
func isVideoLayersRidKnown(rids VideoLayersRid, knownRids VideoLayersRid) bool {
|
||||
for _, rid := range rids {
|
||||
if rid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(knownRids[:], rid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func NormalizeVideoLayersRid(rids VideoLayersRid) VideoLayersRid {
|
||||
out := rids
|
||||
|
||||
normalize := func(knownRids VideoLayersRid) {
|
||||
idx := 0
|
||||
for _, known := range knownRids {
|
||||
if slices.Contains(rids[:], known) {
|
||||
out[idx] = known
|
||||
idx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isVideoLayersRidKnown(rids, videoLayersRidQHF) {
|
||||
normalize(videoLayersRidQHF)
|
||||
}
|
||||
|
||||
if isVideoLayersRidKnown(rids, videoLayersRid210) {
|
||||
normalize(videoLayersRid210)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
@@ -30,129 +31,174 @@ func TestRidConversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackInfo *livekit.TrackInfo
|
||||
mimeType mime.MimeType
|
||||
ridToLayer map[string]RidAndLayer
|
||||
}{
|
||||
{
|
||||
"no track info",
|
||||
nil,
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: HalfResolution, layer: 1},
|
||||
FullResolution: {rid: FullResolution, layer: 2},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: halfResolutionH, layer: 1},
|
||||
fullResolutionF: {rid: fullResolutionF, layer: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
"no layers",
|
||||
&livekit.TrackInfo{},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: HalfResolution, layer: 1},
|
||||
FullResolution: {rid: FullResolution, layer: 2},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: halfResolutionH, layer: 1},
|
||||
fullResolutionF: {rid: fullResolutionF, layer: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
"single layer, low",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: QuarterResolution, layer: 0},
|
||||
FullResolution: {rid: QuarterResolution, layer: 0},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: quarterResolutionQ, layer: 0},
|
||||
fullResolutionF: {rid: quarterResolutionQ, layer: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"single layer, medium",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: QuarterResolution, layer: 0},
|
||||
FullResolution: {rid: QuarterResolution, layer: 0},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: quarterResolutionQ, layer: 0},
|
||||
fullResolutionF: {rid: quarterResolutionQ, layer: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"single layer, high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: QuarterResolution, layer: 0},
|
||||
FullResolution: {rid: QuarterResolution, layer: 0},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: quarterResolutionQ, layer: 0},
|
||||
fullResolutionF: {rid: quarterResolutionQ, layer: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"two layers, low and medium",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: HalfResolution, layer: 1},
|
||||
FullResolution: {rid: HalfResolution, layer: 1},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: halfResolutionH, layer: 1},
|
||||
fullResolutionF: {rid: halfResolutionH, layer: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
"two layers, low and high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: HalfResolution, layer: 1},
|
||||
FullResolution: {rid: HalfResolution, layer: 1},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: halfResolutionH, layer: 1},
|
||||
fullResolutionF: {rid: halfResolutionH, layer: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
"two layers, medium and high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: HalfResolution, layer: 1},
|
||||
FullResolution: {rid: HalfResolution, layer: 1},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: halfResolutionH, layer: 1},
|
||||
fullResolutionF: {rid: halfResolutionH, layer: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
"three layers",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]RidAndLayer{
|
||||
"": {rid: QuarterResolution, layer: 0},
|
||||
QuarterResolution: {rid: QuarterResolution, layer: 0},
|
||||
HalfResolution: {rid: HalfResolution, layer: 1},
|
||||
FullResolution: {rid: FullResolution, layer: 2},
|
||||
"": {rid: quarterResolutionQ, layer: 0},
|
||||
quarterResolutionQ: {rid: quarterResolutionQ, layer: 0},
|
||||
halfResolutionH: {rid: halfResolutionH, layer: 1},
|
||||
fullResolutionF: {rid: fullResolutionF, layer: 2},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -160,10 +206,10 @@ func TestRidConversion(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for testRid, expectedResult := range test.ridToLayer {
|
||||
actualLayer := RidToSpatialLayer(testRid, test.trackInfo)
|
||||
actualLayer := RidToSpatialLayer(test.mimeType, testRid, test.trackInfo, DefaultVideoLayersRid)
|
||||
require.Equal(t, expectedResult.layer, actualLayer)
|
||||
|
||||
actualRid := SpatialLayerToRid(actualLayer, test.trackInfo)
|
||||
actualRid := SpatialLayerToRid(test.mimeType, actualLayer, test.trackInfo, DefaultVideoLayersRid)
|
||||
require.Equal(t, expectedResult.rid, actualRid)
|
||||
}
|
||||
})
|
||||
@@ -178,11 +224,13 @@ func TestQualityConversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackInfo *livekit.TrackInfo
|
||||
mimeType mime.MimeType
|
||||
qualityToLayer map[livekit.VideoQuality]QualityAndLayer
|
||||
}{
|
||||
{
|
||||
"no track info",
|
||||
nil,
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_LOW, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_MEDIUM, layer: 1},
|
||||
@@ -192,6 +240,7 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"no layers",
|
||||
&livekit.TrackInfo{},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_LOW, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_MEDIUM, layer: 1},
|
||||
@@ -201,10 +250,16 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"single layer, low",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_LOW, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_LOW, layer: 0},
|
||||
@@ -214,10 +269,16 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"single layer, medium",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_MEDIUM, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_MEDIUM, layer: 0},
|
||||
@@ -227,10 +288,16 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"single layer, high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_HIGH, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_HIGH, layer: 0},
|
||||
@@ -240,11 +307,17 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"two layers, low and medium",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_LOW, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_MEDIUM, layer: 1},
|
||||
@@ -254,11 +327,17 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"two layers, low and high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_LOW, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_HIGH, layer: 1},
|
||||
@@ -268,11 +347,17 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"two layers, medium and high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_MEDIUM, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_MEDIUM, layer: 0},
|
||||
@@ -282,12 +367,18 @@ func TestQualityConversion(t *testing.T) {
|
||||
{
|
||||
"three layers",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]QualityAndLayer{
|
||||
livekit.VideoQuality_LOW: {quality: livekit.VideoQuality_LOW, layer: 0},
|
||||
livekit.VideoQuality_MEDIUM: {quality: livekit.VideoQuality_MEDIUM, layer: 1},
|
||||
@@ -299,10 +390,10 @@ func TestQualityConversion(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for testQuality, expectedResult := range test.qualityToLayer {
|
||||
actualLayer := VideoQualityToSpatialLayer(testQuality, test.trackInfo)
|
||||
actualLayer := VideoQualityToSpatialLayer(test.mimeType, testQuality, test.trackInfo)
|
||||
require.Equal(t, expectedResult.layer, actualLayer)
|
||||
|
||||
actualQuality := SpatialLayerToVideoQuality(actualLayer, test.trackInfo)
|
||||
actualQuality := SpatialLayerToVideoQuality(test.mimeType, actualLayer, test.trackInfo)
|
||||
require.Equal(t, expectedResult.quality, actualQuality)
|
||||
}
|
||||
})
|
||||
@@ -313,120 +404,165 @@ func TestVideoQualityToRidConversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackInfo *livekit.TrackInfo
|
||||
mimeTye mime.MimeType
|
||||
qualityToRid map[livekit.VideoQuality]string
|
||||
}{
|
||||
{
|
||||
"no track info",
|
||||
nil,
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: HalfResolution,
|
||||
livekit.VideoQuality_HIGH: FullResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: halfResolutionH,
|
||||
livekit.VideoQuality_HIGH: fullResolutionF,
|
||||
},
|
||||
},
|
||||
{
|
||||
"no layers",
|
||||
&livekit.TrackInfo{},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: HalfResolution,
|
||||
livekit.VideoQuality_HIGH: FullResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: halfResolutionH,
|
||||
livekit.VideoQuality_HIGH: fullResolutionF,
|
||||
},
|
||||
},
|
||||
{
|
||||
"single layer, low",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: QuarterResolution,
|
||||
livekit.VideoQuality_HIGH: QuarterResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: quarterResolutionQ,
|
||||
livekit.VideoQuality_HIGH: quarterResolutionQ,
|
||||
},
|
||||
},
|
||||
{
|
||||
"single layer, medium",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: QuarterResolution,
|
||||
livekit.VideoQuality_HIGH: QuarterResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: quarterResolutionQ,
|
||||
livekit.VideoQuality_HIGH: quarterResolutionQ,
|
||||
},
|
||||
},
|
||||
{
|
||||
"single layer, high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: QuarterResolution,
|
||||
livekit.VideoQuality_HIGH: QuarterResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: quarterResolutionQ,
|
||||
livekit.VideoQuality_HIGH: quarterResolutionQ,
|
||||
},
|
||||
},
|
||||
{
|
||||
"two layers, low and medium",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: HalfResolution,
|
||||
livekit.VideoQuality_HIGH: HalfResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: halfResolutionH,
|
||||
livekit.VideoQuality_HIGH: halfResolutionH,
|
||||
},
|
||||
},
|
||||
{
|
||||
"two layers, low and high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: HalfResolution,
|
||||
livekit.VideoQuality_HIGH: HalfResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: halfResolutionH,
|
||||
livekit.VideoQuality_HIGH: halfResolutionH,
|
||||
},
|
||||
},
|
||||
{
|
||||
"two layers, medium and high",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: QuarterResolution,
|
||||
livekit.VideoQuality_HIGH: HalfResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: quarterResolutionQ,
|
||||
livekit.VideoQuality_HIGH: halfResolutionH,
|
||||
},
|
||||
},
|
||||
{
|
||||
"three layers",
|
||||
&livekit.TrackInfo{
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW},
|
||||
{Quality: livekit.VideoQuality_MEDIUM},
|
||||
{Quality: livekit.VideoQuality_HIGH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]string{
|
||||
livekit.VideoQuality_LOW: QuarterResolution,
|
||||
livekit.VideoQuality_MEDIUM: HalfResolution,
|
||||
livekit.VideoQuality_HIGH: FullResolution,
|
||||
livekit.VideoQuality_LOW: quarterResolutionQ,
|
||||
livekit.VideoQuality_MEDIUM: halfResolutionH,
|
||||
livekit.VideoQuality_HIGH: fullResolutionF,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -434,9 +570,323 @@ func TestVideoQualityToRidConversion(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for testQuality, expectedRid := range test.qualityToRid {
|
||||
actualRid := VideoQualityToRid(testQuality, test.trackInfo)
|
||||
actualRid := VideoQualityToRid(test.mimeTye, testQuality, test.trackInfo, DefaultVideoLayersRid)
|
||||
require.Equal(t, expectedRid, actualRid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSpatialLayerForRid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackInfo *livekit.TrackInfo
|
||||
mimeType mime.MimeType
|
||||
ridToSpatialLayer map[string]int32
|
||||
}{
|
||||
{
|
||||
"no track info",
|
||||
nil,
|
||||
mime.MimeTypeVP8,
|
||||
map[string]int32{
|
||||
quarterResolutionQ: InvalidLayerSpatial,
|
||||
halfResolutionH: InvalidLayerSpatial,
|
||||
fullResolutionF: InvalidLayerSpatial,
|
||||
},
|
||||
},
|
||||
{
|
||||
"no layers",
|
||||
&livekit.TrackInfo{},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]int32{
|
||||
// SIMULCAST-CODEC-TODO
|
||||
// quarterResolutionQ: InvalidLayerSpatial,
|
||||
// halfResolutionH: InvalidLayerSpatial,
|
||||
// fullResolutionF: InvalidLayerSpatial,
|
||||
quarterResolutionQ: 0,
|
||||
halfResolutionH: 0,
|
||||
fullResolutionF: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"no rid",
|
||||
&livekit.TrackInfo{},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]int32{
|
||||
"": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"single layer",
|
||||
&livekit.TrackInfo{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]int32{
|
||||
quarterResolutionQ: 0,
|
||||
halfResolutionH: 0,
|
||||
fullResolutionF: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"layers",
|
||||
&livekit.TrackInfo{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0, Rid: quarterResolutionQ},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1, Rid: halfResolutionH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]int32{
|
||||
quarterResolutionQ: 0,
|
||||
halfResolutionH: 1,
|
||||
// SIMULCAST-CODEC-TODO
|
||||
// fullResolutionF: InvalidLayerSpatial,
|
||||
fullResolutionF: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"layers - no rid",
|
||||
&livekit.TrackInfo{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[string]int32{
|
||||
quarterResolutionQ: 0,
|
||||
halfResolutionH: 0,
|
||||
fullResolutionF: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for testRid, expectedSpatialLayer := range test.ridToSpatialLayer {
|
||||
actualSpatialLayer := GetSpatialLayerForRid(test.mimeType, testRid, test.trackInfo)
|
||||
require.Equal(t, expectedSpatialLayer, actualSpatialLayer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSpatialLayerForVideoQuality(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackInfo *livekit.TrackInfo
|
||||
mimeType mime.MimeType
|
||||
videoQualityToSpatialLayer map[livekit.VideoQuality]int32
|
||||
}{
|
||||
{
|
||||
"no track info",
|
||||
nil,
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]int32{
|
||||
livekit.VideoQuality_LOW: InvalidLayerSpatial,
|
||||
livekit.VideoQuality_MEDIUM: InvalidLayerSpatial,
|
||||
livekit.VideoQuality_HIGH: InvalidLayerSpatial,
|
||||
livekit.VideoQuality_OFF: InvalidLayerSpatial,
|
||||
},
|
||||
},
|
||||
{
|
||||
"no layers",
|
||||
&livekit.TrackInfo{},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]int32{
|
||||
livekit.VideoQuality_LOW: 0,
|
||||
livekit.VideoQuality_MEDIUM: 0,
|
||||
livekit.VideoQuality_HIGH: 0,
|
||||
livekit.VideoQuality_OFF: InvalidLayerSpatial,
|
||||
},
|
||||
},
|
||||
{
|
||||
"not all layers",
|
||||
&livekit.TrackInfo{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0, Rid: quarterResolutionQ},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1, Rid: halfResolutionH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]int32{
|
||||
livekit.VideoQuality_LOW: 0,
|
||||
livekit.VideoQuality_MEDIUM: 1,
|
||||
livekit.VideoQuality_HIGH: 1,
|
||||
livekit.VideoQuality_OFF: InvalidLayerSpatial,
|
||||
},
|
||||
},
|
||||
{
|
||||
"all layers",
|
||||
&livekit.TrackInfo{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0, Rid: quarterResolutionQ},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1, Rid: halfResolutionH},
|
||||
{Quality: livekit.VideoQuality_HIGH, SpatialLayer: 2, Rid: fullResolutionF},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[livekit.VideoQuality]int32{
|
||||
livekit.VideoQuality_LOW: 0,
|
||||
livekit.VideoQuality_MEDIUM: 1,
|
||||
livekit.VideoQuality_HIGH: 2,
|
||||
livekit.VideoQuality_OFF: InvalidLayerSpatial,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for testVideoQuality, expectedSpatialLayer := range test.videoQualityToSpatialLayer {
|
||||
actualSpatialLayer := GetSpatialLayerForVideoQuality(test.mimeType, testVideoQuality, test.trackInfo)
|
||||
require.Equal(t, expectedSpatialLayer, actualSpatialLayer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVideoQualityorSpatialLayer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackInfo *livekit.TrackInfo
|
||||
mimeType mime.MimeType
|
||||
spatialLayerToVideoQuality map[int32]livekit.VideoQuality
|
||||
}{
|
||||
{
|
||||
"no track info",
|
||||
nil,
|
||||
mime.MimeTypeVP8,
|
||||
map[int32]livekit.VideoQuality{
|
||||
InvalidLayerSpatial: livekit.VideoQuality_OFF,
|
||||
0: livekit.VideoQuality_OFF,
|
||||
1: livekit.VideoQuality_OFF,
|
||||
2: livekit.VideoQuality_OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
"no layers",
|
||||
&livekit.TrackInfo{},
|
||||
mime.MimeTypeVP8,
|
||||
map[int32]livekit.VideoQuality{
|
||||
InvalidLayerSpatial: livekit.VideoQuality_OFF,
|
||||
0: livekit.VideoQuality_OFF,
|
||||
1: livekit.VideoQuality_OFF,
|
||||
2: livekit.VideoQuality_OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
"layers",
|
||||
&livekit.TrackInfo{
|
||||
Codecs: []*livekit.SimulcastCodecInfo{
|
||||
{
|
||||
MimeType: mime.MimeTypeVP8.String(),
|
||||
Layers: []*livekit.VideoLayer{
|
||||
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0, Rid: quarterResolutionQ},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1, Rid: halfResolutionH},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mime.MimeTypeVP8,
|
||||
map[int32]livekit.VideoQuality{
|
||||
InvalidLayerSpatial: livekit.VideoQuality_OFF,
|
||||
0: livekit.VideoQuality_LOW,
|
||||
1: livekit.VideoQuality_MEDIUM,
|
||||
2: livekit.VideoQuality_OFF,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for testSpatialLayer, expectedVideoQuality := range test.spatialLayerToVideoQuality {
|
||||
actualVideoQuality := GetVideoQualityForSpatialLayer(test.mimeType, testSpatialLayer, test.trackInfo)
|
||||
require.Equal(t, expectedVideoQuality, actualVideoQuality)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeVideoLayersRid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rids VideoLayersRid
|
||||
normalized VideoLayersRid
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
VideoLayersRid{},
|
||||
VideoLayersRid{},
|
||||
},
|
||||
{
|
||||
"unknown pattern",
|
||||
VideoLayersRid{"3", "2", "1"},
|
||||
VideoLayersRid{"3", "2", "1"},
|
||||
},
|
||||
{
|
||||
"qhf",
|
||||
videoLayersRidQHF,
|
||||
videoLayersRidQHF,
|
||||
},
|
||||
{
|
||||
"scrambled qhf",
|
||||
VideoLayersRid{"f", "h", "q"},
|
||||
videoLayersRidQHF,
|
||||
},
|
||||
{
|
||||
"partial qhf",
|
||||
VideoLayersRid{"h", "q"},
|
||||
VideoLayersRid{"q", "h", ""},
|
||||
},
|
||||
{
|
||||
"210",
|
||||
videoLayersRid210,
|
||||
videoLayersRid210,
|
||||
},
|
||||
{
|
||||
"scrambled 210",
|
||||
VideoLayersRid{"2", "0", "1"},
|
||||
videoLayersRid210,
|
||||
},
|
||||
{
|
||||
"partial 210",
|
||||
VideoLayersRid{"1", "2"},
|
||||
VideoLayersRid{"2", "1", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
normalizedRids := NormalizeVideoLayersRid(test.rids)
|
||||
require.Equal(t, test.normalized, normalizedRids)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user