Author SHA1 Message Date
evgeny 6a1c289ab2 Keep Bluetooth SCO active for radio reception without capture 2026-10-22 17:59:11 +02:00
evgeny bb380441a3 Fix shared microphone crackle from timestamp jitter 2026-10-22 16:53:25 +02:00
evgeny c9a3a1334b Restore shared media playback to speaker after headset loss 2026-10-22 15:24:13 +02:00
evgeny 2b63c680b2 Verify shared Android audio ownership on a physical device 2026-10-22 15:07:26 +02:00
evgeny 44a3b33c3a Expose shared audio devices in stream viewing and document them 2026-10-22 15:07:26 +02:00
evgeny 5d10a16baa Use shared audio controls in radio and stream publication 2026-10-22 15:07:25 +02:00
evgeny 33ccba1f3f Add a common audio device selector for radio and streams 2026-10-22 15:07:25 +02:00
evgeny bebc579180 Share media playback devices and await cleanup before calls 2026-10-22 15:07:25 +02:00
evgeny a893f6b4f2 Route radio and stream capture through the shared controller 2026-10-22 15:07:25 +02:00
evgeny ecada765a2 Own Android media devices in one shared audio controller 2026-10-22 15:07:25 +02:00
evgeny b72e51accb Keep shared microphone PCM continuous across device changes 2026-10-22 15:07:25 +02:00
evgeny ad0505bd31 Record verified media primitive fixes and remaining location audit 2026-10-22 13:40:39 +02:00
evgeny 64519c41a7 Restrict fortified stdio test hook to glibc 2026-10-22 13:35:25 +02:00
evgeny 0d55c98add Inject fortified fread failures in sanitizer regressions 2026-10-22 13:34:54 +02:00
evgeny 5549a69b1c Cover failed file stream creation and document media cancellation effects 2026-10-22 13:31:13 +02:00
evgeny 1ceb0beb8d Check generated media identifiers in integration fixtures 2026-10-22 10:28:32 +02:00
evgeny 386ea85fea Log precise copy and media indexing failure operations 2026-10-22 10:27:42 +02:00
evgeny 607de2a074 Bound recursive walker stack and report exact copy failure phases 2026-10-22 10:26:56 +02:00
evgeny bf3834b95d Use existing Linux condition for media fault injection 2026-10-22 10:24:16 +02:00
evgeny 3a00caef50 Add fault injection regressions for media files and worker ownership 2026-10-22 10:23:56 +02:00
evgeny 5505afd1f3 Keep thumbnail path bounds and check UUID status in stress test 2026-10-22 10:21:41 +02:00
evgeny 0e418c7fa9 Propagate RNG errors and preserve 64-bit media sizes through callers 2026-10-22 10:20:02 +02:00
evgeny f399ded5ba Preserve files on copy errors and check media primitive failures 2026-10-22 10:18:23 +02:00
evgeny 85d96fc857 Share UTF-8 Windows path conversion with file workers 2026-10-22 10:18:22 +02:00
evgeny 3100985e4c Specify media worker ownership errors and primitive contracts 2026-10-22 10:16:57 +02:00
evgeny 6355bd1d73 Use shared file publication in video processing 2026-10-22 10:15:39 +02:00
evgeny 3ed4b73ebf Extract checked file identity and temporary publication primitives 2026-10-22 10:15:13 +02:00
evgeny 41cc04bdab Record media async primitive and contract audit findings 2026-10-22 10:07:20 +02:00
evgeny d8621c7f27 Record bootstrap regression coverage and successful validation 2026-10-22 10:02:57 +02:00
evgeny 3c49c2a4aa Initialize callback epoch for pending transport shutdown test 2026-10-22 09:59:13 +02:00
evgeny 633f1d3044 Schedule pending deadline resets from probe creation 2026-10-22 09:47:23 +02:00
evgeny 34602e3e7e Separate cold bootstrap from controlled pending probe reset 2026-10-22 09:45:48 +02:00
evgeny 330e346a7b Record bootstrap test race in TCP session initialization 2026-10-22 09:40:08 +02:00
evgeny 6d39800351 Record completed video fixes and validation coverage 2026-10-22 00:20:40 +02:00
evgeny df2f90d1f5 Allow MP4 millisecond edit rounding in audio tail regression 2026-10-22 00:17:55 +02:00
evgeny 1d6596dc61 Check resampler tail duration and actual 1080p to 720p conversion 2026-10-22 00:17:16 +02:00
evgeny 0653121fc4 Document video publication and cancellation in service lifecycle 2026-10-22 00:15:54 +02:00
evgeny 3f603534e9 Log exact video failure phases and check buffered file seeks 2026-10-22 00:11:02 +02:00
evgeny 631cc924cf Measure AAC signal onset above verified codec pre-echo level 2026-10-22 00:09:49 +02:00
evgeny 9f1825dbf8 Verify decoded video content, AAC signal timing and FFmpeg-free stubs 2026-10-22 00:08:22 +02:00
evgeny 9163025912 Test video I/O failures, audio timing, cancellation and concurrent ownership 2026-10-22 00:05:30 +02:00
evgeny 5929bb4028 Normalize unspecified PCM channel layouts before AAC setup 2026-10-22 00:03:12 +02:00
evgeny fe313cf8f8 Match FFmpeg AVIO and codec capability APIs across supported versions 2026-10-22 00:02:29 +02:00
evgeny cb9102f381 Prepare video through checked temporary output and complete codec drains 2026-10-22 00:01:47 +02:00
evgeny 9a8bc9b461 Select video regression branch from configured FFmpeg support 2026-10-21 23:59:06 +02:00
evgeny c4bf292085 Specify video publication, remux and completion contracts 2026-10-21 23:58:55 +02:00
evgeny 8d9704036f Add real video fixtures and file preservation regressions 2026-10-21 23:56:55 +02:00
evgeny fb01a9bb2e Record video audit failures and missing API contracts 2026-10-21 23:28:53 +02:00
evgeny 1d7e12c562 Avoid random HEX substring collisions in config updater assertions 2026-10-21 23:12:12 +02:00
evgeny 7c473dd500 Reject signed NAT forward IDs hidden by leading whitespace 2026-10-21 23:11:32 +02:00
evgeny 1ccb266971 Describe NAT transport backpressure, ownership and mapping holds 2026-10-21 23:08:31 +02:00
evgeny 7de54ab3e4 Document NAT hash, expiry, mapping holds and bounded transport ownership 2026-10-21 22:58:28 +02:00
evgeny 2c10272e15 Add repeatable NAT benchmarks for table size, payload size and full-pool misses 2026-10-21 22:55:24 +02:00
evgeny 55cc721c9b Test held NAT mappings under TUN congestion and reject invalid provider input 2026-10-21 22:54:24 +02:00
evgeny dea768c21a Hold outgoing NAT mappings through asynchronous TUN delivery 2026-10-21 22:52:19 +02:00
evgeny 824c663ebd Protect NAT mappings referenced by pending TUN transmissions 2026-10-21 22:51:25 +02:00
evgeny 80b481827c Verify bounded global NAT pressure and retained destination across mapping reuse 2026-10-21 22:46:09 +02:00
evgeny 8360e48622 Exercise NAT backpressure, mapping reuse, copy ownership and shutdown 2026-10-21 22:44:46 +02:00
evgeny 41a68b86bc Close NAT channels after cancelling producers and quiet transient TUN backpressure 2026-10-21 22:43:54 +02:00
evgeny cfc0051088 Test NAT boundary validation, hash collisions, expiry and allocation failures 2026-10-21 22:34:06 +02:00
evgeny cecc5d7d1f Validate provider replies before client TUN delivery 2026-10-21 22:32:29 +02:00
evgeny 5e2e6c15cf Preserve NAT index invariants across transport and reuse tests 2026-10-21 22:31:55 +02:00
evgeny d0c83cfff1 Add bounded NAT backpressure and forward TUN packets without copying 2026-10-21 22:30:01 +02:00
evgeny 9d6d6454b0 Reject malformed NAT configuration and use mapping lifecycle APIs in tests 2026-10-21 22:27:16 +02:00
evgeny 94fa3207f6 Index NAT mappings by hash and maintain free ports in constant time 2026-10-21 22:24:17 +02:00
evgeny 5a837f903c Validate NAT packet boundaries and distinguish skipped packets 2026-10-21 22:21:37 +02:00
evgeny 00b4b92749 Align NAT documentation with node ownership and router framing 2026-10-21 22:01:08 +02:00
evgeny 19a5870d5f Document NAT ownership, packet formats and API contracts 2026-10-21 22:00:06 +02:00
evgeny bfa23afae9 Restrict NAT delivery to its group and configured provider 2026-10-21 21:58:10 +02:00
evgeny 909005c1a4 Complete standalone NAT forward configuration fixture 2026-10-21 21:57:23 +02:00
evgeny ddc163a2b7 Cover NAT forward owners, full port range and invalid rules 2026-10-21 21:56:20 +02:00
evgeny ca471d777e Route static NAT forwards to explicit owners on all ports 2026-10-21 21:55:13 +02:00
evgeny 2c4f107e63 Require a target node for NAT port forward configuration 2026-10-21 21:54:41 +02:00
evgeny 3d0c4ff0a5 Isolate NAT mappings by source node identity 2026-10-21 21:53:17 +02:00
evgeny a9820bea42 Preserve IPv4 UDP checksum encoding through NAT 2026-10-21 21:51:49 +02:00
evgeny 52a73ea6a7 Correct incremental NAT checksum arithmetic 2026-10-21 21:50:59 +02:00
evgeny 6e9bacd843 Fix NAT flow lookup at the maximum port boundary 2026-10-21 21:48:28 +02:00
evgeny fbbd62acc4 Fix NAT packet ownership after router send failures 2026-10-21 21:43:00 +02:00
evgeny ef673eb3b7 Merge branch 'master' of http://git.syqiq.tech/jeka/utun2 2026-10-21 21:22:49 +02:00
evgeny 05b0927619 Fix TUN packet ownership, lifecycle and backend consistency
Unify TUN initialization and raw-IP writes, preserve socketpair directions, and bound reader delivery with queue backpressure. Make deferred packet ownership safe across close and uasync teardown, and apply per-interface MTU limits to lwIP MSS.

Validation: full check.sh passed 172 tests, 9 proxy integration checks, burst and load. TUN ASAN/UBSAN regressions passed 30 cases; TCP exchange passed with asymmetric and minimum MTUs. Sanitized NAT integration exposed an existing unaligned uint16_t read in eim_nat.c; that separate defect remains unchanged. Native Windows and FreeBSD validation remains outstanding.
2026-10-21 21:19:42 +02:00
evgeny e3f2feb480 Merge branch 'master' of http://git.syqiq.tech/jeka/utun2 2026-10-21 20:26:26 +02:00
evgeny a65b0c8786 Fix queue extraction in NTP regression cleanup 2026-10-21 20:18:49 +02:00
evgeny 6c84e680af Improve NTP failure diagnostics and source transition coverage 2026-10-21 20:18:17 +02:00
evgeny d6eed66926 Document NTP contracts and verify corrected wall time 2026-10-21 20:16:38 +02:00
evgeny 88bf65e07d Cover NTP correction, retries, faults and peer lifecycle 2026-10-21 20:15:17 +02:00
evgeny 13c1831baa Fix peer time cache ownership and callback teardown 2026-10-21 20:12:04 +02:00
evgeny 5609d57947 Fix NTP offset and bound asynchronous polling cycles 2026-10-21 20:11:04 +02:00
evgeny 488be0edd1 Record recurring attachment test timeout during control validation 2026-10-21 20:06:51 +02:00
evgeny e620567804 Finalize control server regression fixture cleanup 2026-10-21 20:04:30 +02:00
evgeny fdf2912427 Cover control server regressions and correct API documentation 2026-10-21 20:04:30 +02:00
evgeny 51922cd18e Fix control server buffer bounds and client resource ownership 2026-10-21 20:04:19 +02:00
evgeny 79946bb07c Keep firewall reload regression on the selective path 2026-10-21 20:00:28 +02:00
evgeny 41c32cf063 Set router packet length in firewall integration fixture 2026-10-21 19:59:34 +02:00
evgeny 4f627bddff Align firewall examples with symmetric destination filtering 2026-10-21 19:53:50 +02:00
evgeny 2a2a0ee267 Cover firewall parsing, bidirectional TUN filtering and reload failures 2026-10-21 19:52:51 +02:00
evgeny 5de0eb5115 Filter both directions of the main TUN and remove obsolete firewall guide 2026-10-21 19:49:32 +02:00
evgeny cf6c3c99e8 Reject invalid firewall configuration and preserve policy on reload failure 2026-10-21 19:48:54 +02:00
evgeny c3a7f77b54 Make firewall rule replacement atomic and validate IPv4 packets 2026-10-21 19:48:19 +02:00
evgeny 4f5651906c Report precise INI read failures and identity repair reasons 2026-10-21 19:09:03 +02:00
evgeny 2df21efc22 Bound edited INI growth and cover effective duplicate values 2026-10-21 19:06:59 +02:00
evgeny b0f5055c31 Describe shared INI parsing and distribute its internal header 2026-10-21 19:04:54 +02:00
evgeny 80eb331d6a Document config identity and publication guarantees 2026-10-21 19:04:53 +02:00
evgeny bb87a0845b Verify config updates preserve files and allocations under OOM 2026-10-21 19:03:39 +02:00
evgeny f7c8b95af6 Bound INI buffers to allocator limits and log allocation errors 2026-10-21 19:03:38 +02:00
evgeny 33130c1d74 Test identity preservation and failed config publication 2026-10-21 19:02:20 +02:00
evgeny f61c0c3e38 Preserve config identity and publish verified updates safely 2026-10-21 19:00:06 +02:00
evgeny 2410c39b71 Share INI parsing and validate complete configuration values 2026-10-21 18:54:43 +02:00
evgeny 5a57b59b50 Test emulator forwarding and partial initialization cleanup 2026-10-21 18:14:03 +02:00
evgeny ff3eb02bff Update emulator uasync lifecycle and document forwarding rules 2026-10-21 18:12:06 +02:00
evgeny 56d5fcd75a Test broadcast UUID lifetime and callback registration contracts 2026-10-21 18:10:43 +02:00
evgeny 484b620016 Preserve broadcast UUIDs until TTL and document callback contracts 2026-10-21 18:08:50 +02:00
evgeny 16f81575ce Consolidate TCP queue and resource lifecycle documentation in header 2026-10-21 17:59:08 +02:00
evgeny 10b039686c Consolidate queue usage and backpressure documentation in header 2026-10-21 17:57:34 +02:00
evgeny 194bf7bb45 Consolidate debug configuration documentation in header 2026-10-21 17:56:13 +02:00
evgeny a288afd8ce Consolidate socket lifecycle and wrapper documentation in header 2026-10-21 17:55:27 +02:00
evgeny 4f3f6d3e60 Consolidate platform adapter documentation in header 2026-10-21 17:55:27 +02:00
evgeny 1c4010ebed Consolidate serialization format and ownership documentation in header 2026-10-21 17:54:20 +02:00
evgeny ace9cb70d0 Consolidate timeout heap ownership and complexity documentation 2026-10-21 17:54:20 +02:00
evgeny af3c7a4217 Consolidate pool lifecycle and diagnostics documentation in header 2026-10-21 17:53:00 +02:00
evgeny e403a2f689 Consolidate memory ownership and allocation documentation in header 2026-10-21 17:53:00 +02:00
evgeny 3944459c29 Consolidate sliding minimum documentation in header 2026-10-21 17:52:02 +02:00
evgeny 48383ef95e Consolidate outgoing IP documentation in header 2026-10-21 17:52:02 +02:00
evgeny f020a6f62b Clarify CPU indicator polling and pending responses 2026-10-21 17:48:35 +02:00
evgeny d23a96a08e Document uasync scheduling, ownership and lifecycle contracts 2026-10-21 17:40:20 +02:00
evgeny dffcd19d21 Clarify queue callback scheduling and ownership contracts 2026-10-21 17:27:04 +02:00
evgeny 9079ab5c24 Fix debug timestamps, default output and log reopening 2026-10-21 17:10:44 +02:00
evgeny 7e39f16e0f Fix allocation size overflow, zero-size OOM and debug category parsing 2026-10-21 17:07:11 +02:00
evgeny 00819654cf Document POSIX descriptor zero exception in socket close 2026-10-21 16:57:44 +02:00
evgeny 346c06f31d Record TCP lifecycle validation and IPv6 selection caveat 2026-10-21 16:52:11 +02:00
evgeny ba7c2873c5 Initialize proxy timers before notifying connected owners 2026-10-21 16:48:25 +02:00
evgeny 0e15234ce1 Clarify TCP lifecycle and platform address selection contracts 2026-10-21 16:46:45 +02:00
evgeny e5fafacf99 Cover TCP timer replacement failures and pending FIN cleanup 2026-10-21 16:45:53 +02:00
evgeny d467d3aa8f Fix TCP connect timer ownership and pending FIN state 2026-10-21 16:44:19 +02:00
evgeny d5e489598d Clarify DNS lifetimes and parser and video plane limits 2026-10-21 16:33:16 +02:00
evgeny 1ff02188ca Fix Silero model cleanup and document runtime configuration 2026-10-21 16:29:44 +02:00
evgeny f31b34d0f0 Clarify serialization limits, cleanup and error codes 2026-10-21 16:18:51 +02:00
evgeny ecd5b9fc9d Clarify memory pool counters and strbuf error documentation 2026-10-21 16:14:48 +02:00
evgeny aca6364378 Correct call routing, media channels and API documentation 2026-10-21 16:11:36 +02:00
evgeny 88e930cc0a docs: correct member file protocol and media download contracts 2026-10-21 16:01:30 +02:00
evgeny be235e01aa docs: correct DM payload, history and transfer cancellation contracts 2026-10-21 15:53:28 +02:00
evgeny 66f004f725 docs: describe actual DM identity derivation and crypto modules 2026-10-21 15:52:45 +02:00
evgeny cbe22899c2 docs: align chat invite and member sync descriptions with code 2026-10-21 15:47:56 +02:00
evgeny df36e504fc docs: remove completed and obsolete project tasks 2026-10-21 15:47:30 +02:00
evgeny 7c814f5cb9 Route desktop codec diagnostics through the shared log
Quiet capability probes, preserve runtime errors, and register the desktop portal application. Add logging and desktop launcher regression coverage.
2026-10-21 15:45:40 +02:00
evgeny 9ba78c084b docs: correct topology API and router timeout descriptions 2026-10-21 15:40:56 +02:00
evgeny d644126cac docs: align ETCP handshake and epoch descriptions with code 2026-10-21 15:37:01 +02:00
evgeny f59f64ea48 docs: remove reviewed header backup files 2026-10-21 15:22:42 +02:00
evgeny f5f106c682 docs: clarify header APIs and preserve originals in .h1 2026-10-21 15:21:27 +02:00
evgeny abf342534f Encrypt P2P call media and validate service IDs
Require signed and encrypted call audio/video using static identity keys. Fix attachment service ID collisions and media download handle lifetime; isolate LAN tests and audit the shared service registry.
2026-10-21 15:06:27 +02:00
evgeny 8e46cf9254 docs: refresh project file descriptions in AGENTS.md 2026-10-21 14:54:06 +02:00
evgeny 15b037aef7 Scroll to latest chat messages with End key 2026-10-21 13:31:50 +02:00
evgeny 69e534304b Darken desktop chat backgrounds and lighter panels 2026-10-21 13:26:05 +02:00
evgeny 4dbc543cd3 Increase contrast of chat scrollbar thumb only 2026-10-21 13:21:30 +02:00
evgeny 4f6808156f Add chat history keyboard scrolling and remove unused PTT setting 2026-10-21 13:05:31 +02:00
evgeny 5870efdf04 Default stream redundancy to zero on Android and desktop 2026-10-21 12:42:23 +02:00
evgeny 34db81c5cf Show green stream watch buttons only for active streams 2026-10-21 12:30:08 +02:00
evgeny a31436e421 Expand chat README and add stream image 2026-10-21 02:31:00 +02:00
evgeny 550f384c69 Tidy stream video worker source formatting 2026-10-21 01:40:04 +02:00
evgeny a6b909b70e Add device regression for independent stream audio and video workers 2026-10-21 01:38:50 +02:00
evgeny fcb0768052 Protect SoundTouch lookahead when catching up live stream audio 2026-10-21 01:38:50 +02:00
evgeny 5d9da0c6b1 Preserve exact source EOF across tempo changes and expose DSP lookahead 2026-10-21 01:38:50 +02:00
evgeny e8ee608ba9 Document independent audio preparation and live stream catchup 2026-10-21 01:28:20 +02:00
evgeny 4e5530aca0 Isolate Android live video decoding from audio preparation 2026-10-21 01:23:35 +02:00
evgeny 4fa3b18cc2 Feed Android stream mixer from prepared PCM without holding DSP lock 2026-10-21 01:18:56 +02:00
evgeny 50cf61e9cd Mix partial stream PCM without copying stale buffer tails 2026-10-21 01:11:35 +02:00
evgeny c6ed01110e Exercise audio continuity during video stalls and generation changes 2026-10-21 01:03:19 +02:00
evgeny 1764603977 Isolate live video decoding from audio preparation in ChatGUI 2026-10-21 01:00:31 +02:00
evgeny 6c619d5f2a Recover stream playback stalls with bounded proportional catchup 2026-10-21 00:56:47 +02:00
evgeny 58c0fc6bba Separate stream audio DSP from bounded PCM consumption 2026-10-21 00:54:30 +02:00
evgeny 32e648ea9e Release audio holds at video EOF and stop Android batches promptly 2026-10-21 00:15:34 +02:00
evgeny b2efab9806 Document per-track completion and Android batch wakeups 2026-10-21 00:13:01 +02:00
evgeny a342316e7a Verify delayed decoders with unequal audio and video tails 2026-10-21 00:13:01 +02:00
evgeny c887780022 Consume Android batches with independent track completion 2026-10-21 00:13:01 +02:00
evgeny a538f99bf0 Expose viewer wake signals and complete audio independently on Android 2026-10-21 00:13:01 +02:00
evgeny 9fb4823df0 Batch Android viewer records and wake readers on data or stop 2026-10-21 00:13:01 +02:00
evgeny a2fb562a22 Drain desktop tracks independently and cancel obsolete media on reset 2026-10-20 23:54:55 +02:00
evgeny 852c05590d Expose track completion to desktop and retain the longer media tail 2026-10-20 23:53:58 +02:00
evgeny a54031081b Deliver independent track ends and invalidate batches on gaps 2026-10-20 23:53:23 +02:00
evgeny 23c4e4efbc Diagnose delayed decoder completion with buffered audio 2026-10-20 23:46:59 +02:00
evgeny 5bc0c1ece4 Document bounded desktop stream batches and worker wakeups 2026-10-20 23:42:28 +02:00
evgeny fbab6eae5d Anchor batch regression at a single initial keyframe 2026-10-20 23:41:12 +02:00
evgeny 0d9c728efe Test bounded batches, track budgets, notifications and FIN 2026-10-20 23:34:57 +02:00
evgeny 039c4a9ed4 Consume desktop media batches with per-track backpressure and wakeups 2026-10-20 23:31:50 +02:00
evgeny 179f248db8 Deliver bounded media batches to desktop consumers 2026-10-20 23:30:10 +02:00
evgeny 589cdcc81f Add bounded viewer batches and readiness notifications 2026-10-20 23:29:23 +02:00
evgeny 6997a82af1 Enable publisher timing logs in diagnostic test runs 2026-10-20 22:32:33 +02:00
evgeny c6f7d50838 Measure stream viewer request and worker delays 2026-10-20 22:30:04 +02:00
evgeny 7ce60ca0f2 Trace desktop stream stalls across playback and preview UI 2026-10-20 22:20:31 +02:00
evgeny b1112981fa Timestamp Android decoder output at the observation point 2026-10-20 21:44:10 +02:00
evgeny a4419b4c89 Validate Android surface timestamps in stream diagnostics 2026-10-20 21:40:20 +02:00
evgeny 1b036e4ea9 Trace stream startup decoding and presentation delays 2026-10-20 21:34:24 +02:00
evgeny f38aa3b08d Log stream startup key age and buffer handoff delays 2026-10-20 21:27:51 +02:00
evgeny c92d36354a List calling radio and multiview features in README 2026-10-20 21:14:57 +02:00
evgeny a0d49c4bbd Add Vibechat overview and group access notes 2026-10-20 21:14:16 +02:00
evgeny e3dbeee6a8 Document desktop stream tile behavior and verification 2026-10-20 20:47:55 +02:00
evgeny 8673673fcb Handle stream tile coordinates with Qt 5 and Qt 6 APIs 2026-10-20 20:46:08 +02:00
evgeny 03e8caf849 Isolate publisher return test and use native widget input 2026-10-20 20:44:42 +02:00
evgeny 2781208361 Exercise native preview clicks and publisher controls escape 2026-10-20 20:42:23 +02:00
evgeny 64064c2cbf Cover desktop local preview and multiview navigation 2026-10-20 20:39:24 +02:00
evgeny e5510e9f62 Connect desktop multiview to publisher preview and controls 2026-10-20 20:37:13 +02:00
evgeny 44db42180e Notify caller when desktop stream controls close 2026-10-20 20:33:23 +02:00
evgeny 6dab5db56a Show silent local preview and focus remote desktop stream tiles 2026-10-20 20:32:52 +02:00
evgeny f1cccb1c92 Document stream controls navigation and Samsung verification 2026-10-20 20:19:28 +02:00
evgeny b26538c48f Keep replacement camera preview active during tile disposal 2026-10-20 20:15:08 +02:00
evgeny 39bca58515 Open Android stream controls from own multiview tile 2026-10-20 20:11:49 +02:00
evgeny ca7e169811 Keep Android publisher screen focused on capture controls 2026-10-20 20:10:36 +02:00
evgeny fe9c3c74bd Record global stream shortcut verification on Samsung 2026-10-20 20:02:25 +02:00
evgeny d7053a2366 Open active stream controls from any Android header 2026-10-20 19:58:55 +02:00
evgeny 59858a726a Share active stream button across Android group and home headers 2026-10-20 19:56:50 +02:00
evgeny c4da659dcb Keep publisher shortcut available in every desktop group 2026-10-20 19:55:11 +02:00
evgeny 5b6ed6ee78 Exclude own stream from remote stream selectors 2026-10-20 19:52:22 +02:00
evgeny 7e95c3043b Add own live stream shortcut to Android chat header 2026-10-20 19:49:27 +02:00
evgeny 352f5b3f94 Show active publisher shortcut in desktop channel header 2026-10-20 19:46:32 +02:00
evgeny 6c74749dd4 Document direct local multiview preview and Samsung verification 2026-10-20 19:27:38 +02:00
evgeny 5c1619420a Show own Android multiview source through silent camera preview 2026-10-20 19:17:59 +02:00
evgeny c4b2052b27 Label multiview tap targets and record Samsung fullscreen verification 2026-10-20 19:06:14 +02:00
evgeny b46f13920c Expand Android multiview tiles on tap without restarting playback 2026-10-20 19:00:40 +02:00
evgeny d7d018d591 Record Samsung multiview fix verification on live streams 2026-10-20 18:51:32 +02:00
evgeny ba26b14f16 Include decoder frame state in Android stream failure logs 2026-10-20 18:46:57 +02:00
evgeny ea5d1fcd45 Consume repeated viewer demand when delivering its reply 2026-10-20 18:45:49 +02:00
evgeny 68fb937063 Record reproduced multiview lifecycle defects and audio test flake 2026-10-20 17:44:24 +02:00
evgeny 03f7d4341d Document multiview ownership, independent clocks and validation 2026-10-20 17:14:08 +02:00
evgeny b3ebc09b9b Wait for shared Android stream output release before starting calls 2026-10-20 17:13:59 +02:00
evgeny c98fdc6297 Reject duplicate Android catalogue entries before updating grid 2026-10-20 17:12:21 +02:00
evgeny b3116173ca Verify shared stream mixing and avoid retrying terminal closures 2026-10-20 17:12:16 +02:00
evgeny 4a44149c00 Translate Android multiview controls and empty state 2026-10-20 17:07:37 +02:00
evgeny 5f0325e817 Expose watch-all streams from Android chat and preserve catalogue on transient errors 2026-10-20 17:07:31 +02:00
evgeny 9e5b2ad435 Add adaptive Android grid with automatic source reconciliation 2026-10-20 17:07:19 +02:00
evgeny 14b152bb5a Keep Android stream surfaces alive until decoder release and fit grid geometry 2026-10-20 17:07:12 +02:00
evgeny 75806249bd Run Android players concurrently and fall back between decoders 2026-10-20 16:58:41 +02:00
evgeny 4e386fc8e3 Mix Android streams on one AudioTrack with independent clocks 2026-10-20 16:58:32 +02:00
evgeny 57d28376b6 Use independent shared viewers in the Android native bridge 2026-10-20 16:58:25 +02:00
evgeny c8bcf41f4a Translate desktop multiview controls and catalogue states 2026-10-20 16:39:59 +02:00
evgeny fd05ec47ab Connect the all-streams window to chat actions and service lifetime 2026-10-20 16:39:50 +02:00
evgeny bad9ee3a0c Add an automatically updated desktop grid of active streams 2026-10-20 16:39:43 +02:00
evgeny 05deb15c91 Embed stream players and verify grid and concurrent playback 2026-10-20 16:39:34 +02:00
evgeny 1b49c4023b Offer all active streams and share catalogue parsing in the GUI 2026-10-20 16:29:17 +02:00
evgeny 103e0b6865 Keep per-stream playback clocks on the shared audio output 2026-10-20 16:29:06 +02:00
evgeny a0b6dd7ef5 Mix live stream PCM into the shared desktop audio output 2026-10-20 16:27:24 +02:00
evgeny 94697c1af0 Preserve desktop catalogue watches across chat selection 2026-10-20 16:27:17 +02:00
evgeny 4fa9d474ab Track live stream catalogues independently for each owner 2026-10-20 16:27:10 +02:00
evgeny 39446b6e2c Use independent viewer sessions in the desktop stream bridge 2026-10-20 16:13:14 +02:00
evgeny 0bc3b62f46 Add independent live stream viewer sessions in the shared core 2026-10-20 15:53:44 +02:00
evgeny 9c7b49e91f Add call status sharing and automatic stream orientation 2026-10-20 15:49:32 +02:00
evgeny f87cda8b13 Record multicast validation and stabilize report timing test 2026-10-20 15:13:59 +02:00
evgeny b250e8a44d Cover multicast report backpressure, chunking and timer cleanup 2026-10-20 15:08:59 +02:00
evgeny 961c3447d0 Create multicast report channel before checking backpressure 2026-10-20 15:05:16 +02:00
evgeny e59bc9857b Test multicast routing, overload and descriptor lifetime 2026-10-20 15:01:27 +02:00
evgeny 4a1a65c3ba Implement multicast descriptors and receiver subscriptions 2026-10-20 14:53:23 +02:00
evgeny e416d69212 Define descriptor-based multicast API and usage contract 2026-10-20 14:44:23 +02:00
evgeny 8d5459f8fa Connect multicast queues to topology updates and builds 2026-10-20 14:34:55 +02:00
evgeny 5aa28a11d6 Register multicast protocol and core lifecycle 2026-10-20 14:34:30 +02:00
evgeny b02ad6f4cf Add multicast branching queues and bounded loss reports 2026-10-20 14:33:40 +02:00
evgeny de6712de91 Record stream settings validation under an isolated display 2026-10-20 02:30:11 +02:00
evgeny 1e49d2554c Limit desktop and Android stream bitrate menus to 12 Mbit/s 2026-10-20 02:29:46 +02:00
evgeny d2f0828917 Bound restored manual stream bitrate choices to 12 Mbit/s 2026-10-20 02:29:00 +02:00
evgeny 390747343b Document background codec preparation and complete probe validation 2026-10-20 01:28:59 +02:00
evgeny 475bec52ca Distinguish encoder KEY checks from receiver capability probes in tests 2026-10-20 01:27:03 +02:00
evgeny 4db89eeb32 Check cancelled codec preparation cannot publish partial capabilities 2026-10-20 01:25:24 +02:00
evgeny 36e8ab228c Verify startup codec preparation keeps GUI responsive and reuses probes 2026-10-20 01:23:21 +02:00
evgeny 91478e2593 Warm Android video codecs in the background and accelerate probe tails 2026-10-20 01:22:52 +02:00
evgeny e3b74de7ef Prepare and cache desktop video codecs once at application startup 2026-10-20 01:10:44 +02:00
evgeny 0cd1955606 Keep all eight decoder probe frames while accelerating the tail 2026-10-20 01:10:29 +02:00
evgeny 8ad5c09474 Finish decoder capability probes after first paced output and drain 2026-10-20 01:07:31 +02:00
evgeny 7ae204ba4f Document Android startup checks and remaining Linux probe delay 2026-10-20 01:05:53 +02:00
evgeny a4b9367344 Trace first video frame and verify capability upgrades preserve RX 2026-10-20 00:58:37 +02:00
evgeny 74313498f2 Cache verified video codecs and publish receive capabilities early 2026-10-20 00:56:53 +02:00
evgeny d71d0dc4af Check fullscreen video fitting without cropping or rotating thumbnails 2026-10-20 00:54:29 +02:00
evgeny 8b438f26a5 Fit fullscreen call video along the screen without rotating controls 2026-10-20 00:50:05 +02:00
evgeny bea8f1f901 Document Samsung surface ACK deadlock and hardware regression results 2026-10-20 00:43:06 +02:00
evgeny dd5dee2767 Preserve legacy timestamp waits and validate actual surface acknowledgements 2026-10-20 00:42:05 +02:00
evgeny 3eccf58c5e Add hardware TextureView ACK regression probe 2026-10-20 00:39:18 +02:00
evgeny 65f7babb2a Register Android video presentation ACK before the frame draw 2026-10-20 00:38:05 +02:00
evgeny 2c828d481d Record full 60 fps Samsung to Unisoc decode verification 2026-10-20 00:26:11 +02:00
evgeny 49317aab75 Document 60 fps camera modes and presentation recovery 2026-10-20 00:25:33 +02:00
evgeny 3a7aab9186 Cancel Android codec verification when the call stops 2026-10-20 00:24:28 +02:00
evgeny 521dce5528 Use verified Android encoder levels and expose negotiated frame rate 2026-10-20 00:21:21 +02:00
evgeny 3037c5fa87 Verify 50 fps backends and decode all frames during display catch-up 2026-10-20 00:16:23 +02:00
evgeny 52d9a347c3 Bound persistent presentation lag and report catch-up decisions 2026-10-20 00:15:42 +02:00
evgeny 8c826fa2e7 Exercise both video orientations at 30, 50 and 60 fps 2026-10-20 00:15:42 +02:00
evgeny 3db52c755c Preserve phase when sampling 50 fps from a 60 fps camera 2026-10-20 00:15:22 +02:00
evgeny 9bc3e7228c Test Android frame pacing and extend hardware diagnostics to 60 fps 2026-10-20 00:13:48 +02:00
evgeny 0a73d140e6 Account for full refresh-cycle GUI acknowledgement latency 2026-10-20 00:12:26 +02:00
evgeny d8d9f7c4a4 Select camera resolution and frame rate in Android calls 2026-10-20 00:11:15 +02:00
evgeny 27cbf2ba44 Recover persistent video lag by skipping decoded presentation 2026-10-20 00:09:57 +02:00
evgeny 4a759115c9 Probe Android video codecs at 30, 50 and 60 fps 2026-10-20 00:09:56 +02:00
evgeny d991b4c63e Preserve complete generated decoder probe byte arrays 2026-10-20 00:08:16 +02:00
evgeny 238ebcc751 Validate 60 fps probe bitstreams in both orientations 2026-10-20 00:07:53 +02:00
evgeny 0830fe4461 Carry selected video frame rate through desktop call settings 2026-10-20 00:07:36 +02:00
evgeny f6701f81f9 Probe desktop decoders independently at 30 and 60 fps 2026-10-20 00:07:08 +02:00
evgeny 0d08c21330 Select decoder probe rate and preserve HEVC level 4.1 2026-10-20 00:06:45 +02:00
evgeny 4f1af6456d Generate real 60 fps video decoder probe fixtures 2026-10-20 00:06:29 +02:00
evgeny a852a9738b Select camera capture rate and verify 30/60 fps pacing 2026-10-20 00:03:01 +02:00
evgeny ee1e28bff9 Carry selected camera frame rate through isolated capture IPC 2026-10-20 00:02:25 +02:00
evgeny 74a4135cdc Accept faster camera capture modes and pace up to 60 fps 2026-10-20 00:01:13 +02:00
evgeny 9d6dc1b469 Document verified Samsung HEVC High tier support 2026-10-19 23:40:38 +02:00
evgeny c0d5db318f Keep codec mode probes consistent with live encoding 2026-10-19 23:38:53 +02:00
evgeny 7be4b8884c Verify production HEVC modes and cross-platform reception 2026-10-19 23:36:58 +02:00
evgeny 08c74894ef Probe HEVC High tier on Android and desktop receivers 2026-10-19 23:34:15 +02:00
evgeny f2d67990f8 Cover High tier negotiation and independent KEY probes 2026-10-19 23:33:51 +02:00
evgeny 7122d70ea6 Negotiate HEVC Main High tier with explicit level limits 2026-10-19 23:32:29 +02:00
evgeny 5c1807a712 Show actual video call codecs on Android 2026-10-19 23:30:13 +02:00
evgeny 9b5743123b Add standalone Samsung HEVC comparison probe 2026-10-19 23:15:40 +02:00
evgeny 833eeb0a8d Document Full HD call modes and verified codec recovery checks 2026-10-19 22:56:35 +02:00
evgeny 72cb1fa829 Clarify camera mode ownership and codec probe structure 2026-10-19 22:54:55 +02:00
evgeny 7b341d9f4f Cancel desktop decoder probing when a call stops 2026-10-19 22:52:40 +02:00
evgeny c211e17af9 Log Android verified camera modes and guard early camera events 2026-10-19 22:49:57 +02:00
evgeny fcdc098415 Refresh call camera modes on device changes outside the GUI 2026-10-19 22:47:45 +02:00
evgeny 6e0d3f3022 Exercise real Full HD codecs and camera buffer boundaries 2026-10-19 22:46:35 +02:00
evgeny cf2fc67a53 Update camera integration test to nonblocking audio clocks 2026-10-19 22:46:09 +02:00
evgeny 902e7bb921 Use explicit HEVC Main tier and diagnose fixture rejection 2026-10-19 22:45:38 +02:00
evgeny bf8dfeec02 Probe desktop Full HD codecs and retain GPU support by size 2026-10-19 22:37:29 +02:00
evgeny 720ee64fd1 Cover Full HD KEY geometry in native validation tests 2026-10-19 22:37:12 +02:00
evgeny fb660f2b3e Select verified KEY fixtures by actual video geometry 2026-10-19 22:35:56 +02:00
evgeny f50f717b35 Generate landscape and portrait Full HD decoder fixtures 2026-10-19 22:35:34 +02:00
evgeny 7a0bc9516c Populate call resolutions from camera modes and test Full HD IPC 2026-10-19 22:35:05 +02:00
evgeny eb11d2ebc5 Carry native camera modes through Full HD capture protocol 2026-10-19 22:33:59 +02:00
evgeny 9a3ef7b622 Expand Android call previews into available screen space 2026-10-19 22:29:02 +02:00
evgeny 51c6bbb96f Queue Android call camera startup until video probing completes 2026-10-19 21:30:02 +02:00
evgeny c3c88457a8 Document independent video scheduling and verified recovery behavior 2026-10-19 21:22:39 +02:00
evgeny 55e87d20bf Verify generation changes and stale ACK isolation after restart 2026-10-19 21:18:03 +02:00
evgeny 9913222e03 Check video queue publication and nonblocking audio clock parity 2026-10-19 21:16:55 +02:00
evgeny 8c22df8f8e Clarify GUI ownership and preserve video queue payload lifetime 2026-10-19 21:16:10 +02:00
evgeny d0232e4c6e Smooth video presentation phase across render latency changes 2026-10-19 21:15:18 +02:00
evgeny 6f542bfae9 Test video timing with slow decoder and missing presentation ACK 2026-10-19 21:13:21 +02:00
evgeny d9843b8cbd Separate video decoding from event-driven presentation timing 2026-10-19 21:12:02 +02:00
evgeny 93f6fa1d52 Allow video playout diagnostics outside the timer thread 2026-10-19 21:09:22 +02:00
evgeny 402c0c0611 Add bounded video SPSC slots and mutex-free wake events 2026-10-19 21:08:41 +02:00
evgeny 9ca0e61361 Expose nonblocking call clock access through both mutexes 2026-10-19 21:07:44 +02:00
evgeny 562bdf54a6 Add nonblocking jitter clock reads for video scheduling 2026-10-19 21:07:44 +02:00
evgeny 193a549d47 Expose lock-free cumulative call video receive count 2026-10-19 21:07:19 +02:00
evgeny 7b38fd0269 Diagnose decoder work crossing video presentation deadlines 2026-10-19 20:57:45 +02:00
evgeny 10018d008e Update call video controls and sensor rotation across Android and desktop 2026-10-19 20:24:39 +02:00
evgeny ae1d05deda Fix FFmpeg D3D11VA implementation selection on Windows 2026-10-19 19:45:00 +02:00
evgeny 64761fd945 Record verified video builds and intermittent stream audio timing assertion 2026-10-19 19:27:50 +02:00
evgeny 5e38e5b71d Use minimal timer access and diagnose failed video image transforms 2026-10-19 19:24:07 +02:00
evgeny 9d45488369 Require live GPU output before EOS and document presentation-clock buffering 2026-10-19 19:21:48 +02:00
evgeny 16bdf95ba3 Prefer verified Android hardware codecs and use measured low-latency decoder pipeline 2026-10-19 19:21:01 +02:00
evgeny 2a08fd8acd Report decoder stage timings and detect real output stalls independently of playback waits 2026-10-19 19:19:27 +02:00
evgeny ecdba8fb63 Measure presentation cadence and stop latency while preserving unaccepted GPU KEY on fallback 2026-10-19 19:16:17 +02:00
evgeny 243233a20b Test delayed decoder output, four slice threads and complete generation drains 2026-10-19 19:15:05 +02:00
evgeny a4a687be4c Recover real GPU decoder failures on CPU without treating ordinary EAGAIN as frame loss 2026-10-19 19:14:03 +02:00
evgeny f9348ce36f Verify actual bitstreams through asynchronous decoder API and CPU encoder fallback 2026-10-19 19:13:00 +02:00
evgeny 5f45b38cfb Exercise acknowledged frame timing, lossless stalls and queue-dependent deceleration 2026-10-19 19:12:23 +02:00
evgeny 8eb869e41c Enable device-backed FFmpeg hardware decoding and verify exact RX queue accounting 2026-10-19 19:11:07 +02:00
evgeny 4bb2e42a7e Keep decoder reserve based on paced probe rather than transient burst depth 2026-10-19 19:09:17 +02:00
evgeny 895cdd5ac2 Apply presentation-clock queue pacing and three decoded slots to Android 2026-10-19 19:08:51 +02:00
evgeny ecae1cc3c2 Drive desktop video from presentation ACKs with three decoded slots and interruptible deadlines 2026-10-19 19:07:51 +02:00
evgeny 713a972c01 Decode call video through bounded submit/drain with verified GPU and four CPU slice threads 2026-10-19 19:06:52 +02:00
evgeny dc22014bcc Use four CPU encoder workers and retain GPU priority with software fallback 2026-10-19 19:04:08 +02:00
evgeny 28f3b264b8 Anchor video pacing to presentation acknowledgements and total backlog 2026-10-19 19:03:48 +02:00
evgeny baeb4973dd Expose RX frame depth and wake decoding when complete video arrives 2026-10-19 19:02:51 +02:00
evgeny c92f7991ec Record Windows startup GUI stalls and capture-gap scheduling correction 2026-10-19 18:25:34 +02:00
evgeny 40dcc76adb Trace GUI event loop stalls separately from video painting and dispatch 2026-10-19 18:24:57 +02:00
evgeny 47033718ad Apply capture gap recovery cadence consistently on Android 2026-10-19 18:23:25 +02:00
evgeny 526ee5ec8b Test queued capture gaps without dropping frames or delaying ready video 2026-10-19 18:23:25 +02:00
evgeny 9bffe49e62 Bound video display cadence independently of capture timestamp gaps 2026-10-19 18:22:47 +02:00
evgeny 642fc34bcc Bound video recovery correction before media timestamp arithmetic 2026-10-19 17:45:17 +02:00
evgeny 12e9e4da37 Verify GPU backlog drains fully without additional camera inputs 2026-10-19 17:44:14 +02:00
evgeny d266c5da25 Respect GPU pipeline capacity before submitting a new camera frame 2026-10-19 17:43:45 +02:00
evgeny 0f22be6885 Check audio source rate at the actual PCM span boundary 2026-10-19 17:42:49 +02:00
evgeny b7ae7ef700 Document lossless recovery contract and verify actual audio source rate 2026-10-19 17:40:51 +02:00
evgeny a9b91145a6 Document sequential presentation diagnostics and simplify GUI frame ownership 2026-10-19 17:39:54 +02:00
evgeny 4ed2c4bbab Wake video transmission on router queue readiness without polling delay 2026-10-19 17:38:31 +02:00
evgeny 5f1985f9ab Verify RX drain and explicit decoder rejection semantics 2026-10-19 17:37:24 +02:00
evgeny 943f9f2646 Drain receiver backlog before admitting new production and avoid double waiting 2026-10-19 17:35:57 +02:00
evgeny 02e7bbd9af Acknowledge Android frames after RenderThread consumption on all supported APIs 2026-10-19 17:34:44 +02:00
evgeny b11a1beb44 Test lossless mailbox retry after partial video transmission 2026-10-19 17:33:25 +02:00
evgeny 322947777c Stop invalid encoder output explicitly while preserving transient backpressure retries 2026-10-19 17:32:49 +02:00
evgeny c0422a73c0 Drain accepted MediaCodec inputs before bitrate fallback replacement 2026-10-19 17:31:46 +02:00
evgeny 0e53a9f5dd Log retained video batches and verify every GPU input across bitrate changes 2026-10-19 17:31:15 +02:00
evgeny 486d3314ef Acknowledge Android texture consumption before presenting the next frame 2026-10-19 17:30:03 +02:00
evgeny 13f09cfc20 Drain old GPU frames before bitrate replacement and poll outputs while blocked 2026-10-19 17:29:10 +02:00
evgeny 5f772fa97f Drain GPU outputs independently of capture and verify lossless recovery 2026-10-19 17:28:13 +02:00
evgeny 0bc2715a30 Preserve Android encoder outputs and throttle raw input before encoding 2026-10-19 17:25:50 +02:00
evgeny 0a2700a387 Use measured media tempo for sequential Android video playout 2026-10-19 17:25:16 +02:00
evgeny 4e9cce8a01 Present desktop video sequentially through GUI paint acknowledgement 2026-10-19 17:23:58 +02:00
evgeny 0eb743d627 Retain video mailbox tails and pause raw input under backpressure 2026-10-19 17:22:51 +02:00
evgeny 284a1268e4 Preserve encoded chain on temporary video transport backpressure 2026-10-19 17:22:09 +02:00
evgeny 7cbe5375d4 Define audio paced lossless video scheduling interface 2026-10-19 17:21:07 +02:00
evgeny 02a11f7d0a Diagnose Windows call video timing and queue losses 2026-10-19 17:20:12 +02:00
evgeny 21a20da5ee Coordinate Android call audio ownership and improve video controls 2026-10-19 16:51:51 +02:00
evgeny 39ed42da2e Translate call policies and verify desktop phone button colors 2026-10-19 15:20:25 +02:00
evgeny 94e79e5630 Color Android member phone actions using live call presence 2026-10-19 15:20:25 +02:00
evgeny a1f8eaeec2 Show explicit caller choice and already-active radio outcome on Android 2026-10-19 15:20:25 +02:00
evgeny 187fc7fee8 Handle Telecom presentation and pending caller choice notifications 2026-10-19 15:20:25 +02:00
evgeny bd2dd056d6 Honor Telecom incoming UI permission and report phone busy outcomes 2026-10-19 15:20:25 +02:00
evgeny 4cebd0be54 Wire incoming call policy and caller continuation through JNI 2026-10-19 15:20:25 +02:00
evgeny 7d5399cd62 Add five independent Android incoming call preferences 2026-10-19 15:20:25 +02:00
evgeny 97b537c005 Document incoming call policies, busy confirmation and radio status replies 2026-10-19 15:20:04 +02:00
evgeny 558b2a2ac0 Describe ephemeral phone presence in existing BGP flags 2026-10-19 15:19:22 +02:00
evgeny a9d63480cf Document incoming policy and already-active radio contracts 2026-10-19 15:18:49 +02:00
evgeny 27b0f64ec0 Record media regression failures found by full test run 2026-10-19 15:18:09 +02:00
evgeny cda46c0b82 Translate and test already-active radio response with packet loss 2026-10-19 15:05:27 +02:00
evgeny 4deee21d9f Notify caller when subscriber already listens to the channel radio 2026-10-19 15:05:09 +02:00
evgeny 5a318f6f60 Isolate packet loss fixtures between call policy scenarios 2026-10-19 15:03:59 +02:00
evgeny babcb7786a Log phone policy regression state and allow bounded teardown coverage 2026-10-19 15:02:13 +02:00
evgeny b21aeaa6f8 Test normal call busy confirmation and all incoming rejection policies 2026-10-19 14:59:55 +02:00
evgeny 79b4235960 Cover radio phone policy, BGP color flags, retries and caller choice timeout 2026-10-19 14:59:17 +02:00
evgeny 4683c98c08 Apply independent call and radio blocking before phone busy handling 2026-10-19 14:56:58 +02:00
evgeny a72668a491 Represent independent incoming call preferences as one policy mask 2026-10-19 14:56:58 +02:00
evgeny 03a4462407 Reject phone busy calls before any incoming UI when policy is enabled 2026-10-19 14:55:55 +02:00
evgeny a2fdbb6afb Define automatic rejection policy for phone busy subscribers 2026-10-19 14:55:55 +02:00
evgeny d4cd80d3dd Translate phone presence and caller confirmation controls 2026-10-19 14:54:23 +02:00
evgeny 37cf6089e7 Add caller decision controls and colored phone presence on desktop 2026-10-19 14:54:01 +02:00
evgeny b68116eedf Expose phone busy event to desktop call interface 2026-10-19 14:54:01 +02:00
evgeny 6a4a0f45d8 Separate phone status publication from incoming caller warning 2026-10-19 14:51:12 +02:00
evgeny b7aaf268af Wait for caller override before ringing a phone-busy radio subscriber 2026-10-19 14:48:39 +02:00
evgeny be367a08b6 Gate incoming calls on explicit caller decision when phone is busy 2026-10-19 14:47:59 +02:00
evgeny 6ad9c8c80b Define pending decision state for telephone busy warning 2026-10-19 14:46:47 +02:00
evgeny 7ed1a9b167 Carry phone presence in existing BGP and member flag bytes 2026-10-19 14:46:47 +02:00
evgeny 505185430c Define live phone presence and call continuation API 2026-10-19 14:46:16 +02:00
evgeny 1d28ced4f4 Replace Android member radio badge with a green radio call button 2026-10-19 14:30:35 +02:00
evgeny fdd39e82e2 Show desktop radio subscription on the call icon and allow Android icon tint 2026-10-19 14:30:35 +02:00
evgeny aca14bfb70 Select the radio ringtone in both Android ringtone playback paths 2026-10-19 14:25:27 +02:00
evgeny 7693db7b4d Expose radio ringtone enable and volume settings on desktop 2026-10-19 14:25:12 +02:00
evgeny 30f79f7a8d Play the dedicated desktop ringtone for incoming radio calls 2026-10-19 14:25:11 +02:00
evgeny cf4edbbf62 Add a distinct three-tone radio call ringtone for desktop and Android 2026-10-19 14:25:11 +02:00
evgeny 9712a0a583 Check desktop call buttons, online changes, drags, and shifted radio mute control 2026-10-19 14:22:46 +02:00
evgeny cd58903101 Connect member row call actions and retain invitations in the context menu 2026-10-19 14:21:52 +02:00
evgeny a8c2ce8401 Render and handle speaker, radio call, and phone buttons in desktop member rows 2026-10-19 14:21:37 +02:00
evgeny dad819282a Expose live member availability to desktop action buttons 2026-10-19 14:21:37 +02:00
evgeny 04acb6f836 Check radio timer ownership through public service lifecycle 2026-10-19 14:14:52 +02:00
evgeny c0626e0387 Verify idle radio calls have no timers and reject invalid invitation expiry 2026-10-19 14:13:45 +02:00
evgeny e61dc8cd06 Wait for Android channel loading once before radio activation 2026-10-19 14:11:49 +02:00
evgeny a0e440c0f5 Use readable radio call states and completion reasons in logs 2026-10-19 14:11:49 +02:00
evgeny 1b3269f76b Require active subscription on radio acceptance and verify live BGP gating 2026-10-19 14:10:35 +02:00
evgeny 613a75becc Schedule only active radio deadlines and reject late terminal answers 2026-10-19 14:09:10 +02:00
evgeny 5f9e9975d3 Initialize radio router channels before checking send backpressure 2026-10-19 14:07:39 +02:00
evgeny a73d829080 Exercise radio invitations over routed network with deadlines and packet loss 2026-10-19 14:05:29 +02:00
evgeny a831cf6295 Translate desktop radio invitation windows and outcomes 2026-10-19 14:04:45 +02:00
evgeny b57c8e5041 Move Android message action to member details and translate radio call UI 2026-10-19 14:03:24 +02:00
evgeny 25376197db Open channel radio from Android calls and move invitation into user details 2026-10-19 14:02:56 +02:00
evgeny 62a976aef5 Add Android radio call controls and incoming overlay 2026-10-19 14:01:52 +02:00
evgeny 9bd9f04bcf Dispatch Android radio call events and clear them before core restart 2026-10-19 14:01:28 +02:00
evgeny 949c300c53 Set radio invitation timeout to one minute 2026-10-19 14:01:27 +02:00
evgeny 23ece16940 Own pending Android radio calls independently of Activity and ViewModel 2026-10-19 14:00:44 +02:00
evgeny dbb07df7e9 Support incoming radio ringtone and notification actions on Android 2026-10-19 14:00:43 +02:00
evgeny b48a91853d Confirm Android radio activation and support explicit channel selection 2026-10-19 13:59:35 +02:00
evgeny 34890ea709 Bridge Android radio invitation commands to the core thread 2026-10-19 13:59:34 +02:00
evgeny a97c915663 Fix radio signaling and desktop channel type references 2026-10-19 13:58:39 +02:00
evgeny 3f97dafbf7 Show desktop radio calls and activate channel radio after acceptance 2026-10-19 13:58:06 +02:00
evgeny 427794fed8 Expose explicit desktop radio activation for accepted invitations 2026-10-19 13:57:30 +02:00
evgeny 858ba38caf Add radio call action to desktop channel members 2026-10-19 13:57:29 +02:00
evgeny 310d997af3 Deliver radio invitation events to desktop UI 2026-10-19 13:57:29 +02:00
evgeny 849f1c8618 Start and stop radio invitation service with chat lifecycle 2026-10-19 13:56:35 +02:00
evgeny b0fe2d66af Coordinate busy state between telephone and radio invitations 2026-10-19 13:56:35 +02:00
evgeny 7f4ac2fadf Register radio invitation service and shared core events 2026-10-19 13:56:17 +02:00
evgeny 639be7fcc2 Add channel radio invitation signaling and bounded result cache 2026-10-19 13:56:06 +02:00
evgeny a84b226b18 chatgui: enlarge member availability indicator and remove white outline 2026-10-19 13:23:40 +02:00
evgeny 214498f892 Open broadcast controls when selecting the local stream 2026-10-19 12:54:43 +02:00
evgeny 62633bc924 Show the group stream icon only for active broadcasts
Open a single active stream directly and show a selection for multiple streams. Move stream publishing and management into the group menu.
2026-10-19 12:51:56 +02:00
evgeny ffe563bb9f Enable VibeChat UTUN service and package the Wintun driver
Add a settings button to open the current config in an external editor. Wait for core, UTUN and chat startup before reporting readiness; verify startup failures and restarts. Include Wintun DLL and license notices in Windows builds and installers.
2026-10-19 12:33:35 +02:00
evgeny bdb6bf80a4 Free Android stream preview space with start and stop icons 2026-10-19 12:18:42 +02:00
evgeny d0bbe8444e Refine Android stream icons and show channel in title 2026-10-19 12:08:57 +02:00
evgeny 1084cd10ec Compact Android stream controls into one icon toolbar 2026-10-19 11:51:33 +02:00
evgeny ec545a8665 Remove the minimized-to-tray notification 2026-10-19 10:44:29 +02:00
evgeny d7f30d7bb1 Activate the existing VibeChat window on repeated launches 2026-10-19 10:29:12 +02:00
evgeny 50017b7f74 Sync configured chat names and embed the Windows application icon 2026-10-19 09:54:42 +02:00
evgeny 7140775d8d Preserve source context when applying the live QP dependency patch 2026-10-18 22:30:28 +02:00
evgeny 5568331510 Keep the QP adaptation tooltip readable in source 2026-10-18 22:26:23 +02:00
evgeny 242d0f5493 Verify the QP ceiling constrains real HEVC output under bitrate pressure 2026-10-18 22:24:55 +02:00
evgeny 6e0b842858 Test live QP driver updates and rejection handling against patched FFmpeg 2026-10-18 22:23:23 +02:00
evgeny 2aa890201d Package live AMF QP support with the FFmpeg build profile 2026-10-18 22:20:44 +02:00
evgeny aff8a21ae4 Stop raising QP as soon as measured bitrate returns to target 2026-10-18 22:11:48 +02:00
evgeny fae794d39e Persist broadcast QP controls and verify smooth rate feedback 2026-10-18 22:11:29 +02:00
evgeny fe86a16cac Support live AMF QP ceiling updates in bundled FFmpeg 2026-10-18 22:08:47 +02:00
evgeny 00698537ad Add QP ceiling and adaptation controls to the broadcast window 2026-10-18 22:07:36 +02:00
evgeny df02aa8170 Adapt the stream QP ceiling from measured encoded bitrate 2026-10-18 22:06:34 +02:00
evgeny 0070221045 Apply configured QP ceilings and expose live encoder adjustment capability 2026-10-18 22:05:53 +02:00
evgeny 54b50f4177 Define stream QP ceiling and smooth bitrate feedback controller 2026-10-18 22:05:33 +02:00
evgeny a51af91718 Use AMF remote desktop latency mode for HEVC broadcasts 2026-10-18 21:51:54 +02:00
evgeny bd92ac0c88 Ensure the minimal Windows FFmpeg build includes HEVC header parsing 2026-10-18 19:52:41 +02:00
evgeny 4b88402f84 Verify FFmpeg diagnostic callback restoration 2026-10-18 19:49:04 +02:00
evgeny 0cfc29d9eb Restore FFmpeg diagnostics after Qt multimedia initializes its logger 2026-10-18 19:48:34 +02:00
evgeny d1ba26f8ac Log converted encoder input detail alongside HEVC quantization 2026-10-18 19:46:35 +02:00
evgeny 6d5a626b9b Include HEVC header tracing in bundled FFmpeg and round-trip tests 2026-10-18 19:45:36 +02:00
evgeny f7853f145c Trace HEVC key frame quantization and reduce AMF log noise 2026-10-18 19:44:38 +02:00
evgeny 78b147f806 Expose AMF driver diagnostics and effective encoder settings 2026-10-18 18:50:20 +02:00
evgeny b8bc9b409a Fix Android stream layout and show measured publisher bitrate 2026-10-18 18:24:30 +02:00
evgeny e246357f92 Document valid HEVC PPS support and shared startup verification 2026-10-18 17:33:13 +02:00
evgeny aedb0e6170 Cover HEVC PPS flags and incompatible AMF startup output 2026-10-18 17:21:05 +02:00
evgeny f8827a9466 Accept valid HEVC PPS flags and verify call GPU probes fully 2026-10-18 17:19:57 +02:00
evgeny 0730fb6bdd Описать GPU-путь звонка, границы буферизации и проверку задержки 2026-10-18 17:10:17 +02:00
evgeny 5e672f01d9 Проверить отмену пачки видеокадров при завершении звонка 2026-10-18 17:05:54 +02:00
evgeny 66526ad4a5 Проверить готовую пачку GPU через транспорт и обновить JNI видеопорта 2026-10-18 17:05:00 +02:00
evgeny 86a278ee99 Передавать готовую пачку GPU атомарно через TX mailbox 2026-10-18 17:01:22 +02:00
evgeny 0343825746 Показывать фактическую глубину GPU-конвейера без дополнительной TX-очереди 2026-10-18 16:57:49 +02:00
evgeny 09f48403c5 Проверить GPU-конвейер на задержке, пакетной выдаче и отказе замены 2026-10-18 16:55:31 +02:00
evgeny 2facdb6949 Сохранять выдачу старого GPU до первого IDR замены 2026-10-18 16:54:36 +02:00
evgeny ae8eeab490 Выдавать готовые кадры GPU без дополнительного ожидания в TX 2026-10-18 16:51:55 +02:00
evgeny 6715acdef8 Добавить GPU-конвейер звонка с исходными PTS и подтверждением смены по IDR 2026-10-18 16:50:33 +02:00
evgeny 3ed9acb384 Устранить сжатие панели звонка и распараллелить HEVC внутри кадра 2026-10-18 16:28:36 +02:00
evgeny 21e4821e22 Ограничить камеру звонка до 30 fps перед конвертацией 2026-10-18 16:27:43 +02:00
evgeny f6051d3bcd Показывать этапы первого долгого кадра в интервале 2026-10-18 16:18:42 +02:00
evgeny 5a55bcb500 Диагностика задержек программного видео на desktop 2026-10-18 16:17:51 +02:00
evgeny 03ff431d78 Avoid Windows interface macro collision in local transport 2026-10-18 15:29:34 +02:00
evgeny 4c62f45984 Bound local discovery traffic and validate invitations 2026-10-18 15:18:40 +02:00
evgeny 80b23997c0 Use stream category filters in desktop stream tests 2026-10-18 15:10:59 +02:00
evgeny f6f8647091 Decouple file network test from temporary packet logs 2026-10-18 15:10:57 +02:00
evgeny 0e2253cdc8 Remove Android video timing probes and categorize radio logs 2026-10-18 15:10:06 +02:00
evgeny 044a20d4a0 Clean desktop stream lifecycle and event bridge logging 2026-10-18 15:09:24 +02:00
evgeny b40ba9308f Route desktop call and PTT diagnostics to their categories 2026-10-18 15:08:57 +02:00
evgeny 74e2238b7f Categorize file transfer and browser logs and reduce I/O noise 2026-10-18 15:08:42 +02:00
evgeny 81ee235f73 Keep file request diagnostics in media and drop packet noise 2026-10-18 15:07:45 +02:00
evgeny dcdc700d8d Remove temporary per-packet file routing diagnostics 2026-10-18 15:06:54 +02:00
evgeny 81de8747e2 Move video call and stream diagnostics to module categories 2026-10-18 15:06:16 +02:00
evgeny 28826e050c Add local UDP discovery and invitation delivery for chat clients 2026-10-18 14:46:04 +02:00
evgeny aad295c9d0 Render encrypted group attachments correctly on Android 2026-10-18 13:14:56 +02:00
evgeny b3495349e1 Offer attachment encryption after selecting a file 2026-10-18 11:47:53 +02:00
evgeny 7e08135bba Remove unpublished cipher block indexes during recovery 2026-10-18 10:47:24 +02:00
evgeny 057bdb3112 Recover cancelled unpublished encryption jobs without retaining keys 2026-10-18 10:46:56 +02:00
evgeny a31ade1054 Exercise group encryption choice through real Qt input events 2026-10-18 10:44:23 +02:00
evgeny 98f71735a0 Document encrypted group attachment format and grant lifecycle 2026-10-18 10:42:16 +02:00
evgeny 1896a19eba Explain exact encrypted container rejection reasons 2026-10-18 10:41:27 +02:00
evgeny 8a46a53a21 Verify cache restoration and channel cleanup for encrypted attachments 2026-10-18 10:40:24 +02:00
evgeny 5d2522b8de Account decrypted copies in media quota and protect active containers 2026-10-18 10:39:29 +02:00
evgeny feb327afc7 Preserve durable publication and restore evicted decrypted files 2026-10-18 10:39:03 +02:00
evgeny 3c395dfe5d Share durable encrypted file publication with direct messages 2026-10-18 10:39:03 +02:00
evgeny 4844253f12 Size storage-role query for complete signed-role predicate 2026-10-18 10:37:19 +02:00
evgeny 01e1772615 Use signed storage role and reject revoked key recipients 2026-10-18 10:37:12 +02:00
evgeny fd61aedce3 Use signed storage role in attachment replication fixture 2026-10-18 02:31:55 +02:00
evgeny 9f36fb0ddd Release fixture history gate after verified member synchronization 2026-10-18 02:26:30 +02:00
evgeny de1da1c9b1 Translate encrypted attachment controls and request states 2026-10-18 02:26:01 +02:00
evgeny 3f48a145ae Trace group history readiness in attachment network test 2026-10-18 02:25:35 +02:00
evgeny 6c8e2763e5 Expose manual download API and distinguish storage replication in test 2026-10-18 02:19:45 +02:00
evgeny 945208ce77 Register encrypted attachment unit and network tests 2026-10-18 02:19:11 +02:00
evgeny 62a4f60508 Test encrypted containers and consent delivery across restart 2026-10-18 02:19:10 +02:00
evgeny ada1406d79 Keep membership signature regressions on shared verifier 2026-10-18 02:18:21 +02:00
evgeny ade9401e05 Fix desktop attachment UI include path 2026-10-18 02:17:54 +02:00
evgeny ad7050ad2c Fix shared cipher linkage and transport type includes 2026-10-18 02:17:18 +02:00
evgeny 7cffb15c73 Add author request list with individual key approvals 2026-10-18 02:17:08 +02:00
evgeny 2b7def369d Render locked group attachments and local decrypted media 2026-10-18 02:16:29 +02:00
evgeny 9f813424bb Deliver attachment key state changes to desktop UI 2026-10-18 02:15:25 +02:00
evgeny 4703a46eb9 Add per-send encryption toggle for group files and voice 2026-10-18 02:15:16 +02:00
evgeny 61d1561612 Bind encrypted manifests and request keys after verified download 2026-10-18 02:14:31 +02:00
evgeny 41696b7e0b Route encrypted group preparation through key owner 2026-10-18 02:14:26 +02:00
evgeny e5a1fc5a48 Start and stop attachment key owner with chat service 2026-10-18 02:13:54 +02:00
evgeny 60e040677b Declare attachment grant service and lifecycle state 2026-10-18 02:13:40 +02:00
evgeny 321ed8e747 Implement durable encrypted attachment grants and requests 2026-10-18 02:13:28 +02:00
evgeny e45401449e Share complete membership verification for private grants 2026-10-18 02:10:11 +02:00
evgeny 6a90d79d04 Add authenticated encrypted attachment container 2026-10-18 02:09:30 +02:00
evgeny b4ef4ec919 Switch message cipher consumers to shared media API 2026-10-18 02:08:52 +02:00
evgeny 9c22b04cb0 Use common file cipher primitives in direct messages 2026-10-18 02:08:45 +02:00
evgeny d6db3d18ce Extract shared CCM and streaming media cryptography 2026-10-18 02:08:29 +02:00
evgeny be295f768c feat(chat): add local DM deletion and improve Android member list
Delete local conversations atomically, retain replay protection and sequence counters, and guard asynchronous media work by history generation. Add Android avatar menu and confirmation, persistent file cleanup, and regression coverage.

Fix unnamed member wire padding and compact Android member action spacing.

Validation: 158 core tests passed; targeted DM tests, desktop build, Android unit tests and debug APK build passed.
2026-10-18 02:04:05 +02:00
evgeny 97710b609f test(nat): require OPEN for public TCP listeners receiving connections 2026-10-18 01:02:08 +02:00
evgeny 75d383f89d docs(nat): describe passive incoming connection evidence 2026-10-18 00:59:11 +02:00
evgeny a20a219063 fix(core): free pending ping payloads when destroying instance 2026-10-18 00:57:11 +02:00
evgeny 6075b4cd0f test(nat): verify public incoming profiles on isolated routed network 2026-10-18 00:55:37 +02:00
evgeny 49385ab91c test(nat): isolate active measurements from incoming transport evidence 2026-10-18 00:54:51 +02:00
evgeny 094a8bb499 test(nat): cover accepted UDP and TCP profiles and LAN exclusions 2026-10-18 00:42:35 +02:00
evgeny df2f6ff300 feat(nat): confirm direct open profile from accepted public connections 2026-10-18 00:41:53 +02:00
evgeny 37e092672a fix(nat): retain cold ingress reservation after partial measurements 2026-10-17 23:53:09 +02:00
evgeny 7bf877b7d6 test(nat): exercise full retry budget with high RTT and lost reports 2026-10-17 23:45:35 +02:00
evgeny 305b61ccab fix(nat): advance after an unfinished pair uses its full retry budget 2026-10-17 23:43:21 +02:00
evgeny 1d1f5d081f test(nat): reserve fresh helper before publishing it during recheck 2026-10-17 23:41:51 +02:00
evgeny edc78cbf4a test(nat): cover helper rotation, preserved OPEN and budget resume 2026-10-17 23:39:17 +02:00
evgeny a72e514dc5 fix(nat): keep fresh ingress checks active during profile rechecks 2026-10-17 23:38:52 +02:00
evgeny 6ad3674904 fix(nat): continue helper traversal after partial mapping results 2026-10-17 23:37:10 +02:00
evgeny 7c7b9646f3 test(android): distinguish untranslated and unmeasured interface addresses 2026-10-17 23:20:55 +02:00
evgeny 575a7bfe32 fix(android): show no NAT and inbound status instead of interface 2026-10-17 23:20:55 +02:00
evgeny cb8e654639 fix(android): preserve measured profiles in member address details 2026-10-17 23:20:55 +02:00
evgeny 3f7cfe1c86 fix(chatgui): distinguish untranslated addresses from unmeasured profiles 2026-10-17 23:19:25 +02:00
evgeny a485b10e63 fix(chatgui): show measured NAT profile in member addresses 2026-10-17 23:18:57 +02:00
evgeny 3e29a7ca1c test(android): expect distinct NAT? and EIM? address labels 2026-10-17 22:17:53 +02:00
evgeny 6cbcb860d6 fix(android): distinguish unmeasured NAT from NAT with unknown type 2026-10-17 22:07:59 +02:00
evgeny 86c08092af feat(android): show NAT profile and external address for own sockets 2026-10-17 22:07:58 +02:00
evgeny 1dfe349eb8 fix(android): consume local socket snapshots on core events 2026-10-17 22:07:58 +02:00
evgeny c69f8d8975 fix(nat): notify GUI and include profiles in Android socket snapshots 2026-10-17 22:07:58 +02:00
evgeny 60104ea093 fix(chatgui): label detected NAT with unknown type as NAT? 2026-10-17 22:02:36 +02:00
evgeny 2513e9ae81 tests: finish cold UDP detection before opening additional TCP links 2026-10-17 21:22:22 +02:00
evgeny d2a8e6fda7 db: accept TCP IP-only addresses while preserving stored projections 2026-10-17 19:58:54 +02:00
evgeny 2c7c3c9e8b tests: allow TCP IP observations from existing incoming links 2026-10-17 19:54:18 +02:00
evgeny 857092508a docs: explain TCP NAT presence and IP-only addresses 2026-10-17 19:53:34 +02:00
evgeny b588f8e9f4 tests: verify TCP IP-only detection across real NAT gateways 2026-10-17 19:52:39 +02:00
evgeny 62aee9f01c tests: cover TCP NAT presence and zero-port address publication 2026-10-17 19:50:33 +02:00
evgeny c42d75c2f5 nat: limit helper selection and topology retries to UDP 2026-10-17 19:49:42 +02:00
evgeny 5c0db6ce9e nat: remove TCP listener probes from endpoint detection 2026-10-17 19:47:37 +02:00
evgeny 6363c8b7ea nat: publish TCP external IPs with an unknown port 2026-10-17 19:46:33 +02:00
evgeny 5eb3526186 etcp: exchange observed IPs on established TCP links 2026-10-17 19:46:02 +02:00
evgeny 95f209d098 nat: determine TCP translation from one public IP observation 2026-10-17 19:45:29 +02:00
evgeny d7ad58b380 docs: clarify STUN result fields and API usage 2026-10-17 18:50:39 +02:00
evgeny dbe2640e74 nat: reject local STUN paths as Internet mapping evidence 2026-10-17 18:50:10 +02:00
evgeny 7a40ba536e tests: verify STUN estimates and ETCP promotion in isolated networks 2026-10-17 18:48:06 +02:00
evgeny 8983026625 docs: describe same-interface STUN estimates and working endpoint addresses 2026-10-17 18:47:41 +02:00
evgeny f5d923a671 tests: cover tentative STUN mapping, expiry and native priority 2026-10-17 18:46:38 +02:00
evgeny 417b848b50 nat: publish observed working addresses with tentative STUN profiles 2026-10-17 18:45:20 +02:00
evgeny 88ac08333e nat: use same-interface STUN mapping as tentative EIM 2026-10-17 18:45:02 +02:00
evgeny aede36388f docs: define EIM? as matching mapping with unknown inbound access 2026-10-17 18:27:16 +02:00
evgeny 906f247588 tests: verify tentative EIM with known helper and unknown inbound access 2026-10-17 18:25:53 +02:00
evgeny fad9fbe6c3 android: label unverified EIM and observed unknown NAT addresses 2026-10-17 18:19:47 +02:00
evgeny b539a6f338 gui: display EIM? when unsolicited ingress remains unverified 2026-10-17 18:19:23 +02:00
evgeny 58755dd75e nat: distinguish EIM with unverified unsolicited ingress 2026-10-17 18:19:23 +02:00
evgeny bbb8cff4a6 nat: prefer fresh public helpers while retaining known mapping candidates 2026-10-17 18:10:23 +02:00
evgeny 3c321ac59c docs: explain observed NAT candidates in signed node snapshots 2026-10-17 18:05:30 +02:00
evgeny eb09a675c7 nat: preserve the NAT address kind when mapping is unknown 2026-10-17 18:01:09 +02:00
evgeny 4284233e6f nat: keep unchanged public addresses distinct from unknown NAT 2026-10-17 17:59:22 +02:00
evgeny 5a8b05c50f nat: try known mapping peers before observer-only fallback 2026-10-17 17:57:15 +02:00
evgeny b9a5981f5c gui: shorten unknown NAT status to NAT? 2026-10-17 17:55:32 +02:00
evgeny 78f71ddbb0 nat: distinguish public interface addresses from unknown NAT candidates 2026-10-17 17:53:20 +02:00
evgeny 37461bb938 nat: finish observer-only rounds and document unknown-type candidates 2026-10-17 17:50:28 +02:00
evgeny 71abf74bfc tests: cover unknown NAT candidates and stalled ingress control 2026-10-17 17:48:55 +02:00
evgeny 79d8789410 nat: prioritize mapping candidates and label unknown NAT explicitly 2026-10-17 17:47:18 +02:00
evgeny 6037819269 nat: retain observed candidates and continue mapping after ingress failures 2026-10-17 17:45:24 +02:00
evgeny b96ae1a718 docs: introduce NAT detector results and basic usage 2026-10-17 17:18:07 +02:00
evgeny 6a2164717e docs: describe NAT detector state and public API arguments 2026-10-17 17:12:40 +02:00
evgeny eb3dd7f237 docs: explain NAT detection through its main idea and steps 2026-10-17 17:06:04 +02:00
evgeny d70d6cb66a tests: ignore stale helper probes and document local source rejection 2026-10-17 16:56:21 +02:00
evgeny 83efa229ed nat: reject local probe sources and preserve internet profiles on LAN 2026-10-17 16:54:07 +02:00
evgeny 308eb07742 docs: align endpoint and STUN contracts with the implemented detector 2026-10-17 16:32:08 +02:00
evgeny d5715a1f4f tests: wait for the requested NAT round instead of an earlier automatic result 2026-10-17 16:26:35 +02:00
evgeny 27a87412c5 tests: verify STUN diagnostics without publishing a foreign-port profile 2026-10-17 16:23:18 +02:00
evgeny 7673316dc8 docs: specify NAT v4 independent mapping reports and receiver-verified ingress 2026-10-17 16:19:47 +02:00
evgeny 4fab849917 nat: align probe timing and test receiver IP rejection and endpoint failover 2026-10-17 16:06:21 +02:00
evgeny 38447da5e8 nat: retry partial ingress measurements and keep known-IP anchor replies available 2026-10-17 16:02:01 +02:00
evgeny 8158d6a435 tests: exercise NAT mapping, filtering, delayed ingress and contaminated history 2026-10-17 15:57:49 +02:00
evgeny 3cb971fcba tests: wait for READY group routes before measuring NAT 2026-10-17 15:56:19 +02:00
evgeny 110dd82932 tests: separate STUN diagnostics and allow complete NAT probe rounds 2026-10-17 15:51:32 +02:00
evgeny 26ab3b13b4 nat: separate routed mapping reports from unsolicited UDP ingress 2026-10-17 15:43:38 +02:00
evgeny df80c5745d transport: support one-way authenticated NAT probes 2026-10-17 15:24:57 +02:00
evgeny 1dc26bf715 Add bounded mouse zoom and panning to live stream viewer 2026-10-17 01:49:35 +02:00
evgeny 93dc21f3c8 Exercise capture conversion failure with distinct source generations 2026-10-17 01:30:48 +02:00
evgeny fd73fae77f Keep stream capture failures and cursor snapshots tied to their source 2026-10-17 01:30:06 +02:00
evgeny 9c7d088f8b Compare retained image pixels without assuming SIMD color rounding 2026-10-17 01:30:05 +02:00
evgeny 64bbbdc631 Guard RGB conversion bounds and use padded frames in file playback 2026-10-17 01:28:42 +02:00
evgeny f81b93ad0d Allocate padded RGB frames for FFmpeg video conversion 2026-10-17 01:28:04 +02:00
evgeny 88e3e27145 Show cursor in Windows stream preview 2026-10-17 01:02:46 +02:00
evgeny bf729502a1 Improve Windows screen capture frame rate 2026-10-17 00:38:29 +02:00
evgeny f5660a4642 Hide window capture sources 2026-10-17 00:19:28 +02:00
evgeny e440c62339 Use native Windows monitor handle for cursor overlay 2026-10-17 00:16:56 +02:00
evgeny 4a6f95cebd Add native Windows cursor overlay to screen broadcasts 2026-10-16 23:50:52 +02:00
evgeny 49d55a25a1 Poll stream image thread cleanup from GUI loop 2026-10-16 23:09:29 +02:00
evgeny 1e21912dc6 Trace stream image worker and GUI shutdown waits 2026-10-16 22:45:58 +02:00
evgeny a63af43e59 Handle AMF periodic keyframes 2026-10-16 21:52:10 +02:00
evgeny 98d0040b27 Fix AMF live encoder startup probe 2026-10-16 21:26:58 +02:00
evgeny 2a439754ba Fix Windows FFmpeg configure link flags 2026-10-16 19:42:47 +02:00
evgeny 4401c4b1e4 Fix libaom build with NASM 3 2026-10-16 19:27:51 +02:00
evgeny 8cb656dbdc Minimize FFmpeg features for ChatGUI 2026-10-16 19:08:53 +02:00
evgeny eb98d2c724 Fetch x265 tags before configuring version metadata 2026-10-16 19:04:25 +02:00
evgeny f90cbae94a Enable AMD AMF in Windows ChatGUI FFmpeg build 2026-10-16 18:50:42 +02:00
evgeny 5116496565 Record thread exit evidence from latest Windows stream log 2026-10-16 18:09:51 +02:00
evgeny 7a2fa9e199 Move captured frame conversion to persistent worker 2026-10-16 18:08:42 +02:00
evgeny 5c9bf9e47c Log capture conversion thread and completion 2026-10-16 18:05:58 +02:00
evgeny 8d7ab6a1a6 Keep a converted frame for encoder reconfiguration 2026-10-16 18:05:07 +02:00
evgeny 97e20d2f1c Keep Qt video frame conversion off the publisher worker 2026-10-16 18:04:27 +02:00
evgeny c27ee36cfb Record passing publisher lifecycle regression suite 2026-10-16 17:39:12 +02:00
evgeny 37eccd06ea Record adaptive audio validation and confirmed stream startup failure 2026-10-16 17:38:32 +02:00
evgeny 16e547d15d Drain publisher while asynchronous microphone teardown completes 2026-10-16 17:34:21 +02:00
evgeny 114c0b3e2f Aggregate recurring audio glitch logs between buffer changes 2026-10-16 17:33:32 +02:00
evgeny 688d559787 Wait for native thread exit outside GUI and cover FIN drain in tests 2026-10-16 17:31:20 +02:00
evgeny 0f5a5580c5 Test publisher shutdown with GUI-dependent thread-local cleanup 2026-10-16 17:27:15 +02:00
evgeny 85101765eb Keep GUI responsive until publisher threads finish resource cleanup 2026-10-16 17:26:30 +02:00
evgeny fb2a615165 Reproduce Android audio startup deadlock with real Opus playback 2026-10-16 17:25:19 +02:00
evgeny c03fe3da71 Prime late stream audio without waiting for its playback clock 2026-10-16 17:24:25 +02:00
evgeny 0c26daf3f5 Document adaptive buffer limits and preserve failed-open backoff 2026-10-16 17:18:13 +02:00
evgeny b463fc3309 Keep adaptive requests above baseline and test backend fault events 2026-10-16 17:17:22 +02:00
evgeny e37b3be7da Defer new shared subscribers during microphone buffer resizing 2026-10-16 17:15:51 +02:00
evgeny ca09073a52 Test gradual audio buffer adaptation and nonblocking microphone stop 2026-10-16 17:13:59 +02:00
evgeny efe9e8311c Detach shared capture safely while its audio buffer is resizing 2026-10-16 17:12:51 +02:00
evgeny 3c73c7d645 Keep microphone buffer resizing outside the GUI close path 2026-10-16 17:11:57 +02:00
evgeny 0b58bf3c87 Increase audio buffer gradually after repeated backend glitches 2026-10-16 17:10:31 +02:00
evgeny 360d5dddaa Expose WASAPI and Linux audio buffer glitches to device owners 2026-10-16 17:08:55 +02:00
evgeny 38eae06161 Document microphone watchdogs and pending stream diagnostics 2026-10-16 17:04:46 +02:00
evgeny c37be1c3e8 Trace stream audio startup demand and link Windows COM cleanup 2026-10-16 16:58:01 +02:00
evgeny 014a110fd5 Verify stopping during asynchronous microphone cleanup 2026-10-16 16:53:10 +02:00
evgeny 69cee1bec4 Check continuing H265 screen video and avoid waiting on failed input 2026-10-16 16:51:43 +02:00
evgeny 2de9ac85a2 Test missing microphone PCM in calls and radio without GUI stalls 2026-10-16 16:50:44 +02:00
evgeny cb84d1049f Translate call and radio microphone timeout messages 2026-10-16 16:49:47 +02:00
evgeny 91357ebf90 Show microphone timeout errors in call and radio controls 2026-10-16 16:49:09 +02:00
evgeny 110af9b0c5 Keep normal capture lifecycle synchronous after healthy input 2026-10-16 16:49:09 +02:00
evgeny 432f2dcf00 Stop radio transmission when microphone PCM stops arriving 2026-10-16 16:48:34 +02:00
evgeny 26f6e2ee83 Disable stalled call microphones and close capture asynchronously 2026-10-16 16:48:10 +02:00
evgeny 48cb101047 Expose microphone PCM timeout to shared capture consumers 2026-10-16 16:48:10 +02:00
evgeny 05f1fe485c Trace stream video progress and detect missing microphone PCM 2026-10-16 16:47:25 +02:00
evgeny 349869f732 Record stream startup validation and Windows follow-up 2026-10-16 16:21:11 +02:00
evgeny 230687d155 Wait for asynchronous microphone release in stream tests 2026-10-16 16:19:02 +02:00
evgeny de71617e9f Initialize COM for capture cleanup and release completed leases 2026-10-16 16:19:02 +02:00
evgeny 543c9decba Exclude retiring capture sessions from new subscriptions 2026-10-16 16:15:20 +02:00
evgeny 9629e9311c Defer CPU rejection until dialog destruction completes 2026-10-16 16:15:07 +02:00
evgeny 21f4de492c Test stream startup consent and nonblocking microphone failure 2026-10-16 16:13:35 +02:00
evgeny 7a660688af Translate stream startup failures and CPU confirmation 2026-10-16 16:12:37 +02:00
evgeny 9500581e0b Gate stream encoding on microphone input and CPU consent 2026-10-16 16:11:04 +02:00
evgeny 3ba7601ba4 Close failed stream capture without blocking the GUI 2026-10-16 16:03:14 +02:00
evgeny 86da11b2e1 Separate hardware and software live encoder selection 2026-10-16 15:58:25 +02:00
evgeny 903568d9d6 Apply the selected language immediately in first-run setup 2026-10-16 15:34:31 +02:00
evgeny debfccd7fa Add language selection to the ChatGUI first-run setup 2026-10-16 15:34:20 +02:00
evgeny 91620cec4e Add nickname setup on the first ChatGUI launch 2026-10-16 15:32:33 +02:00
evgeny 5074bad57c Fix Android avatar viewer bounds around system bars 2026-10-16 15:17:12 +02:00
evgeny 0475df8412 Close Android stream viewer after playback ends 2026-10-16 15:03:31 +02:00
evgeny 7454bebb14 Verify PipeWire node addressing with both cursor modes 2026-10-16 14:51:46 +02:00
evgeny ea971b0856 Preserve packed RGB formats and fix PipeWire test setup 2026-10-16 14:50:50 +02:00
evgeny 5725cfb686 Document PipeWire dependency and translate capture status 2026-10-16 14:38:45 +02:00
evgeny 3228cce0fb Test Wayland portal lifecycle and real PipeWire video delivery 2026-10-16 14:38:28 +02:00
evgeny 44c9c3bcaf Use native portal capture for Wayland broadcasts 2026-10-16 14:35:33 +02:00
evgeny ce0fa1f063 Add portal and PipeWire capture with embedded cursor support 2026-10-16 14:34:47 +02:00
evgeny cf3c8c7c34 Test X11 cursor placement and live visibility controls 2026-10-16 14:16:12 +02:00
evgeny ae1682bbe2 Document XFixes dependency and translate cursor controls 2026-10-16 14:14:37 +02:00
evgeny 7c1955f029 Add a persistent cursor toggle to X11 broadcast controls 2026-10-16 14:14:04 +02:00
evgeny 5753177f29 Include the cursor in X11 stream frames by default 2026-10-16 14:14:03 +02:00
evgeny e24ab45725 Add native X11 cursor composition for screen frames 2026-10-16 14:13:10 +02:00
evgeny d60f8e4b1b Preserve stream capture settings and refine audio playback 2026-10-16 14:05:54 +02:00
evgeny 198ebc0acb Restore the current preview when reopening stream controls 2026-10-16 14:02:30 +02:00
evgeny 00e9ed227a Capture the selected X11 monitor through QScreen 2026-10-16 14:00:56 +02:00
evgeny ac8e701406 Keep live capture bound to the selected monitor instance 2026-10-16 13:57:57 +02:00
evgeny 3d2f90e8ae Distinguish monitors with identical Qt screen names 2026-10-16 13:56:57 +02:00
evgeny 218249f9a5 Apply measured screen geometry through the GUI configuration path 2026-10-16 13:54:40 +02:00
evgeny 9bacde3bc1 Select proportional screen and advertised camera stream modes 2026-10-16 13:53:33 +02:00
evgeny 6228873ce2 Resolve desktop capture modes from source capabilities 2026-10-16 13:51:38 +02:00
evgeny ea46c56bc1 Define source-specific desktop stream video modes 2026-10-16 13:50:03 +02:00
evgeny bb5759728a Record live SM wake and reconnect verification on colo2 2026-10-16 13:12:01 +02:00
evgeny fc526d16c0 Test stream announcements after foreground wake and packet reordering 2026-10-16 12:55:10 +02:00
evgeny 919c3d76fe Fix peer standby cancellation on normal keepalive from a new link 2026-10-16 12:46:27 +02:00
evgeny 700034eec8 Add broadcast RS settings to desktop and Android 2026-10-16 09:49:58 +02:00
evgeny 6cdc2ce4dc Allow Android audio toggles during capture startup and test native control 2026-10-16 01:56:43 +02:00
evgeny 8f71a1161b Release stream radio suppression on END and test shared PCM mute 2026-10-16 01:55:28 +02:00
evgeny 7e7d86e4d3 Add Android live stream audio switch and document radio behavior 2026-10-16 01:54:32 +02:00
evgeny e3fb8377ab Track Android stream audio state independently of microphone ownership 2026-10-16 01:53:56 +02:00
evgeny c4fc4e9b1e Mute only the Android stream PCM branch during live publication 2026-10-16 01:52:31 +02:00
evgeny 25d27bfffe Add Android native control for source audio state 2026-10-16 01:51:37 +02:00
evgeny 559d66ae20 Test stream audio toggles and viewer scoped radio suppression 2026-10-16 01:49:30 +02:00
evgeny 2a572af2bb Label stream audio controls in English and Russian 2026-10-16 01:48:50 +02:00
evgeny e47ff579b3 Keep desktop stream mute controls synchronized with publication 2026-10-16 01:48:39 +02:00
evgeny f4d4b6033e Expose stream audio control to the desktop publisher 2026-10-16 01:48:39 +02:00
evgeny c9c1bb7cf1 Publish live audio toggles through the shared stream actor 2026-10-16 01:48:07 +02:00
evgeny db7264e271 Suppress source radio playback only while viewing audible streams 2026-10-16 01:47:44 +02:00
evgeny d14f9233ed Separate effective radio silence from stored member mute 2026-10-16 01:47:11 +02:00
evgeny 0f2e81bd0f Advertise stream audio state in signed source options 2026-10-16 01:46:39 +02:00
evgeny d766a50447 Keep Android capture ownership until device release and update busy messages 2026-10-16 01:34:37 +02:00
evgeny 829e4d41fa Document shared microphone ownership and test distinct desktop inputs 2026-10-16 01:29:55 +02:00
evgeny b523282e8e Allow simultaneous stream publication and radio in Android UI 2026-10-16 01:28:59 +02:00
evgeny 2038e33d8d Reset radio detector after Android capture queue discontinuities 2026-10-16 01:21:36 +02:00
evgeny 051d453c2d Validate Android input routes and isolate capture queue diagnostics 2026-10-16 01:20:34 +02:00
evgeny 9d69cce8e8 Connect Android stream and radio to shared capture and preserve stream audio mode 2026-10-16 01:18:26 +02:00
evgeny 4a49c830d1 Add bounded Android microphone subscriptions with independent ownership 2026-10-16 01:13:23 +02:00
evgeny d4eb1da233 Test shared microphone lifecycle and stream priority 2026-10-16 01:06:59 +02:00
evgeny 95942382bc Share desktop publisher microphone with radio while preserving stream format 2026-10-16 01:06:09 +02:00
evgeny 05ed516cf5 Use shared capture subscription for desktop radio 2026-10-16 01:05:14 +02:00
evgeny 6d7b3e8e02 Add shared desktop microphone sessions with stream profile priority 2026-10-16 00:25:25 +02:00
evgeny 7c5b76b482 Add minimized Windows autostart and preserve terminal output 2026-10-15 21:27:32 +02:00
evgeny abdca1acc7 Load chat background from embedded resources 2026-10-15 20:47:51 +02:00
evgeny 3e4d5d235f Isolate video call camera capture from the GUI process 2026-10-15 20:16:59 +02:00
evgeny 0b136ae2ab Fix Windows deployment classification of AVRT.dll 2026-10-15 18:55:35 +02:00
evgeny b88d76a8ab Fix Windows stream test and compact Android call diagnostics 2026-10-15 18:35:08 +02:00
evgeny 7477da42a5 Reduce Linux voice capture latency and verify device timing 2026-10-15 17:26:09 +02:00
evgeny 3276204196 docs: record live video flush latency and remaining clock gaps 2026-10-15 16:48:04 +02:00
evgeny 612fb7816d docs: record bounded video flush lifecycle and validation 2026-10-15 16:44:57 +02:00
evgeny 9886bc2fde call: continue bounded video flush without waiting for media ticks 2026-10-15 16:38:49 +02:00
evgeny ff51e30a55 Record final video timing measurements and identify fragment budget continuation gap 2026-10-15 16:30:13 +02:00
evgeny a373180c10 Document video playout timing contract tests and remaining live cadence issues 2026-10-15 16:24:02 +02:00
evgeny 717d965fec Keep Android wake deadlines absolute and report causes of the largest video gaps 2026-10-15 16:22:18 +02:00
evgeny 0d65c5bb74 Exercise delayed receive workers UI stalls and call restart with real video decoding 2026-10-15 16:14:51 +02:00
evgeny b211da9a04 Use shared video playout on Android without blocking decoder input while waiting 2026-10-15 16:13:48 +02:00
evgeny 5255133f27 Pace desktop call video independently and coalesce immediate UI publication 2026-10-15 15:58:33 +02:00
evgeny eb43876bb6 Timestamp video availability before receive worker scheduling 2026-10-15 15:57:05 +02:00
evgeny 408c0be021 Test video cadence against data stalls scheduler delays and audio corrections 2026-10-15 15:56:31 +02:00
evgeny 7f9eef8579 Add shared absolute video playout scheduler with bounded audio phase 2026-10-15 15:54:53 +02:00
evgeny 2ff14519fa Record measured desktop Android video latency and presentation timing limits 2026-10-15 15:25:07 +02:00
evgeny 24ba454821 Trace receive decode and UI publication for the reverse video direction 2026-10-15 15:18:35 +02:00
evgeny d7788b87ac Distinguish TextureView update notifications from committed frames 2026-10-15 15:18:35 +02:00
evgeny d1dae1ac2a Measure Android video decode audio wait and surface presentation separately 2026-10-15 15:06:24 +02:00
evgeny e2102bbbcc Trace sampled video frames through desktop encoding and transport 2026-10-15 15:04:51 +02:00
evgeny 19a135d552 Document live low latency route tests and Samsung headset MMAP fallback 2026-10-15 14:58:17 +02:00
evgeny 23c8a190dd Use AudioTrack for headsets whose physical MMAP routing is unreliable 2026-10-15 14:56:24 +02:00
evgeny 29b01237aa Keep call headset hotplug automatic after selecting speaker manually 2026-10-15 14:52:41 +02:00
evgeny 8add40ec00 Reopen call output on confirmed routes and recompute AEC from physical latency 2026-10-15 14:46:03 +02:00
evgeny a165372bbe Preserve device timestamp offsets across output reopen and counter wrap 2026-10-15 14:46:03 +02:00
evgeny 3226865ad7 Test route-local retry policy and output position continuity 2026-10-15 14:42:04 +02:00
evgeny 075ffc4d3b Add bounded AAudio voice output with AudioTrack fallback 2026-10-15 14:40:41 +02:00
evgeny a3854f2836 Allow call playback to invalidate a replaced hardware clock 2026-10-15 14:36:25 +02:00
evgeny f836ba67ce Expose device clock suspension without resetting jitter buffer 2026-10-15 14:36:25 +02:00
evgeny 0a56c01e29 Suspend hardware clock during output replacement without resetting media 2026-10-15 14:36:03 +02:00
evgeny ede911e7e4 Use bounded AAudio write pacing in probe and trace audio controls 2026-10-15 14:36:00 +02:00
evgeny 7a6ecf700c Trace headset endpoint changes and bound AAudio probe queue 2026-10-15 14:30:59 +02:00
evgeny 9d422dd798 Probe AAudio nonblocking writes alongside callback output 2026-10-15 14:27:21 +02:00
evgeny 43522936ff Record Android low latency output probe and route recovery results 2026-10-15 14:23:23 +02:00
evgeny e68df70aad Extend audio probe with route roundtrip and 40 ms control buffer 2026-10-15 14:19:03 +02:00
evgeny 11a3a20655 Add isolated Android AudioTrack and AAudio voice output probe 2026-10-15 14:14:48 +02:00
evgeny 9d948d399e Record live call results and Samsung voice output FAST rejection 2026-10-15 14:05:57 +02:00
evgeny 54e672420c Record measured output latency and validation of low-latency call output 2026-10-15 14:02:03 +02:00
evgeny 1a97219c43 Invalidate output measurements across concurrent Android route changes 2026-10-15 13:59:54 +02:00
evgeny b3ed8b06e7 Back off PCM reserve probes after starvation and exclude output pauses 2026-10-15 13:59:01 +02:00
evgeny 9c71115363 Localize output latency metrics and synchronize worker assertion 2026-10-15 13:57:41 +02:00
evgeny e1bb12d16a Expose desktop output latency and verify hardware clock transitions 2026-10-15 13:57:03 +02:00
evgeny 3083cc05dc Account for complete output callbacks and test adaptive PCM reserve 2026-10-15 13:56:38 +02:00
evgeny cfa3ed1f61 Adapt desktop call PCM reserve to output period and underruns 2026-10-15 13:55:49 +02:00
evgeny cb9852e8d5 Use Android low-latency output and presentation timestamps for call clocks 2026-10-15 13:55:49 +02:00
evgeny b6b0726bfe Document shared delay processing and verified frame alignment 2026-10-15 13:36:18 +02:00
evgeny c244808b77 Support Android C++11 in shared delay processor 2026-10-15 13:24:04 +02:00
evgeny b7b043777b Exercise network recovery and changing device block sizes 2026-10-15 13:23:28 +02:00
evgeny 26c91e6295 Test phase alignment after device silence and keep diagnostics current 2026-10-15 13:22:24 +02:00
evgeny 6896038f30 Include device silence in delay control timing 2026-10-15 13:20:50 +02:00
evgeny 22a03c5880 Report PCM mode, exact tempo and frame phase in call diagnostics 2026-10-15 13:19:39 +02:00
evgeny c1cc3c459b Specify jitter reserve at startup and expose delay diagnostics 2026-10-15 13:19:39 +02:00
evgeny aaff1a5494 Integrate delay processing with call jitter and verify PCM handoffs 2026-10-15 13:18:18 +02:00
evgeny 5f9262dd9a Register shared delay processor and its test target 2026-10-15 13:14:30 +02:00
evgeny 3f13860eac Align direct playback to source frames using bounded acceleration 2026-10-15 13:14:30 +02:00
evgeny 962067c2ac Add shared PCM delay processor with lossless mode boundaries 2026-10-15 13:10:45 +02:00
evgeny ff16adda17 Expose SoundTouch starvation with a 20 ms jitter reserve 2026-10-15 12:39:00 +02:00
evgeny f1aec9bb2d Lower shared call jitter reserve floor to 20 ms 2026-10-15 12:36:54 +02:00
evgeny 39c5274819 Record call teardown regression coverage and verified builds 2026-10-15 12:28:54 +02:00
evgeny 92f62a0121 Validate terminal call packets and document end acknowledgements 2026-10-15 12:26:40 +02:00
evgeny e6954b4152 Acknowledge call termination across router epoch changes 2026-10-15 12:24:55 +02:00
evgeny 3f41676a3d Define acknowledged call termination and enable route tracing 2026-10-15 12:18:47 +02:00
evgeny 59fb020711 Cancel stale video port commands and expose hangup regression 2026-10-15 12:15:07 +02:00
evgeny 1b07a8a708 Keep call metrics visible outside settings on both clients 2026-10-15 12:06:27 +02:00
evgeny 7d75afd4c5 Fix false queue-full detection in audio diagnostics 2026-10-15 11:52:07 +02:00
evgeny 233056a83a Document call latency measurements and capture period 2026-10-15 11:41:51 +02:00
evgeny 3c750d018c Measure Android capture age and device startup stages 2026-10-15 11:40:43 +02:00
evgeny 4cc7d5eaf4 Report call audio processing and send queue timing 2026-10-15 11:38:47 +02:00
evgeny 4476e875ab Measure call audio processing and send queue residence 2026-10-15 11:38:36 +02:00
evgeny dba813e8c5 Report desktop voice queue and capture processing latency 2026-10-15 11:36:58 +02:00
evgeny 96725caf03 Request 20 ms call capture and test output latency reports 2026-10-15 11:35:53 +02:00
evgeny 378d4103a8 Measure backend playback latency outside audio callbacks 2026-10-15 11:35:26 +02:00
evgeny 91030de5ea Cover immediate buffer probe failures and document tuning 2026-10-15 11:11:45 +02:00
evgeny 50bcf5539a Adapt Android call output buffer to device underruns 2026-10-15 11:10:08 +02:00
evgeny cbc7b296bb Add tested adaptive playback device buffer policy 2026-10-15 11:07:39 +02:00
evgeny 92a942ea8b Document continuous capture timing and device queue tuning scope 2026-10-15 03:37:45 +02:00
evgeny 80f6997e1b Keep capture PCM timestamps continuous across backend timing updates 2026-10-15 03:35:59 +02:00
evgeny 6765cf186d Handle headset routing and cancel pending endpoint requests on shutdown 2026-10-15 03:16:03 +02:00
evgeny d944418676 Test capture clock diagnostics and document routing ownership 2026-10-15 03:13:19 +02:00
evgeny 1da19276e5 Trace raw capture timing and PulseAudio latency without callback logging 2026-10-15 03:12:16 +02:00
evgeny a7c67ee488 Activate Telecom before call audio and adopt late connections 2026-10-15 03:11:45 +02:00
evgeny 9c4219d837 Use Telecom as the sole route owner for Android calls 2026-10-15 03:11:05 +02:00
evgeny a439b222da Bound Android call AudioTrack working buffer and pace PCM by device 2026-10-15 03:04:43 +02:00
evgeny a82c2ede6a Clarify audio diagnostic timing and reporter lifetime 2026-10-15 02:58:42 +02:00
evgeny b51e2c0817 Move AudioTrack diagnostic queries off playback and time operations separately 2026-10-15 02:58:42 +02:00
evgeny ac2029c82b Document audio trace fields and verify diagnostic reads preserve jitter events 2026-10-15 02:49:01 +02:00
evgeny c5efa56939 Move repetitive successful video key validation logs to trace 2026-10-15 02:47:45 +02:00
evgeny b4092d1bd6 Record bounded call audio traces and report event windows off playback thread 2026-10-15 02:47:33 +02:00
evgeny 39dbfa7673 Expose filtered Android call buffer diagnostics over JNI 2026-10-15 02:46:15 +02:00
evgeny f341d1db4b Provide call audio diagnostics without consuming stats events 2026-10-15 02:44:35 +02:00
evgeny e53ea2e04a Expose non-destructive jitter buffer diagnostic snapshots 2026-10-15 02:44:10 +02:00
evgeny d1cbb64884 Reduce Android call playback startup buffering and log output queue 2026-10-15 02:37:15 +02:00
evgeny 4c2d8aa4cc Prevent delayed camera startup from shifting video into the future 2026-10-15 02:32:35 +02:00
evgeny ac3801761e Keep video latency diagnostics within their clock domains 2026-10-15 02:29:34 +02:00
evgeny 15e4fcb710 Measure capture clock offset and video audio-barrier latency 2026-10-15 02:29:09 +02:00
evgeny 4447f25e37 Record live camera FPS diagnosis and guard ownership 2026-10-15 02:21:16 +02:00
evgeny f84b24806e Keep UVC camera frame rate stable during video calls 2026-10-15 02:20:32 +02:00
evgeny 459b1ecc01 Display every new desktop call preview frame 2026-10-15 02:18:54 +02:00
evgeny 55293483da Document video timeout diagnosis and independent Android rotation 2026-10-15 02:14:14 +02:00
evgeny fc0559c974 Preserve fractional frame budget when downsampling cameras 2026-10-15 02:13:42 +02:00
evgeny f8e960c844 Retain jitter tolerance across desktop camera phase drift 2026-10-15 02:12:16 +02:00
evgeny 5666da8859 Bound Android bitrate wait by requested keyframe cadence 2026-10-15 02:11:45 +02:00
evgeny 0c214dfca0 Rotate call video independently of Android call controls 2026-10-15 02:11:23 +02:00
evgeny 78246e249e Document measured call FPS loss and bounded audio synchronization queue 2026-10-15 02:07:04 +02:00
evgeny fe2ccb07e1 Keep 60 fps downsampling away from half-period ties 2026-10-15 02:05:29 +02:00
evgeny ee9af6533d Avoid accumulated rounding in call frame pacing 2026-10-15 02:05:08 +02:00
evgeny f3eed42c3f Preserve desktop camera cadence under timestamp jitter 2026-10-15 02:04:26 +02:00
evgeny 209f2ceb7c Queue video frames while awaiting matching audio timestamps 2026-10-15 02:04:25 +02:00
evgeny 95ab911caf Track desktop video frame flow and update call preview geometry 2026-10-15 01:59:59 +02:00
evgeny b665191b9e Render Android call camera preview directly through Camera2 2026-10-15 01:59:42 +02:00
evgeny 572392511b Measure call video throughput and audio gating losses 2026-10-15 01:58:25 +02:00
evgeny 4f91f575a1 Document Android call video navigation and preview limits 2026-10-15 01:53:47 +02:00
evgeny 5cd2df9125 Opt in to stable system bar inset measurements 2026-10-15 01:53:12 +02:00
evgeny 964c17f1bf Add Android call controls navigation and expandable video thumbnails 2026-10-15 01:52:38 +02:00
evgeny 64f1fe4dfb Add call video navigation labels 2026-10-15 01:51:35 +02:00
evgeny 129f809b7b Ignore stale proximity events after listener replacement 2026-10-15 01:44:08 +02:00
evgeny e7ff764ed0 Document video orientation and proximity-gated speaker activation 2026-10-15 01:43:24 +02:00
evgeny f61e1c0a9f Defer video speaker activation until the phone is away from the ear 2026-10-15 01:42:17 +02:00
evgeny 1b3d9ff8ee Distinguish unknown proximity from confirmed far before speaker changes 2026-10-15 01:42:17 +02:00
evgeny 97e3b3e9ef Match Android call screen orientation to the received video 2026-10-15 01:39:08 +02:00
evgeny a58cfcedc6 Document separate video and call controls windows 2026-10-15 01:37:11 +02:00
evgeny 170a6b0d57 Translate separate call video window actions 2026-10-15 01:36:33 +02:00
evgeny 9f8fb64e6e Show call video in a separate resizable desktop window 2026-10-15 01:36:14 +02:00
evgeny 401ca96d7d Record hardware bitrate verification and final call video checks 2026-10-15 01:31:40 +02:00
evgeny e1b29af8ce Keep queued video frames during in-place bitrate changes 2026-10-15 01:30:55 +02:00
evgeny 1376f9f059 Validate bitrate ceilings and recovery without requiring filler data 2026-10-15 01:29:20 +02:00
evgeny eeb3ca5927 Avoid uncached per-pixel chroma writes in codec probe 2026-10-15 01:26:35 +02:00
evgeny 5e22da3098 Document call video layout and clock discontinuity fixes 2026-10-15 01:25:22 +02:00
evgeny 13feaca674 Use bounded contrast and bulk YUV writes in hardware rate probe 2026-10-15 01:23:34 +02:00
evgeny 607952b709 Verify hardware rate changes on continuous noisy video 2026-10-15 01:14:27 +02:00
evgeny 1b0b429835 Exercise asynchronous rate changes with continuous video output 2026-10-15 01:13:54 +02:00
evgeny 0dec628e49 Prepare call video bitrate encoders without pausing capture 2026-10-15 01:13:36 +02:00
evgeny 152d89f0dc Apply Android call bitrate changes in place on validated keyframes 2026-10-15 01:11:02 +02:00
evgeny 8582d2e2b0 Log call video surface lifetime and preview visibility 2026-10-15 00:50:36 +02:00
evgeny 5c6f71c602 Keep Camera2 frames when timestamp jitter crosses nominal frame duration 2026-10-15 00:49:52 +02:00
evgeny 6ff21c12ef Regress video playback across repeated audio clock epoch changes 2026-10-15 00:49:08 +02:00
evgeny b1390a9542 Ignore stale Android call surfaces during activity replacement 2026-10-15 00:48:12 +02:00
evgeny 1c5ef12c8f Keep call display format internal to the Android application 2026-10-15 00:47:42 +02:00
evgeny b6961f5fbc Localize Android call video settings 2026-10-15 00:46:38 +02:00
evgeny 2db038beee Separate Android full screen call video from controls and settings 2026-10-15 00:46:26 +02:00
evgeny 898154b2b6 Keep desktop video in a resizable window and retain decoder on audio clock updates 2026-10-15 00:45:22 +02:00
evgeny 21c22bd911 Preserve Android video decoder across audio clock corrections and expose previews 2026-10-15 00:44:45 +02:00
evgeny a0db643acb Place camera thumbnail inside remote picture and document call layout 2026-10-15 00:34:07 +02:00
evgeny 87a2075dec Keep call diagnostics in settings and translate video controls 2026-10-15 00:32:40 +02:00
evgeny 434b59b1c7 Show remote calls full screen with camera picture in picture 2026-10-15 00:31:50 +02:00
evgeny 4a7d64b2aa Expose call camera preview and clear inactive remote video 2026-10-15 00:31:03 +02:00
evgeny 21ab48b6c3 Document desktop call encoder locale regression and validation 2026-10-15 00:07:07 +02:00
evgeny ef397a6b48 Fix call encoder levels under comma decimal locales 2026-10-15 00:06:20 +02:00
evgeny faee775dff Record the incomplete cross-device AVC decoder probe accurately 2026-10-14 23:45:34 +02:00
evgeny 8b4b278f3c Document the corrected video contract and verified hardware matrix 2026-10-14 23:45:01 +02:00
evgeny 61bcf28720 Verify Android hardware bitstreams through the production Qt decoder 2026-10-14 23:40:48 +02:00
evgeny 5d115c3dac Add cross-device decoding to the explicit debug video probe 2026-10-14 23:40:47 +02:00
evgeny 6910e07c31 Surface failed video configuration and camera capture stalls 2026-10-14 23:40:01 +02:00
evgeny 5f16c79559 Preserve bitrate bounds on format changes and discard late desktop pictures 2026-10-14 23:19:18 +02:00
evgeny 047927a7a5 Check VPS HRD and log actual codec constraints on rejection 2026-10-14 23:19:18 +02:00
evgeny 005d891fb4 Bound video send debt and test decoder acceptance and rejection 2026-10-14 23:18:40 +02:00
evgeny 634ef222a4 Fallback desktop video on decoder rejection and verify receive-only codecs 2026-10-14 23:17:25 +02:00
evgeny f47b7167a2 Fallback Android video after explicit decoder rejection 2026-10-14 23:16:55 +02:00
evgeny 8703e2678a Expose remote video decoder acceptance to Android workers 2026-10-14 23:16:55 +02:00
evgeny afe9437c49 Deliver decoder results through the thread-safe call video port 2026-10-14 23:16:20 +02:00
evgeny d32c8ed6e3 Report decoder acceptance separately from keyframe recovery 2026-10-14 23:16:20 +02:00
evgeny 31a0c4f1a3 Validate HEVC progressive VUI and signaled HRD limits 2026-10-14 23:15:12 +02:00
evgeny aad77cf415 Share normative HRD limits with the bitstream validator 2026-10-14 23:15:11 +02:00
evgeny ea5ef4abc2 Probe hardware rate transitions and clear stale call port commands 2026-10-14 23:13:45 +02:00
evgeny 96e8f6c0ac Test real decoder fixtures and malformed video parameter sets 2026-10-14 23:13:25 +02:00
evgeny 2f1de49e87 Negotiate actual Android modes and replace single encoder on IDR 2026-10-14 23:13:01 +02:00
evgeny fe16509293 Advertise desktop decoder capabilities independently of encoders 2026-10-14 23:12:15 +02:00
evgeny 47cca28d39 Probe Android encoders and decoders independently with actual formats 2026-10-14 23:12:00 +02:00
evgeny 4eece00a2d Add independent AVC and HEVC decoder probe bitstreams 2026-10-14 23:11:00 +02:00
evgeny 5e15fc0d3f Expose common bitstream inspection and actual format checks to Android 2026-10-14 23:10:12 +02:00
evgeny 68e5905d9e Adapt call transport tests and desktop setup to profile negotiation 2026-10-14 23:09:51 +02:00
evgeny 3063270a67 Cover directional profiles and normative DPB limits in video tests 2026-10-14 23:09:30 +02:00
evgeny c7184b55d8 Validate desktop call IDRs with the common bitstream contract 2026-10-14 23:08:47 +02:00
evgeny ce2560ef4b Wire shared video validator and log negotiated profiles 2026-10-14 23:08:46 +02:00
evgeny 53c6437301 Add shared video parameter set and independent IDR validation 2026-10-14 23:08:10 +02:00
evgeny 520f7a29a7 Separate video profiles and network bitrate from encoder VBV 2026-10-14 23:06:40 +02:00
evgeny a06bbc34df Record platform video integration and verified hardware contract blockers 2026-10-14 22:52:15 +02:00
evgeny b2c6500fea Test platform video port over a real bidirectional call transport 2026-10-14 22:50:25 +02:00
evgeny b01d54a431 Request constrained AVC explicitly and diagnose hardware SPS limits 2026-10-14 22:49:31 +02:00
evgeny 156b250432 Exercise production call decoder across real bitrate transitions 2026-10-14 22:46:18 +02:00
evgeny 9becdac331 Add explicit device instrumentation for call codec verification 2026-10-14 22:46:10 +02:00
evgeny ba2a66341a Add Android call video display and live camera codec bitrate controls 2026-10-14 22:45:34 +02:00
evgeny a59ebcd6db Own Android call video in the foreground call service 2026-10-14 22:45:34 +02:00
evgeny 04f4d29e4f Add Android call camera and keyframe encoder replacement worker 2026-10-14 22:44:32 +02:00
evgeny edf7e4d2c5 Render Android call video against the audio playback clock 2026-10-14 22:43:12 +02:00
evgeny 5dbc5bcf59 Probe Android call encoders through real YUV encode and decode 2026-10-14 22:42:16 +02:00
evgeny c986221cb6 Expose shared video negotiation and bounded queues to Android JNI 2026-10-14 22:40:48 +02:00
evgeny d01bc395fa Use current FFmpeg profile constants and qualify video port status 2026-10-14 22:39:56 +02:00
evgeny af35d03399 Add live call video controls and display to the desktop call window 2026-10-14 22:39:13 +02:00
evgeny e682f30ca9 Expose active call bitrate range through the shared video port 2026-10-14 22:38:39 +02:00
evgeny cabae34791 Connect desktop camera workers to negotiated call video and audio clock 2026-10-14 22:37:50 +02:00
evgeny b091aa0df2 Decode call video with bounded input and verified output metadata 2026-10-14 22:36:32 +02:00
evgeny 57a59df983 Bind shared call video port to desktop and Android core lifetime 2026-10-14 22:36:03 +02:00
evgeny 5e37aaa2fb Add bounded cross-thread video port for platform call workers 2026-10-14 22:35:25 +02:00
evgeny 65d7ec875e Record final codec envelope builds and regression results 2026-10-14 22:27:48 +02:00
evgeny 78d3511174 Document negotiated video envelopes and remaining platform integration 2026-10-14 22:27:16 +02:00
evgeny f055f8a9df Use fresh capture PTS after discarded video and diagnose rejected frames 2026-10-14 22:22:02 +02:00
evgeny 8c12dfc02f Verify pending video is discarded when peer capabilities shrink 2026-10-14 22:14:30 +02:00
evgeny ea65d87ec1 Round-trip real codecs using the negotiated level and reject invalid envelopes 2026-10-14 22:10:42 +02:00
evgeny 3f61549769 Open desktop call encoders from the shared negotiated video format 2026-10-14 22:10:21 +02:00
evgeny 534f319d86 Test directional codec envelopes and reject out-of-cap video in live calls 2026-10-14 22:08:48 +02:00
evgeny fa53139163 Bind call rate control and frame admission to negotiated codec envelopes 2026-10-14 22:07:04 +02:00
evgeny d4e6bc5980 Negotiate call video level and bitrate VBV envelopes in the shared protocol 2026-10-14 22:05:55 +02:00
evgeny 2a22d625e1 Record validated desktop call encoder and remaining integration boundaries 2026-10-14 22:01:47 +02:00
evgeny cab992228c Preserve explicit VBV and report inactive call encoder state 2026-10-14 21:57:58 +02:00
evgeny 0f570edbd0 Exercise real H264 and H265 calls across delayed bitrate switches 2026-10-14 21:55:11 +02:00
evgeny 327478475c Bound encoder rate requests by the negotiated mode ceiling 2026-10-14 21:46:53 +02:00
evgeny 3b3ba3c495 Add verified call profiles and defer bitrate replacement until IDR 2026-10-14 21:46:13 +02:00
evgeny 43a90ff23b Build shared live video library independently from stream publication 2026-10-14 21:44:16 +02:00
evgeny 45720ebaba Use shared live encoder types in stream publication 2026-10-14 21:44:16 +02:00
evgeny dca1adc173 Extract live video encoder naming for reuse by calls and broadcasts 2026-10-14 21:43:54 +02:00
evgeny f89494fb4f Specify deferred bitrate changes and validate IDR before releasing old encoder 2026-10-14 21:39:24 +02:00
evgeny abf7ccf566 Probe x265 bitrate switching at scheduled IDR with encoder replacement 2026-10-14 21:38:05 +02:00
evgeny e20f4113a4 Record x265 reconfiguration defect and validated dependency patch 2026-10-14 21:33:41 +02:00
evgeny 5d8c6b8ff3 Add measured x265 bitrate adaptation probe for call backends 2026-10-14 21:32:02 +02:00
evgeny 4b414deb6e Record successful bitrate range regression checks 2026-10-14 21:21:21 +02:00
evgeny a6b8594219 Document call bitrate range settings and validation 2026-10-14 21:19:11 +02:00
evgeny 6980bc7a88 Expose the adaptive call video range in Android audio settings 2026-10-14 21:17:46 +02:00
evgeny cc341221d4 Verify live bitrate range changes and incompatible codec limits 2026-10-14 21:10:12 +02:00
evgeny f04e3fd995 Persist the Android call video bitrate range atomically 2026-10-14 21:10:12 +02:00
evgeny 6cbb13d11d Translate call bitrate range settings 2026-10-14 21:08:13 +02:00
evgeny 0fe9a779f0 Add call video bitrate range controls to desktop settings 2026-10-14 21:07:50 +02:00
evgeny 03d8f71be4 Test video adaptation from poor connectivity to the 4 Mbps ceiling 2026-10-14 21:07:08 +02:00
evgeny 4abc5ef6e7 Apply live call bitrate range within the codec ceiling 2026-10-14 21:06:26 +02:00
evgeny 5789a83ea2 Define atomic call video bitrate range with 300–4000 kbps defaults 2026-10-14 21:06:07 +02:00
evgeny 6b409a5006 Record video transport validation and codec negotiation gap 2026-10-14 20:59:51 +02:00
evgeny ed848f94eb Bound video encoder commands and verify generation-safe feedback 2026-10-14 20:53:53 +02:00
evgeny 23bd954b79 Drive call video rate control from endpoint delivery snapshots 2026-10-14 20:52:41 +02:00
evgeny 2c9904c345 Handle video capability exchange racing with call acceptance 2026-10-14 20:50:32 +02:00
evgeny 996d4a89f5 Exercise video switching and backpressure in a real call 2026-10-14 20:49:54 +02:00
evgeny b466ec30e3 Acknowledge video capabilities before sending frame data 2026-10-14 20:48:58 +02:00
evgeny 609221e384 Separate call audio and video from signaling queues 2026-10-14 20:48:27 +02:00
evgeny 14a56d9b33 Add bounded call video transport and capability exchange 2026-10-14 20:47:44 +02:00
evgeny dffde9691d Share call session ownership with the video transport 2026-10-14 20:46:02 +02:00
evgeny b00c6a2b6d Test call video negotiation and frame wire boundaries 2026-10-14 20:44:42 +02:00
evgeny c8bbce6a94 Define call video capabilities and bounded frame assembly 2026-10-14 20:43:42 +02:00
evgeny 5fbea21482 Record call clock validation results 2026-10-14 19:42:56 +02:00
evgeny 120a865152 Record the implemented call timing protocol and next stages 2026-10-14 19:42:32 +02:00
evgeny 610773fa90 Document and log the call source clock contract 2026-10-14 19:38:59 +02:00
evgeny 4a55c30f95 Verify capture clock metadata over the call transport 2026-10-14 19:37:25 +02:00
evgeny a978bb7b06 Exercise call clocks through Opus and jitter recovery 2026-10-14 19:36:51 +02:00
evgeny e975d24a73 Track Android call capture and playback clocks 2026-10-14 19:36:11 +02:00
evgeny 023f3a335e Pass call audio timing across Android JNI 2026-10-14 19:36:11 +02:00
evgeny 9fe5cd4c27 Test call capture timestamps and device progress 2026-10-14 19:35:56 +02:00
evgeny c041882f5f Observe capture timestamps and device progress in voice tests 2026-10-14 19:30:28 +02:00
evgeny 0195732b4e Keep call audio regressions explicit about untimed sources 2026-10-14 19:29:05 +02:00
evgeny 2f3eb66c4c Preserve measured versus estimated device timing in voice mixer 2026-10-14 19:29:04 +02:00
evgeny 39bfedca86 Drive desktop call clock from rendered PCM and capture timing 2026-10-14 19:28:38 +02:00
evgeny 7bac999d93 Handle source discontinuities and reject stale device epochs 2026-10-14 19:27:54 +02:00
evgeny 9aba8e278c Preserve capture PTS and expose call playback clock to platforms 2026-10-14 19:27:03 +02:00
evgeny ab283eebdc Expose measured Opus codec delay for capture clock mapping 2026-10-14 19:26:09 +02:00
evgeny 96e7983128 Attach source PTS maps to call jitter and playback epochs 2026-10-14 19:25:30 +02:00
evgeny dc3cf4f227 Update call transport tests for explicit capture metadata 2026-10-14 19:24:51 +02:00
evgeny 2e1908a52b Preserve capture clock across call delivery and validate media peer 2026-10-14 19:24:51 +02:00
evgeny ab04b14237 Carry capture timestamps and Opus delay in call media protocol 2026-10-14 19:24:22 +02:00
evgeny fe2fa73cc0 Test audio timeline boundaries and sustained SoundTouch playback 2026-10-14 19:23:40 +02:00
evgeny 991d6bad43 Map produced audio to source PTS and bounded device progress 2026-10-14 19:22:32 +02:00
evgeny b5bd45c4da Define bounded audio timeline and playback clock contracts 2026-10-14 19:21:20 +02:00
evgeny 4939ee7fb0 Account for transposer prefill in source coordinates 2026-10-14 19:19:50 +02:00
evgeny 811b980883 Verify exact SoundTouch provenance and unchanged PCM output 2026-10-14 19:17:46 +02:00
evgeny 1d43578335 Expose optional live source tracking through SoundTouch 2026-10-14 19:16:45 +02:00
evgeny edcd34d942 Report copied and overlapping source ranges from TDStretch 2026-10-14 19:16:03 +02:00
evgeny ceb06bafcf Define SoundTouch source spans and unity interpolation contract 2026-10-14 19:15:43 +02:00
evgeny 183e66e97d Record video foundation checks and unresolved audio clock contract 2026-10-14 18:46:57 +02:00
evgeny d441d9634d Document measured SoundTouch clock drift before video integration 2026-10-14 18:44:15 +02:00
evgeny 05d9e3f22e Check SoundTouch source time against identifiable audio samples 2026-10-14 18:42:46 +02:00
evgeny 801050bacb Exercise video rate feedback with simulated channel changes 2026-10-14 18:41:33 +02:00
evgeny f24a8ca6d3 Add video bitrate regulator with bounded debt feedback 2026-10-14 18:40:35 +02:00
evgeny 0bbd4d6c34 Verify router ACK accounting across retries and epoch resets 2026-10-14 18:34:33 +02:00
evgeny 6a1e7a90dd Record video call implementation contracts and milestones 2026-10-14 18:33:42 +02:00
evgeny 859bb733af Build and test router delivery history boundaries 2026-10-14 18:33:05 +02:00
evgeny f87c6a93bc Expose per-service payload backlog and acknowledged delivery 2026-10-14 18:32:28 +02:00
evgeny 61faca84d7 Add aligned router delivery metering for video adaptation 2026-10-14 18:31:52 +02:00
evgeny 0de5479e8e Document live zoom validation and intermittent STCP test failure 2026-10-14 17:44:51 +02:00
evgeny 8eef506eed Enable pinch zoom and bounded panning in Android live viewer 2026-10-14 17:41:12 +02:00
evgeny c39610f079 Share video zoom geometry between Android viewers 2026-10-14 17:36:38 +02:00
evgeny 7ff95623c1 Document fullscreen live viewer behavior and SM checks 2026-10-14 17:31:08 +02:00
evgeny 931631a4a9 Show Android live streams fullscreen with automatic orientation and timed controls 2026-10-14 17:28:16 +02:00
evgeny d912d53979 Record SM audio playback validation and native snapshot test flake 2026-10-14 17:21:10 +02:00
evgeny ded0d401c7 Match stream audio queue to device buffer and document playback ownership 2026-10-14 17:19:04 +02:00
evgeny 0fdc6e435b Fix Android stream audio backpressure without blocking video 2026-10-14 17:16:45 +02:00
evgeny 91d1a7994e Record stream role lookup measurements and architecture decisions 2026-10-14 16:52:31 +02:00
evgeny a4f508feed Read stream supernode role directly by member ID 2026-10-14 16:49:40 +02:00
evgeny d2f8e57118 Record keyframe recovery fixes and remaining architecture review 2026-10-14 16:34:30 +02:00
evgeny 7dd1176545 Document complete keyframe admission and shared media opening 2026-10-14 16:32:15 +02:00
evgeny 97fea6e809 Use shared media opening in desktop and Android viewers 2026-10-14 16:30:38 +02:00
evgeny 1809416dce Add atomic media reader opening with selected epoch validation 2026-10-14 16:26:41 +02:00
evgeny d75aea1a09 Remove unused media receiver and test the shared ring cursor 2026-10-14 16:24:32 +02:00
evgeny d209ab94ba Gate media cursor revision changes on a complete keyframe 2026-10-14 16:22:15 +02:00
evgeny afa8fd307a Reproduce media cursor revision admission inconsistency 2026-10-14 16:10:26 +02:00
evgeny 84472cc276 Clarify stream bootstrap and log bounded publisher drain 2026-10-14 16:05:17 +02:00
evgeny d0ddc9ae8c Fix partial stream bootstrap and release drained upstream flows 2026-10-14 16:02:24 +02:00
evgeny 1815de059e Record direct preview verification and camera teardown ownership 2026-10-14 15:58:31 +02:00
evgeny 12ad2bf14b Release all preview session outputs when the camera device closes 2026-10-14 15:44:08 +02:00
evgeny 117087d56b Distinguish camera preview failure from a stopped stream 2026-10-14 15:43:24 +02:00
evgeny 2ee3103c8c Avoid double sensor rotation in direct Android camera preview 2026-10-14 15:43:24 +02:00
evgeny b8a2d7a555 Detach direct camera preview immediately when UI goes to background 2026-10-14 15:30:03 +02:00
evgeny 78054b2c1d Measure direct preview frame age using camera timestamp clocks 2026-10-14 15:19:49 +02:00
evgeny 30dae7d7ed Document direct Android camera preview and surface ownership 2026-10-14 15:14:42 +02:00
evgeny 548e64c963 Use direct camera preview instead of a local stream decoder 2026-10-14 14:58:03 +02:00
evgeny e8a63ec40d Send Android camera frames directly to preview surfaces 2026-10-14 14:58:03 +02:00
evgeny 5630bedc51 Record verified AV1 playback fix and regression coverage 2026-10-14 14:25:20 +02:00
evgeny 9ef18b6324 Release Android inflight state at decoder EOS before audio tail 2026-10-14 14:17:35 +02:00
evgeny 7b4dfc062a Document demand driven playback and delayed decoder lifecycle 2026-10-14 14:10:36 +02:00
evgeny 8d0504c759 Exercise codec output delay explicitly in the desktop fixture 2026-10-14 14:04:45 +02:00
evgeny 5ce2877fc0 Bound desktop codec work and drain delayed frames through EOF 2026-10-14 13:57:58 +02:00
evgeny c02222610b Preserve observed decoder lead while its pipeline drains 2026-10-14 13:49:57 +02:00
evgeny 360a6efafc Verify outstanding read replies and explicit video demand 2026-10-14 13:49:14 +02:00
evgeny 74c96ba5e9 Let desktop codecs read ahead while keeping presentation bounded 2026-10-14 13:43:23 +02:00
evgeny 049480408a Test frozen clock decoding and single Android read responses 2026-10-14 13:37:43 +02:00
evgeny 31b10435d3 Use explicit video demand in ring and network reader tests 2026-10-14 13:36:38 +02:00
evgeny 9331c26b22 Distinguish Android decoder startup from waiting for a keyframe 2026-10-14 13:36:38 +02:00
evgeny f72e2fca94 Drive Android decoding by codec readiness and bounded working frames 2026-10-14 13:36:38 +02:00
evgeny 223c9a153c Request the next video frame through JNI without a time horizon 2026-10-14 13:36:38 +02:00
evgeny 5a3999fbdb Prepare one Android media response per player request 2026-10-14 13:36:38 +02:00
evgeny ac339bb61f Prepare one desktop media response per player request 2026-10-14 13:27:10 +02:00
evgeny 33f8dacbfc Expose demand driven video reads through the stream handle 2026-10-14 13:25:50 +02:00
evgeny f317d3e989 Read the next live video frame independently of presentation time 2026-10-14 13:25:50 +02:00
evgeny 47119c73a4 Record completed desktop broadcast implementation and checks 2026-10-14 13:00:15 +02:00
evgeny 4e7b8f5c1a Keep desktop stream failure visible after draining 2026-10-14 12:58:09 +02:00
evgeny c3512b787e Test fatal desktop audio failure after successful capture 2026-10-14 12:56:49 +02:00
evgeny 470001bbab End desktop publication on fatal audio worker failure 2026-10-14 12:56:26 +02:00
evgeny b25070acbd Document shared live publisher and desktop capture lifecycle 2026-10-14 12:54:44 +02:00
evgeny e99a1b9450 Verify live desktop codec changes preserve the audio epoch 2026-10-14 12:53:24 +02:00
evgeny b5f84d4a9b Include complete video frame type in the raw preview integration test 2026-10-14 12:45:44 +02:00
evgeny 16dc3a13aa Verify live bitrate decoder continuity and capture installer completeness 2026-10-14 12:41:56 +02:00
evgeny 8fda2b0751 Format the isolated encoder capability and live setup passes 2026-10-14 12:38:41 +02:00
evgeny 7ad0aa61af Update live x264 bitrate and exclude encoders without forced key support 2026-10-14 12:32:33 +02:00
evgeny e4f9a48c2c Reuse static screen frames across encoder changes and test rollback 2026-10-14 12:27:50 +02:00
evgeny c08415a255 Translate capture termination and frame conversion errors 2026-10-14 12:11:36 +02:00
evgeny 2caa9504c9 Report capture termination and preserve explicit microphone selection 2026-10-14 12:11:36 +02:00
evgeny ee54626de9 Describe own stream management and system screen selection 2026-10-14 12:08:01 +02:00
evgeny 80bd17a28e Open own broadcast controls and keep rejected voice recording idle 2026-10-14 12:08:01 +02:00
evgeny ba17f9908d Render raw preview at 30 fps and preserve fallback audio sample timing 2026-10-14 12:06:45 +02:00
evgeny 27f3726124 Inspect capture plugin dependencies and recognize XWayland sessions 2026-10-14 11:49:04 +02:00
evgeny 97c460e2f1 Observe capture activation raw frames and encoder generations 2026-10-14 11:44:11 +02:00
evgeny 945c75ea07 Install capture dependencies and verify Multimedia plugin deployment 2026-10-14 11:26:50 +02:00
evgeny 7f7bb1c939 Verify forced keyframes and isolate encoder probes from live timestamps 2026-10-14 11:25:38 +02:00
evgeny 7720b7036d Deploy Qt Multimedia dependencies with Windows broadcasts 2026-10-14 11:21:48 +02:00
evgeny 1b85406b13 Trace encoder responses to explicit random access frame requests 2026-10-14 11:21:11 +02:00
evgeny f85f45fbd3 Validate publisher lifecycle in optional codec builds 2026-10-14 10:21:40 +02:00
evgeny 4a4397ace1 Start portal encoder deadlines after the first permitted capture frame 2026-10-14 10:20:50 +02:00
evgeny 240568870a Test desktop codec round trips and real screen publication 2026-10-14 10:19:44 +02:00
evgeny 29313cce54 Reserve broadcast capture exclusively while preserving shared voice I/O 2026-10-14 10:17:48 +02:00
evgeny 386998598e Restore bitrate controls and wait for explicit portal source selection 2026-10-14 02:58:45 +02:00
evgeny d934978fb2 Translate desktop broadcast controls and capture diagnostics 2026-10-14 02:57:56 +02:00
evgeny ce461521e4 Defer source failures and end calls that cannot reserve audio capture 2026-10-14 02:56:36 +02:00
evgeny 50fad4ceff Preserve video color range and handle capture worker failures 2026-10-14 02:55:36 +02:00
evgeny ce46677dfb Keep voice recording from opening an already owned microphone 2026-10-14 02:54:30 +02:00
evgeny b3c698be25 Prevent radio capture from competing with a broadcast microphone 2026-10-14 02:54:20 +02:00
evgeny c1d7460254 Own publication independently of its window and reserve call capture 2026-10-14 02:54:19 +02:00
evgeny ab01c6dc48 Expose broadcast setup even when a channel has no live sources 2026-10-14 02:53:22 +02:00
evgeny c91bb2828f Keep raw preview and source permissions independent of video encoding 2026-10-14 02:52:36 +02:00
evgeny f96443e1a6 Add desktop broadcast controls and aggregate send indicators 2026-10-14 02:51:41 +02:00
evgeny 701a7d0eac Capture camera screen and stereo microphone for desktop publication 2026-10-14 02:49:12 +02:00
evgeny 0055b86b19 Encode low latency desktop video with codec specific keyframes 2026-10-14 02:44:59 +02:00
evgeny 77acdb2d65 Expose radio compressor settings to publication workers 2026-10-14 02:35:55 +02:00
evgeny 125f740a5f Use the shared publisher in Android and desktop stream bridges 2026-10-14 02:34:16 +02:00
evgeny f5d112a712 Share bounded media publication and send telemetry across frontends 2026-10-14 02:32:14 +02:00
evgeny a5f8b8431d Verify video profile fallback without excluding a supported codec 2026-10-14 02:14:24 +02:00
evgeny 499ce21cfc Use available encoder profiles when the preferred video profile is absent 2026-10-14 02:14:24 +02:00
evgeny 643420b644 Record codec and telemetry validation with precise decoder diagnostics 2026-10-14 02:11:59 +02:00
evgeny 49ca49fa1e Assemble partial codec setup and test real out of band decoder configuration 2026-10-14 02:01:44 +02:00
evgeny b4aeb1a3fd Keep preview layout stable and document codec configuration exchange 2026-10-14 01:56:02 +02:00
evgeny 39297d1a07 Explain unavailable video decoders and restored capture settings 2026-10-14 01:47:32 +02:00
evgeny de67de33df Report codec capabilities and document the expanded Android media header 2026-10-14 01:45:50 +02:00
evgeny bf12bd5b05 Count only current viewer media against the preparation limit 2026-10-14 01:43:44 +02:00
evgeny 990713c7a7 Retain complete codec parameter sets across separate configuration callbacks 2026-10-14 01:41:12 +02:00
evgeny a4e6683c5b Exercise modern codec playback and partial video queue telemetry 2026-10-14 01:32:40 +02:00
evgeny 06bd9947ce Test codec keyframes and bitrate selection across camera changes 2026-10-14 01:21:04 +02:00
evgeny 305eacbe95 Verify all media codec IDs and key configuration bounds 2026-10-14 01:20:00 +02:00
evgeny 876a5a65ec Test aggregate send windows with duplicate ACKs and connection teardown 2026-10-14 01:20:00 +02:00
evgeny 3e7197702b Decode all supported video formats from uAV2 on desktop 2026-10-14 01:14:38 +02:00
evgeny c815d06ab0 Add video codec and bitrate controls with aggregate send buffer indicators 2026-10-14 01:13:28 +02:00
evgeny 7a5a94bb64 Keep ten hertz publication telemetry separate from preview state 2026-10-14 01:12:31 +02:00
evgeny 1ac7a40432 Capture and play codec specific access units with live bitrate changes 2026-10-14 01:11:49 +02:00
evgeny d209049f71 Enumerate supported surface encoders and model codec specific keyframes 2026-10-14 01:10:43 +02:00
evgeny 5677e8ec5f Initialize the publication media format with the selected codec 2026-10-14 01:09:30 +02:00
evgeny cd8a5392d5 Expose source codec selection and aggregate send telemetry through JNI 2026-10-14 01:09:30 +02:00
evgeny 8b6c72b704 Publish bounded 100 ms send telemetry from the Android media actor 2026-10-14 01:08:20 +02:00
evgeny 4d03a2db53 Expose pending source deliveries without counting ETCP inflight twice 2026-10-14 01:06:57 +02:00
evgeny b6652c7c7f Record unique acknowledged packets at the instance transport boundary 2026-10-14 01:06:56 +02:00
evgeny ac89601fc5 Add instance lifetime ACK windows and exclusive ETCP send queue snapshots 2026-10-14 01:06:32 +02:00
evgeny 7e693fcc97 Define uAV2 codec IDs and signed keyframe configuration envelopes 2026-10-14 01:06:32 +02:00
evgeny 84246bea89 Record adaptive playback validation and ring reader lifecycle 2026-10-14 00:13:08 +02:00
evgeny 2aafe172d6 Reject large audio timestamp jumps before narrowing PLC packet counts 2026-10-14 00:11:31 +02:00
evgeny 359770ecb4 Await all relay ACCEPTs through the bounded pending-buffer retry 2026-10-14 00:09:12 +02:00
evgeny 366cf94e15 Describe shared ring playback, corrected reserve history and stereo catchup 2026-10-14 00:07:52 +02:00
evgeny c0c4310f08 Clarify desktop demand reader ownership and preparation bounds 2026-10-14 00:06:23 +02:00
evgeny ef0fc85eb9 Document bounded read continuation and retained source ownership after FIN 2026-10-14 00:05:49 +02:00
evgeny 88a2b66050 Keep prior network race fixtures independent of the playback scenario clock 2026-10-14 00:04:52 +02:00
evgeny be7c3c5811 Bound desktop decoder work and drain Android codec with monotonic EOS timestamps 2026-10-14 00:04:08 +02:00
evgeny 9c01535c78 Exercise paused ring readers and graceful source close across live nodes 2026-10-14 00:02:59 +02:00
evgeny 3649e45087 Reseek on media discontinuities and bound disabled track timestamps 2026-10-14 00:01:57 +02:00
evgeny 35521fe188 Verify bounded assembly of large retained frames without new packets 2026-10-13 23:46:30 +02:00
evgeny 5937904b4a Test delayed readers without frontend queue growth and isolate codec fixtures 2026-10-13 23:41:10 +02:00
evgeny 98655284b2 Discard stale playback snapshots before reseeking a media cursor 2026-10-13 23:39:45 +02:00
evgeny 6743f84146 Verify stereo playback tempo, pitch and original media clock mapping 2026-10-13 23:35:13 +02:00
evgeny 3010fe25db Hold video only playback clocks on starvation and drain finished audio 2026-10-13 23:32:27 +02:00
evgeny a12665bbd1 Drive desktop ring playback from corrected history and physical audio time 2026-10-13 23:30:06 +02:00
evgeny e1abcb5d69 Observe actual silent device intervals when adapting playback history 2026-10-13 23:26:55 +02:00
evgeny 5c75487446 Continue bounded media assembly without waiting for new network data 2026-10-13 23:24:04 +02:00
evgeny f3bc2909e1 Initialize Android playback before handling its first media packet 2026-10-13 23:18:43 +02:00
evgeny acf374df4e Use adaptive ring backed playback and stereo tempo on Android 2026-10-13 23:16:06 +02:00
evgeny e6e276aed3 Expose shared playback timing and stereo time stretch to Android 2026-10-13 23:03:29 +02:00
evgeny 541518db81 Keep finished source data alive until delayed media readers close 2026-10-13 22:53:38 +02:00
evgeny 9781f138ed Switch Android media delivery to demand reads from the shared RS ring 2026-10-13 22:53:13 +02:00
evgeny f655f2c1d5 Keep playback audio compatible with the Android voice build 2026-10-13 22:46:02 +02:00
evgeny 8d317dbb85 Keep viewer snapshots scoped and normalize Opus packet cadence 2026-10-13 22:45:07 +02:00
evgeny 650b65078a Switch desktop media delivery to bounded demand reads from the shared ring 2026-10-13 22:42:16 +02:00
evgeny 2c7bd78a64 Track stereo time stretched audio through the device playback timeline 2026-10-13 22:37:09 +02:00
evgeny 9f37c54a3c Expose demand based audio and video reads on local stream handles 2026-10-13 22:30:47 +02:00
evgeny 866ca0923d Give audio device lead and video decode lead independent read limits 2026-10-13 22:30:47 +02:00
evgeny 75bd6f1ce1 Notify media readers when indexed playback data becomes available 2026-10-13 22:29:24 +02:00
evgeny 5c12ba253e Test independent playback readers over one authenticated stream ring 2026-10-13 22:21:29 +02:00
evgeny 485e655fd8 Separate media reader cursors from live stream receive progress 2026-10-13 22:15:00 +02:00
evgeny dc7cebaa65 Register indexed media reads and retain only two working blocks 2026-10-13 22:12:51 +02:00
evgeny 126aa9df10 Read media on demand through indexed cursors over the RS ring 2026-10-13 22:09:46 +02:00
evgeny 4ae8fc7c45 Reuse authenticated immutable RS subsets for playback reads 2026-10-13 22:04:55 +02:00
evgeny 2fac28b7e9 Build and register the shared stream playback controller test 2026-10-13 22:04:16 +02:00
evgeny 8e3ec4cb84 Test adaptive playback against a delayed audio device queue 2026-10-13 22:04:16 +02:00
evgeny b2645aed9d Add a shared playback reserve clock with corrected minute history 2026-10-13 21:56:10 +02:00
evgeny 05f2a3602a Record signed variable RS batch implementation and validation 2026-10-13 20:35:53 +02:00
evgeny 3b922ced78 Cover early BOUND in the delayed helper confirmation regression 2026-10-13 20:28:56 +02:00
evgeny 43a600c5a5 Test immediate relay forwarding and log batch authentication 2026-10-13 20:25:40 +02:00
evgeny 01d7fd5bef Document embedded batch signatures and resize pressure fixtures 2026-10-13 20:20:17 +02:00
evgeny 98d1e40bf3 Validate short media batches with an embedded source signature 2026-10-13 20:14:06 +02:00
evgeny e96b813fcb Test batch authentication after losses and recovery after rejection 2026-10-13 20:13:45 +02:00
evgeny f15c97d11a Size Android publisher buffers for the signed batch format 2026-10-13 20:09:14 +02:00
evgeny 6a5bb84d71 Centralize default stream input capacity with batch overhead 2026-10-13 20:09:14 +02:00
evgeny 0e9478d033 Account variable stream parts and continue after rejected batches 2026-10-13 20:08:07 +02:00
evgeny 47b713ea69 Publish actual part lengths and expose rejected batch status 2026-10-13 20:07:02 +02:00
evgeny cee5b26c13 Sign stream batches before RS and authenticate reconstructed data 2026-10-13 20:06:29 +02:00
evgeny 7c8470f44a Define variable stream parts with a signed RS batch envelope 2026-10-13 20:03:08 +02:00
evgeny 3477a8844c Allow RS batches to choose their block size from input length 2026-10-13 19:59:57 +02:00
evgeny 3f6cc5da52 Document callback-driven supplier discovery and verification 2026-10-13 18:16:37 +02:00
evgeny f2c5187ef1 Retry pending supplier buffers without resetting error backoff 2026-10-13 18:06:58 +02:00
evgeny da0a9ad316 Wake stream supplier discovery on Merkle and BGP callbacks 2026-10-13 17:55:19 +02:00
evgeny fec21ab5c5 Record live startup cancellation diagnosis and relay ownership 2026-10-13 17:27:41 +02:00
evgeny ccc9210232 Retain selected relay issuance before delayed READY and ACCEPT 2026-10-13 17:20:36 +02:00
evgeny 325abd8d6f Document verified 100ms stream bootstrap and aligned playback 2026-10-13 16:47:39 +02:00
evgeny 1d7f70b31c Preserve Opus preroll state across timestamp jitter and short gaps 2026-10-13 16:39:37 +02:00
evgeny eda8dfbc91 Cover paused sources and duplicate relay controls during startup 2026-10-13 16:32:53 +02:00
evgeny 8206e53b5c Warm Android Opus preroll and start audio at the first keyframe 2026-10-13 16:30:41 +02:00
evgeny 79701feef1 Drain startup audio on END and validate Opus fixture failures 2026-10-13 16:27:12 +02:00
evgeny f42b085e12 Align desktop audio startup with the first decoded video frame 2026-10-13 16:26:11 +02:00
evgeny 60aaa16327 Confirm relay readiness after reconstructing first FEC batch 2026-10-13 16:22:00 +02:00
evgeny ad20f954c5 Race initial stream suppliers with relay delay of 100ms 2026-10-13 16:14:35 +02:00
evgeny e708117259 Send relay state transitions without retry delays 2026-10-13 16:09:13 +02:00
evgeny 5def12351f Wake stream selection on validated control responses 2026-10-13 16:06:26 +02:00
evgeny 44cfebf898 Record repeated stream delivery stall during stereo verification 2026-10-13 15:30:59 +02:00
evgeny 63ccff67a2 Record stereo and rotation verification on SM and desktop 2026-10-13 15:22:56 +02:00
evgeny 50005b108f Generate continuous stereo Opus in live playback regression 2026-10-13 14:39:08 +02:00
evgeny 727106f4e9 Document stereo live audio and compact rotation controls 2026-10-13 14:30:07 +02:00
evgeny c251de4f77 Verify stereo media format and AGC channel balance 2026-10-13 14:24:59 +02:00
evgeny 8bee8c10ff Replace stream rotation menu with three marked toggle buttons 2026-10-13 14:24:59 +02:00
evgeny 214a98efb1 Play live stereo on desktop and verify channel separation 2026-10-13 14:17:26 +02:00
evgeny e06e9b0e22 Capture encode and play stereo audio in Android live streams 2026-10-13 14:15:44 +02:00
evgeny 22b62699ac Describe stereo Opus in live media blocks 2026-10-13 14:14:24 +02:00
evgeny 5f0fff5fd6 Record CAMCORDER and live radio compressor verification 2026-10-13 14:06:12 +02:00
evgeny 2a2394b34d Verify live stream compression and document shared radio AGC 2026-10-13 13:46:24 +02:00
evgeny 2e0eb89ad3 Capture CAMCORDER audio and apply radio AGC changes live 2026-10-13 13:40:08 +02:00
evgeny 6bd14c53b5 Compress stream audio before Opus using radio settings 2026-10-13 13:39:12 +02:00
evgeny 149e4d43e5 Record live desktop playback verification after queue fix 2026-10-13 13:22:51 +02:00
evgeny c72a66ba06 Record desktop shutdown remaining after core teardown 2026-10-13 13:07:51 +02:00
evgeny 736fb92ed7 Document live playback queue bounds and latency regression 2026-10-13 12:57:37 +02:00
evgeny ef67e3ed2c Preserve video frames until audio clock reaches their PTS 2026-10-13 12:55:56 +02:00
evgeny 1d984f0d0b Reproduce live video starvation behind audio output latency 2026-10-13 12:51:42 +02:00
evgeny cff92f3dac Remove obsolete debug option from configuration fixture 2026-10-13 12:18:01 +02:00
evgeny f953ab5a8f Test all debug categories and record Android settings fixes 2026-10-13 12:12:30 +02:00
evgeny 5f5d4f964a Preserve Android debug level and accept all core categories 2026-10-13 12:10:38 +02:00
evgeny 7fa7a42673 Expose native debug categories and fix Android level fields 2026-10-13 12:10:08 +02:00
evgeny df400a90aa Record partial stream recovery failure from SM and colo2 logs 2026-10-13 11:36:40 +02:00
evgeny 14e2cdc23d Document supernode stream assistance and completed validation 2026-10-13 10:18:44 +02:00
evgeny bbd13215a4 Apply router backpressure and preserve reusable relay attempts 2026-10-13 09:42:46 +02:00
evgeny e73fab0a73 Verify relay handoff expiry, replay protection and complete teardown 2026-10-13 09:38:50 +02:00
evgeny 035a2eb6d0 Retain and release media replication connection ownership 2026-10-13 09:28:51 +02:00
evgeny 57d2828496 Separate stream routing from member files and scope relay activation 2026-10-13 09:14:06 +02:00
evgeny 8dbfb4d297 Exercise supernode relay delivery and preserve ended receive handles 2026-10-13 01:28:23 +02:00
evgeny 29d91d4b19 Activate supernode stream relays on demand and release idle owners 2026-10-13 01:20:24 +02:00
evgeny 661cf032fe Define routed stream relay requests and generation-scoped replies 2026-10-13 01:15:09 +02:00
evgeny cd5e658fb9 Add internal ownership for exact-epoch stream relays 2026-10-13 01:15:09 +02:00
evgeny 8883002e3f Distinguish relay-only stream subscriptions in member profiles 2026-10-13 01:15:09 +02:00
evgeny 2cc9834e4c Use integral scores to select stream suppliers 2026-10-13 01:14:38 +02:00
evgeny 83c652a810 Merge remote-tracking branch 'origin/master' 2026-10-13 01:08:11 +02:00
evgeny 3a6dacf378 Add stream camera controls and preserve playback on changes 2026-10-13 01:00:53 +02:00
evgeny 72bcbd98e8 Handle SIGPIPE during TCP stream test teardown 2026-10-13 00:58:40 +02:00
evgeny 6a92eec983 Log stream probe backoff and pending bootstrap state 2026-10-13 00:56:18 +02:00
evgeny 3adc4a0be4 Isolate stream bootstrap teardown from shared test events 2026-10-13 00:55:12 +02:00
evgeny bf0b90d362 Exercise initial stream reinit on a single TCP link 2026-10-13 00:51:36 +02:00
evgeny 00b78fd629 Preserve pending stream requests across session reinit 2026-10-13 00:40:53 +02:00
evgeny 5ddfc582f7 Merge remote-tracking branch 'origin/master' 2026-10-12 22:38:49 +02:00
evgeny db2771e8de Fix Windows interface macro collision in NCD test 2026-10-12 22:38:23 +02:00
evgeny 29a8fb2531 Check source discovery requested before desktop startup 2026-10-12 22:31:28 +02:00
evgeny 061eb0f0f8 Preserve initial channel watches until desktop core attaches 2026-10-12 22:30:38 +02:00
evgeny 17606c66b8 Document reactive stream discovery and desktop playback ownership 2026-10-12 22:29:00 +02:00
evgeny 34349bd663 Require selected epochs and verify multiple live publishers 2026-10-12 22:28:15 +02:00
evgeny e9982a2db6 Verify live Opus output and volume with real null audio devices 2026-10-12 22:26:41 +02:00
evgeny dd034a9d42 Cover channel closure and keep stream selection usable for long lists 2026-10-12 22:21:10 +02:00
evgeny cacd377d55 Use only shared source events in Android streaming UI and tests 2026-10-12 22:20:28 +02:00
evgeny dad67360b9 Remove obsolete Android stream polling and simplify lifecycle tests 2026-10-12 22:20:28 +02:00
evgeny 6e596f34a5 Notify viewers when channels close and refresh resumed playback state 2026-10-12 22:18:04 +02:00
evgeny 4e1c36955a Test desktop stream discovery, playback, pressure and teardown 2026-10-12 22:16:31 +02:00
evgeny e5d37f6e74 Translate desktop live stream controls and playback states 2026-10-12 22:15:11 +02:00
evgeny 7addfb5aa3 Keep live decoding low latency and preserve publisher stop controls 2026-10-12 22:15:11 +02:00
evgeny b4ebaa8863 Open desktop stream playback windows from channel sources 2026-10-12 22:12:41 +02:00
evgeny fe7734b9e0 Show live sources and select a stream in desktop channel headers 2026-10-12 22:12:41 +02:00
evgeny 10f197c817 Decode and play desktop live H264 and Opus streams 2026-10-12 22:11:35 +02:00
evgeny 0a5b9b50f1 Connect desktop stream bridge to core lifecycle and events 2026-10-12 22:09:39 +02:00
evgeny d9821fa89c Add bounded desktop live media bridge and source lifecycle tests 2026-10-12 22:07:35 +02:00
evgeny e3d9ac9d31 Report Android stream close reasons and epoch changes 2026-10-12 22:01:38 +02:00
evgeny d5ed19ee34 Open Android live streams from the channel header 2026-10-12 22:01:38 +02:00
evgeny ba3dd70f6c Keep Android live sources in reactive channel state 2026-10-12 22:00:12 +02:00
evgeny ba3dfa9fcc Watch Android stream sources and validate selected epochs 2026-10-12 21:58:56 +02:00
evgeny f91894aa1b Expose verified stream sources and channel watch events 2026-10-12 21:57:20 +02:00
evgeny 90d3206071 Confirm new TCP sessions and bound reciprocal challenge retries
Negotiate new TCP sessions even when the peer epoch is known, and let the retry timer send reciprocal challenges to avoid feedback loops when confirmations are lost.

Cover peer restarts, simultaneous connects, REALITY, confirmation loss, second links, stale confirmations and deadlines. Remove forced client resets and count only validated test payloads in the UDP reconnect test.

Validation: 149 tests passed; proxy, burst and load integration tests passed; Android assembleDebug succeeded.
2026-10-12 21:29:41 +02:00
evgeny d372438b6e Reuse automatically completed profiles in the NAT fixture 2026-10-12 20:57:29 +02:00
evgeny d2b96d1340 Release cold priority after a completed partial measurement 2026-10-12 20:48:33 +02:00
evgeny a32eef9ed1 Wait for the actual DOWN timer in the owner-release test 2026-10-12 20:46:43 +02:00
evgeny c7be1ebbbe Preserve cold NAT trials in the real network fixture 2026-10-12 20:43:39 +02:00
evgeny 391059c967 Refresh signed socket metadata when changing the failure-test profile 2026-10-12 20:33:31 +02:00
evgeny bea9ca88a6 Publish the test peer profile through the instance-owned snapshot 2026-10-12 20:33:16 +02:00
evgeny 1b659335b9 Test failure recheck eligibility and coalescing on a blocked UDP peer 2026-10-12 20:33:03 +02:00
evgeny 61e3ad9d24 Start a cold measurement when automatic detection is enabled 2026-10-12 20:31:45 +02:00
evgeny 406f569989 Protect cold ingress before the third routed node is announced 2026-10-12 20:29:33 +02:00
evgeny 3294f67dbf Log cold candidate eligibility and reservation changes 2026-10-12 20:27:34 +02:00
evgeny 2dc7e8d206 Document failure recheck limits and validate requested endpoints 2026-10-12 20:26:12 +02:00
evgeny 3b894753b1 Trigger NAT rechecks after direct timeout and exhausted reverse 2026-10-12 20:25:42 +02:00
evgeny 952fbe68e2 Schedule bounded endpoint rechecks after compatible UDP failure 2026-10-12 20:25:20 +02:00
evgeny 99382cf823 Cancel UDP route probes with their contexts before socket teardown 2026-10-12 20:21:02 +02:00
evgeny ceab874abc Wait for complete NAT cycles and test an indirect private helper 2026-10-12 20:18:21 +02:00
evgeny 2354e94347 Expose pending NAT candidate cycles in local diagnostic status 2026-10-12 20:17:44 +02:00
evgeny 2ba011d3fd Document grouped control, isolated helper ports and ingress correlation 2026-10-12 20:07:49 +02:00
evgeny 179aa0ef35 Release received packets when cold NAT priority defers PONG 2026-10-12 20:05:47 +02:00
evgeny bb01f243fe Allow routed observers in the NAT integration assertions 2026-10-12 19:54:39 +02:00
evgeny 3635bcf3a9 Keep connection-manager background pings out of cold NAT trials 2026-10-12 19:44:28 +02:00
evgeny e60b03916a Test automatic detection in another group over a TCP routed helper 2026-10-12 19:30:42 +02:00
evgeny 9a24ac6bdf Try controlled ingress before asking the broker for helper mapping 2026-10-12 19:27:58 +02:00
evgeny f71e94c9d8 Trace the first UDP contacts that affect cold NAT classification 2026-10-12 19:17:06 +02:00
evgeny 8906e29102 Keep background PONG replies from contaminating cold ingress checks 2026-10-12 19:01:13 +02:00
evgeny 9bd113054c Defer reachability probes while the first NAT ingress is protected 2026-10-12 18:50:08 +02:00
evgeny 9324b8af8c Give initial NAT measurements bounded priority without opening filters 2026-10-12 18:50:08 +02:00
evgeny 585834ffbe Isolate helper traffic from the measured working sockets 2026-10-12 18:47:54 +02:00
evgeny 96fa1990b0 Keep detection attempt counters and network fixtures group-aware 2026-10-12 18:20:47 +02:00
evgeny 0ee431b490 Prefer untouched helper IPs and exercise NAT through real groups 2026-10-12 18:13:29 +02:00
evgeny 8990574c1d Complete grouped NAT control and cancel tasks with group lifetime 2026-10-12 18:08:47 +02:00
evgeny a0a4493ef2 Measure helper endpoints and protect controlled UDP ingress trials 2026-10-12 17:57:22 +02:00
evgeny 77ca1ec3dd Select NAT candidates across groups and route authenticated control 2026-10-12 17:50:45 +02:00
evgeny 2e7da4f3cc Define grouped NAT probe tags and a routed control service 2026-10-12 17:43:27 +02:00
evgeny a97589e4a6 Check desktop address rendering with compact NAT profiles 2026-10-12 17:18:23 +02:00
evgeny 53c32e5669 Document compact NAT profiles and stable measurement lifetime 2026-10-12 17:13:22 +02:00
evgeny 5429431567 Invalidate changed endpoints and verify the Android address contract 2026-10-12 17:11:56 +02:00
evgeny 6272c8a4ad Verify compact address snapshots, migration and Android JSON 2026-10-12 17:10:48 +02:00
evgeny 89f031f4f9 Keep STUN round diagnostics separate from published NAT profiles 2026-10-12 17:07:35 +02:00
evgeny ba29adf6ef Test stable published profiles across repeated NAT measurements 2026-10-12 17:06:35 +02:00
evgeny 61132a3e84 Align socket monitoring with compact address profiles 2026-10-12 17:05:23 +02:00
evgeny db8a5ee2df Retain stable NAT results and compact the address database 2026-10-12 17:04:59 +02:00
evgeny e47ae480f5 Remove time-dependent profile matching from connection setup 2026-10-12 17:01:28 +02:00
evgeny f900cda9c0 Use the last published address profile without a time limit 2026-10-12 17:01:02 +02:00
evgeny 38eaae8bb6 Remove measurement IDs and timestamps from NODEINFO profiles 2026-10-12 17:00:38 +02:00
evgeny d2e62874bb Fix Android member details layout and address formatting 2026-10-12 16:40:40 +02:00
evgeny a96d5926c7 Fix Android external network monitoring and startup readiness
Select frontend network callbacks for auto_sockets=android instead of opening a denied netlink monitor. Preserve monitor descriptor ownership, keep native monitoring failures fatal, and release caller config on failed Android core creation and restart.

Register ConnectivityManager before requesting startup, report readiness after SERVICE_STARTED, and refresh addresses after initialization with serialized updates and generation checks. Add regression coverage for denied netlink, socket failures, address updates, and fd=0 preservation. Record the separate cancellation issue for startup before readiness.

Validation: full check.sh passed, including proxy/burst/load integrations. Clean Android build installed successfully on SM-A525F and Q8; logcat confirms both cores initialized and entered the poll loop with external monitoring and post-start address refresh.
2026-10-12 16:01:26 +02:00
evgeny 72e2c89e3b Fix sockless TCP keepalive and require complete UTUN initialization
Receive keepalive through the owning ETCP link, preserve RX deadlines, and report routing metrics without TUN. Use independent netlink requests and track installed routes for rollback. Reject incomplete resource initialization and release partially constructed cores while preserving caller config ownership.

Validated startup failure and rollback, padded keepalive, standby, STCP traffic, transport lifecycle, dynamic sockets, and daemon startup. Real TUN/netlink checks passed in an isolated Linux network namespace; modified modules passed FreeBSD compilation checks.
2026-10-12 14:24:32 +02:00
evgeny bb256dccdc Extend Android playback counters across long running streams 2026-10-12 12:54:13 +02:00
evgeny a043dc9a42 Bound audio writes and release codecs after failed initialization 2026-10-12 11:49:09 +02:00
evgeny 98dcaae853 Close viewers on malformed media and release terminal owners immediately 2026-10-12 11:46:13 +02:00
evgeny eee67ac9d9 Close local preview when its publication ends 2026-10-12 11:36:07 +02:00
evgeny af580d9d90 Record Android viewer lifecycle and persisted snapshot diagnostics 2026-10-12 11:31:26 +02:00
evgeny 039c0f71f5 Pause viewers in the background and keep visible playback awake 2026-10-12 11:30:36 +02:00
evgeny 150b3c3bdb Explain invalid media metadata and codec transitions in logs 2026-10-12 11:26:27 +02:00
evgeny 1dcf6a79ce Reject overlapping core ownership of Android stream handles 2026-10-12 11:26:27 +02:00
evgeny 18661f0ef0 Constrain live preview while keeping stream controls visible 2026-10-12 11:21:30 +02:00
evgeny ef76ca5de3 Document audiovisual stream format and Android ownership contracts 2026-10-12 07:24:13 +02:00
evgeny dac5f98052 Select local preview from the verified source snapshot 2026-10-12 07:22:43 +02:00
evgeny c2c202e716 Mute local preview and clear Android media queues at detach 2026-10-12 07:22:43 +02:00
evgeny c1b1713ab1 Verify Android AVC normalization and keyframe configuration 2026-10-12 02:47:15 +02:00
evgeny e8adf03826 Preserve stream control events when discarding delayed media 2026-10-12 02:42:01 +02:00
evgeny 1209d8e4b7 Keep video clock moving through audio underruns and drain final playback 2026-10-12 02:30:36 +02:00
evgeny e09ee919a5 Release capture before service ownership and report startup failures 2026-10-12 02:29:26 +02:00
evgeny c9743ee85d Exercise Android stream queues and teardown against a live core 2026-10-12 02:29:08 +02:00
evgeny 0c613eacbc Show verified source snapshots without borrowing core pointers 2026-10-12 02:28:02 +02:00
evgeny 6c692399c2 Build verified live source snapshots in the core thread 2026-10-12 02:27:44 +02:00
evgeny 4fc1f31651 Prevent competing Android capture while broadcasting 2026-10-12 02:07:04 +02:00
evgeny d4ca812578 Open live streams from Android channel navigation 2026-10-12 02:07:04 +02:00
evgeny 8502d6f2d7 Add localized Android stream controls and player screen 2026-10-12 02:06:27 +02:00
evgeny ee851b4aa6 Play live streams with bounded queues and audio clock synchronization 2026-10-12 02:04:38 +02:00
evgeny f2a5dde761 Declare stream camera permissions and cancel capture at core restart 2026-10-12 02:02:48 +02:00
evgeny e4f2e647d7 Own live camera sessions in an Android foreground service 2026-10-12 02:02:48 +02:00
evgeny c50d5066a9 Capture Android AVC and Opus on a shared monotonic timeline 2026-10-12 02:01:09 +02:00
evgeny 2f707366f3 Expose session scoped Android stream operations 2026-10-12 01:51:10 +02:00
evgeny 6f544b4ca2 Add Android JNI transport and independent stream Opus codecs 2026-10-12 01:51:10 +02:00
evgeny 0ee1163a0b Bind Android media queues to core lifetime and source epochs 2026-10-12 01:50:07 +02:00
evgeny 938289a6f0 Include Android streaming bridge and refine malformed wire tests 2026-10-12 01:49:40 +02:00
evgeny 36fceec6b2 Bridge Android live streams through bounded session queues 2026-10-12 01:49:07 +02:00
evgeny fc3518f5ba Test media boundaries FEC losses and keyframe recovery 2026-10-12 01:46:06 +02:00
evgeny 5c23c10c04 Register audiovisual stream sources and regression target 2026-10-12 01:46:06 +02:00
evgeny 1389bac261 Define FEC protected audiovisual blocks and frame assembly 2026-10-12 01:45:08 +02:00
evgeny 47caca82a1 Expose atomic publication of stream batches 2026-10-12 01:43:31 +02:00
evgeny baeaef3ab1 Preserve logical block boundaries in stream sources 2026-10-12 01:43:21 +02:00
evgeny 4f9fefe9e5 Test and document NCD ownership protecting CM direct probes beyond down timeout 2026-10-12 01:34:44 +02:00
evgeny fdaa20977d Recheck expired down lifetime when the last NCD owner releases it 2026-10-12 01:34:44 +02:00
evgeny 11f1a14063 Retain down ETCP connections while local NCD handles exist 2026-10-12 01:34:44 +02:00
evgeny 0d7f7630b0 Expose actual NCD handle ownership for an ETCP transport 2026-10-12 01:34:44 +02:00
evgeny 6704a18897 Clarify independent RX watchdog and sleep-aware graceful close deadlines 2026-10-12 01:34:44 +02:00
evgeny 24c64967fd Propagate remote UDP link DOWN to the connection expiry timer 2026-10-12 01:34:44 +02:00
evgeny 85b4af5b1d Shorten RX deadline immediately when a sleeping peer wakes 2026-10-12 01:34:44 +02:00
evgeny 2e66cdb122 Preserve cancellation of pending outgoing attempts and size packet fixture correctly 2026-10-12 01:34:44 +02:00
evgeny b57221f6b5 Document ETCP down retention and separate RX lifetime from keepalive sending 2026-10-12 01:34:44 +02:00
evgeny 1c53ef597d Register lifecycle regressions and cancel timers owned by synthetic fixtures 2026-10-12 01:34:44 +02:00
evgeny 0ba8380db4 Test incoming ETCP expiry, reconnect, sleep and graceful close 2026-10-12 01:34:44 +02:00
evgeny f090d697c4 Initialize inbound graceful close and honor remote transport ownership 2026-10-12 01:34:44 +02:00
evgeny 3cfdb9e6ec Keep a finite RX deadline while ETCP keepalive sending is suspended 2026-10-12 01:34:44 +02:00
evgeny 034be69ac1 Expire ETCP transports after 30 seconds continuously down 2026-10-12 01:34:44 +02:00
evgeny b852058501 Record validated STUN fallback, sanitizer checks and IPv6 follow-up 2026-10-12 01:31:29 +02:00
evgeny b3830e0264 Document signed profile source and reproducible isolated STUN bench 2026-10-12 01:28:49 +02:00
evgeny c19f835334 Document implemented interface STUN fallback and provenance semantics 2026-10-12 01:27:40 +02:00
evgeny 9f907d0f31 Stop NAT detectors before service callbacks and verify STUN restart isolation 2026-10-12 01:25:59 +02:00
evgeny 152ebfef48 Release retained STUN port history during final core destruction 2026-10-12 01:24:00 +02:00
evgeny 3d5f009380 Keep STUN port history owned by the core across configuration reloads 2026-10-12 01:24:00 +02:00
evgeny 3735b78328 Verify STUN cache expiry and preserve partial ETCP measurement priority 2026-10-12 01:20:56 +02:00
evgeny 709b0a3271 Wait for the real observer link and distribute STUN bench scripts 2026-10-12 01:15:31 +02:00
evgeny 8c5e4ab691 Exercise STUN fallback and ETCP promotion with real namespace NAT 2026-10-12 01:14:09 +02:00
evgeny a581cb2086 Add isolated STUN fallback scenario and equivalent NAT for test ports 2026-10-12 01:12:28 +02:00
evgeny f433413c49 Test deduplicated STUN groups and fallback promotion through real ETCP 2026-10-12 01:01:11 +02:00
evgeny 164a3a1f0f Keep connection attempts available for STUN hints and verify signed provenance 2026-10-12 00:58:50 +02:00
evgeny 9aa56e6b56 Show NAT profile origin and recheck interface STUN from control 2026-10-12 00:58:50 +02:00
evgeny 76d2e58f19 Expose ETCP and STUN profile provenance in monitor protocol 2026-10-12 00:58:32 +02:00
evgeny 01b0992c39 Select interface STUN fallback only when ETCP lacks independent observers 2026-10-12 00:57:54 +02:00
evgeny 735ac36579 Sign endpoint profile provenance and restrict external-address publication 2026-10-12 00:56:43 +02:00
evgeny 6beae21ec4 Group STUN probes by interface path and refresh independent-port results 2026-10-12 00:55:29 +02:00
evgeny 8b8b73983b Plan interface STUN profiles as fallback for unavailable ETCP probes 2026-10-12 00:42:14 +02:00
evgeny 37e897d0d2 Plan unified STUN and ETCP endpoint measurements 2026-10-12 00:09:13 +02:00
evgeny 5c83aac96e Adapt STUN diagnostics to endpoint profiles and helper sockets 2026-10-12 00:03:07 +02:00
evgeny 39dd038831 Record ASAN validation of endpoint detector lifecycle 2026-10-12 00:01:40 +02:00
evgeny ff69552a61 Measure owned endpoint mapping and inbound reachability separately 2026-10-12 00:01:40 +02:00
evgeny b54307a0df Ignore local hardware directory 2026-10-12 00:01:40 +02:00
evgeny 65f05c86ed Save workspace state before address detection redesign 2026-10-12 00:01:40 +02:00
evgeny 5eb1c86acf Include stream bench validation notes in source distribution 2026-10-12 00:01:40 +02:00
evgeny b8f8818eef Record validated stream bench runs and explicit RPC error diagnostics 2026-10-12 00:01:40 +02:00
evgeny 89c6482a96 Record clean NAT startup and monotonic restart contract in stream specification 2026-10-12 00:01:40 +02:00
evgeny d44b4bc682 Test repeated restart before deferred GAP and prohibit replay under overload 2026-10-12 00:01:40 +02:00
evgeny 802134c846 Preserve delivered cursor when stream restarts from a stale supplier 2026-10-12 00:01:40 +02:00
evgeny 1969ca5535 Require monotonic consumer cursor across stream receive restart 2026-10-12 00:01:40 +02:00
evgeny 5084670820 Recreate bench network after calibration to clear NAT and conntrack state 2026-10-12 00:01:40 +02:00
evgeny e78eaf45f7 Document namespace stream bench and completion after consumer drain 2026-10-12 00:01:40 +02:00
evgeny a877ba8a73 Keep automatic stream recovery active until signed END is drained 2026-10-12 00:01:40 +02:00
evgeny a4e899807c Clear closed fixture handles before terminal stream regression 2026-10-12 00:01:40 +02:00
evgeny d99b0e5525 Reproduce automatic stream stall after early END with incomplete FEC batch 2026-10-12 00:01:40 +02:00
evgeny 46ac9a03b9 Add forced TCP bench mode and verify observed source timestamp wraps 2026-10-12 00:01:40 +02:00
evgeny 57bf4e490d Bound namespace startup and measure LAN quality before stream tests 2026-10-12 00:01:40 +02:00
evgeny 649be77fa3 Use core clock for stream metrics and require new data after WAN recovery 2026-10-12 00:01:40 +02:00
evgeny 5c1bb06294 Expose source clock and stream metrics in bench snapshots 2026-10-12 00:01:40 +02:00
evgeny 284b603e6e Account initial GAP independently of first delivered stream record 2026-10-12 00:01:40 +02:00
evgeny e92e08e5cc Document stream lab and test namespace isolation guards 2026-10-12 00:01:40 +02:00
evgeny 3dc682087a Wait for verified Merkle profiles as well as BGP reachability 2026-10-12 00:01:40 +02:00
evgeny ebe3232268 Apply optional load collection only to peer queue telemetry 2026-10-12 00:01:40 +02:00
evgeny 0eae531706 Include local BGP presence and make queue diagnostics opt-in 2026-10-12 00:01:40 +02:00
evgeny e9e93ef26b Wire live stream scenarios into isolated bench launcher 2026-10-12 00:01:40 +02:00
evgeny fe4791d6f5 Add sixteen-node live JOIN and streaming scenarios 2026-10-12 00:01:39 +02:00
evgeny 6332e75e61 Add real uTun stream bench node with payload and latency validation 2026-10-12 00:01:39 +02:00
evgeny 856fbc61b1 Keep NAT probes independent of cached conntrack and DHCP timers 2026-10-12 00:01:39 +02:00
evgeny 47948fe847 Add rootless isolated stream bench launcher 2026-10-12 00:01:39 +02:00
evgeny 0b1a081e34 Implement isolated stream bench networking and NAT calibration 2026-10-12 00:01:39 +02:00
evgeny 0172aa3081 Create dedicated stream network-namespace bench topology 2026-10-12 00:01:39 +02:00
evgeny 4096691eb2 Record full-suite and final sanitizer verification for automatic streaming 2026-10-12 00:01:39 +02:00
evgeny fc63b801f9 Clarify generation discovery on every stream INFO probe 2026-10-12 00:01:39 +02:00
evgeny af1f3cc007 Document nonce-bound unilateral stream generation recovery 2026-10-12 00:01:39 +02:00
evgeny 6b18ad45ea Restore automatic suppliers after proven resynchronization and test unilateral generation changes 2026-10-12 00:01:39 +02:00
evgeny a8cd802aad Resynchronize unilateral peer generations through nonce-bound INFO without epoch ping-pong 2026-10-12 00:01:39 +02:00
evgeny 9a648219cf Document automatic stream selection, wire v3, active-mode ownership and verified behavior 2026-10-12 00:01:39 +02:00
evgeny 6749d09025 Enforce distinct manual and automatic upstream ownership 2026-10-12 00:01:39 +02:00
evgeny 0d2d7c913d Keep optimizer diagnostic format strings within repository line limits 2026-10-12 00:01:39 +02:00
evgeny 090964a786 Log provider offers and measured reasons for confirmed stream migrations 2026-10-12 00:01:39 +02:00
evgeny fcbdbd6002 Test probe burst, second-level admission and complete-cycle timing boundaries 2026-10-12 00:01:39 +02:00
evgeny c6bf960eec Isolate monotonic probe scheduling policy for precise boundary checks 2026-10-12 00:01:39 +02:00
evgeny ab4b75986d Reopen closed peer ownership and drive reverse requests from current stream attempts 2026-10-12 00:01:39 +02:00
evgeny 7144bbe694 Verify deferred standby restoration and END-only automatic streams 2026-10-12 00:01:39 +02:00
evgeny 03814b9660 Tie active-mode holds to stream ownership and deliver signed END for empty live sources 2026-10-12 00:01:39 +02:00
evgeny bccf8c6b38 Preserve frontend activity requests while stream owners hold the core active 2026-10-12 00:01:39 +02:00
evgeny 1308e8201e Verify direct, reverse-only and incompatible NCD capability checks without side effects 2026-10-12 00:01:39 +02:00
evgeny aa3fb9cac7 Admit reverse-capable providers without requiring an outgoing-compatible address 2026-10-12 00:01:39 +02:00
evgeny 22342b310c Bound stale offer storage, detect initial DATA stalls and report verified FEC protection 2026-10-12 00:01:39 +02:00
evgeny 2d9616eca5 Refresh matching active path RTT through probes and isolate integration event counters 2026-10-12 00:01:39 +02:00
evgeny 7db3008e5c Wait for the actual automatic stream source in Merkle subscription checks 2026-10-12 00:01:39 +02:00
evgeny 31d0db939a Include NCD compatibility API in automatic stream integration test 2026-10-12 00:01:39 +02:00
evgeny bc49b452f9 Exercise automatic stream selection, INFO-only probes and verified overlap failover 2026-10-12 00:01:39 +02:00
evgeny 99359f987c Use observed provider quality and keep empty-source queue accounting bounded 2026-10-12 00:01:39 +02:00
evgeny cb9ef94501 Keep protocol and overload fixtures under explicit manual upstream control 2026-10-12 00:01:39 +02:00
evgeny 94c482ed35 Handle empty sources and next-batch tail subscriptions without startup races 2026-10-12 00:01:39 +02:00
evgeny ef48fa2a37 Enable automatic upstream selection for subscriptions with lifecycle-safe cleanup 2026-10-12 00:01:39 +02:00
evgeny 576c79657b Implement stream supplier probe cycles, FEC-aware selection and overlap migration 2026-10-12 00:01:39 +02:00
evgeny 3db2130b56 Define per-stream automatic selection settings and diagnostic manual mode 2026-10-12 00:01:39 +02:00
evgeny 0b9b395879 Keep SUBSCRIBE deadlines and prevent ambiguous pending INFO correlation 2026-10-12 00:01:39 +02:00
evgeny 35ad47c653 Implement DATA-free stream probes with ranges, paths and load offers 2026-10-12 00:01:39 +02:00
evgeny 984153b90a Expose monotonic age of verified substream progress 2026-10-12 00:01:39 +02:00
evgeny 9cd285ddb0 Define INFO-only stream probes and provider offer snapshots 2026-10-12 00:01:39 +02:00
evgeny ca5a374042 Expose side-effect-free NCD transport compatibility using shared link matching 2026-10-12 00:01:39 +02:00
evgeny ee8baae160 Record full and sanitizer checks for stream overload delivery 2026-10-12 00:01:39 +02:00
evgeny f10078e482 Document timestamp metrics and negotiated stream overload cuts 2026-10-12 00:01:39 +02:00
evgeny 0282fe7e17 Keep newer INFO probes alive when old queued probes expire 2026-10-12 00:01:39 +02:00
evgeny 4142c7691a Expire stale stream control without retaining orphan issuances 2026-10-12 00:01:39 +02:00
evgeny 223bb145e6 Compare overload request history when checking CPU-only load 2026-10-12 00:01:39 +02:00
evgeny c9ce8d3a74 Expose overload admission recovery and wait for it in tests 2026-10-12 00:01:39 +02:00
evgeny bee7bdaa70 Expose stream load and network APIs with C linkage 2026-10-12 00:01:39 +02:00
evgeny 695741d9d3 Exclude stale growth and pending cuts from global overload budgets 2026-10-12 00:01:39 +02:00
evgeny 01324d2ebb Avoid an extra FEC cut for a marginal overload safety reserve 2026-10-12 00:01:39 +02:00
evgeny 3d6c843958 Verify global stream pressure requires backlog as well as CPU 2026-10-12 00:01:39 +02:00
evgeny 97495377b2 Assert rate-based parity cuts rather than a fixed close count 2026-10-12 00:01:39 +02:00
evgeny 1174d71a06 Wait for CLOSED delivery after unblocking overload control 2026-10-12 00:01:39 +02:00
evgeny 108e97b550 Validate partial overload cuts preserve six FEC data streams 2026-10-12 00:01:39 +02:00
evgeny a4e080da91 Prefer FEC parity cuts and refresh receive progress on inspection 2026-10-12 00:01:39 +02:00
evgeny f11592be3e Exercise stream overload under real ETCP backpressure 2026-10-12 00:01:39 +02:00
evgeny 61df1204f8 Expect expired arrival intervals to leave the load window 2026-10-12 00:01:39 +02:00
evgeny df474a33d0 Check stream pressure timing, accounting and wide metric fields 2026-10-12 00:01:39 +02:00
evgeny c10e0c7e3c Negotiate frozen-rate stream cuts with bounded overload grace 2026-10-12 00:01:39 +02:00
evgeny a07c53a397 Measure stream issuances, probe RTT and pending data demand 2026-10-12 00:01:39 +02:00
evgeny 921340ae60 Track signed timestamp progress in each local stream subflow 2026-10-12 00:01:39 +02:00
evgeny fabd7ac91f Define stream pressure negotiation and timestamp metric snapshots 2026-10-12 00:01:39 +02:00
evgeny fec1916a58 Observe stream cache arrivals and exact pending block age 2026-10-12 00:01:39 +02:00
evgeny e90ad5acbd Account stream demand and confirm sustained queue pressure 2026-10-12 00:01:39 +02:00
evgeny ddc386d16c Record ASAN and full checks for live NCD stream delivery 2026-10-12 00:01:39 +02:00
evgeny 007f5d21b2 Document live NCD stream delivery, wire format and limits 2026-10-12 00:01:39 +02:00
evgeny 67152f6b7b Verify independent stream groups share one direct ETCP session 2026-10-12 00:01:39 +02:00
evgeny ee36c578e2 Reject premature stream completion and failed ACCEPT enqueue 2026-10-12 00:01:39 +02:00
evgeny 0b202bb3e7 Bound stream path allocations and validate wire rejection cases 2026-10-12 00:01:39 +02:00
evgeny fc80186043 Restart stream receives and expose confirmed delivery gaps 2026-10-12 00:01:39 +02:00
evgeny d6f935d779 Locate recoverable stream batches when skipping delivery gaps 2026-10-12 00:01:39 +02:00
evgeny 109936f846 Revalidate stream peers and respect asynchronous ETCP reset 2026-10-12 00:01:39 +02:00
evgeny 09e86a16f6 Exercise live stream relay, common start, leases and epochs 2026-10-12 00:01:39 +02:00
evgeny d83e85b8ca Report stream ring bounds and handle expired and final cursors 2026-10-12 00:01:39 +02:00
evgeny c2179b3959 Implement NCD stream issuances, ring cursors and routed reverse 2026-10-12 00:01:39 +02:00
evgeny 36c4ff6050 Connect local stream lifetime and signed END to network delivery 2026-10-12 00:01:39 +02:00
evgeny b31aa9f71d Retain stream block paths and locate gaps in ring delivery 2026-10-12 00:01:39 +02:00
evgeny 18fe51ad25 Define direct stream messages, paths and signed completion 2026-10-12 00:01:39 +02:00
evgeny 12c0de503d Document installer builds and register packaging checks 2026-10-12 00:01:39 +02:00
evgeny 804007c997 Test Windows installer contents and build error propagation 2026-10-12 00:01:39 +02:00
evgeny 6d3c555ac4 Record successful full stream integration checks 2026-10-12 00:01:39 +02:00
evgeny fa79b7d30c Document stream CHAT integration and signed member option format 2026-10-12 00:01:39 +02:00
evgeny 5f9cb86ff0 Package only imported DLLs and log firewall command output 2026-10-12 00:01:39 +02:00
evgeny a62540800b Cancel local streams when signed membership is invalidated 2026-10-12 00:01:39 +02:00
evgeny c250e703f7 Create VibeChat installer from Windows BAT entry points 2026-10-12 00:01:39 +02:00
evgeny 197696075c Release owned BGP path entries before removing their queues 2026-10-12 00:01:39 +02:00
evgeny 78e0312fee Validate stream lifecycle and Merkle BGP integration 2026-10-12 00:01:39 +02:00
evgeny 1b406202b5 Integrate installer generation into Windows build scripts 2026-10-12 00:01:39 +02:00
evgeny 2e70c3baed Add VibeChat Windows installer and runtime packaging 2026-10-12 00:01:39 +02:00
evgeny 30106c8f93 Attach stream subscriptions to CHAT lifecycle and member profiles 2026-10-12 00:01:39 +02:00
evgeny b14f00a4b2 Check Windows ChatGUI config writability directly 2026-10-12 00:01:38 +02:00
evgeny 753aa4217d Use LocalAppData fallback for Windows ChatGUI storage 2026-10-12 00:01:38 +02:00
evgeny b6f80668d1 Define signed stream member options and BGP eligibility 2026-10-12 00:01:38 +02:00
evgeny a33666b778 Add atomic updates for signed member profile options 2026-10-12 00:01:38 +02:00
evgeny ec82d95a03 Add signed FEC stream core and source buffering 2026-10-12 00:01:38 +02:00
evgeny 2c5deafe56 Add independent STUN NAT probes with RFC 5780 discovery 2026-10-11 23:35:47 +02:00
evgeny 9cc996b775 Document Reed-Solomon module and update streaming design 2026-10-10 19:45:14 +02:00
evgeny 2967d2fb71 Fix matrix inverse syntax and make bounded conversions explicit 2026-10-10 19:38:08 +02:00
evgeny d9d4a7807f Test Reed-Solomon recovery and integrate codec into builds 2026-10-10 19:37:07 +02:00
evgeny 831018f161 Add batch Reed-Solomon codec with caller-owned buffers 2026-10-10 19:35:10 +02:00
evgeny 84a171dcac tests: keep build script regressions outside the regular check 2026-10-10 17:48:20 +02:00
evgeny 8de64174bb tests: remove unnecessary barriers from parallel checks 2026-10-10 17:42:31 +02:00
evgeny 69a99c7860 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	tests/Makefile.am
2026-10-10 17:25:40 +02:00
evgeny c85a8116ef tests: run checks in parallel with available CPU count by default 2026-10-10 17:14:29 +02:00
evgeny b16da44e42 tests: assign STCP ping and REALITY fallback ports at bind 2026-10-10 16:54:56 +02:00
evgeny 7ae2f74643 tests: discover live endpoints in forked JOIN and network polls 2026-10-10 16:51:38 +02:00
evgeny 482d2787fb tests: retain automatically assigned endpoints across restart 2026-10-10 16:48:19 +02:00
evgeny 12d4e2433e dummynet: route between explicit independently bound endpoints 2026-10-10 16:46:19 +02:00
evgeny 70b55e4626 tests: assign CM phase listeners and failure endpoints dynamically 2026-10-10 16:40:56 +02:00
evgeny e836090a02 tests: allocate live and blackhole multilink endpoints dynamically 2026-10-10 16:39:17 +02:00
evgeny 4b499a076a tests: derive NCD fixture addresses from bound sockets 2026-10-10 16:37:36 +02:00
evgeny b0ca182fc5 tests: preserve diagnostic filters when wiring peer endpoints 2026-10-10 16:35:07 +02:00
evgeny 45a6320b5e tests: assign JOIN, member files and DM endpoints dynamically 2026-10-10 16:31:54 +02:00
evgeny e4fe534382 tests: isolate media delivery endpoints and scratch files 2026-10-10 16:23:09 +02:00
evgeny 336b6871f3 tests: allocate broadcast, avatar and invite listeners dynamically 2026-10-10 16:14:53 +02:00
evgeny e25d5cc220 tests: use bound endpoints for CM and database synchronization 2026-10-10 16:11:00 +02:00
evgeny 101266d1a7 tests: assign independent CM and chat sync listener ports 2026-10-10 16:07:43 +02:00
evgeny efdfb1d835 tests: inherit the ready echo listener on an automatic port 2026-10-10 16:06:07 +02:00
evgeny 4e2cc0e35a tests: hold OS-assigned HTTP and SOCKS listener ports 2026-10-10 16:04:19 +02:00
evgeny 08865b2909 tests: allocate UDP proxy and NAT transport ports automatically 2026-10-10 15:56:28 +02:00
evgeny 0af9a3092b tests: allocate ports independently in every STCP scenario 2026-10-10 15:55:28 +02:00
evgeny dc0dbbf720 headless: expose OS-assigned control and audio listener ports 2026-10-10 15:51:04 +02:00
evgeny 91909fbb1e tests: use independent ports for radio and call topologies 2026-10-10 15:45:38 +02:00
evgeny ce8144b79e tests: assign independent ports to BGP topology listeners 2026-10-10 15:44:00 +02:00
evgeny 0c09252dd6 tests: assign Merkle and DM media fixture ports dynamically 2026-10-10 15:40:34 +02:00
evgeny 98529b3268 tests: allocate STCP traffic and link listener ports automatically 2026-10-10 15:32:47 +02:00
evgeny 0dcecdd7b2 tests: remove PID ports and allocate SOCKS listeners dynamically 2026-10-10 15:30:48 +02:00
evgeny 44522027e7 tests: use ephemeral ports for ETCP traffic fixtures 2026-10-10 15:29:46 +02:00
evgeny 3f4a67f552 tests: assign NAT and route ping ports through bind zero 2026-10-10 15:28:48 +02:00
evgeny 4afd5df55f tests: use bound ephemeral ports for ETCP and IPv6 peers 2026-10-10 15:27:47 +02:00
evgeny 991fc0a2c1 transport: publish ports assigned by the operating system 2026-10-10 15:26:08 +02:00
evgeny 4908cb7e19 Document shared FreeBSD builds and locate Bash through PATH 2026-10-10 15:25:58 +02:00
evgeny c16334d551 Run build regressions with native FreeBSD tools and Windows binaries 2026-10-10 15:25:58 +02:00
evgeny e0ba199e20 Share incremental builds on FreeBSD and select portable hardening flags 2026-10-10 15:25:58 +02:00
evgeny d6c6a4d111 Use verified incremental builds instead of mandatory clean 2026-10-10 15:25:58 +02:00
evgeny 62a853e73c Detect compiler path changes and verify safe configuration cleanup 2026-10-10 15:25:58 +02:00
evgeny ff2eae2efb Verify build mode transitions and propagate GUI job counts 2026-10-10 15:25:58 +02:00
evgeny ff9d205d5b Track build configuration and preserve incremental header dependencies 2026-10-10 15:25:58 +02:00
evgeny 6437665a53 tests: accelerate session, recovery and SOCKS retry timers 2026-10-10 15:11:26 +02:00
evgeny aec9536522 tests: await proxy server readiness and retain worker failures 2026-10-10 15:10:09 +02:00
evgeny f843b553c7 tests: accelerate proxy regression event waits 2026-10-10 15:08:47 +02:00
evgeny d9c8e006b5 tests: accelerate standby phases and group connection retries 2026-10-10 15:06:56 +02:00
evgeny 361c7df812 tests: accelerate radio timers and run it among short checks 2026-10-10 14:55:53 +02:00
evgeny a1fdca09f6 tests: accelerate Merkle setup and drain waits 2026-10-10 14:54:39 +02:00
evgeny 7506da41f0 tests: keep Merkle stress records changing and verify final versions 2026-10-10 14:53:37 +02:00
evgeny 17777c540b tests: finish CPU load phases after a complete real window 2026-10-10 14:51:59 +02:00
evgeny 91081fc5c7 tests: accelerate autosocket waits and payload verification 2026-10-10 14:49:42 +02:00
evgeny 3eee716b50 tests: run short checks before slow and load scenarios 2026-10-10 14:22:06 +02:00
evgeny 0448a84286 tests: replace slow disk sleep with six-second clock-driven gate 2026-10-10 14:19:27 +02:00
evgeny 3baaa42c82 tests: accelerate BGP waits and require single-link DOWN and UP 2026-10-10 14:17:21 +02:00
evgeny 39594ad800 tests: accelerate DM retries and TTL with synchronized clocks 2026-10-10 14:14:03 +02:00
evgeny 414d40c00c tests: accelerate JOIN waits and verify timeout boundaries 2026-10-10 13:55:04 +02:00
evgeny cb32aa5aac tests: accelerate connection manager phase and recovery waits 2026-10-10 13:52:27 +02:00
evgeny 069ecc900a tests: accelerate router fault timers with real I/O clock driver 2026-10-10 13:46:50 +02:00
evgeny ec66a7e4f5 tests: detect avatar BUSY ACK repeats with ACK counter 2026-10-10 13:29:47 +02:00
evgeny 2a9a92cc34 tests: finish wrong-key STCP case when handshake closes 2026-10-10 13:27:27 +02:00
evgeny 9b879d6ced tests: wait for avatar backpressure and shorten NCD timeout fixture 2026-10-10 13:25:46 +02:00
evgeny 5805edc3dc tests: shorten mock NTP and DNS shutdown waits 2026-10-10 13:25:04 +02:00
evgeny 3f9edc88c4 Make privilege, port and fd handling reliable in tests 2026-10-10 13:09:45 +02:00
evgeny 5a9e61164b Measure uasync CPU load and display it in desktop and Android GUIs
Sample thread CPU usage without waking idle loops, expose a one-second moving average, and refresh compact GUI indicators at 3 Hz while visible on Android. Add load and bridge regression coverage, streaming notes, and task list cleanup.
2026-10-10 12:31:13 +02:00
evgeny 8bd6e4fb2d Remove obsolete trailing space after channel moc cleanup 2026-10-10 11:18:59 +02:00
evgeny e8a4b8dd4e Translate the group header menu labels 2026-10-10 11:18:34 +02:00
evgeny 61f2ca7a88 Remove group row icons and give titles the freed space 2026-10-10 11:18:17 +02:00
evgeny bfdd9cde15 Remove inline action hit targets from channel rows 2026-10-10 11:17:32 +02:00
evgeny b14bfa95c2 Collect group actions in the conversation header menu 2026-10-10 11:17:00 +02:00
evgeny 42efbbc7da Remove the file manager button from the channel footer 2026-10-10 11:07:02 +02:00
evgeny 8f908c320a Open own group files from the conversation header 2026-10-10 11:06:45 +02:00
evgeny 5ed813fe06 Record recursive removal checks and the receipt cache limit 2026-10-10 11:03:44 +02:00
evgeny 7eefb499f3 Cover recursive confirmation and storage receipt exhaustion 2026-10-10 11:02:31 +02:00
evgeny b4a9339459 Test recursive removal against real disk storage 2026-10-10 10:59:58 +02:00
evgeny afc54eae00 Confirm recursive deletion and localize removal progress 2026-10-10 10:58:45 +02:00
evgeny 7244f22722 Remove directory trees after a complete preflight scan 2026-10-10 10:58:25 +02:00
evgeny 973c734226 Defer closing until batch cancellation and cleanup finish 2026-10-10 09:38:35 +02:00
evgeny 4a6765c49a Wait for the file manager to finish copying every entry 2026-10-10 09:37:44 +02:00
evgeny db91bccda3 Keep deferred drag results owned by their operation 2026-10-10 01:58:13 +02:00
evgeny 11fc932905 Restore keyboard focus and test changed-tree cleanup 2026-10-10 01:57:28 +02:00
evgeny f7ff2df9a7 Place the native drag fixture beside the file manager 2026-10-10 01:43:06 +02:00
evgeny 3a6d075cbb Add an opt-in native file manager drag integration test 2026-10-10 01:42:42 +02:00
evgeny f0e68edad1 Translate batch file and directory operations 2026-10-10 01:41:42 +02:00
evgeny 1ed467d236 Validate batch snapshots and freeze deletion confirmation context 2026-10-10 01:41:42 +02:00
evgeny 8785fc358a Test range selection and recursive file tree transfers 2026-10-10 01:40:39 +02:00
evgeny 400b9fa93c Adapt browser regressions to asynchronous batch completion 2026-10-10 01:39:43 +02:00
evgeny 2e9babe73b Build batch transfers and wait for cancellation on close 2026-10-10 01:38:54 +02:00
evgeny 7bfa75b32c Connect multi-selection and folder transfers to the browser 2026-10-10 01:38:28 +02:00
evgeny 884eb4b412 Add cancellable file tree batch operations 2026-10-10 01:37:14 +02:00
evgeny ac17ffda54 Honor the expected file version before downloading 2026-10-10 01:35:28 +02:00
evgeny 468aeeebc7 Test Android file wire formats and nonempty directory status 2026-10-10 01:27:34 +02:00
evgeny 11b6f78924 Share exported files safely and show background transfer progress 2026-10-10 01:27:34 +02:00
evgeny a630401d10 Localize file operations and refine transfer notifications 2026-10-10 01:27:17 +02:00
evgeny 4abfbc3e9c Open Android file browser from channels and members 2026-10-10 01:27:17 +02:00
evgeny 6a3def3c9a Add a mobile member file browser with safe file actions 2026-10-10 01:27:17 +02:00
evgeny e97885eb22 Expose member file commands through Android JNI 2026-10-10 01:15:32 +02:00
evgeny 51e7f821e9 Own Android file transfers and core activity independently of UI 2026-10-10 01:15:32 +02:00
evgeny 6e5f86b2b5 Share Android file codecs and request IDs with avatars 2026-10-10 01:11:55 +02:00
evgeny e0a287d797 Report nonempty member file directories precisely 2026-10-10 01:11:54 +02:00
evgeny 39946e3cd9 Reactivate offscreen browser after confirmation dialogs 2026-10-10 00:45:49 +02:00
evgeny eb9d379efc Wait for modal focus restoration in file shortcut test 2026-10-10 00:44:42 +02:00
evgeny 93a6277ca4 Keep file navigation focused and open entries with Enter 2026-10-10 00:41:18 +02:00
evgeny 043257cabf Add Delete and Backspace shortcuts to group files 2026-10-10 00:40:25 +02:00
evgeny 6d8c28f228 Verify delayed file MIME across success failure and cancellation 2026-10-10 00:22:22 +02:00
evgeny c61a6da884 Translate file manager export actions and status 2026-10-10 00:20:28 +02:00
evgeny 9640a18b03 Test file manager copy and deferred drag completion 2026-10-10 00:20:27 +02:00
evgeny 4049a3f884 Deliver remote file drags in one gesture using deferred MIME 2026-10-10 00:19:08 +02:00
evgeny e8ca27b590 Export group files through the clipboard and native file drags 2026-10-10 00:16:46 +02:00
evgeny 7d6b45ba5d Accept file drops inside the Group files window 2026-10-10 00:13:33 +02:00
evgeny 04e8a18c9a Translate file drop and upload queue messages 2026-10-09 23:32:47 +02:00
evgeny 17f5c0ecbd Cover footer file drops and upload queue lifecycle 2026-10-09 23:32:29 +02:00
evgeny d4cef49f88 Validate dropped file sources outside the GUI thread 2026-10-09 23:31:27 +02:00
evgeny 49ee66d49c Accept file drops across the channel footer 2026-10-09 23:30:33 +02:00
evgeny b631c4ed72 Add sequential member file uploads with destination locking 2026-10-09 23:29:56 +02:00
evgeny 91a3a621d0 Translate the own files button tooltip 2026-10-09 22:58:05 +02:00
evgeny 2e4ed0347b Add own files button to the channel list footer 2026-10-09 22:58:01 +02:00
evgeny 7f3247bfcc radio: add activation controls, live VAD meters and signal diagnostics 2026-10-09 16:11:11 +02:00
evgeny fddbba217d android: add level-triggered radio and inline activation settings 2026-10-09 00:06:55 +02:00
evgeny 44af542f61 chatgui: add radio activation by signal level with hysteresis 2026-10-08 17:53:16 +02:00
evgeny 2845270016 Add diagnostics for stalled PTT shortcut recording 2026-10-08 15:00:25 +02:00
evgeny d149bad5fc radio: keep drain diagnostics at burst boundaries and test queued PCM 2026-10-08 14:33:09 +02:00
evgeny f054f9c8d9 test: cover radio receive guard timing and playback progress 2026-10-08 14:30:21 +02:00
evgeny a1e8bfe80a android: keep radio VAD blocked until AudioTrack plays the receive tail 2026-10-08 14:28:08 +02:00
evgeny 06c35839ef chatgui: report radio output progress with device timing 2026-10-08 14:27:05 +02:00
evgeny fe7aabb44a radio: gate automatic VAD through receive and playback drain 2026-10-08 14:27:05 +02:00
evgeny c7c47d5ca3 android: use larger adaptive launcher icon with full background 2026-10-08 10:39:40 +02:00
evgeny dff2da0522 test: exercise Android replies through native bridge and media workers 2026-10-08 10:29:30 +02:00
evgeny 1ab51f3da1 android: translate channel reply actions and errors 2026-10-08 10:29:30 +02:00
evgeny 161af93347 android: connect channel reply navigation and camera context 2026-10-08 10:29:30 +02:00
evgeny f72fb177ba android: add reply menu and preserve drafts until submission completes 2026-10-08 10:29:30 +02:00
evgeny 30102f5f42 android: render reply quotes and support message long press 2026-10-08 10:29:30 +02:00
evgeny d28b7062d2 android: track saved replies and navigate original messages 2026-10-08 10:29:30 +02:00
evgeny 22e3c1b0a5 android: model channel reply drafts and native message references 2026-10-08 10:29:29 +02:00
evgeny 368f629948 android: expose reply submission and anchored history in bridge 2026-10-08 10:29:29 +02:00
evgeny 5f79d00f7a android: pass reply context through video preparation 2026-10-08 10:29:29 +02:00
evgeny 7aada92be9 android: preserve reply context while recording voice 2026-10-08 10:29:29 +02:00
evgeny 721160fbd9 android: carry channel reply references through file and photo sends 2026-10-08 10:29:08 +02:00
evgeny 4463f8870a Keep the viewed reply target stable until returning to latest messages 2026-10-08 00:50:17 +02:00
evgeny fde359b4af Set speaking state in the radio button color test fixture 2026-10-08 00:38:04 +02:00
evgeny ba0b21b999 Preserve newer composer edits when a reply finishes sending 2026-10-08 00:36:52 +02:00
evgeny adf1e9507a Fill reply history windows at boundaries and preserve read position 2026-10-08 00:36:52 +02:00
evgeny 3ea830fa00 Verify signed reply references across two peers over real PUSH 2026-10-08 00:32:06 +02:00
evgeny ec0666ca2a Keep quote activation separate from text selection gestures 2026-10-08 00:31:27 +02:00
evgeny 484d6f2fa4 Test signed replies, media completion and old-history lookup 2026-10-08 00:31:13 +02:00
evgeny a9e407a39f Test reply draft acknowledgements and quote clicks across content types 2026-10-08 00:30:08 +02:00
evgeny c6390575ad Translate channel reply actions and unavailable-message states 2026-10-08 00:30:07 +02:00
evgeny 7773a0c648 Keep return-to-latest action available in historical message windows 2026-10-08 00:27:02 +02:00
evgeny 38257a7f54 Connect reply sending, quote resolution and history navigation 2026-10-08 00:27:02 +02:00
evgeny 49e898b6f3 Deliver channel submission acknowledgements to the desktop composer 2026-10-08 00:24:35 +02:00
evgeny 562901c84f Add reply composer with cancellation and correlated send results 2026-10-08 00:24:20 +02:00
evgeny eebef4d14f Render clickable compact replies for text and media messages 2026-10-08 00:23:16 +02:00
evgeny 68df26d4a5 Read reply references and bounded history around message IDs 2026-10-08 00:21:58 +02:00
evgeny e811ac395a Preserve channel reply context through attachment preparation 2026-10-08 00:20:48 +02:00
evgeny 68037b25a5 Store signed replies by message ID and confirm channel submissions 2026-10-08 00:20:48 +02:00
evgeny 99e9b1ae85 Define channel reply references and submission result events 2026-10-08 00:19:35 +02:00
evgeny efebf8c5e8 Fix settings drafts, enlarge avatars, and show desktop version 2026-10-08 00:04:41 +02:00
evgeny f408abcf4e android: move advanced settings into a developer tab 2026-10-07 23:50:36 +02:00
evgeny 3aa0cd4c06 tests: add standalone Merkle sync reconnect and triangle stress test 2026-10-07 23:50:02 +02:00
evgeny 34a151e4cb chatgui: translate PTT latch and release hints 2026-10-07 21:44:14 +02:00
evgeny e4a84fd997 chatgui: verify latched PTT ownership and channel reset 2026-10-07 21:44:13 +02:00
evgeny 37485820b5 chatgui: latch radio PTT on double click and release on next press 2026-10-07 21:42:40 +02:00
evgeny 653853588a logging: preserve system diagnostics and inherit Android debug level 2026-10-07 21:21:33 +02:00
evgeny 1bb885fd11 logging: explain failed probes and RTT persistence errors 2026-10-07 21:15:39 +02:00
evgeny 7eb6d6ee68 logging: move socket mechanics and periodic probe decisions to trace 2026-10-07 21:14:38 +02:00
evgeny 15d460ee7c logging: explain BGP readiness, route selection and peer removal 2026-10-07 21:13:53 +02:00
evgeny a74a62079e logging: include REALITY handshake stage in connection failures 2026-10-07 21:11:34 +02:00
evgeny 6d54a7c102 logging: distinguish STCP probes from data link lifecycle 2026-10-07 21:11:12 +02:00
evgeny adc8b2a670 logging: describe STCP state and endpoint on timeout and close 2026-10-07 21:10:29 +02:00
evgeny b16a4995b7 logging: quiet routine audio and route polling, report audio recovery 2026-10-07 21:09:46 +02:00
evgeny e85a8c8310 logging: scope member file polling and worker traces to media 2026-10-07 21:09:27 +02:00
evgeny f3b1300628 Record Android radio speaker rollout and headless build issue 2026-10-07 19:35:46 +02:00
evgeny 2c407540a0 Test Android radio talk lifecycle and translate speaker status 2026-10-07 19:33:53 +02:00
evgeny e95fc090df Render Android speaker waves independently of local radio mute 2026-10-07 19:33:53 +02:00
evgeny bcda9b82f4 Track Android radio speakers and actual transmission by group 2026-10-07 19:33:53 +02:00
evgeny d5174262d8 Show speaking waves for locally muted radio members 2026-10-07 19:16:15 +02:00
evgeny 61818572cc Verify speaker timeout and record completed radio indication 2026-10-07 19:10:49 +02:00
evgeny 6496448569 Verify radio speaker icons through the bridge and actual PTT/VAD 2026-10-07 19:03:48 +02:00
evgeny 70fd5c077f Translate radio speaker status tooltips 2026-10-07 19:01:53 +02:00
evgeny e61fc8e392 Show silent, speaking and muted radio speakers in member rows 2026-10-07 19:01:53 +02:00
evgeny 5cbab70035 Track radio speakers independently of member list refreshes 2026-10-07 19:01:13 +02:00
evgeny 376695e410 Avoid Windows poll macro collision in activity monitor 2026-10-07 18:35:11 +02:00
evgeny c3468987cb Handle poll questions rendered as a standalone emoji 2026-10-07 17:33:26 +02:00
evgeny b6d8e822b4 Verify poll clicks preserve selection and message controls 2026-10-07 17:18:40 +02:00
evgeny ddcc36aa26 Open desktop chat polls with a single left click 2026-10-07 17:11:43 +02:00
evgeny 7e88d15e1d Validate unmanaged notification overlay independently of EWMH 2026-10-07 16:58:40 +02:00
evgeny 4a7f37b920 Use independent X11 notification overlay across desktops 2026-10-07 16:57:01 +02:00
evgeny bd163fffff Reapply all-desktop scope after notification mapping 2026-10-07 16:50:12 +02:00
evgeny e96e6b7751 Make desktop scope test independent of current workspace changes 2026-10-07 16:43:52 +02:00
evgeny 0d618964a7 Test popup remapping and keep headless input tests isolated 2026-10-07 16:38:33 +02:00
evgeny d62fae9e88 Verify notification desktop scope with a real window manager 2026-10-07 16:38:33 +02:00
evgeny 247479e411 Show Linux message notifications on all X11 desktops 2026-10-07 16:35:29 +02:00
evgeny 1108c773dc Own the independent popup and handle all keyboard mouse input 2026-10-07 16:05:34 +02:00
evgeny bc42df12f5 Keep notification independent of main window minimization 2026-10-07 16:05:33 +02:00
evgeny 2caa653f02 Trace cancellation of pending desktop activity queries 2026-10-07 16:02:10 +02:00
evgeny 16a9094770 Test live message metadata and document notification limits 2026-10-07 16:01:22 +02:00
evgeny c6cb3f0e8e Test notification timing, replacement, and activity fallbacks 2026-10-07 15:58:10 +02:00
evgeny 1da667b09e Complete notification translation and activity watcher cleanup 2026-10-07 15:57:15 +02:00
evgeny f2e35da1fe Wire notification platform dependencies and event diagnostics 2026-10-07 15:55:39 +02:00
evgeny 33c98a014f Notify about live incoming channel and direct messages 2026-10-07 15:54:25 +02:00
evgeny 261198ab47 Expose initial synchronization in channel message events 2026-10-07 15:53:39 +02:00
evgeny 77f2cdc34c Add message popup that expires after first user activity 2026-10-07 15:52:54 +02:00
evgeny 40b8e1a740 Add desktop user activity monitoring with platform fallbacks 2026-10-07 15:51:43 +02:00
evgeny 3279aa86ce Fix Windows system DLL detection in chatgui deployment 2026-10-07 14:07:32 +02:00
evgeny e5d8c34514 Fix desktop uasync readiness for radio member mute requests 2026-10-07 13:49:06 +02:00
evgeny 7e19cbaf3a Add local per-member radio mute controls on desktop and Android 2026-10-07 13:35:25 +02:00
evgeny a069d11ef9 Protect REALITY STCP traffic with authenticated TLS-like records 2026-10-07 12:30:45 +02:00
evgeny a49cf5434e Add randomized ETCP padding and isolate STCP CTR streams per connection 2026-10-07 10:45:42 +02:00
evgeny 1f5b42845b Document horizontal pickers and the signed single-reaction rule 2026-10-07 08:24:25 +02:00
evgeny b1bf094bfd Verify signed multi-reaction rejection and document the single-choice rule 2026-10-07 08:17:52 +02:00
evgeny 066fa8552d Display the reaction picker as one horizontal row in both clients 2026-10-07 08:14:50 +02:00
evgeny 0f14abd6c8 Test single reaction replacement, cancellation and multi-emoji rejection 2026-10-07 08:12:32 +02:00
evgeny a444f28134 Enforce one reaction per author and replace the previous emoji atomically 2026-10-07 08:12:32 +02:00
evgeny 87501a24fe Document reactions inside the timestamp row and device validation state 2026-10-07 02:06:01 +02:00
evgeny 033c24881c Verify reaction rows remain inside bubbles and grow with live updates 2026-10-07 02:00:13 +02:00
evgeny 3dbbe02f69 Keep desktop reactions and timestamp inside the bubble bottom row 2026-10-07 01:53:18 +02:00
evgeny 1f4266f8b1 Move Android reactions inside bubbles beside the timestamp 2026-10-07 01:45:43 +02:00
evgeny 2cf0f9c928 Document transparent reaction controls and verified desktop interactions 2026-10-07 01:35:13 +02:00
evgeny aaa05e761e Check reaction clicks against downloadable media and portable preview paths 2026-10-07 01:30:04 +02:00
evgeny 4f0870b961 Verify transparent reaction footer clicks across text and media layouts 2026-10-07 01:28:56 +02:00
evgeny fc79fa7345 Keep message footer layout and click gesture states consistent 2026-10-07 01:27:04 +02:00
evgeny f71e5531e0 Open reaction picker from desktop heart and toggle existing reactions 2026-10-07 01:22:34 +02:00
evgeny 00ca006d26 Render and hit-test reaction footer for every desktop message type 2026-10-07 01:21:52 +02:00
evgeny 29ee55d751 Enlarge desktop chat and member avatars 2026-10-07 01:18:31 +02:00
evgeny aa58c7fd3d Place transparent outline reaction heart below Android message bubbles 2026-10-07 01:16:33 +02:00
evgeny f8d0acb38d Record root certificate repair and pending SM device verification 2026-10-07 00:56:41 +02:00
evgeny 0218ea9ddd Require channel signatures for root feedback authors and verify profile copies 2026-10-07 00:47:32 +02:00
evgeny 9299c95f10 Verify root formation and invitation certificate replication 2026-10-07 00:46:36 +02:00
evgeny a9a65d53bf Preserve invitation certificates and accept confirmed roots without profile changes 2026-10-07 00:44:50 +02:00
evgeny 7cddb47edc Record verified SM and local history repair 2026-10-07 00:33:52 +02:00
evgeny 8cd54f9802 Verify restored schema constraints and legacy index fixture 2026-10-07 00:27:35 +02:00
evgeny a014d518bf Test atomic history schema repair and preserved message data 2026-10-07 00:26:59 +02:00
evgeny 2f5a49b011 Repair existing chat history tables with stable message IDs 2026-10-07 00:25:11 +02:00
evgeny 918a3cc061 Merge chat polls and reactions into master 2026-10-06 23:08:07 +02:00
evgeny 1f026baf45 Show a circular crop preview in the desktop avatar editor 2026-10-06 23:05:21 +02:00
evgeny a8c81f7e2b Record verified poll implementation and completed checks 2026-10-06 22:56:31 +02:00
evgeny 255853dd76 Document complete poll protocol and verified recovery behavior 2026-10-06 22:47:06 +02:00
evgeny 587a980081 Document poll creation, voting and reaction commands 2026-10-06 22:43:13 +02:00
evgeny bbc8b7693a Restart member and feedback synchronization over existing transports 2026-10-06 22:40:47 +02:00
evgeny 9edc76e5eb Fix Android avatar crop layout and circular preview
Keep the crop actions inside the available dialog window, show a circular mask, and clamp gesture limits to prevent float rounding crashes.
2026-10-06 22:37:04 +02:00
evgeny 7f4a1a66fa Include transport state definitions in poll network regression 2026-10-06 22:33:52 +02:00
evgeny 0c16c2b09a Wait for authenticated bootstrap transport before opening group requests 2026-10-06 22:33:35 +02:00
evgeny 67d1718450 Measure proxy test durations with a monotonic clock 2026-10-06 22:31:53 +02:00
evgeny e86de44259 Test poll synchronization through three nodes and chat restart 2026-10-06 22:30:52 +02:00
evgeny 4966600d52 Localize desktop interaction errors 2026-10-06 22:21:13 +02:00
evgeny 6686dd1815 Translate desktop poll and reaction controls 2026-10-06 22:21:13 +02:00
evgeny 8dc7595e46 Preserve Android feedback state and supplementary Unicode characters 2026-10-06 22:15:44 +02:00
evgeny 1d05ff34c6 Connect Android poll screens and localize interaction controls 2026-10-06 22:15:04 +02:00
evgeny 44e0b7849e Add Android poll voting, results and message reaction components 2026-10-06 22:14:29 +02:00
evgeny 0a615913eb Drive Android feedback commands and live updates through the core thread 2026-10-06 21:45:59 +02:00
evgeny 8f8c8c267f Add typed poll and reaction states to Android message models 2026-10-06 21:45:59 +02:00
evgeny 2c31e3fe03 Expose UTF-8 feedback commands and full message identities on Android 2026-10-06 21:43:21 +02:00
evgeny 39e94fdfd3 Add message reaction menus and distinguish own reactions 2026-10-06 21:39:14 +02:00
evgeny cf869770fb Add desktop poll creation, voting and live result dialogs 2026-10-06 21:39:13 +02:00
evgeny b0933eb282 Keep avatar cache pruning tied to the originating Android account 2026-10-06 21:37:18 +02:00
evgeny ebc96712a2 Track feedback by message identity in desktop UI 2026-10-06 21:36:43 +02:00
evgeny c1048686d1 Route feedback state and command results to the desktop thread 2026-10-06 21:34:38 +02:00
evgeny 0a05a2095e Read stable message identities and decode escaped text in desktop 2026-10-06 21:34:38 +02:00
evgeny 52f4628fe0 Bound avatar manifest sizes before numeric conversion 2026-10-06 21:34:15 +02:00
evgeny e068fe5d3a Test poll codecs, voting rules and delayed source messages 2026-10-06 21:31:20 +02:00
evgeny da2c13dcf1 Add CLI poll creation, voting and emoji reaction commands 2026-10-06 21:26:37 +02:00
evgeny f99a8c1da2 Expose polls and reactions through headless control safely 2026-10-06 21:26:37 +02:00
evgeny ef476bf4e2 Use canonical reaction and answer payloads in feedback tests 2026-10-06 21:24:53 +02:00
evgeny 48d7f2b21b Build poll modules and adapt media insertion callbacks 2026-10-06 21:24:53 +02:00
evgeny 17444b8c8a Evaluate delayed answers after their source message arrives 2026-10-06 21:24:14 +02:00
evgeny 60b4870549 Pass stable identities to insertion callbacks and bound signed bodies 2026-10-06 21:24:13 +02:00
evgeny 865e4cda59 Log delayed answer validation and keep JSON percentages locale independent 2026-10-06 21:23:37 +02:00
evgeny e6db9fef60 Validate feedback data and provide poll command snapshots 2026-10-06 21:23:37 +02:00
evgeny 0af35630d1 Expose full message data and stable identities in listings 2026-10-06 21:17:24 +02:00
evgeny a4ca31f406 Return submitted message identity and encode JSON safely 2026-10-06 21:17:24 +02:00
evgeny d0aa60fe1a Define canonical poll, answer and reaction formats 2026-10-06 21:15:57 +02:00
evgeny d40f58d79a Add message identities and plan the remaining feedback work 2026-10-06 21:12:53 +02:00
evgeny 3e1cc8af85 Map avatar crop frames using full original dimensions on both clients 2026-10-06 21:10:24 +02:00
evgeny 8676893d59 Record feedback implementation milestone and remaining work 2026-10-06 20:17:48 +02:00
evgeny 04ff801531 Document Merkle v3 and signed feedback state contracts 2026-10-06 20:11:25 +02:00
evgeny 6437bf780b Include feedback source in the core library source list 2026-10-06 20:09:48 +02:00
evgeny 4f4e63bb0b Test feedback signatures, versions, cancellation and atomic pages 2026-10-06 20:08:53 +02:00
evgeny f6cc3b6e53 Make feedback roots depend on signed state and validate rebuilt keys 2026-10-06 20:07:44 +02:00
evgeny 982e5a06ab Integrate feedback tables with channel and sync lifecycle 2026-10-06 20:06:39 +02:00
evgeny 8121f93d2e Build feedback model and expose committed state events 2026-10-06 20:06:38 +02:00
evgeny 641a8318a8 Add signed versioned feedback storage and Merkle adapter 2026-10-06 20:05:18 +02:00
evgeny a0fbf51be0 Describe avatar object paths consistently 2026-10-06 20:04:40 +02:00
evgeny 17dd06221a Log Android gallery selections and finish image gestures safely 2026-10-06 20:04:40 +02:00
evgeny cdc5474279 Document avatar bundles, crop selection and original-image galleries 2026-10-06 20:02:45 +02:00
evgeny cee3228e5f Separate each Android gallery swipe from pinch and pan gestures 2026-10-06 20:02:45 +02:00
evgeny 864a250eea Preserve input cursor when member page outputs alias it 2026-10-06 20:02:41 +02:00
evgeny 3f4df4233b Keep member avatar drags separate from viewer clicks 2026-10-06 20:01:38 +02:00
evgeny 1f9dbd9dea Open desktop message avatars only after a completed click 2026-10-06 20:01:38 +02:00
evgeny 7d69404532 Verify five centered previews while navigating six avatar originals 2026-10-06 20:00:58 +02:00
evgeny 49dc837df2 Correct all member dataset arguments in callers 2026-10-06 19:59:43 +02:00
evgeny f0314ab51c Test dataset isolation and colliding cursor prefixes 2026-10-06 19:59:00 +02:00
evgeny 3be5cdd365 Show Android preview loading states and retry failed neighbors 2026-10-06 19:58:37 +02:00
evgeny 96af98f063 Exercise avatar crop gestures, gallery navigation and EXIF originals 2026-10-06 19:58:36 +02:00
evgeny f6bfd36a6c Include dataset identity in Merkle diagnostics 2026-10-06 19:57:58 +02:00
evgeny 1350c95db6 Identify desktop crop controls for interaction checks 2026-10-06 19:57:32 +02:00
evgeny bc7864466c Offer retries for desktop avatar catalog and image failures 2026-10-06 19:57:32 +02:00
evgeny fad96603a7 Display gallery loading and failure placeholders in the image viewer 2026-10-06 19:57:32 +02:00
evgeny 894b24698e Update Merkle integration tests and channel index cleanup 2026-10-06 19:57:25 +02:00
evgeny 5e146f20bc Adapt member sync and protocol tests to dataset cursors 2026-10-06 19:56:52 +02:00
evgeny 539bfd6db9 Extend Merkle sessions with datasets and full record cursors 2026-10-06 19:56:17 +02:00
evgeny c36fd3bfc8 Remove obsolete Android PNG user-avatar preparation 2026-10-06 19:55:20 +02:00
evgeny feccea21a5 Keep desktop crop workers owned until their dialog closes 2026-10-06 19:55:20 +02:00
evgeny 37b80b612b Restore Android gallery selections and validate editor source formats 2026-10-06 19:55:20 +02:00
evgeny 5b4b17ae49 Keep selected Android originals while the catalog finishes loading 2026-10-06 19:51:54 +02:00
evgeny 43c300cba3 Release Android crop sources rejected after account changes 2026-10-06 19:51:53 +02:00
evgeny cbe6da09ee Translate desktop avatar editor and gallery controls 2026-10-06 19:51:53 +02:00
evgeny 27fd9ee62d Share avatar metadata downloads and cancel closed desktop galleries 2026-10-06 19:48:24 +02:00
evgeny 6bd6ea540c Cancel avatar transfers after their last Android viewer closes 2026-10-06 19:48:24 +02:00
evgeny 7e5b60aa10 Include WebP runtime dependencies and avatar test painter 2026-10-06 19:48:24 +02:00
evgeny 7d71b08def Validate avatar object types and distinguish original downloads 2026-10-06 19:46:59 +02:00
evgeny a221452cb6 Correct Kotlin types in avatar catalog and crop coordinates 2026-10-06 19:46:59 +02:00
evgeny 863441c418 Test Android avatar catalog pagination and updated member snapshots 2026-10-06 19:45:19 +02:00
evgeny ab22277e7b Test avatar originals, selected lossy crops and storage limits 2026-10-06 19:44:56 +02:00
evgeny 32eaf9df04 Translate Android avatar crop and empty-gallery states 2026-10-06 19:43:51 +02:00
evgeny af6cd31e71 Open Android avatar galleries with originals and five previews 2026-10-06 19:43:27 +02:00
evgeny 87b8c6bd3a Use exact image coordinates in the desktop crop editor 2026-10-06 19:42:22 +02:00
evgeny 5f3725b72f Add Android avatar crop selection before publication 2026-10-06 19:42:22 +02:00
evgeny d1080129d2 Publish complete Android avatar bundles after crop confirmation 2026-10-06 19:41:02 +02:00
evgeny ea6b895df9 Load Android gallery objects and catalogs through the shared cache 2026-10-06 19:41:01 +02:00
evgeny f27419165d Preserve Android avatar originals and encode selected WebP crops 2026-10-06 19:38:40 +02:00
evgeny 6e9a50f2ec Bridge Android avatar catalogs and per-object transfer limits 2026-10-06 19:37:45 +02:00
evgeny 07aa436380 Decode avatar manifests and paginated catalogs on Android 2026-10-06 19:37:45 +02:00
evgeny c4c11ad813 Build avatar UI and deploy the desktop WebP codec 2026-10-06 19:36:35 +02:00
evgeny fd8054bc7b Open user avatar galleries from desktop member icons 2026-10-06 19:36:35 +02:00
evgeny 3a793e25b3 Open user avatar galleries from desktop messages 2026-10-06 19:36:34 +02:00
evgeny 9970b2235c Show desktop avatar originals with five centered previews 2026-10-06 19:36:00 +02:00
evgeny 4134200b4e Allow avatar galleries to display validated cached images 2026-10-06 19:35:59 +02:00
evgeny ccae89fb60 Add desktop square crop editor before avatar upload 2026-10-06 19:35:59 +02:00
evgeny 1a936f33ef Upload complete avatar bundles and page desktop galleries 2026-10-06 19:34:10 +02:00
evgeny 68fb29e385 Add avatar gallery sources and manifest-aware desktop cache 2026-10-06 19:33:24 +02:00
evgeny f0cc198f72 Prepare avatar originals, WebP crops and hashed manifests 2026-10-06 19:32:22 +02:00
evgeny 17491082d4 Validate signed profile avatar manifest references 2026-10-06 19:29:42 +02:00
evgeny c80e82d104 Define member avatar references as manifest objects 2026-10-06 19:29:37 +02:00
evgeny 50efddd5f0 Allow immutable avatar originals and manifests in storage 2026-10-06 19:29:17 +02:00
evgeny 40eafafe3d chatgui: verify drag highlight cleanup and attachment file picker reuse 2026-10-06 19:11:12 +02:00
evgeny 348cd855b9 chatgui: detach file drop callbacks before composer child destruction 2026-10-06 19:06:37 +02:00
evgeny a175129f93 chatgui: complete audio dependencies for composer integration tests 2026-10-06 19:04:31 +02:00
evgeny 9c7442544b chatgui: add Qt integration tests for file drops and recipient changes 2026-10-06 19:02:04 +02:00
evgeny cc8e47cfc8 chatgui: localize file drop hints and attachment rejection messages 2026-10-06 19:00:18 +02:00
evgeny 0279ed84a9 chatgui: stage dropped files through the attachment composer 2026-10-06 18:58:33 +02:00
evgeny 7b1143b9d4 android: hide download overlay on locally available photos 2026-10-06 17:57:36 +02:00
evgeny 27fb1426d9 android: cover decimal channel IDs from SM in avatar regression tests 2026-10-06 17:34:03 +02:00
evgeny eae3ef5c32 android: parse decimal channel IDs for avatar requests and group keys 2026-10-06 17:32:43 +02:00
evgeny b3c0a2fa7b android: harden avatar cache reads and refresh current download sources 2026-10-06 16:39:26 +02:00
evgeny 15dbb6fe51 android: test avatar event framing, limits and member updates 2026-10-06 16:32:36 +02:00
evgeny 9c70eab075 android: cancel avatar operations before native teardown and bound image preparation 2026-10-06 16:30:54 +02:00
evgeny 15d0772a20 android: preserve avatar references in JSON refresh and live member snapshots 2026-10-06 16:29:06 +02:00
evgeny 57888f8d9e android: display member avatars in conversations and member details 2026-10-06 16:27:35 +02:00
evgeny c77066162a android: show group icons and open properties from chat header 2026-10-06 16:25:51 +02:00
evgeny b9d41d179d android: add avatar editors and localized group properties dialog 2026-10-06 16:24:25 +02:00
evgeny 73019cfd83 android: add shared avatar components and guard picker account identity 2026-10-06 16:22:24 +02:00
evgeny 22aa5d47a5 android: load and cache user and signed group avatars asynchronously 2026-10-06 16:20:45 +02:00
evgeny 8efe075679 android: add asynchronous avatar JNI operations and wire models 2026-10-06 16:15:00 +02:00
evgeny 714370a1a2 Verify direct deployment after a failed Qt deployment tool 2026-10-06 15:53:03 +02:00
evgeny f316554ca3 Retain deployment diagnostics and model executable runtime imports 2026-10-06 15:48:10 +02:00
evgeny e7be94a3d2 Test Qt runtime deployment and missing plugin dependencies 2026-10-06 15:46:46 +02:00
evgeny c4217a8aa6 Verify runtime dependencies and deploy matching Qt plugins every build 2026-10-06 15:44:58 +02:00
evgeny 3d277349be Deploy the selected Qt version and recognize Windows system DLLs 2026-10-06 15:40:54 +02:00
evgeny c81e8e5025 Link Winsock after uasync and include socket headers first 2026-10-06 15:11:16 +02:00
evgeny e9e38ee029 Guard the file protocol against the Windows menu macro 2026-10-06 14:55:31 +02:00
evgeny ed3815a406 Use the portable remove command in the browser and storage tests 2026-10-06 14:54:23 +02:00
evgeny fe03feaf4a Avoid the Win32 MF_REMOVE macro in the file protocol 2026-10-06 14:54:23 +02:00
evgeny e8365fdec6 Upgrade group avatar columns before notifying database readiness 2026-10-06 14:04:49 +02:00
evgeny 5c772aeecf Preserve channel identity in member adapter fixtures 2026-10-06 14:04:49 +02:00
evgeny 2bcba58131 Reject group avatar commands outside a running chat service 2026-10-06 13:59:17 +02:00
evgeny b6a5f98d41 Document 16 KiB signed group avatars and 4 KiB wire blocks 2026-10-06 13:57:12 +02:00
evgeny d0173e1b73 Test atomic schema upgrades and report precise avatar validation errors 2026-10-06 13:57:12 +02:00
evgeny f8d95fcb25 Initialize avatar peers from the current group session epoch 2026-10-06 13:52:19 +02:00
evgeny 35ce2a2b93 Check group avatar encoding, rendering and key-based controls 2026-10-06 13:51:18 +02:00
evgeny 3b47a56076 Trace group avatar assembly resets and block mismatches 2026-10-06 13:49:54 +02:00
evgeny 46f4ce7091 Test real group avatar block delivery, permissions and recovery 2026-10-06 13:48:14 +02:00
evgeny 4062ecdfb1 Display group avatars and restrict editing to group key holders 2026-10-06 13:44:49 +02:00
evgeny 8e3ff6fd81 Deliver group avatar publication results through the GUI bridge 2026-10-06 13:44:49 +02:00
evgeny 2f9dd6a27b Decode and prepare bounded group avatar images outside the GUI thread 2026-10-06 13:43:49 +02:00
evgeny a816b842e5 Read group avatar metadata separately from image bytes 2026-10-06 13:43:49 +02:00
evgeny 56897368b3 Own group avatar synchronization in the chat service lifecycle 2026-10-06 13:41:55 +02:00
evgeny f67f090b2a Synchronize signed group avatars in bounded 4 KiB blocks 2026-10-06 13:41:29 +02:00
evgeny e3745f279e Store signed group avatars without replacing channel keys 2026-10-06 13:37:18 +02:00
evgeny 8173677475 Avoid refreshing avatar controls on unchanged member telemetry 2026-10-06 13:04:57 +02:00
evgeny 74bd4eed7c Keep outdated avatar transfers from delaying new references 2026-10-06 13:03:53 +02:00
evgeny ae461e746f Update Android consumers for extended member snapshots 2026-10-06 13:02:01 +02:00
evgeny 8f83167c0f Forward full join profiles and document canonical profile lengths 2026-10-06 12:58:00 +02:00
evgeny 6d3187db57 Keep profile controls consistent with the saved avatar server 2026-10-06 12:56:20 +02:00
evgeny 5276cd0fa0 Simplify profile updates and report database read failures 2026-10-06 12:54:44 +02:00
evgeny 80c81502fb Protect avatar paths across platforms and initialize join fixture nodes 2026-10-06 12:53:05 +02:00
evgeny 9bfdf3ea41 Allow avatar replacement alongside downloads and correct join fixtures 2026-10-06 12:51:22 +02:00
evgeny dc156c7f2b Test bounded downloads and late completion after avatar replacement 2026-10-06 12:49:26 +02:00
evgeny d946a1b93a Finalize unsigned initial profiles before channel bootstrap 2026-10-06 12:48:20 +02:00
evgeny cebd138190 Expose profile diagnostics in verbose join integration runs 2026-10-06 12:47:20 +02:00
evgeny 8b327a3f27 Bound upload cache growth and reject stale transfer completions 2026-10-06 12:46:32 +02:00
evgeny 972910c763 Verify profile signatures survive avatar and display name updates 2026-10-06 12:45:45 +02:00
evgeny 9842de7ab7 Document avatar storage, profile exchange and client caching 2026-10-06 12:45:13 +02:00
evgeny c04f51d424 Use bounded 16-bit profile lengths in join member records 2026-10-06 12:44:18 +02:00
evgeny 76c02012eb Preserve complete signed profiles in channel bootstrap messages 2026-10-06 12:44:18 +02:00
evgeny c0a4bc7d89 Persist the desired avatar reference atomically 2026-10-06 12:43:11 +02:00
evgeny 9279ded7f3 Read complete signed profiles and escape initial display names 2026-10-06 12:43:11 +02:00
evgeny 3e1641aaf0 Persist avatar removal intent and prioritize profile changes 2026-10-06 12:41:05 +02:00
evgeny f2d2e6be8f Test avatar immutability, hash validation and quota accounting 2026-10-06 12:40:07 +02:00
evgeny 405755f06a Reuse shared channel avatars in direct message conversations 2026-10-06 12:40:07 +02:00
evgeny b8ddf0f52d Translate avatar profile controls and operation statuses 2026-10-06 12:38:51 +02:00
evgeny e642551dfe Expose signed storage roles independently of route priority 2026-10-06 12:38:11 +02:00
evgeny 2fe5f34c5c Trace avatar discovery and profile events for integration checks 2026-10-06 12:37:25 +02:00
evgeny 8fb9129300 Verify large signed profiles alongside full owner records 2026-10-06 12:36:35 +02:00
evgeny eceb69c58e Test avatar publication, channel replication and persistent cache 2026-10-06 12:35:46 +02:00
evgeny 7582509792 Protect pending avatar uploads while bounding disk cache retention 2026-10-06 12:35:45 +02:00
evgeny 1988afa84d Notify profile changes with actual member properties 2026-10-06 12:33:26 +02:00
evgeny 247e3a2efe Add avatar upload, preview and removal to profile settings 2026-10-06 12:33:00 +02:00
evgeny 694ed8e263 Display cached user avatars in member lists and chat messages 2026-10-06 12:32:33 +02:00
evgeny 78bed5efe4 Route avatar events alongside file browser events in ChatGUI 2026-10-06 12:32:02 +02:00
evgeny 8ae45e9e39 Manage avatar replication, background downloads and shared cache 2026-10-06 12:31:16 +02:00
evgeny f91251be14 Add bounded persistent avatar cache and image normalization 2026-10-06 12:28:59 +02:00
evgeny e8a3d5fa16 Bound avatar transfers and enforce immutable hashed uploads 2026-10-06 12:27:32 +02:00
evgeny cbb1a03ccd Define canonical avatar names and size limits 2026-10-06 12:27:32 +02:00
evgeny 61ef23b885 Include avatar references in member snapshots and change events 2026-10-06 12:27:07 +02:00
evgeny 4fbb1dafe1 Preserve profile attributes and publish signed avatar references 2026-10-06 12:26:38 +02:00
evgeny 8029b6e3f5 Extend signed member profiles to 1024 bytes 2026-10-06 12:25:21 +02:00
evgeny 02bd10b005 Document incremental object counts and quota protocol version 4 2026-10-06 11:36:30 +02:00
evgeny 9a06d19984 Test incremental counts, empty replacements and startup-only scans 2026-10-06 11:33:57 +02:00
evgeny e6bf925a94 Translate file and directory statistics in the quota display 2026-10-06 11:29:43 +02:00
evgeny 64d46cdef0 Display file and directory totals in client quota snapshots 2026-10-06 11:29:43 +02:00
evgeny d1034cb4ea Maintain file and directory statistics incrementally per user 2026-10-06 11:25:18 +02:00
evgeny c8236373b1 Extend quota snapshots with file and directory counts 2026-10-06 11:25:18 +02:00
evgeny e76dc6cb82 Report actual directory creation separately from durability errors 2026-10-06 11:25:18 +02:00
evgeny 69f04dec87 Document per-user quotas and the client quota query 2026-10-06 00:29:28 +02:00
evgeny 092e3e3cdc Validate quota scan failures and deny unauthorized quota queries 2026-10-06 00:29:28 +02:00
evgeny fb9e7c3351 Translate quota display and reservation details 2026-10-06 00:25:07 +02:00
evgeny db6acf8662 Test quota display, stale responses and startup directory retries 2026-10-06 00:25:07 +02:00
evgeny 594017ba5f Test user quota isolation, reservations, restarts and wire queries 2026-10-06 00:22:17 +02:00
evgeny 4fdecb697b Show live user storage quota in the member file browser 2026-10-06 00:18:00 +02:00
evgeny 359a327ec4 Enforce per-user reservations and initialize storage at startup 2026-10-06 00:16:20 +02:00
evgeny 3b641a2423 Add the file quota query and per-user quota configuration 2026-10-06 00:14:03 +02:00
evgeny 3354c0b374 Initialize per-user file quota accounts from the storage tree 2026-10-06 00:11:21 +02:00
evgeny 25e2ffeea7 Stream member files with router backpressure and parallel transfer handles 2026-10-05 23:36:49 +02:00
evgeny f7e1752fc2 Document focused member file debug tracing 2026-10-05 21:52:12 +02:00
evgeny 977748b16c Expose fork ownership errors in file debug traces 2026-10-05 21:43:44 +02:00
evgeny c259a7330b Trace file router sessions and crypto decisions in debug 2026-10-05 21:41:46 +02:00
evgeny 0706ccce8a Trace browser state, transfer I/O and daemon PID 2026-10-05 21:36:43 +02:00
evgeny aca10e8268 Trace file bridge callbacks and use scoped VAD test logs 2026-10-05 21:34:39 +02:00
evgeny e913a27f0c Trace file storage and worker lifecycle in debug 2026-10-05 21:33:04 +02:00
evgeny 8982bb034e Move legacy sound and attachment errors to media 2026-10-05 21:30:37 +02:00
evgeny 0773a82342 Keep debug focused on file browser event delivery 2026-10-05 21:30:01 +02:00
evgeny b771e95997 Clear legacy GUI bridge and list messages from debug 2026-10-05 21:28:45 +02:00
evgeny 5b78ec04f6 Move old radio and attachment diagnostics out of debug 2026-10-05 21:26:09 +02:00
evgeny 5dea9394bf Trace member file requests in debug category 2026-10-05 21:25:02 +02:00
evgeny 5d1b8d26e7 Daemonize before creating core workers 2026-10-05 21:19:19 +02:00
evgeny 78a655bb05 Add asynchronous member file storage and desktop browser 2026-10-05 18:44:58 +02:00
evgeny 27d93ecaf9 Add English and Russian ChatGUI interface translations 2026-10-05 16:55:43 +02:00
evgeny 972c6b7f0a Raise rlottie shape budget and log truncated animations 2026-10-05 16:48:00 +02:00
evgeny 7423133229 Build bundled rlottie automatically before Windows ChatGUI 2026-10-05 14:41:03 +02:00
evgeny 5e309c27ca Vendor rlottie sources with CMake and MinGW build adjustments 2026-10-05 14:31:35 +02:00
evgeny 8feb7902b9 Search local rlottie build for its CMake package configuration 2026-10-05 14:15:25 +02:00
evgeny bba44fbe11 Always copy rlottie DLL during ChatGUI runtime deployment 2026-10-05 13:55:43 +02:00
evgeny bc6d372064 Treat imagehlp and bcryptprimitives as Windows system DLLs 2026-10-05 13:43:57 +02:00
evgeny e4c591e1e6 Disable ZXing size optimization for MinGW GCC string link issue 2026-10-05 13:38:16 +02:00
evgeny 05064f7f3d Refresh compiler runtime DLLs before Windows ChatGUI builds 2026-10-05 13:22:16 +02:00
evgeny 3cfa9f8da1 Compare cc1 startup across directories and inspect runtime DLLs 2026-10-05 13:12:11 +02:00
evgeny 586f62fd0a Diagnose Opus bands compilation and replay native Ninja command 2026-10-05 13:07:55 +02:00
evgeny 2c5293bff0 Extend compiler diagnostics with standalone and CMake Opus checks 2026-10-05 13:02:08 +02:00
evgeny a154bacffb Add MSYS2 UCRT64 compiler diagnostic scripts 2026-10-05 12:58:40 +02:00
evgeny 9aaac3cb20 Exclude POSIX tcp_io test scenarios from Windows compilation 2026-10-05 12:27:15 +02:00
evgeny 577d1bd72f Handle delayed TCP delivery in etcpmon regression checks 2026-10-05 12:22:39 +02:00
evgeny d62916edf0 Validate etcpmon response strings and close failed streams 2026-10-05 12:20:25 +02:00
evgeny 45efaf095d Add Windows regression coverage for etcpmon refresh lifecycle 2026-10-05 12:14:04 +02:00
evgeny 24906283fe Refresh etcpmon connections and preserve selection by peer ID 2026-10-05 12:11:43 +02:00
evgeny 9465ed4939 Fix etcpmon response caches and pending request lifecycle 2026-10-05 12:08:05 +02:00
evgeny 0dbdd47130 Fix Windows IPv6 socket option and const link address 2026-10-05 12:00:10 +02:00
evgeny 341da73398 Fix Windows getsockopt argument in TCP error callback 2026-10-05 11:46:54 +02:00
evgeny ba7146f90a Fix HTTP proxy framing, keep-alive and stream lifecycle 2026-10-05 09:27:23 +02:00
evgeny 9005c67b8b Skip Windows DLL deployment when runtimes are present 2026-10-05 09:27:11 +02:00
evgeny 0b72364f10 Recognize DirectWrite and spooler system imports 2026-10-04 23:00:23 +02:00
evgeny be9a953136 Read Windows DLL import dumps through a temporary file 2026-10-04 22:49:17 +02:00
evgeny 6e4b1ae9c4 Trace Windows import parsing and dependency resolution 2026-10-04 22:41:08 +02:00
evgeny ba2bafe56e Diagnose Windows DLL scanning and recognize system imports 2026-10-04 22:25:11 +02:00
evgeny d02c55dbcd Document web link handling, validation and Q8 verification 2026-10-04 19:18:00 +02:00
evgeny 4fcf45d545 Accept browser URL paths on Android and drag within the tested link 2026-10-04 19:12:18 +02:00
evgeny ec856cc73f Verify web link geometry, full addresses, mixed schemes and drag copying 2026-10-04 19:08:39 +02:00
evgeny 4ce7e29a37 Share URL boundary fixtures between Android and Qt tests 2026-10-04 19:08:39 +02:00
evgeny dc2b6c3d68 Keep invitation and gesture regressions on the generic link API 2026-10-04 19:07:11 +02:00
evgeny 2e41610693 Open Android web links from shared channel and PM message UI 2026-10-04 19:06:44 +02:00
evgeny e2472cb5c3 Localize failures opening external message links 2026-10-04 19:06:44 +02:00
evgeny c191b777b7 Annotate Android web links without altering addresses containing emoji 2026-10-04 19:06:44 +02:00
evgeny 0a060d04ca Parse Android message links with matching punctuation rules 2026-10-04 19:05:09 +02:00
evgeny a76ba87501 Open HTTP and HTTPS messages through the desktop URL handler 2026-10-04 19:05:08 +02:00
evgeny 01fa1bc552 Route message link activation through the Qt conversation view 2026-10-04 19:05:08 +02:00
evgeny 5b321c6304 Generalize Qt link gestures while preserving drag selection 2026-10-04 19:04:11 +02:00
evgeny 3482e4a327 Share Qt text layout for web links and invitations 2026-10-04 19:04:11 +02:00
evgeny 394ac93dc4 Extract message links with explicit schemes and punctuation boundaries 2026-10-04 19:03:11 +02:00
evgeny 93c0c5bdfc Show channel member inviter in Android details 2026-10-04 18:34:17 +02:00
evgeny b6fd489b8e Record compact invitation validation and Q8 installation 2026-10-04 18:21:08 +02:00
evgeny 35266d2a31 Verify copy through Qt keyboard action and log Android invite presentation 2026-10-04 18:12:58 +02:00
evgeny d14babea33 Create signed test channel with its final invitation title 2026-10-04 18:12:23 +02:00
evgeny af27eaa33b Size invitation JSON buffers to the escaping contract 2026-10-04 18:11:54 +02:00
evgeny ec7ec2a229 Document localized invitation titles and compact link presentation 2026-10-04 18:11:08 +02:00
evgeny f70d5c7460 Verify invitation title delivery and one-line full-link actions 2026-10-04 18:09:41 +02:00
evgeny 6ad89a4f6a Elide Qt invitation links with consistent layout and full-address selection 2026-10-04 18:08:43 +02:00
evgeny 12e09cb0ed Render one-line Android invitations and retain full links for copy 2026-10-04 18:07:46 +02:00
evgeny 0ce6e9e83a Expose invitation group tags and localized Android titles 2026-10-04 18:06:55 +02:00
evgeny 306aaa94ba Carry encrypted group name metadata in PM invitations 2026-10-04 18:06:26 +02:00
evgeny be65962e96 Record final history recovery validation 2026-10-04 17:57:04 +02:00
evgeny a6aea51ff3 Document invitation history recovery and device verification 2026-10-04 17:46:20 +02:00
evgeny 607bd5d475 Report history wait reasons and registration failures precisely 2026-10-04 17:20:07 +02:00
evgeny 387f95208d Keep message content type alive through JSON serialization 2026-10-04 17:16:58 +02:00
evgeny fb92f1a8c9 Verify history recovery when a group session loses readiness 2026-10-04 17:10:49 +02:00
evgeny 4cd52c27d5 Include transport definition in history fault injection test 2026-10-04 16:57:06 +02:00
evgeny a5c616ba0f Exercise history retry after a lost initial exchange 2026-10-04 16:56:24 +02:00
evgeny 65264137f8 Apply JOIN metadata through the transactional member model 2026-10-04 16:55:00 +02:00
evgeny b390f3c28f Cover pre-invitation history with both synchronization roles 2026-10-04 16:48:01 +02:00
evgeny 274f4f2d63 Prepare explicit group sessions in history protocol fixtures 2026-10-04 16:47:01 +02:00
evgeny eaac611465 Gate history on member synchronization and group readiness 2026-10-04 16:45:47 +02:00
evgeny 6a67cc2038 Prepare channel history before enabling network synchronization 2026-10-04 16:40:32 +02:00
evgeny 865657b3bd Expose group peer readiness transitions to subscribers 2026-10-04 16:39:19 +02:00
evgeny 5caa1131c8 Test Android member radio flags and update preservation 2026-10-04 16:10:41 +02:00
evgeny cf77fffaeb Keep the stroke import used by radio controls 2026-10-04 16:07:08 +02:00
evgeny 51467296c3 Test radio member snapshots, updates and route withdrawal 2026-10-04 16:05:40 +02:00
evgeny 334521ce3f Display active radios in Android member rows 2026-10-04 16:02:57 +02:00
evgeny 47082c4e2e Preserve Android radio presence across member refreshes 2026-10-04 16:02:57 +02:00
evgeny 658c248867 Request Android member snapshots on the core thread 2026-10-04 16:00:41 +02:00
evgeny 6c5c1ef0a0 Show enabled radios in desktop member rows 2026-10-04 15:59:45 +02:00
evgeny 0f9210e491 Share the desktop portable radio icon painter 2026-10-04 15:59:45 +02:00
evgeny 9dd2dffc80 Expose live radio subscriptions in member updates 2026-10-04 15:58:54 +02:00
evgeny 81d71690c2 Document PM invitation flow, lifetime and verification 2026-10-04 15:55:42 +02:00
evgeny 38c4c0772b Use explicit cursor index type for Qt 5 and Qt 6 2026-10-04 15:54:06 +02:00
evgeny 69442dcbb8 Start wrapped-link selection test inside the first link line 2026-10-04 15:52:18 +02:00
evgeny 17179c9c4a Use invite text layout for selection and wrapped emoji positions 2026-10-04 15:51:51 +02:00
evgeny 0e5778c013 Reuse PM automatic route resolution and check persistence after successful send 2026-10-04 15:50:38 +02:00
evgeny f02894f164 Release pending invitation dialogs when Android chat stops 2026-10-04 15:46:35 +02:00
evgeny 2afb7268a1 Restore PM group route and verify messaging after invite JOIN 2026-10-04 15:46:14 +02:00
evgeny efcc9a1761 Schedule member sync when joining over an existing transport 2026-10-04 15:43:15 +02:00
evgeny c800d9253d Trace group connection and membership after opening a received invitation 2026-10-04 15:39:06 +02:00
evgeny 92b41ec300 Test wrapped message links and per-member invitation button hit targets 2026-10-04 15:38:10 +02:00
evgeny 8e25ffb392 Wait for member replication after JOIN and include desktop diagnostics 2026-10-04 15:24:06 +02:00
evgeny 570e637795 Verify desktop invitation clicks preserve drag selection 2026-10-04 15:12:26 +02:00
evgeny b9af72e71d Test PM invitation cancellation, commit errors and joining another group 2026-10-04 15:12:26 +02:00
evgeny f07a5b7a83 Keep invite hit testing in the message presenter and show link cursor 2026-10-04 15:10:02 +02:00
evgeny 149c397b7b Include desktop diagnostic macros for invitation actions 2026-10-04 15:08:16 +02:00
evgeny 3c3f3896f7 Handle message invitation links in Android channel and PM screens 2026-10-04 15:04:14 +02:00
evgeny 68103d0aca Make invite links clickable in Android text with inline animated emoji 2026-10-04 15:03:54 +02:00
evgeny 442a2a7b04 Complete Android invitation dialog imports and show share link lifetime 2026-10-04 15:03:53 +02:00
evgeny d13663eb23 Add Android PM and member invitation buttons with group picker 2026-10-04 15:02:42 +02:00
evgeny e5a87bd043 Track Android PM invitations and joining from message links in ViewModel 2026-10-04 15:02:27 +02:00
evgeny f841c8eb8a Localize PM invitation actions, lifetime and error messages 2026-10-04 15:00:45 +02:00
evgeny 81f8f6d4d0 Use asynchronous post API for invitation requests 2026-10-04 15:00:44 +02:00
evgeny 9bdfb93b50 Expose PM invitation operations and membership checks through JNI 2026-10-04 14:59:08 +02:00
evgeny ef62a80eb8 Filter desktop invitation targets by local membership 2026-10-04 14:59:08 +02:00
evgeny 55d9e79983 Expose read-only channel membership for invitation UI 2026-10-04 14:59:07 +02:00
evgeny fb21f9c1af Show invitation lifetime and correlate desktop join results by group 2026-10-04 14:57:12 +02:00
evgeny d369ff968d Wire desktop invitation sending and joining from message links 2026-10-04 14:57:12 +02:00
evgeny b7821121c3 Activate desktop invite links on clicks while preserving drag selection 2026-10-04 14:56:11 +02:00
evgeny 56fe1cab95 Render and hit-test wrapped invitation links in desktop messages 2026-10-04 14:56:10 +02:00
evgeny 01aa692a56 Add group invitation button to desktop PM header 2026-10-04 14:54:59 +02:00
evgeny 44e75e661e Forward member invitation actions to main window 2026-10-04 14:54:58 +02:00
evgeny fe23f4ca56 Add invitation action to desktop member rows 2026-10-04 14:54:58 +02:00
evgeny 6bc6d28e9e Add desktop group picker for PM invitations with ten minute notice 2026-10-04 14:53:59 +02:00
evgeny fb68e66ca4 Dispatch PM invitation results to desktop UI 2026-10-04 14:53:58 +02:00
evgeny 6f99ad3103 Add cancellable PM invitation operation with correlated results 2026-10-04 14:51:33 +02:00
evgeny 123b47c612 Improve Android VAD lifecycle, audio settings and diagnostics 2026-10-04 14:36:52 +02:00
evgeny 20a1211e09 Add GPLv3 licensing and bundle third-party notices 2026-10-04 14:18:18 +02:00
evgeny a97e8bcc1f docs: update file inventory and module header contracts
Refresh both AGENTS guides and clarify module purpose, usage, ownership, threading, return values and wire formats in headers. Remove the unused timeout_heap freed_count field and unimplemented getter.
2026-10-04 13:10:01 +02:00
evgeny c19eff67f6 android chat: refine message bubble colors and shadows 2026-10-03 14:22:21 +03:00
evgeny 5c74b7cd3e android radio: add plus and minus to volume PTT icon 2026-10-03 14:21:20 +03:00
evgeny 8358210241 docs: record Android volume PTT panel verification 2026-10-03 14:13:48 +03:00
evgeny de54c2d059 android radio: add volume PTT toggle to radio panel 2026-10-03 14:08:17 +03:00
evgeny 40e6bd36c3 android radio: release volume PTT on focus loss and option disable 2026-10-03 14:08:17 +03:00
evgeny 1805b3dd27 android radio: track independent manual PTT owners 2026-10-03 14:08:17 +03:00
evgeny 163621ddfa Document verified desktop radio panel and VAD controls 2026-10-03 13:34:50 +03:00
evgeny c1bec9aca5 Wait for queued radio packets and verify persistent VAD settings 2026-10-03 13:29:33 +03:00
evgeny 33f276a69e Cover radio panel shutdown and microphone recovery states 2026-10-03 13:19:45 +03:00
evgeny a2bafb61c7 Own pending radio settings in cancellable uasync tasks 2026-10-03 13:18:09 +03:00
evgeny 312c2dd386 Test desktop VAD panel through Qt buttons and real audio workers 2026-10-03 13:16:15 +03:00
evgeny 8820e9a3c9 Build desktop radio panel and retain audio recovery coverage 2026-10-03 13:13:28 +03:00
evgeny 625846fa33 Apply shared audio changes without resetting saved preferences 2026-10-03 13:13:28 +03:00
evgeny afbce0efeb Load radio settings after core startup and apply from dialog 2026-10-03 13:12:17 +03:00
evgeny 9f41415dc7 Expose radio VAD thresholds and independent compressor gains 2026-10-03 13:11:54 +03:00
evgeny d1e2220e54 Place radio controls above desktop chat messages 2026-10-03 13:10:38 +03:00
evgeny fa70093866 Add desktop radio panel with device menus and VAD switch 2026-10-03 13:09:38 +03:00
evgeny dcb07c9124 Add live desktop radio VAD settings and transmission state 2026-10-03 13:08:04 +03:00
evgeny ffd26c56b4 Reduce radio VAD prefetch to 600 milliseconds 2026-10-03 11:32:34 +03:00
evgeny 6985a2cf18 Document Android radio toolbar VAD control 2026-10-03 11:28:23 +03:00
evgeny 9ff1f5645f Move Android VAD activation to the radio toolbar 2026-10-03 11:18:04 +03:00
evgeny 099c9b7eac Record successful VAD validation and full test results 2026-10-03 01:53:03 +03:00
evgeny 13b5136b92 Document radio VAD compressor behavior and validation 2026-10-03 01:50:31 +03:00
evgeny c9131fff39 Ship deterministic speech fixture for VAD regressions 2026-10-03 01:48:37 +03:00
evgeny 50e276cb94 Include previous audio context in streaming Silero inference 2026-10-03 01:48:37 +03:00
evgeny 9c8f635e63 Test VAD compression, quiet speech and independent prefetch audio 2026-10-03 01:34:27 +03:00
evgeny cd57597975 Expose radio VAD gain limit in Android settings 2026-10-03 01:30:31 +03:00
evgeny cbee3da2c9 Add independent compressor branch before radio VAD 2026-10-03 01:30:31 +03:00
evgeny 389fca1de0 Expose compressor gain and register separate radio VAD limit 2026-10-03 01:27:42 +03:00
evgeny 1e9264735b Fix desktop hover and emoji playback; share Android chat background 2026-10-02 23:19:13 +03:00
evgeny 35648a19ce Avoid Windows poll macro collision in PTT hotkey manager 2026-10-02 17:32:46 +03:00
evgeny 23368a5bbe Add Android boot startup, lazy settings, emoji and DM calls
Add battery optimization controls and configurable boot startup. Keep message sounds in the application and render general settings lazily to avoid ANRs. Add animated emoji controls and live BGP availability for direct-message calls on Android and desktop.
2026-10-02 17:18:24 +03:00
evgeny 66afcc8625 Add extensible Russian and English interface languages on Android 2026-10-02 16:05:33 +03:00
evgeny df346fc097 Log standby retry transitions and await the actual wake phase 2026-10-02 12:04:34 +03:00
evgeny 7ec5f931bb Defer automatic retries until Android standby wakes 2026-10-02 11:56:44 +03:00
evgeny f12bab8676 Include concrete transport definitions for UP checks 2026-10-02 11:52:36 +03:00
evgeny f4b273d0be Cover UP-based chat peer selection and fallback transitions 2026-10-02 11:50:28 +03:00
evgeny 1d189195f9 Advertise desktop clients and log unknown automatic candidates 2026-10-02 11:47:24 +03:00
evgeny dc34faaada Route chat retries through automatic peer selection 2026-10-02 11:44:46 +03:00
evgeny a7632cfee1 Select chat auto peers by device class and transport UP 2026-10-02 11:43:10 +03:00
evgeny 1c499dc230 Classify chat supernodes independently of address type 2026-10-02 11:37:09 +03:00
evgeny 01bc6e4cf0 Опиши контракты аудиотракта и результаты проверок 2026-10-02 10:22:47 +03:00
evgeny bd082a26bf Раздели аудиопотоки и привяжи AEC к фактическому общему выходу 2026-10-02 10:22:34 +03:00
evgeny d7fb0c83f9 Fix bounds and unsigned decoding in ETCP packet dumps 2026-10-02 02:52:32 +03:00
evgeny ac45554439 Fix ETCP INIT collision scope and backpressure regression tests 2026-10-02 02:25:40 +03:00
evgeny 217e5795bf Раздели PCM-сессии и исправь частоту и перемотку звука 2026-10-02 02:00:50 +03:00
evgeny 40b974b29c Исправь потоковый AGC и применяй mute после AEC 2026-10-02 01:56:10 +03:00
evgeny bdcf8b5634 Сохрани накопленные исправления аудио, async и диагностики 2026-10-02 01:49:32 +03:00
evgeny b1e3193899 Document typed PM media, shared preparation and completed validation 2026-10-01 19:22:06 +03:00
evgeny 7ae56f03b3 Keep Android video metadata truthful and remove obsolete composer imports 2026-10-01 19:16:27 +03:00
evgeny 203ae61eda Share bounded thumbnail workers and keep PM downloads in DM transport 2026-10-01 19:12:25 +03:00
evgeny c863b80917 Validate Android voice streams and keep playback cancellation off UI waits 2026-10-01 19:09:07 +03:00
evgeny dd8041b30f Retain picker cache ownership on native PM rejection 2026-10-01 19:06:19 +03:00
evgeny fe9441d63b Acknowledge Android PM file ownership only after native enqueue 2026-10-01 19:06:18 +03:00
evgeny 05476fb102 Test Qt voice file validation and decoder cancellation 2026-10-01 18:56:57 +03:00
evgeny 6ccf71b20d Exercise separate conversations and codec format boundaries 2026-10-01 18:56:57 +03:00
evgeny 418329962d Test pinned preparation targets, worker ownership and typed offline media 2026-10-01 18:55:34 +03:00
evgeny f2ff3ee5ab Report preparation rejection and release abandoned owned sources 2026-10-01 18:53:08 +03:00
evgeny 3b0609fe76 Reject corrupt voice containers and remove unused synchronous playback 2026-10-01 18:51:44 +03:00
evgeny 8e108425ae Track Android video probing and clean rejected cache files 2026-10-01 18:50:32 +03:00
evgeny 20322e7ecb Show PM voice and video delivery status in both shared renderers 2026-10-01 18:49:59 +03:00
evgeny 5092bdf9fe Validate capture format and diagnose capture thread failures 2026-10-01 18:48:57 +03:00
evgeny be8dbcf25e Remove duplicate GUI voice encoding and keep shared worker codec 2026-10-01 18:48:09 +03:00
evgeny 72233d2365 Quiesce Android capture before detaching PCM and consume PM cache files 2026-10-01 18:41:17 +03:00
evgeny 79f0f2a07f Keep Android media preparation status scoped to its conversation 2026-10-01 18:41:17 +03:00
evgeny 225e11265f Connect Qt preparation notifications to the conversation view 2026-10-01 18:39:31 +03:00
evgeny 6da6471204 Apply task-scoped media preparation events to the shared Qt composer 2026-10-01 18:39:31 +03:00
evgeny e4a57970f0 Show preparation state for the current Qt conversation only 2026-10-01 18:39:31 +03:00
evgeny 55b21fdcda Forward pinned attachment preparation events to Qt 2026-10-01 18:37:49 +03:00
evgeny 51a820ac82 Track PM encryption preparation and clean cancelled source files 2026-10-01 18:37:23 +03:00
evgeny c900adbc69 Report pinned preparation results and clean abandoned media outputs 2026-10-01 18:36:59 +03:00
evgeny b1ee7857f7 Identify media preparation events by conversation and task UUID 2026-10-01 18:36:59 +03:00
evgeny 318d72708c Preserve voice autoplay ordering while decoding in a worker 2026-10-01 18:29:43 +03:00
evgeny 172cc3a649 Update shared Qt playback controls after background decoding 2026-10-01 18:29:07 +03:00
evgeny d4a27d1975 Decode voice files asynchronously before starting Qt playback 2026-10-01 18:27:44 +03:00
evgeny c77cb9bb0a Adopt decoded voice buffers without large copies on the Qt thread 2026-10-01 18:27:44 +03:00
evgeny add016894b Use bounded independent Android cache files for each attachment 2026-10-01 18:24:12 +03:00
evgeny fd3fc971fa Return Android video recording to its original channel or PM 2026-10-01 18:19:37 +03:00
evgeny fef800ef7a Use the same Android media interface for channels and PM 2026-10-01 18:19:12 +03:00
evgeny 1cd366c0e1 Share Android conversation composition attachments and message lists 2026-10-01 18:16:23 +03:00
evgeny dbec6e75e8 Pin Android recording targets and stop capture outside the main thread 2026-10-01 18:13:35 +03:00
evgeny 80b4e6f3e9 Normalize PM voice and video into the Android message model 2026-10-01 18:12:14 +03:00
evgeny 6185869c6d Use fixed conversation targets for Android media preparation 2026-10-01 18:11:54 +03:00
evgeny 038fd173c2 Detach Android recordings for shared background preparation 2026-10-01 18:01:45 +03:00
evgeny 14935378bb Check queued worker completion cancellation and attachment codec tests 2026-10-01 18:00:02 +03:00
evgeny 0dc2b0135a Test typed PM delivery and strict voice attachment metadata 2026-10-01 17:59:16 +03:00
evgeny c58b92ecd7 Render typed PM media using shared Qt message roles 2026-10-01 17:57:43 +03:00
evgeny a281165450 Keep channel download commands out of the shared message delegate 2026-10-01 17:56:29 +03:00
evgeny 4f76b4d765 Dispatch Qt attachments using the target captured by the composer 2026-10-01 17:54:48 +03:00
evgeny d1f03f2f80 Enable shared Qt voice and video composition for PM 2026-10-01 17:53:41 +03:00
evgeny 0d3f2952c1 Move Qt recording buffers to background preparation without copies 2026-10-01 17:52:38 +03:00
evgeny ba409427c6 Bound media worker concurrency and reject incomplete file copies 2026-10-01 17:51:09 +03:00
evgeny 1b3794e9f9 Include common attachment preparation in both core targets 2026-10-01 17:50:46 +03:00
evgeny 555b6fcf86 Prepare voice and video before dispatching to a fixed conversation 2026-10-01 17:49:52 +03:00
evgeny 1bd310c859 Detach recorder compressors while preserving capture settings 2026-10-01 17:47:39 +03:00
evgeny 20acf975d9 Return final video metadata from the preparation worker 2026-10-01 17:46:55 +03:00
evgeny 984b9e8123 Keep PM attachment kinds scoped to files voice and video 2026-10-01 17:43:53 +03:00
evgeny c7310e1fad Register shared voice encoding in core build targets 2026-10-01 17:43:10 +03:00
evgeny a5f90ec207 Share voice file encoding and final PCM metadata across clients 2026-10-01 17:42:44 +03:00
evgeny 72d1d3f582 Validate PM attachment metadata before committing messages 2026-10-01 17:41:19 +03:00
evgeny 7b8e2cd96c Transfer temporary source ownership before launching PM workers 2026-10-01 17:40:49 +03:00
evgeny c1185b62c1 Carry typed encrypted metadata with PM file transfers 2026-10-01 17:39:48 +03:00
evgeny 7357a63fdf Define bounded typed attachment metadata for PM and GUI 2026-10-01 17:38:25 +03:00
evgeny adff64e483 Deliver media completions without polling or timer failure joins 2026-10-01 17:36:43 +03:00
evgeny 00e890e0c3 Reserve worker completions before starting background tasks 2026-10-01 17:36:12 +03:00
evgeny 142eefaf86 Record completed PM media integration and validation results 2026-10-01 17:00:55 +03:00
evgeny 7e780bb6a7 Document PM file custody, worker ownership and client integration 2026-10-01 16:44:38 +03:00
evgeny ef5cd63d9f Set injected control packet lengths so replay and quota checks execute 2026-10-01 16:40:53 +03:00
evgeny 7bd3e0d4de Test PM media rollback, quota, snapshots and transfer cancellation 2026-10-01 16:35:55 +03:00
evgeny 18ee21cf93 Allow receipt-authorized custody cleanup after role revocation 2026-10-01 16:34:00 +03:00
evgeny 934f1ccd1f Use the existing diagnostic macros for desktop PM errors 2026-10-01 16:32:14 +03:00
evgeny a90274f24c Test event-loop heartbeat during sustained PM file encryption 2026-10-01 16:31:51 +03:00
evgeny dc1110af4d Add Android PM attachment selection, saving and delivery labels 2026-10-01 16:29:12 +03:00
evgeny 1457275139 Update Android PM screens from asynchronous core events 2026-10-01 16:27:58 +03:00
evgeny 52e38ff5d6 Parse PM snapshots and delivery states in the Android repository 2026-10-01 16:27:58 +03:00
evgeny 8403697013 Post Android PM reads and file sends to the core event loop 2026-10-01 16:27:01 +03:00
evgeny d9eee4e520 Bound and safely serialize PM conversation snapshots 2026-10-01 16:25:52 +03:00
evgeny e562896d63 Define asynchronous conversation snapshots for PM clients 2026-10-01 16:25:52 +03:00
evgeny 7fee3ec551 Render asynchronous PM history and submit desktop file sends 2026-10-01 16:24:09 +03:00
evgeny 1ceb09062f Show PM file delivery state without invoking group downloads 2026-10-01 16:22:54 +03:00
evgeny 570f3dfab3 Enable original file attachments in the desktop PM composer 2026-10-01 16:22:32 +03:00
evgeny 4cabbc32f5 Fit file chunks within the signed router packet limit 2026-10-01 16:22:07 +03:00
evgeny 3d6f9c1be4 Sign media test fixtures with the derived Ed25519 seeds 2026-10-01 16:14:39 +03:00
evgeny 76497481f7 Test direct and offline DM media with separate supernode and storage nodes 2026-10-01 16:04:35 +03:00
evgeny 139356bd18 Deliver DM history snapshots asynchronously as JSON events 2026-10-01 16:00:39 +03:00
evgeny 1cdead18de Expose bounded DM history snapshots with text and media delivery state 2026-10-01 15:59:38 +03:00
evgeny b47918fad5 Keep media publication and durable deletion off the event loop 2026-10-01 15:57:30 +03:00
evgeny 55c9d79a01 Integrate atomic media registration and orderly transfer/worker shutdown 2026-10-01 15:53:53 +03:00
evgeny d7a12b24bf Coordinate offline DM media custody and durable deletion on supernodes 2026-10-01 15:52:05 +03:00
evgeny 41fc5902be Implement durable DM downloads, media-node custody, quota and expiry 2026-10-01 15:47:05 +03:00
evgeny e47c7fb2ac Allow file owners to revoke active transfers on custody expiry or deletion 2026-10-01 15:42:54 +03:00
evgeny 84c148acff Prepare and publish target-encrypted DM files with exact media receipts 2026-10-01 15:42:02 +03:00
evgeny d559cbf473 Add configurable seven-day DM custody TTL and reserved storage quota 2026-10-01 15:36:31 +03:00
evgeny 5e9a9059c8 Bind signed file descriptors to atomic DM enqueue and receive 2026-10-01 15:28:40 +03:00
evgeny 27fa3a4812 Define independent DM media metadata, receipts and custody contracts 2026-10-01 15:27:44 +03:00
evgeny 3d0ab35efe Register file transfer and DM media services in the common core 2026-10-01 15:22:10 +03:00
evgeny f39ba840b8 Add bounded signed file transfers with per-operation CM ownership 2026-10-01 15:21:44 +03:00
evgeny 6c86e380a8 Record DM stage validation and pending media delivery work 2026-10-01 15:06:08 +03:00
evgeny c63343cf2b Stop DM test event loops promptly on scenario failure 2026-10-01 14:51:37 +03:00
evgeny bd550159f0 Verify DM queues drain beyond one delivery batch after restart 2026-10-01 14:48:37 +03:00
evgeny 43c89ca1ed Document implemented DM guarantees and remaining media integration 2026-10-01 14:46:24 +03:00
evgeny 1740bec94f Test mailbox admission, exact deletion and late PUT replay 2026-10-01 14:43:50 +03:00
evgeny 0e0f611d3c Check atomic DM enqueue rollback and explain sequence conflicts 2026-10-01 14:41:33 +03:00
evgeny bd05ab6d3a Verify DM media record boundaries and corrupted artifacts 2026-10-01 14:35:43 +03:00
evgeny 467d2e7605 Prepare DM media as authenticated records using target key 2026-10-01 14:33:45 +03:00
evgeny 10de4f1c71 Commit DM activity timestamps together with message state 2026-10-01 14:28:47 +03:00
evgeny 794d7b70f3 Authorize DM custody by signed group role instead of NAT type 2026-10-01 14:27:54 +03:00
evgeny e6c78c17b7 Keep DM test diagnostics focused on delivery state 2026-10-01 14:24:28 +03:00
evgeny 3cdb3e6580 Create DM router streams before checking queue capacity 2026-10-01 14:24:18 +03:00
evgeny febc3419b0 Exercise DM reordering, rollback and signed acknowledgments 2026-10-01 14:22:46 +03:00
evgeny 937ef9fcef Test CCM limits and offline DM custody across restart 2026-10-01 14:20:19 +03:00
evgeny 5800902a42 Deliver offline DM through durable supernode custody 2026-10-01 14:18:50 +03:00
evgeny cc072cceb2 Persist DM outbox and verify exact delivery receipts 2026-10-01 14:14:46 +03:00
evgeny 67bbf80769 Fix DM CCM length setup and separate media nonces 2026-10-01 14:04:25 +03:00
evgeny ce5f5bb7d0 Improve Android onboarding, member UI and PTT permissions 2026-10-01 13:55:28 +03:00
evgeny 577bfb4e6f Record independent audio startup checks and phone updates 2026-10-01 03:05:00 +03:00
evgeny fe1d29f007 Check refill below the 60 ms reserve including SoundTouch 2026-10-01 02:59:50 +03:00
evgeny fbf50b093d Document independent call audio startup and delivery reserve 2026-10-01 02:58:45 +03:00
evgeny 5b0262d53f Diagnose refill expectation with the corrected reserve 2026-10-01 02:57:18 +03:00
evgeny 4a2efe21c7 Start Android capture and playback independently 2026-10-01 02:56:29 +03:00
evgeny 7d9f13b8bc Test startup backlog independently of delivery jitter 2026-10-01 02:51:04 +03:00
evgeny 50e3db81ea Separate playback backlog from adaptive call reserve 2026-10-01 02:49:35 +03:00
evgeny d8745fd303 Measure call jitter from media delivery timestamps 2026-10-01 02:48:53 +03:00
evgeny bc797550d9 docs: record SM startup timings after phone update 2026-10-01 02:25:49 +03:00
evgeny 5ebab7072c docs: record call startup validation results 2026-10-01 02:19:58 +03:00
evgeny 9ff4e68246 docs: describe call audio startup and loss tone gating 2026-10-01 02:14:15 +03:00
evgeny 00666499fe android: start call capture before playback setup 2026-10-01 02:12:44 +03:00
evgeny 5487bc8c20 call: suppress loss tone until first received PCM 2026-10-01 02:08:17 +03:00
evgeny a3f6b6e86e docs: outline simple Android call startup improvements 2026-10-01 02:00:56 +03:00
evgeny 6f66df61f7 docs: record false audio loss tone during call startup 2026-10-01 01:58:20 +03:00
evgeny 72b5a9887f chatgui: measure duplex audio stalls and record SM call evidence 2026-10-01 01:36:34 +03:00
evgeny 076cc3c617 docs: record full recovery checks and root socket test 2026-10-01 01:19:37 +03:00
evgeny 91a69b939c docs: record conn_mgr recovery validation and check timeout limitation 2026-10-01 01:15:15 +03:00
evgeny 969c085085 tests: resend withdrawn relay advertisements during recovery 2026-10-01 01:09:15 +03:00
evgeny 426fc2f4e8 conn_mgr: verify route loss and document handle lifetime recovery 2026-10-01 01:06:56 +03:00
evgeny 464575ef44 conn_mgr: exercise persistent retries with valid signed node updates 2026-10-01 01:01:49 +03:00
evgeny a65ef30d78 conn_mgr: resume recovery when local sockets change 2026-10-01 00:51:17 +03:00
evgeny 8f5f2b089c conn_mgr: keep demand alive across bounded recovery attempts 2026-10-01 00:50:44 +03:00
evgeny 111432aa64 conn_mgr: race connection probes and report call path with correct RTT 2026-10-01 00:42:47 +03:00
evgeny 1c51df039f Fix call gap tone to follow 500ms of missing jitter PCM 2026-09-30 21:14:37 +03:00
evgeny 9e3a417059 Synchronize standby transitions and immediate keepalive phase announcements 2026-09-30 20:42:40 +03:00
evgeny e914a1bdc1 Improve Android member details contrast 2026-09-30 20:13:25 +03:00
evgeny b36c685cfa Show ongoing Android calls on the lock screen 2026-09-30 19:42:12 +03:00
evgeny 3c5c9604b3 Keep Android calls active independently of screen and UI 2026-09-30 19:12:04 +03:00
evgeny fb37ca747a 1 2026-09-30 17:37:13 +03:00
evgeny 2d21c53b4b Improve call buffering, CM handle lifecycle and radio PTT controls 2026-09-30 17:31:53 +03:00
evgeny 752c1f140c Add unified Android dark theme and improve UI contrast 2026-09-30 13:41:45 +03:00
evgeny db3cacd33e Rename Android app to vibechat 2026-09-30 13:10:09 +03:00
evgeny 7ebc282eb2 Prioritize supernodes in invites and update chat background and ringtone 2026-09-30 12:56:59 +03:00
evgeny 0d44ada922 Improve Android radio capture lifecycle, VAD and PTT controls 2026-09-30 12:25:42 +03:00
evgeny 52ffb91101 Fix TCP proxy use-after-free during synchronous close 2026-09-30 10:43:02 +03:00
evgeny 745ec3dbd2 android radio: preserve output route on reconfigure, link input/output settings 2026-09-29 22:35:08 +03:00
evgeny 15958a078e android: self-managed Telecom ConnectionService for call audio routing 2026-09-29 21:26:24 +03:00
evgeny 61bf235ef4 radio: PTT double-tap latch + single-tap off (in-chat & overlay); compressor initial gain = mid-range 2026-09-29 20:00:28 +03:00
evgeny 556b0bc7b7 call/radio: mic AGC compressor + selectable input device; proximity lock 2026-09-29 19:34:23 +03:00
evgeny f12f61d7ee call: add SpeexDSP AEC (acoustic echo cancellation) for desktop + Android 2026-09-29 17:01:30 +03:00
evgeny 1176411b79 radio: Silero VAD + auto-PTT; chatgui/android: app icon and background 2026-09-29 16:19:25 +03:00
evgeny 74ca98b38f proxy: rename pcb guard, dedupe abort; tasks: FIN_WAIT_2 leak 2026-09-29 13:52:35 +03:00
evgeny 961a234d7d proxy: detach lwIP pcb on local close, guard abort against freed pcb
При half-close (exit закрылся первым, браузер держит соединение) pcb уходил в
TIME_WAIT, а conn_finish ждал flow.fin_sent (доставку FIN в exit) — при backpressure
это не наступало, pc->pcb оставался висячим. Таймер TIME_WAIT освобождал pcb без
err_cb, и CLOSE_ALL давал double free. Теперь pcb отсоединяется по завершении
локальной стороны, а tcp_abort/tcp_close защищены проверкой memory_pool_is_freed.
2026-09-29 13:37:00 +03:00
evgeny a73ae13df4 lwip/proxy: trace PCB lifetime in debug category, drop old noisy debug 2026-09-29 13:18:18 +03:00
evgeny d8cbe2e589 Add proxy bug list and lightsout game (desktop + Android) 2026-09-29 12:19:23 +03:00
evgeny e1946b208d Preserve proxy TCP streams across connect, backpressure and FIN 2026-09-29 04:00:34 +03:00
evgeny 09bc8c35c5 Release TUN proxy connections when lwIP aborts their PCB 2026-09-29 03:59:30 +03:00
evgeny f19ff11b36 Scope TCP proxy streams and restart messages to their peer 2026-09-29 03:58:53 +03:00
evgeny 268243feeb Isolate datagram proxy state and replies between instances and peers 2026-09-29 03:58:14 +03:00
evgeny 4be1e62251 Parse IPv4 options and reject malformed TUN proxy packets 2026-09-29 03:57:37 +03:00
evgeny 10b66e2ad0 Validate SOCKS5 authentication and destination addresses 2026-09-29 03:57:21 +03:00
evgeny 7a41fcf512 Notify TCP writers when the final synchronous send drains 2026-09-29 03:57:15 +03:00
evgeny 518df33b0e Fix UDP and ICMP packet construction at the TUN boundary 2026-09-29 03:56:55 +03:00
evgeny fdec5b65c5 Add independent radio compressor and Android audio controls 2026-09-29 03:37:47 +03:00
evgeny ce1f23fe46 Radio: stereo capture/playback with mono fallback; CAMCORDER source; bump RADIO_MAX_OPUS
- stereo capture: Android AudioRecord stereo (CAMCORDER source for stereo,
  voice sources fall back to CAMCORDER) + desktop separate miniaudio
  capture/playback devices (no asymmetric duplex)
- Opus stereo encode/decode, voice_jitter stereo (per-channel frame_size,
  slot-based SoundTouch)
- radio_audio_start takes explicit capture_channels (stereo->mono fallback)
- RADIO_MAX_OPUS 256->512 (128kbps stereo frames up to 320B)
- radio_capture_stereo setting + desktop radio settings UI (opus mode/bitrate,
  capture stereo)
- radio_send_to send stats logging (etcp_send ok/fail per second)
2026-09-29 02:31:41 +03:00
evgeny 331970a121 Radio: Opus mode/bitrate + mic source + PTT release tail; drop online status from member JSON 2026-09-29 00:40:10 +03:00
evgeny e3caf15f32 Guard routing loops and reconcile blocked transit against current paths
Track visited nodes outside the signed body for all routed packet types. Recheck queued transit routes without losing FIFO priority, reject unknown-group transit, and suppress BGP announcements caused only by RTT jitter.

Document routing rules and wire format. Validate graph churn, REINIT, encrypted transit, malformed paths, loop drops and queue cleanup; full suite: 106 passed, 1 skipped, plus proxy integration and sanitizer checks.
2026-09-28 15:36:47 +03:00
evgeny fa9e102271 Preserve config string terminator when preparing key updates 2026-09-28 15:16:57 +03:00
evgeny 39ce249361 Prune unconfirmed BGP paths and restart stalled exchanges 2026-09-28 14:58:13 +03:00
evgeny b76e6553bc Reconcile BGP paths and advertise selected routes per neighbor 2026-09-28 14:37:44 +03:00
evgeny 546e0c185d Fix WITHDRAW removing nodes with surviving paths 2026-09-28 14:20:55 +03:00
evgeny 4503bdfa7e diagnostics: explain and rate-limit rejected ETCP session data 2026-09-28 13:16:39 +03:00
evgeny c63e93715f fix: accept invite public keys with a leading zero byte 2026-09-28 12:26:13 +03:00
evgeny 904a9fbda1 docs: refresh development guides for current architecture 2026-09-28 12:10:39 +03:00
evgeny 01fcefdec5 docs: clarify core, group and chat header contracts 2026-09-28 12:01:08 +03:00
evgeny 72fee9e245 Separate core, UTUN and chat lifecycles with owned asynchronous work 2026-09-28 11:39:08 +03:00
evgeny 5838e63cde Use shared group requests for config and chat connection planners 2026-09-28 11:07:25 +03:00
evgeny 0756a6e020 Serialize group topology lazily through a cancellable backpressure sender 2026-09-28 02:45:00 +03:00
evgeny 792776a1f6 Negotiate group sessions and scope topology messages to peer epochs 2026-09-28 02:15:54 +03:00
evgeny 52ef7a4b3c Own recovery attempts in group sessions and require restored routes 2026-09-27 23:06:48 +03:00
evgeny 66c59501fc Track topology exchange readiness per group and peer 2026-09-27 22:14:03 +03:00
evgeny bce17d9cfc Route UTUN configuration and group ownership through NCD 2026-09-27 21:59:32 +03:00
evgeny 9745a987ac Define join contract and require committed CHAT membership for BGP 2026-09-27 20:34:24 +03:00
evgeny 1a8072e9b2 Release group NCD ownership when destroying peers 2026-09-27 20:08:17 +03:00
evgeny 26e6c05313 Apply fresh BGP addresses to shared NCD connections 2026-09-27 19:18:40 +03:00
evgeny ec51b2d61f Version signed node snapshots with persistent timestamps 2026-09-27 19:11:11 +03:00
evgeny 7374d1648c Support 8 KiB member owner tags without signature or serialization truncation 2026-09-27 17:12:13 +03:00
evgeny de714f5023 Add bounded Merkle pull lifecycle and terminal error handoff 2026-09-27 16:17:04 +03:00
evgeny 3e25bcef02 Refactor Merkle sync into bounded bidirectional rounds with atomic member updates 2026-09-27 14:44:39 +03:00
evgeny 1b92b4885b Restrict dynamic auto socket test to Linux 2026-09-27 12:48:40 +03:00
evgeny 10d3ecfda4 Record final root transport and full suite validation 2026-09-27 12:19:52 +03:00
evgeny 03f71fd94e Bound ETCP receive window and fix timestamp and allocator UB 2026-09-27 12:16:15 +03:00
evgeny 59f4802904 Fix autosocket ownership and exercise interface churn under load 2026-09-27 11:57:29 +03:00
evgeny 3131dcbb7c Fix ETCP session recovery and router lifecycle; confirm chat invites before publishing 2026-09-27 02:51:47 +03:00
evgeny 018c8e795d android: фикс UAF AudioTrack при переключении группы рации (start → releaseLocked) 2026-09-26 19:18:42 +03:00
evgeny 0209bcf5d3 android: рация — единый режим per-канал, не гаснет при выходе/заходе в группу 2026-09-26 19:12:07 +03:00
evgeny 44d4265084 ll_queue: queue_waiter_cancel отменяет отложенный call_soon при internal==NULL (фикс UAF waiter_defer) 2026-09-26 18:58:27 +03:00
evgeny f6e6e2c73f stcp: write-error закрывает conn отложенно (фикс UAF keepalive/etcp_tcp_send)
Синхронный send-путь (stcp_link_send → queue_data_put → tx_queue_cb →
flush_pending → try_send) при ошибке сокета вызывал stcp_conn_do_close →
on_close → tcp_link_close_cb → etcp_link_close внутри стека вызывающего,
освобождая ETCP_LINK под keepalive_timer_cb/etcp_tcp_send (use-after-free,
SIGSEGV в keepalive_timer_cb:210).

Теперь write-error планирует отложенный close (uasync_call_soon) с отменой
handle в stcp_conn_do_close; первый err сохраняется; flush_pending не работает
вне STCP_STATE_DATA. Мёртвый hook on_write_error удалён.
2026-09-26 16:42:51 +03:00
evgeny 8aaac45020 ll_queue: отложенный первый запуск автозабора (callback_defer) + тест
queue_set_callback_defer(q, 1) переносит первый запуск callback из
queue_data_put* в uasync_call_soon (очистка стека производителя).
Повторные запуски (queue_resume_callback) и так всегда deferred.
2026-09-26 16:42:46 +03:00
evgeny cc2d2b5f78 etcp: фикс UAF teardown (двойное закрытие линка) + вытеснение down-линка при коллизии адреса
- tcp_link_close_cb (серверная ветка): убрать преждевременный огонь link_status/on_link_down,
  etcp_link_close сам снимает линк из списка и стреляет коллбэки в безопасном порядке
  (устраняет двойное закрытие линка — UAF из callring 45502)
- etcp_connection_close: при callbacks_running откладывать закрытие через uasync_call_soon
  вместо *(int*)0=0
- callbacks_running: uint8_t-флаг -> int-счётчик вложенности (++/--)
- etcp_fire_conn_status / etcp_fire_link_status_cbk: поднимать callbacks_running вокруг огня
- insert_link_queue: при коллизии адреса с чужим conn — ERROR при одинаковом pubkey,
  отказ при up-линке, вытеснение (etcp_link_close) при down-линке

Попутно: доки/комментарии в etcp_router, ACK-интервал 100мс->10мс (ROUTER_ACK_INTERVAL_TB).
2026-09-26 13:40:33 +03:00
evgeny 265421eb96 tun proxy: фикс TCP-суммы (нечётный pbuf), FIN до дренажа to_lwip, tcp_bind/rexmit_fast/active_pcbs_changed + нагрузочный burst-тест 2026-09-25 23:36:45 +03:00
evgeny a20d73d2be proxy/tun: добавить DEBUG-логи диагностики застревания keep-alive (lwIP IN/OUT/RECVED/RTO/PERSIST + per-conn DIAG) 2026-09-25 22:12:47 +03:00
evgeny 4353daf913 freebsd: уникальный dst-адрес per-interface (tunN -> 192.0.2.(N+1)) 2026-09-25 20:16:39 +03:00
evgeny 3a4daf6e77 etcp: строгая последовательность коллбэков при закрытии (NEW→UP↔DOWN→DELETE)
- close: финальный per-connection DOWN только если был UP, до state=2
- etcp_cbk_fire / etcp_fire_conn_status / etcp_link_ready_callback / etcp_on_up: guard state==2
- документация контракта в etcp_api.h / etcp.h
2026-09-25 19:13:38 +03:00
evgeny ab5d1c5f91 topo_group: диагностика senders_list (new_conn/remove_conn) для локализации UAF
- new_conn: DEBUG_INFO ADD с conn/peer/state/ch
- remove_conn: state в enter-логе, детали в !found_in_list WARN, REMOVED-лог
2026-09-25 18:18:39 +03:00
evgeny 08e8b2f1cc route_connectivity: фикс UAF при удалении узла с влётными пробами; отмена TCP-пингов при teardown
- topo_nodeq_remove_node: единый хелпер удаления nq (cancel проб + free paths/subnets + remove + free), заменяет все free-сайты
- route_connectivity_cancel_all: итерация по всем группам + зануление ctx->nq + отмена standby-defer
- etcp_send_tcp_ping: out_cli — хендл stcp_client для досрочной отмены; conn_probe_ctx.tcp_cli
- conn_mgr: cand_tcp_pings[] — отмена влётных candidate TCP-пингов в conn_mgr_destroy
2026-09-25 16:01:19 +03:00
evgeny 3f7b4351cd stcp: фикс UAF log_dump, дренаж очередей при закрытии, защита pre-closed серверной ветки, логирование тихих сбросов rx 2026-09-25 13:49:43 +03:00
evgeny ceaf466b1c chatgui-android: входящий звонок поверх блокировки (inline-оверлей) + вибрация всегда кроме silent 2026-09-24 12:08:49 +03:00
evgeny fb4343fca6 transport: reality-релей idle-timeout (конфиг, дефолт 5с) + warn при >64 релеях; убрать лимит FD_SETSIZE; проверка socket_id в stcp_server 2026-09-24 12:08:45 +03:00
evgeny cc269e810c Звонки: надёжность отмены (INVITE-кодограмма + watchdog) и вибрация входящего по ringer-режиму 2026-09-24 10:15:19 +03:00
evgeny b90dc752d2 call: две фазы исходящего вызова — индикация + разные гудки (desktop/Android) 2026-09-23 19:16:53 +03:00
evgeny 7198154106 chatgui: клик по медиа с отсутствующим файлом — сброс пометки и перекачка 2026-09-22 21:59:59 +03:00
evgeny d237dfec7c diag: кольцевой трейс вызовов teardown + дамп при SIGSEGV (размотка UAF link close) 2026-09-22 20:52:02 +03:00
evgeny 4f29839de5 chat: инвайтер шлёт CONNECT_RESULT(0) после приёма джойнера
- chat_join_process_request: постить CHAT_EVT_CONNECT_RESULT(result=0) в GUI инвайтера
  (иначе диалог «Пригласить в канал» висел на «ожидание ответа»)
- cs_handle_join_request: снять pending_invite_* и погасить invite_send_timer после join
2026-09-22 18:26:44 +03:00
evgeny b46fd599fa chat: push-invite join key + запрет флагов на плейсхолдере
- CHANNEL_INVITE теперь несёт join_key (регистрируется инвайтером локально),
  джойнер шлёт JOIN_INFO_REQ с валидным ключом — чинит автоjoin chatserver-узлов
- ask-путь сохраняет join_key для chat_sync_accept_invite
- chat_member_tags_commit отвергает установку флагов на source!=0 (плейсхолдер)
- member_sync_apply_record: defensive ERROR при adm_tags на плейсхолдере
2026-09-22 17:58:50 +03:00
evgeny 0e625da109 call: ретрансмит INVITE каждые 2с + fail-fast при отсутствии маршрута (6с) 2026-09-22 16:06:52 +03:00
evgeny 93becd36e6 keepalive: TCP-линки без ретраев/ACK; не пинговать спящих пиров, bg-ping цикл ≤ 1/мин 2026-09-22 03:47:50 +03:00
evgeny a3e890b711 keepalive: раздельный sleep-код (announce+ACK-ретраи в окне, dead-link) + без flagless в спячке 2026-09-22 03:29:42 +03:00
evgeny a3df1720b4 keepalive: симметричный sleep-протокол (без is_server) + отдельный ACK-бит 2026-09-22 02:41:15 +03:00
evgeny 331c7849f0 keepalive: убрать периодический keepalive в standby (обе стороны молчат, только фазовые анонсы + один ack) 2026-09-22 02:19:28 +03:00
evgeny 945c6a82b0 keepalive: гвард нулевого тайминга фазы (busy-loop) + разделение echo/announce на сервере 2026-09-22 01:35:25 +03:00
evgeny cc0dcb071e chat: UI настройки звука канала (Android toggle + JNI, desktop через C API) + trampoline 2026-09-22 01:25:55 +03:00
evgeny 3d9c79e752 db_sync: троттлинг push к спящему пиру в тихом чате 2026-09-22 01:21:51 +03:00
evgeny b39b85db13 chat: троттлинг по фазе спячки (member_sync send-only, db_sync sound-aware) + настройка звука 2026-09-22 01:20:32 +03:00
evgeny 9f89fd0f7a transport: keepalive duty-cycle тайминг (active_remaining+sleep_duration) + серверная фаза троттлинга 2026-09-22 01:20:26 +03:00
evgeny 0a1ba79d6f chat: member_sync per-peer throttling on peer sleep (cancel_peer + recv/start gating) 2026-09-22 00:42:24 +03:00
evgeny e4e9fca243 transport: per-peer sleep callback (peer_sleep_cbk) fired from keepalive protocol 2026-09-22 00:40:32 +03:00
evgeny 5628138d16 etcp: keepalive sleep-протокол на флагах (KA_FLAG_SLEEP + sleep_time, announce-retry, ack/echo) 2026-09-21 23:30:39 +03:00
evgeny 8a9d8d0097 fix: утечки памяти в router-сервисах и при shutdown
- conn_mgr_router_recv_handler / md_etcp_recv не освобождали entry (рантайм-утечка на каждое сообщение)
- reality_relay: registry + shutdown hook (отложенный free не отрабатывал при остановке)
- etcp_router: uasync_drain_immediate после destroy (router_close_finalize)
- мелкие callback-списки: activity_cbks, node_cbks, props_cbks/apply_cbks
2026-09-21 21:55:11 +03:00
evgeny 2f3001458f android: standby keepalive sync + rx logging during sleep 2026-09-21 21:17:35 +03:00
evgeny 845af9200d Stop call ring sound immediately on accept 2026-09-21 19:41:21 +03:00
evgeny 328d36376c chat: пишем local_attrs атомарно при вставке своего медиа (гонка с MSG_RECEIVED) 2026-09-21 19:36:07 +03:00
evgeny 6e2a2f0011 media_download: сброс таймера простоя при CONN→REQ + правки диагностики
- md_dl_conn_cb: обнулять last_progress_tb при переходе CONN→REQ, иначе время
  установки indirect-соединения (~2с) засчитывается как простой блока и
  вызывал преждевременный фейловер с держателя, реально имеющего блок
- STALL-лог: печатать миллисекунды корректно (значение в единицах 0.1мс /10)
- media_index: INSERT OR IGNORE — повторная регистрация блока как servable
  больше не шумит как ERROR (UNIQUE constraint)
2026-09-21 19:16:27 +03:00
evgeny 9793733cd5 chatgui: диагностика воспроизведения голосовых сообщений (Windows)
- sound_manager: ma_engine_init/playRawPcm логируют через DEBUG_* (qWarning/qDebug терялись в GUI-приложении без консоли); детальный результат старта PCM (rc/is_playing/cursor)
- main: ранняя инициализация лога в GUI-потоке до SoundManager::init, чтобы диагностика инициализации попала в файл
- messagedelegate: лог клика по Play/timeline (filePath/dlState/существует ли файл)
- voiceplayback: лог заголовка opus (sr/ch/frameSamples)

Попутные правки:
- u_async: фикс двойного close wakeup_pipe (eventfd/Windows)
- chat_core: NULL-guard в get_db/is_initialized
- android: sqlite открывается в utun_instance_init, cleanup stale-потока без утечки fd
- android SettingsScreen: компактная раскладка имени интерфейса
2026-09-21 18:59:04 +03:00
evgeny 19dc076528 chatgui: инициализация call/dm/media backfill при старте узла (fix звонков) 2026-09-21 17:20:41 +03:00
evgeny 6a0373ee73 call: диагностика ставок TX/RX и детект сталла аудио-девайса с хард-ресинком 2026-09-21 16:54:14 +03:00
evgeny 570b421ba0 conn_mgr: parallel reverse+indirect, retransmit, and call teardown fix
- call_on_conn_status: only end call when its own data conn drops
  (INDIRECT call was killed by unrelated topo_group_connect conn DELETE)
- conn_mgr: run REVERSE and INDIRECT in parallel after DIRECT fails;
  reverse is preferred (indirect defers via indirect_ready until reverse
  resolves). REVERSE timeout 15s -> 3s.
- retransmit DIRECT_REQ / EXCHANGE_REQ every 1s (idempotent, same request_id)
  to survive packet loss (router RST dropping stale-channel requests)
- replace main.phase with reverse_active/indirect_active/indirect_ready flags
- add diagnostics to trace the parallel flow
2026-09-21 15:22:16 +03:00
evgeny ee54bd52b3 conn_mgr: fix reverse-phase timer leak causing use-after-free crash
cm_ncd_callback re-entered cm_start_phase_reverse on every NCD timeout
during REVERSE phase (shared NCD revived by other modules), leaking reverse
timers: only the last one was tracked in entry->main.timer, so cm_entry_cleanup
cancelled only that one and the earlier timers fired on the freed entry.

- ignore repeated NCD timeouts while already in REVERSE (phase==2)
- cancel entry->main.timer before overwriting in reverse/indirect phases
- remove entry from reverse_pending in cm_entry_cleanup
2026-09-21 13:57:32 +03:00
evgeny e885fe30f5 chatgui: remove universal black background from video window (fix black boxes around controls) 2026-09-21 12:50:06 +03:00
evgeny cd0757e272 chatgui: declare metatype for shared_ptr<vector<int16_t>> (Qt5 build fix) 2026-09-21 12:42:21 +03:00
evgeny f5fece4959 chatgui: fix video playback on Windows (metatype registration + swscale YUVJ range) 2026-09-21 12:39:18 +03:00
evgeny 01987a75d0 chatgui: verify FFmpeg DLL deployment + fix dependency resolution order 2026-09-21 11:52:21 +03:00
evgeny 9d4538935d chatgui: restore FFmpeg build script (build_ffmpeg.sh + .gitignore)
Скрипт был случайно удалён в d773f172 вместе с .gitignore/.opencode.
Без него на Windows не собирается ffmpeg/ffmpeg_build/install -> HAVE_FFMPEG
не определяется -> воспроизведение и транскодирование видео отключены.
2026-09-21 11:29:47 +03:00
evgeny 6ee9bad17d chat: defer media auto-download to per-channel backfill after sync
- db_sync insert-cb gets initial_sync flag (SEND_DATA=1, PUSH/local=0)
- on_msg_inserted skips media download during initial sync
- new chat_media_autodownload_backfill_channel (newest-first) triggered
  on SYNC_DONE per channel (2s debounce)
2026-09-21 02:07:24 +03:00
evgeny a5047c29f7 chatgui: always deploy non-Qt DLLs (FFmpeg, mingw-runtime) next to vibechat.exe 2026-09-21 00:58:56 +03:00
evgeny 2f1545de38 chatgui: fix Qt5 build — QMouseEvent/QWheelEvent position() -> localPos()/posF() 2026-09-21 00:51:48 +03:00
evgeny de850db548 chat: remove join_password check from per-channel JOIN_INFO_REQ (server-link only) 2026-09-20 22:23:42 +03:00
evgeny 39a1da4c28 chat: fix use-after-free in invite_candidates trampoline + accept JOIN_INFO_RESP/READY/ERROR for unknown channels 2026-09-20 22:03:31 +03:00
evgeny d1432b7d19 tests: fix Windows build — SKIP fork-based e2e, cross-platform mkdtemp/setenv 2026-09-20 20:21:50 +03:00
evgeny a7a3666d85 radio: AGC release-rate (без кликов), jitter dead-zone, TX/RX-статистика, FGS-гонка и кэш настроек 2026-09-20 20:07:48 +03:00
evgeny ba1c2d85d3 reality_relay: async DNS вместо getaddrinfo + AGC-компрессор по умолчанию (-12 dBFS) 2026-09-20 15:28:46 +03:00
evgeny 738cfe7835 radio: адаптивный джиттер-буфер (pre-roll, симметричный темп, reorder) + AGC-компрессор микрофона
- voice_jitter: vj_create_cfg (target_ms/min_tempo/max_tempo/pre_roll), pre-roll
  с таймаутом, vj_end для дренажа остатка, глубина учитывает внутренний буфер SoundTouch
- radio_audio: reorder по seq (mesh, out-of-order через соседей), force-flush pending
  по таймауту и FIN, компрессор микрофона (AGC, конфиг [chatserver]), статистика буфера
- audio_compressor: clear_output для потокового слива без потери gain_smoothed
- chatgui messagelist: fromUtf8 для emoji-строк
- тесты: pre-roll/симметричный темп, reorder, late-drop
2026-09-20 14:39:25 +03:00
evgeny 27e66f1092 log: move socket diagnostics SYS->SOCKET and conn lifecycle ETCP->CONNECTION (avoid poll() flood) 2026-09-20 01:51:40 +03:00
evgeny 1bde2eca35 diag: log conn add/remove/free pointers to pinpoint BGP senders_list UAF 2026-09-20 01:42:05 +03:00
evgeny 16237b0df8 etcp_send: fix contract (caller owns entry on error) + fix 7 leak sites 2026-09-20 01:35:30 +03:00
evgeny fae0a7c069 uasync: cast function pointers in socket_add log to void* 2026-09-20 01:18:33 +03:00
evgeny b33caa56b8 uasync: pass socket name to all add_socket call sites 2026-09-20 01:13:38 +03:00
evgeny c36cebe357 uasync: add socket name diagnostics and HUP/ERR auto-unregister safety net 2026-09-20 01:11:30 +03:00
evgeny 90520a83ff topo_group_connect: не подключаться к самому себе в авто-цикле
Phase 1/2/3 и on_up/on_down не фильтровали self: узел попадал в список
connected-пиров (get_connected_peers не исключает self) и разворачивал
node_conn_direct_open на собственные адреса (12 линков на 5C31->5C31).
Ручной путь (connect_node_once) фильтр уже имел.

Добавлен skip self в Phase1 init/cycle_restart, Phase2, Phase3 и в
on_up/on_down (self не помечается connected и не считается в active_conn_count).
2026-09-19 23:40:06 +03:00
evgeny 6b92964794 radio/android: PTT FGS microphone permission + детект потери микрофона
- FOREGROUND_SERVICE_MICROPHONE запрос + foregroundServiceType microphone|specialUse (Android 14+)
- RadioOverlayService.start: не стартовать без mic-разрешения, ServiceCompat.startForeground с TYPE_MICROPHONE
- RadioAudioEngine: micDead-флаг и лог при обрыве captureLoop/read
- radio.c: лог первого кадра burst на транзитных узлах
- SettingsScreen: категория radio в списке debug-категорий
2026-09-19 23:13:04 +03:00
evgeny 688785fc6e etcp: фикс зависшего reinit_pending после реинита готового соединения
etcp_conn_queue_set_ready() делает ранний выход по state!=0, поэтому при
реините уже готового соединения (peer restart / duplicate INIT / collision)
reinit_pending оставался в 1 навсегда. Добавленные в 841b3042 guard'ы
etcp_conn_input/etcp_output_try_assembly дропали все входящие пакеты.

Снятие reinit_pending + REINIT перенесено в etcp_conn_ready.
2026-09-19 23:13:04 +03:00
evgeny 5e955168d2 chatgui-android: PTT-оверлей — talking flow, цвет idle/talking, drag-lock
- RadioAudioEngine: StateFlow talking для реактивной подсветки кнопки
- RadioOverlayService: градиент idle/talking, drag-lock, сброс isShown при destroy
- ChatScreen: скрывать PTT-кнопку при активном оверлее, подсветка по talking
2026-09-19 21:17:38 +03:00
evgeny 841b3042cf etcp: фикс double-free буферов паковщика и повторных free после неудачного queue_data_put
- pn_send_to_etcp/pn_unpacker_cb: занулять pn->data/pn->recvpart ДО queue_data_put
  (ре-энтрантный close/reinit в синхронной цепочке освобождал буфер повторно)
- etcp_output_try_assembly: гард state==2/reinit_pending + убрать re-put освобождённого entry
- etcp_conn_input: гард ре-энтрантного reinit/close в цикле секций, локальный etcp вместо pkt->link
- убраны повторные free после queue_data_put (он сам освобождает entry+dgram) в 6 местах
- диагностика: obj=%p в DOUBLE FREE, логи free dgram в pn_deinit/ack_recv
2026-09-19 21:17:33 +03:00
evgeny 4a785772be chatgui-android: PTT по volume-кнопкам (вверх/вниз) + синхронизация оверлея
- volume up/down обе держат PTT (holdCount), настройка radio_volume_ptt
- syncRadioOverlay при включении/выключении рации
2026-09-19 18:16:46 +03:00
evgeny 8a9184675a radio: дедупликация дублей в mesh + фикс OOB в hop_list
- radio_recv: кэш (src,stream,seq) с TTL 2с гасит кадры, пришедшие по второму пути (diamond/mesh)
- фикс записи за границу new_hops[hop_count] при hop_count==RADIO_MAX_HOPS
- счётчик c_dup_dropped + radio_dup_dropped() для диагностики
- тест test_radio_mesh (4 узла, diamond): C получает ровно N кадров, dup>0
2026-09-19 18:16:42 +03:00
evgeny b96ed8bd00 radio: PTT-радио в каналах (src/radio + chatgui + chatgui-android)
Новый модуль src/radio (radio/radio_audio/radio_headless) — push-to-talk
радио поверх topo-групп, доставка аудио в канал, headless-режим и тесты.
Интеграция в chatgui (ptt_hotkey_manager, radio_audio_engine, радионастройки,
вызовы) и chatgui-android (RadioAudioEngine, RadioOverlayService, UI).
2026-09-19 17:09:00 +03:00
evgeny 398078a28f headless/call: закрывать клиента и логировать при ошибке send() (EPIPE/ECONNRESET) 2026-09-19 10:43:04 +03:00
evgeny 6c42390fc2 utun: игнорировать SIGPIPE, чтобы send() в закрытый TCP-сокет не убивал демон 2026-09-19 10:32:57 +03:00
evgeny 1de1409699 conn_mgr: handle-based API (conn_mgr_open/close), фазы DIRECT/REVERSE/INDIRECT, тесты phases+handles 2026-09-19 09:31:08 +03:00
evgeny 734a414186 etcp: единый реестр svc_id + убрать коллизию CHAT/CONN_MGR (0x11)
Все ETCP_ID_*/ETCP_RT_ID_* сведены в etcp_api.h (единственное место
определения) с реестром и предупреждением про коллизии в пределах одного
пространства имён. Убраны локальные define из topo_group.h,
topo_group_invite.h, db_sync.h, chat_sync.h, chat_join.h; хардкод 0x31 в
member_sync.c заменён на ETCP_RT_ID_MEMBER_SYNC.

Коллизия: chatgui-десктоп биндил мёртвый ETCP_RT_ID_CHAT 0x11 поверх
ETCP_RT_ID_CONN_MGR 0x11 — затирался conn_mgr_router_recv_handler, из-за
чего INDIRECT-обмен conn_mgr не работал и голос звонка не устанавливался.
Удалён мёртвый код recvCallback/messageReceived/send в utun_node.{cpp,h}.
2026-09-19 00:18:39 +03:00
evgeny 6d3ad965ee topo: remember REQUEST_TABLE for not-yet-created groups, fulfill on create
При REQUEST_TABLE для ещё не созданной группы (CHAT-канал не загружен) пир
дропал запрос, и проситель навсегда не узнавал таблицу — односторонняя
видимость (звонок android→linux падал «peer offline»). Теперь запрос
запоминается в pending_table_reqs[32] и выполняется при создании группы
(topo_group_fulfill_table_reqs → handle_request_table).

Попутно: test_chat_join_e2e — критерий успеха через CHAT_EVT_CONNECT_RESULT
(JOIN_READY) вместо senders_list; doxygen-комментарии в etcp_api.h.
2026-09-18 23:36:45 +03:00
evgeny 8bfb456161 etcp: fix OOB/DoS in packet parsing and ping path
- FILLER: drop (uint16_t) truncation in fill_len bound (OOB pointer)
- ACK: require len >= ETCP_ACK_BASE_SIZE (8); wrap-aware clamp of till
  to next_tx_id + 16MB cumulative-ACK iteration limit (DoS)
- ping: bound len before sc_encrypt (stack overflow), ulen bound,
  allocate ETCP_DGRAM + PACKET_DATA_SIZE (heap overflow)

Signed-off-by: uTun <dev@utun>
2026-09-18 17:58:40 +03:00
evgeny 1340ab123b reality/stcp: fix OOB read in key_share parse, padding clamp, shbuf leak
- reality_parse_client_hello: bound shares_len by extension data length
  (len-2) to prevent stack-buffer-overflow from malformed ClientHello
- stcp handshake: reject padding_size > STCP_HS_PADDING_MAX (512)
- reality_ch_body_cb: free shbuf on stcp_try_send error (leak)
- tests: fuzz key_share parse; raw-socket handshake control/clamp/garbage

Signed-off-by: uTun <dev@utun>
2026-09-18 17:20:19 +03:00
evgeny e7fc69bb1d topo: комментарии к функциям topo_node/topo_group
conn_mgr: src node_id вместо conn->peer_node_id, препенд ETCP_RT_ID_CONN_MGR к dgram, bind CONN_MGR после etcp_router_init

call: guard CALL_ST_CONNECTING при accept
2026-09-17 17:30:05 +03:00
evgeny 1fce405204 debug: вычистить логи из категории DEBUG_CATEGORY_DEBUG (удалить дампы, перекатегоризовать ERROR-проверки в ETCP) 2026-09-17 00:17:32 +03:00
evgeny e6a4ffd418 reality: fix leak of ClientHello init buffer in reality_server_start_relay 2026-09-17 00:05:42 +03:00
evgeny 1f1df4f437 mem: детект аномально больших аллокаций (LARGE ALLOC log + calloc overflow guard) 2026-09-16 21:12:46 +03:00
evgeny 51a5c5bcbf router: fix UAF on rconn->recv_conn after ETCP_CONN destroy
recv_conn хранил заимствованный указатель на физический conn без очистки
при его освобождении. Добавлен etcp_router_conn_destroyed(), вызываемый из
etcp_connection_close фазы 1; RST теперь отправляется через живой conn,
доставивший пакет, а не через устаревший recv_conn.
2026-09-16 20:27:03 +03:00
evgeny 51d6d22a27 sock_match: классификация сокета по адресу, а не по type из конфига (fix Q8/Android→LAN)
ip_is_public() вынесен в lib/platform_compat (единый классификатор IPv4 с ntohl).
auto_socket_reconcile_config: тип сокета выставляется по фактическому адресу.
sock_collect_views: has_priv/has_inet/is_eim — по классификации адреса и NAT-детекции.
is_local_subnet упрощён до !ip_is_public(). Тест test_sock_match обновлён.
2026-09-16 20:26:55 +03:00
evgeny 21d9a4e5b7 dm: заполнять peer_name при автосоздании беседы (иначе в списке 0x...) 2026-09-16 20:26:55 +03:00
evgeny 5293fdd3a9 call(android): звать call_audio_init перед start; логировать feed_pcm без instance
Без call_audio_init(inst) g_inst оставался NULL и call_set_audio_ops не
регистрировался: TX (feed_pcm) падал на !ua, RX (audio_ops->on_media) не
вызывался — тишина и спам 'callAudioFeed returned false'. Зеркалим desktop:
utun_bridge_call_audio_start теперь делает call_audio_init + call_audio_start.
2026-09-16 20:26:55 +03:00
evgeny 2de554f8b9 call: единый голосовой стек (libutun_voice) + SoundTouch на Android
- voice_jitter (кодированные Opus-кадры + decode-on-pull + SoundTouch time-stretch)
  вместо PCM-буфера audio_jitter; излишек держится до декодирования
- call_audio.c/.h — единая реализация для desktop/Android/headless (C-интерфейс)
- libutun_voice (voice_sources.cmake): SoundTouch + voice_jitter + call_audio + call_tones
- Android: pull-модель (nativeCallAudioPull), убран Kotlin FIFO, ANDROID_STL=c++_static
- headless audio-сокет: PCM-протокол (decode-only, без тактирования), daemon остаётся C-only
- CALL_STATS расширен dropped/underruns; удалены audio_jitter.cpp и libutun_lite/call_audio.c
- тест test_voice_jitter (ёмкость/догон/стационар), test_call_headless переведён на PCM
2026-09-16 20:26:55 +03:00
evgeny de930b21a2 ncd: fix UAF on deferred UP delivery after handle close
ncd_deliver_up_cb (uasync_call_soon) держал сырой указатель handle h,
который мог быть освобождён node_conn_direct_close/force_close раньше
выполнения отложенного callback'а. Сохраняем токен call_soon в handle
и отменяем его при close через uasync_call_soon_cancel.
2026-09-16 20:26:55 +03:00
evgeny e4557b0473 dm: маршрутизация через общую группу (group_id), JSON-эскейпинг; звонки: иконки/UI в desktop и android 2026-09-16 15:05:48 +03:00
evgeny b483335c06 bgp/ncd: владение conn через NCD-handle в senders_list; call audio route persist + incoming call sound 2026-09-16 13:29:28 +03:00
evgeny b11e77e4c0 call: P2P аудиозвонки — сигналинг, релей медиа, аудио-контур, headless/desktop/android
- src/call/: сигналинг (call.c, call_proto.h, ETCP_RT_ID_CALL 0x36) + релей медиа
  + headless аудио-сокет (call_headless.c, call_audio_bind) + Opus/jitter/тоны
- chat: события CHAT_EVT_CALL_* (34–40), CHAT_EVT_DM_MESSAGES (41), DM-трамплины
- chatgui: call window, miniaudio I/O, audio_frame_ring, test_audio_jitter
- chatgui-android: CallScreen/IncomingCallDialog, аудио-роутинг, JNI, рингтон
- tests: test_call, test_call_headless
2026-09-16 10:17:57 +03:00
evgeny 0157f7f96b build: fix --asan to build lib/libopus dependency 2026-09-15 22:44:27 +03:00
evgeny 21ec740d82 socks_proxy: defer conn free to fix UAF from synchronous write_queue flush 2026-09-15 22:39:27 +03:00
evgeny b085715654 tun/nat: fix UAF при teardown и use-after-free в tun_read_packet 2026-09-15 22:07:55 +03:00
1558 changed files with 434150 additions and 25157 deletions

No files matched your search

+7
View File
@@ -2,7 +2,9 @@
/build/
build-win/
tools/chatgui/build/
tools/chatgui/rlottie_build/
tools/chatgui/ffmpeg/ffmpeg_build/
tools/lightsout/build/
# Build outputs
*.o
@@ -11,6 +13,8 @@ tools/chatgui/ffmpeg/ffmpeg_build/
*.a
*.so
*.so.*
# Prebuilt native runtime для Android VAD (onnxruntime) — коммитим несмотря на *.so
!tools/chatgui-android/app/src/main/jniLibs/*/libonnxruntime.so
/utun
/src/utun
test_serialize
@@ -28,6 +32,7 @@ index.html
*.exe
# Autotools
/.utun-build-state*
Makefile
!tools/etcpmon/Makefile
Makefile.in
@@ -80,6 +85,7 @@ logs/
tests/test_*
!tests/test_*.c
!tests/test_*.h
!tests/test_*.py
!tests/test_*.conf
!tests/test_*.json
tests/bench_*
@@ -108,6 +114,7 @@ tools/proxy/udp_proxy
# Reference projects (not part of utun)
tools/tdesktop-dev/
/hardware/
# Debug/temp artifacts
*.bak
+444 -132
View File
@@ -32,12 +32,17 @@ This file contains essential information for AI coding agents working in the uTu
Доствп на узел colo2: ssh -p44322 logs@colo2
Структура .h файлов:
- в начале краткое и понятное описание: что делает модуль, как пользоваться.
- поля структур также кратко описываем - что, для его, если есть нюансы/значения
- для функций: описываем назначение, как пользоваться, описание аргументов, возврата. кратко но понятно.
## Quick Reference
**Repository:** uTun - Secure VPN tunnel with ETCP protocol
**Repository:** uTun — общее сетевое ядро ETCP, независимо запускаемые UTUN и P2P-чат
**Language:** C (C99)
**Build System:** GNU Autotools (autoconf/automake)
**Cryptography:** OpenSSL (AES-CCM, X25519, SHA256)
**Cryptography:** OpenSSL (AES-CCM, X25519, Ed25519, SHA256)
## Build Commands
@@ -51,9 +56,19 @@ This file contains essential information for AI coding agents working in the uTu
powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64
```
### Build (FreeBSD)
```sh
# Первичная установка инструментов от root:
pkg install -y bash gmake autoconf automake libtool pkgconf openssl python3
./build.sh -j4 # общий скрипт; автоматически выбирает gmake и Clang
./build_bsd_full.sh -j4 # полная сборка через общий скрипт
```
Python 3 нужен для проверок скрипта сборки. SHA-256 берётся из базовой системы FreeBSD.
### Incremental Build
```bash
./build.sh -j4 # make с авто-конфигурацией если надо
./build.sh -j4 # инкрементальная сборка, конфигурация и очистка при необходимости
./build.sh --no-gui -j4 # только ядро и тесты, без отдельной сборки GUI
```
```powershell
powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log
@@ -68,9 +83,10 @@ make distclean # Clean everything including configure files
### Partial Builds
```bash
cd lib && make # Build only the library (libuasync.a)
cd src && make # Build only the main program
cd tests && make # Build only tests
make -C lib # базовые библиотеки
make -C lib/libopus # встроенный Opus
make -C src # основной бинарник src/utun (библиотеки уже должны быть собраны)
make -C tests # тесты (используют объекты src/)
```
### Direct Build (Windows, без autotools)
@@ -86,7 +102,7 @@ python3 tools/chatcli members <ch_id> # мемберы с полн
python3 tools/chatcli messages <ch_id> [count] # последние сообщения
python3 tools/chatcli send <ch_id> "text" # отправить сообщение
python3 tools/chatcli invite <ch_id> # создать invite-ссылку
python3 tools/chatcli connect <utun://...> # join по ссылке
python3 tools/chatcli connect 'utun://...' # join по ссылке
python3 tools/chatcli create "Name" # создать канал
python3 tools/chatcli listen # слушать события (Ctrl+C выход)
```
@@ -94,13 +110,14 @@ python3 tools/chatcli listen # слушать событи
### ASAN Build (AddressSanitizer, Linux)
```bash
./build.sh --asan -j4 # lib/ + src/ only (tests not ASAN-compatible)
./build.sh --asan -j4 # собирает lib/ + src/; тесты этот режим скрипта пропускает
```
Запуск с ASAN (detect_leaks отключены чтобы не спамить при shutdown):
```bash
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0:log_path=/tmp/utun_asan ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
```
Краш-логи ASAN пишутся в `/tmp/utun_asan.<pid>`.
В этом примере отчёты ASAN пишутся в `/tmp/utun_asan.<pid>` благодаря `log_path`.
Для отдельного теста нужно собирать его зависимости с теми же sanitizer-флагами; обычные и ASAN-объекты не смешивать.
## Test Commands
@@ -118,13 +135,14 @@ cd tests/
./test_etcp_two_instances
```
### Run Single Test with Debug Info
### Build and Run a Single Test
```bash
cd tests/
gcc -I../src -I../lib -I../tinycrypt/lib/include \
-o my_test test_file.c ../src/*.c ../lib/*.c ../tinycrypt/lib/source/*.c
./my_test
make -C tests test_services
cd tests && ./test_services
```
Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`.
Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`.
Проверки самого скрипта сборки запускаются отдельно: `make -C tests check-build-script` (нужен Python 3); в `./check.sh` они не входят.
## Code Style Guidelines
@@ -137,7 +155,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \
### Formatting
- **Indentation:** 4 spaces, no tabs
- **Braces:** Same line for functions, new line for control structures:
- **Braces:** Открывающая скобка на строке функции или условия:
```c
int function(void) {
if (condition) {
@@ -145,7 +163,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \
}
}
```
- **Comments:** Primary language is Russian for business logic, English for API docs
- **Comments:** Краткие описания на русском; сохранять принятые имена API и протокольных полей.
- **Line Length:** Aim for 80-100 characters, but can go up to 150 if logically coherent
### Include Order
@@ -201,7 +219,7 @@ sc_context_t ctx;
sc_init_ctx(&ctx, &my_keys);
// 2. Set peer public key (for key exchange)
sc_set_peer_public_key(&ctx, peer_public_key, SC_PEER_PUBKEY_HEX); // 0=bin, 1=hex
sc_set_peer_public_key(&ctx, peer_public_key_bin, SC_PEER_PUBKEY_BIN); // 32 бинарных байта; HEX — для hex-строки
// 3. Ready for encrypt/decrypt
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len);
@@ -227,69 +245,107 @@ debug - отладка: всё что нужно для понимание су
info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения
warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться.
### Debug Categories (28 категорий)
```
NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6,
TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13,
SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20,
KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26,
MEMBER_SYNC=27
```
### Debug Categories
Актуальные имена и константы — в `lib/debug_config.h`, текстовые имена — в `lib/debug_config.c`.
Основные: `sys`, `connection`, `etcp`, `crypto`, `config`, `tun`, `routing`, `timers`, `bgp`,
`socket`, `control`, `dump`, `traffic`, `debug`, `general`, `nat`, `keepalive`, `etcp_route`,
`media`, `etcp_dump`, `chat`, `chat_sync`, `member_sync`, `proxy`, `video`, `reality`, `dm`, `call`, `radio`, `vad`, `aec`.
UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в ETCP; старые числовые списки не использовать.
### Настройка отладки
- В конфиге: `debug = etcp=trace,config=info` (формат: `категория=уровень,...`)
- В INI-конфиге: секция `[debug]`, каждая категория отдельной строкой (`etcp=trace`, `config=info`).
- Строку `etcp=trace,config=info` принимает `debug_parse_config()`; GUI хранит её в `[gui] debug_categories`.
- В коде: глобальный уровень и per-category уровни из `debug_config_t g_debug_config`
- Макросы: `DEBUG_ERROR(cat,fmt,...)` `DEBUG_WARN` `DEBUG_INFO` `DEBUG_DEBUG` `DEBUG_TRACE`
- `log_dump(prefix, data, len)` — hex dump в лог
- `log_dump(level, category, prefix, data, len)` — hex dump в лог
### Dual Output
- Консоль и файл настраиваются раздельно (`debug_set_console_level`, `debug_set_file_level`)
- Фильтр уровней общий для выходов; `debug_enable_console()` включает/выключает консоль.
- `debug_enable_file_output(path, truncate)` / `debug_disable_file_output()`
## Architecture
### Ядро, сервисы и владение ресурсами
- `utun_instance_create*()` создаёт ядро; `utun_core_start()` запускает общие сетевые службы.
- `utun_service_start/stop()` управляет UTUN-группой, TUN/NAT и запросами к пирам из конфига.
- `chat_service_start/stop()` управляет чатом и CHAT-группами. Чат не требует UTUN; desktop и Android запускают ядро и чат явно.
- Ядро владеет общей SQLite (`db_path/chats.db`, без пути — `:memory:`), идентичностью, транспортом и маршрутизатором.
Остановка сервиса сохраняет БД и ресурсы другого сервиса. `utun_instance_destroy()` освобождает ядро, но не переданный `UASYNC`.
- Группа владеет NCD handle с начала CONNECTING. Инициатор хранит отменяемый `TOPO_PEER_REQUEST`;
закрытие своего запроса не разрывает транспорт других владельцев. Готовая сессия сохраняется у группы.
- UP означает работоспособный транспорт. READY означает завершённое согласование и обмен таблицей конкретной группы.
Recovery ждёт нужных маршрутов через READY-пиров; наличие `ETCP_CONN*` само по себе не гарантирует UP.
- JOIN добавляет подписанного мембера, Merkle распространяет запись. Доступ к CHAT-группе каждый узел даёт по своей таблице участников;
invite-ключ не даёт временного членства. Протокол описан в `src/chat/chat_join.h`, групповое согласование — в `src/routing_layer/topo_group.h`.
- `TOPO_NODE` — общая подписанная запись узла. Более свежий timestamp заменяет запись целиком, с полями и подписью;
адреса не версионируются отдельно. BGP обновляет запись и передаёт изменения NCD (см. `doc/node_snapshot.md`).
- BGP хранит один путь к назначению от каждого соседа. WITHDRAW удаляет только путь отправителя;
NODEINFO с прежним timestamp также обновляет путь. Выбор: живой путь с минимумом хопов, затем меньший ID соседа.
REINIT сохраняет пути до проверки нового снимка; TABLE_COMPLETE удаляет неподтверждённые, зависший обмен перезапускается.
- ETCP-router дописывает ID при транзите и отбрасывает повторное посещение для DATA/ACK/control.
Список переходов находится вне подписанного тела; формат описан в `src/routing_layer/etcp_router.h`.
Все операции с ядром выполняются в его uasync-потоке; stop/destroy — вне callbacks останавливаемых сервисов.
Порядок освобождения ресурсов и нюансы media workers: `doc/service_lifecycle.md`.
### Directory Structure
```
├── lib/ # Core libraries (18 .c + 20 .h + libopus/liblmdb)
├── lib/ # uasync, очереди, SQLite, Opus, SpeexDSP, Silero VAD, SoundTouch, LMDB
├── src/ # Main source code
│ ├── transport_layer/ # ETCP stack, crypto, STCP, normalizer, BBR (~30 файлов)
│ ├── routing_layer/ # Routing, BGP, conn_mgr, etcp_router, NAT detection (~20 файлов)
│ ├── chat/ # P2P чат, db_sync, merkle_sync, member_sync (17 файлов)
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси (10 файлов)
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек (7 файлов)
│ ├── media_delivery/ # Доставка медиафайлов (6 файлов)
│ ├── media_async/ # Асинхронный движок медиа (2 файла)
│ └── *.c/h # Корневые модули: utun, config, TUN, firewall, NAT, NTP, control (~14 файлов)
├── tests/ # ~70 тестов, 64 в check_PROGRAMS
│ ├── transport_layer/ # ETCP, сессии, crypto, STCP/REALITY, SOCKS-клиент, NCD, BBR
│ ├── routing_layer/ # Routing, группы/BGP, recovery, conn_mgr, etcp_router
│ ├── chat/ # P2P чат, join, db_sync, merkle_sync, member_sync
│ ├── dm/ # Личные сообщения, E2E-вложения и offline-mailbox
│ ├── call/ # Звонки и общий голосовой стек
│ ├── radio/ # Групповая PTT-рация
│ ├── stream/ # Прямые трансляции: подписанные RS-блоки, выбор поставщиков, аудио/видео
│ ├── member_files/ # Файловое хранилище участников CHAT-группы и асинхронные передачи
│ ├── local_transport/ # Обнаружение устройств и доставка приглашений в локальной сети по UDP
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
│ ├── media_delivery/ # Блоки медиа каналов и передача неизменяемых файлов
│ ├── media_async/ # Фоновые задачи, метаданные вложений, подготовка голоса/видео
│ ├── video/ # Пробинг и транскодирование видео через FFmpeg
│ ├── dnsmasq/ # Архив исходников для встроенной сборки dnsmasq
│ └── *.c/h # utun, instance, config, TUN, firewall, NAT, NTP, control, broadcast
├── tests/ # Состав тестов и цели запуска: tests/Makefile.am
├── doc/ # Technical Specifications
├── tools/ # Auxiliary tools
│ ├── etcpmon/ # GUI монитор ETCP
│ ├── chatgui/ # GUI чат (Qt 6 с Qt 5 fallback, сборка CMake)
│ ├── chatgui-android/ # Android P2P чат (Jetpack Compose + libutun_lite)
│ ├── logreceiver/ # Приём UDP-логов Android
│ ├── chat_tcp_test/ # Сценарий проверки чата через TCP
│ ├── lightsout/ # Отдельная игра Lights Out на Qt
│ ├── lightsout-android/ # Android-версия Lights Out
│ ├── tdesktop-dev/ # Референс: Telegram Desktop (только для изучения)
│ ├── proxy/ # UDP прокси для тестов
│ └── bping/ # BPing (bandwidth ping)
│ ├── bping/ # BPing (bandwidth ping)
│ └── chatcli # Headless CLI; описание команд — chatcli_commands.txt
├── tinycrypt/ # TinyCrypt crypto library (external)
├── net_emulator/ # Network emulator (delays, loss, reordering)
└── c2/ # Test instance 2 (конфиг и бинарник для тестов)
└── net_emulator/ # Network emulator (delays, loss, reordering)
```
### File Overview
Пути ниже относительны к каталогу секции; `name.c/h` означает пару `.c` и `.h`.
Перечислены модули проекта; внутренние файлы сторонних библиотек сгруппированы по каталогам.
**Core (src/)**
- `utun.c` - Main program entry point, CLI parsing, daemon mode
- `utun_instance.c/h` - Root instance lifecycle, config loading, all submodule init
- `utun_instance.c/h` - Создание ядра и независимый жизненный цикл UTUN/чата, владение общими ресурсами
- `tun_if.c/h` - TUN interface API (init/write/close, cross-platform)
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
- `tun_route.c/h` - TUN routing table sync
- `config_parser.c/h` - INI-style config file parsing
- `config_updater.c/h` - Config file modification utilities
- `config_updater.c/h` - Проверка и сохранение согласованных X25519-ключей и node_id в INI-конфиге
- `control_server.c/h` - Control/monitoring server (etcpmon backend API)
- `firewall.c/h` - Firewall rules (per-interface filtering)
- `eim_nat.c/h` - Endpoint-Independent Mapping NAT engine
- `nat_transport.c/h` - NAT transport layer (packet relay)
- `ntp_time.c/h` - NTP time synchronization
- `ntp_node_time.c/h` - Inter-node time synchronization
- `nat_transport.c/h` - Передача IPv4 между отдельным TUN и удалённым NAT-провайдером через UTUN-router; локальный провайдер использует eim_nat
- `ntp_time.c/h` - Асинхронная NTP-синхронизация: поправка времени instance без изменения часов ОС
- `ntp_node_time.c/h` - Получение временной поправки от синхронизированных ETCP-пиров и диагностика расхождения часов
- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL)
- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p)
- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки
@@ -298,74 +354,142 @@ MEMBER_SYNC=27
- `etcp.c/h` - ETCP protocol implementation (inflight queues, retrans, ACK, RTT/jitter)
- `etcp_api.c/h` - ETCP public API (send/recv/bind callbacks)
- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
- `etcp_session.c/h` - Согласование эпох ETCP-сессии через HELLO/CHALLENGE/CONFIRM и проверка принадлежности DATA сессии
- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
- `etcp_debug.c/h` - ETCP packet dump/formatting
- `etcp_debug.c/h` - Диагностический разбор секций ETCP_DGRAM и форматирование IPv4-адресов
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
- `etcp_bbr.c/h` - BBR congestion control for ETCP
- `etcp_connect.c/h` - ETCP outbound connections
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
- `packet_dump.c/h` - Packet hex dump utility
- `dummynet.c/h` - Network emulator integrated into utun (for testing)
- `secure_channel.c/h` - AES-CCM encryption with X25519 key exchange, pubkey obfuscation
- `packet_dump.c/h` - Текстовая сводка IPv4-пакета: адреса, поля транспорта и короткий hex-фрагмент
- `dummynet.c/h` - Двунаправленный UDP-эмулятор между заданными endpoint: задержка, ограничение скорости и случайные потери
- `secure_channel.c/h` - X25519, пакетное AES-128-CCM, потоковое AES-CTR, подписи Ed25519 и обфускация публичного ключа
- `etcp_padding.c/h` - Выбор длины случайного padding и периодическое обновление диапазона для instance/соединения
- `etcp_tx_stats.c/h` - Общая статистика исходящих очередей, inflight и подтверждённых за секунду байтов ядра
- `crc32.c/h` - CRC32 checksums
- `stcp.c/h` - STCP протокол (Secure TCP)
- `stcp_server.c/h` - STCP сервер
- `stcp_client.c/h` - STCP клиент
- `stcp_link.c/h` - STCP линк-уровень
- `node_conn_direct.c/h` - Direct config-based connections
- `socket_monitor.c/h` - Socket state monitoring
- `stcp.c/h` - Зашифрованные кадры поверх TCP: согласование ключей/эпох, AES-CTR, CRC, очереди и закрытие соединения
- `stcp_server.c/h` - Приём входящих STCP-соединений, handshake и передача готового соединения владельцу
- `stcp_client.c/h` - Исходящее STCP-подключение, в том числе через SOCKS5 и REALITY
- `stcp_link.c/h` - Подключение STCP к ETCP_LINK: передача пакетов, backpressure и жизненный цикл TCP-линка
- `socks_client.c/h` - Исходящий SOCKS5-транспорт: TCP CONNECT и UDP ASSOCIATE с переподключением control-сокета
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
- `socket_monitor.c/h` - Наблюдение за адресами интерфейсов и маршрутами ОС, обновление сетевого состояния ETCP-сокетов
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
- `reality_io.c/h` - Внешний REALITY-handshake и поток защищённых записей STCP, Finished, replay-защита и выбор relay
- `reality_record.c/h` - TLS-подобные AES-128-GCM записи: вывод ключей, счётчики направлений, padding и KeyUpdate
- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт
- `stun_nat.c/h` - Проверка mapping/filtering NAT через внешние STUN-серверы, RFC 5780 и кеш результатов по IP/интерфейсу
- `call_ring.c/h` - Кольцевой трейс контрольных точек освобождения ETCP/STCP-ресурсов для диагностики teardown/UAF
**BBR (src/transport_layer/BBR/)**
- `bbr_v3.c` - BBR congestion control algorithm v3
**Routing Layer (src/routing_layer/)**
- `routing.c/h` - Routing table (local)
- `route_lib.c/h` - Routing library
- `route6_lib.c/h` - IPv6 routing
- `route_ping.c/h` - Route ping probing (NAT check, liveness)
- `route_connectivity.c/h` - Route connectivity checks
- `topo_group.c/h` - BGP-like route exchange between nodes (NODEINFO/WITHDRAW, изолированные группы UTUN/CHAT)
- `topo_group_connect.c/h` - Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл)
- `topo_group_invite.c/h` - Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP)
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
- `topo_recovery.c/h` - Восстановление каскадно отвалившихся узлов после разрыва соединения
- `routing.c/h` - Обмен IPv4-пакетами UTUN между локальным TUN и сервисом DATA ETCP-router
- `route_lib.c/h` - Локальная таблица IPv4-подсетей UTUN: добавление/удаление маршрутов и поиск узла назначения
- `route6_lib.c/h` - Локальная таблица IPv6-подсетей на radix tree с поиском по самому длинному префиксу
- `route_ping.c/h` - Пинг цели через явно выбранного BGP-посредника для NAT-проверки
- `route_connectivity.c/h` - Прямые UDP/TCP-пробы адресов узла для оценки достижимости и RTT при выборе пиров
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы по роли: суперузлы → desktop → mobile; история/RTT внутри класса, backoff и standby burst
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
- `topo_node.c/h` - Модель TOPO_NODE/TOPO_GROUP_NODE, подписанные снимки узлов, сериализация и общий node_registry
- `topo_node_sqlite.c/h` - Хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
- `nat_detection.c/h` - NAT type detection
- `etcp_router.c/h` - Надёжные логические каналы сервисов поверх BGP-маршрутов: ACK, окна, транзит и защита от циклов
- `route_tx_stats.c/h` - Скользящее окно подтверждённых payload-байтов логического канала для оценки скорости доставки
- `multicast.c/h` - Рассылка списку узлов группы с разделением маршрута на ветви, локальными подписками и отчётами о потерях
- `nat_detection.c/h` - Проверка mapping и доступности входящих рабочих сокетов через ETCP-пиров с учётом предварительных STUN-измерений
- `address_profile.h` - Профиль endpoint: изменение адреса, доступность входящих и источник измерения ETCP/STUN
- `sock_match.c/h` - Классификация локальных сокетов и адресов пира, отбор совместимых пар для прямого подключения
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
**Chat (src/chat/)** — децентрализованный P2P чат: сообщения и участники автоматически синхронизируются между всеми узлами канала без центрального сервера.
- `chat_core.c/h` - Главный модуль чата. Единственная точка входа из GUI: принять сообщение, создать канал, изменить настройку. Управляет БД и жизненным циклом всех chat-модулей
- `chat_core.c/h` - API сообщений, каналов, профиля и настроек; chat_service_start/stop управляют чат-сервисом поверх общей БД ядра
- `chat_core_priv.h` - Внутренний API для частей chat_core: общий контекст и утилиты БД. Снаружи не используется
- `chat_sync.c/h` - Связывает чат с ETCP-сетью: отслеживает появление/разрыв соединений, обновляет онлайн-статус узлов, запускает синхронизацию участников каналов, обрабатывает приглашения
- `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик
- `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей
- `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок
- `chat_setting.c/h` - Проверка и хранение настроек чата per-instance; общая логика для конфигурации и GUI
- `chat_join.c/h` - Регистрация invite-ключа с ACK, пересылка запроса инвайтеру и добавление подписанного мембера; контракт join в заголовке
- `db_sync.c/h` - Репликация подписанных записей SQLite через прямые ETCP-соединения; расхождения по цепочке хешей, новые записи через PUSH
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core)
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
- `chat_profile.c` - Подписанный профиль мембера: имя, аватар и опции сервисов; локальное UI-состояние (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
- `chat_feedback.c/h` - Подписанные реакции и ответы: версии состояния, сохранение отмен и Merkle-синхронизация между участниками
- `chat_poll.c/h` - Создание опросов, голосование, реакции и JSON-снимки результатов для UI/headless
- `chat_poll_codec.c` - Проверка и wire-кодеки опросов, ответов и реакций; общий каталог эмодзи (API в chat_poll.h)
- `chat_attachment.c/h` - Зашифрованные вложения канала: ключи, запросы доступа, ручное одобрение автора и сохраняемый outbox
- `chat_local.c/h` - Обнаружение устройств чата и обмен готовыми invite-ссылками через локальный UDP-транспорт
- `group_avatar.c/h` - Подписанная аватарка канала: хранение версии в SQLite, проверка подписи и передача блоков через router
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу
- `chat_whisper.c/h` - Whisper speech-to-text транскрипция голосовых сообщений (опционально, асинхронно через media_async)
- `chat_headless_control.c/h` - TCP control-socket для headless chat CLI (JSON line protocol)
- `invite_build.c/h` - Сборка invite-ссылок utun:// (выбор лучшего узла + адреса)
- `invite_link.c/h` - Формат invite-ссылок utun:// для каналов (base64: version|password|channel_id|pubkey|addrs)
- `invite_link.c/h` - Кодирование utun://: версия, пароль, channel_id, join_key, Reality-параметры, ключ и адреса узла
**Личные сообщения (src/dm/)**
- `dm_core.c/h` - Беседы двух узлов, подписанные E2E-сообщения, SQLite/outbox, доставка через router/mailbox и квитанции
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и E2E-контейнеры медиа через media_crypto
- `dm_history.c/h` - Локальное удаление истории и скрытие бесед; поколения задач и отпечатки удалённых сообщений против повторов
- `dm_mailbox.c/h` - Offline-очередь суперузла: хранение подписанных сообщений и повтор доставки до квитанции получателя
- `dm_media.c/h` - Подготовка, доставка и публикация E2E-вложений; ciphertext-хранилище и отдельные квитанции файлов
- `dm_media_priv.h` - Внутренние SQL/control-операции для dm_media и dm_mailbox_media
- `dm_mailbox_media.c/h` - Сохраняемые задания суперузла для размещения медиа, доставки метаданных и удаления копий до DELETE_ACK
**Звонки (src/call/)**
- `call.c/h` - P2P аудио/видеозвонок: подписанный E2E-сигналинг, состояния, политика занятости и оптимизация пути через conn_mgr
- `call_priv.h` - Общее внутреннее состояние сессии для call.c и call_video_transport.c; доступ только из uasync
- `call_proto.h` - Wire-формат сигналинга, аудиокадров и управления видеотреком; треки/кодеки и причины завершения
- `call_audio.c/h` - Общий аудио-движок: кодирование/декодирование Opus, jitter-буфер и тоны, API feed/pull PCM
- `call_jitter.cpp/h` - FIFO Opus, адаптивный сетевой запас, декодирование на pull и восстановление воспроизведения после пауз
- `call_jitter_window.h` - Окно из десяти секундных интервалов для оценки глубины очереди и изменения задержки доставки
- `call_clock.h` - Метки времени захвата и снимок реально проигранного аудио для синхронизации видеотрека
- `call_delay.cpp/h` - Уменьшение накопленной задержки PCM прямым ускорением и SoundTouch с сохранением исходной шкалы времени
- `call_timeline.cpp/h` - Карта исходного PTS через обработку SoundTouch до позиции аудиоустройства
- `call_video.c/h` - Общий формат H.264/H.265 видеотрека, согласование возможностей и сборка кадров из фрагментов
- `call_video_bitstream.c/h` - Проверка конфигурации и IDR без reorder, профиля/уровня и геометрии H.264/H.265; KEY-пробы кодеков
- `call_video_transport.c/h` - Отдельные router-каналы control/audio/video, backpressure, привязка видео к аудио и запросы KEY
- `call_video_rate.c/h` - Регулятор битрейта и допуска новых видеокадров по скорости доставки, RTT и долгу очереди
- `call_video_port.c/h` - Потокобезопасный обмен кадрами и командами между платформенными video workers и uasync-транспортом
- `call_video_playout.c/h` - Расписание показа по аудиочасам, ожидание/отбрасывание кадров и подтверждение фактического показа
- `call_video_fixtures.h` - Встроенные H.264/H.265 KEY-пробы для проверки базовых режимов кодека
- `call_video_fast_fixtures.h` - KEY-пробы видеорежимов с частотой 60 fps
- `call_video_fullhd_fixtures.h` - KEY-пробы видеорежимов Full HD
- `call_video_portrait_fixtures.h` - KEY-пробы видеорежимов в портретной ориентации
- `call_tones.c/h` - Генерация PCM-тонов паузы и завершения звонка
- `call_headless.c/h` - Отдельный TCP-аудиосокет headless-клиента: обмен PCM; сигналинг остаётся на control-сокете
**Рация (src/radio/)**
- `radio.c/h` - Передача Opus по CHAT-группе, подписки, дедупликация и пересылка по ветвям с подписчиками
- `radio_proto.h` - Wire-формат PTT: источник, поток, seq, FIN и hop-list против циклов
- `radio_audio.c/h` - Общий аудио-движок: независимые RX/TX, Opus, микширование источников, ручной PTT и VAD-автомат
- `radio_jitter.cpp/h` - Буфер кодированных кадров каждого источника с pre-roll и SoundTouch time-stretch на pull
- `radio_vad.h` - Ресемплинг 48→16 кГц и автомат авто-PTT с подтверждением речи, задержкой завершения и учётом занятого канала
- `radio_auto.h` - Общий автомат автопередачи по VAD/уровню звука, без захвата аудио и сетевого I/O
- `radio_call.c/h` - Адресное приглашение участника в рацию канала: сигналинг вызова, занятость, принятие и отмена
- `radio_headless.c/h` - TCP-аудиосокет headless-рации: PCM и команды начала/конца PTT
**Прокси (src/proxy/)**
- `socks_proxy.c/h` - SOCKS5 прокси
- `udp_proxy.c/h` - UDP прокси
- `icmp_proxy.c/h` - ICMP прокси
- `tcp_proxy_server.c/h` - TCP прокси (серверная сторона)
- `tcp_proxy_client.c/h` - TCP прокси (клиентская сторона)
- `proxy_protocol.h` - Общий TCP-протокол и управление потоком: CONNECTED, DATA, кредит WINDOW, упорядоченный FIN
- `socks_proxy.c/h` - Клиентские SOCKS5 CONNECT и HTTP proxy/CONNECT, DNS и передача потока выбранному exit-узлу
- `http_proxy.c/h` - Потоковый HTTP/1.x кодек: проверка заголовков, разбор тела/chunked и учёт потреблённых байтов для WINDOW
- `udp_proxy.c/h` - Передача UDP из TUN через exit-узел, сопоставление ответов и удаление неактивных потоков
- `icmp_proxy.c/h` - Проксирование IPv4 ICMP echo через raw socket exit-узла с восстановлением исходных id/seq
- `tcp_proxy_server.c/h` - Exit-сторона TCP-прокси: реальные исходящие сокеты, потоки по (peer, stream_id), окна и half-close
- `tcp_proxy_client.c/h` - Клиентская сторона TCP-прокси: адаптер TUN/lwIP и обмен с настроенным exit через ETCP-router
**lwIP TCP (src/lwip_tcp/)**
- `lwip_tcp.c/h` - lwIP TCP стек
@@ -374,31 +498,80 @@ MEMBER_SYNC=27
- `lwip_tcp_priv.h`, `lwip_tcp_opts.h` - Внутренние настройки lwIP
**Media Delivery (src/media_delivery/)**
- `media_delivery.c/h` + `media_delivery_proto.h` - Доставка медиафайлов, протокол
- `media_download.c/h` - Скачивание медиа
- `media_index.c/h` - Индексация медиафайлов
- `media_delivery.c/h` - Доставка блоков медиа каналов, учёт держателей, репликация доступности между суперузлами и relay
- `media_delivery_proto.h` - Кодограммы канального медиа: QUERY, BLOCK_REQ/CHUNK/DONE, HAVE_BLOCK и SUPER_REPL
- `media_download.c/h` - Загрузка блоков с выбором держателей, failover, контролем простоя, проверкой подписей и сборкой файла
- `media_index.c/h` - Индекс media_files: UUID, хеши и подписи блоков, асинхронная регистрация и удаление медиа канала
- `file_transfer.c/h` - Передача неизменяемого файла между заданными узлами; авторизация lookup и владение CM handle до завершения
**Media Async (src/media_async/)**
- `media_async.c/h` - Асинхронный движок (thread-per-task для crypto/file операций)
- `media_async.c/h` - Фоновые crypto/file-задачи: work в pthread, done в uasync; destroy ждёт workers и завершает ожидающие callbacks с CANCELLED
- `attachment.c/h` - Общие метаданные file/voice/video, проверка и компактная wire-сериализация
- `voice_file.c/h` - Worker-кодирование PCM в контейнер Opus с длительностью и waveform для desktop/Android
- `attachment_send.c/h` - Асинхронная подготовка голоса/видео и отправка в зафиксированный канал или DM
- `media_crypto.c/h` - Общие AES-256-CCM примитивы и порционное шифрование/проверка файлов для DM и групповых вложений
- `encrypted_file.c/h` - Контейнер группового вложения: шифрование метаданных/файла, проверка SHA256 и durable-публикация результата
**Трансляции (src/stream/)** — live-аудио/видео CHAT-группы с подписью источника, Reed–Solomon и раздачей через участников.
- `stream.c/h` - Жизненный цикл трансляции, локальные handles издателя/читателей, события и публикация подписок
- `stream_priv.h` - Внутренний доступ к сессиям, приём проверенных DATA/END и взаимодействие с планировщиком relay
- `stream_proto.h` - Форматы подписанной пачки и RS-частей, дескриптор источника, размеры и коды результата
- `stream_block.c/h` - Подпись пачки Ed25519 до RS-кодирования, разбор частей и проверка восстановленных данных
- `stream_buffer.c/h` - Ограниченное кольцо RS-частей: дедупликация, восстановление пачек, проверка подписи и вытеснение старых данных
- `stream_source.c/h` - Упаковка исходных байтов в подписанные RS-пачки, сброс неполной пачки по таймеру и завершение источника
- `stream_member.c/h` - Объявления источников и подписок в опциях подписанного мембера; разбор и проверка принадлежности источника
- `stream_wire.c/h` - Протокол выдач и relay: кодограммы управления, пути, метрики и подписанное завершение END
- `stream_network.c/h` - Выдача блоков через прямой ETCP, NCD-владение, управляющие очереди, аренда подписок и reverse-запросы
- `stream_optimizer.c/h` - Автоматический выбор и перенос поставщиков субпотоков, пробы доступности и привлечение relay
- `stream_metrics.c/h` - Оценка относительного отставания субпотока и истории проблем по часам источника без NTP
- `stream_load.c/h` - Учёт логической очереди DATA и её роста для обнаружения перегрузки раздающего узла
- `stream_media.c/h` - Формат uAV2: упаковка/проверка видеокадров, конфигурации кодека, PTS и Opus внутри FEC-пачек
- `stream_media_buffer.c/h` - Медиаиндекс общего RS-кольца и независимые курсоры audio/video; сборка кадров и восстановление с KEY
- `stream_publisher.c/h` - Общий потокобезопасный publisher desktop/Android: очередь готовых кадров, публикация и завершение с FIN
- `stream_viewer.c/h` - Независимые viewers: запрос ограниченных батчей по бюджетам треков, уведомления готовности и конца audio/video
- `stream_playback_audio.cpp/h` - Opus/SoundTouch и очередь готового PCM; ускорение без изменения высоты и привязка к позиции устройства
- `stream_playback_clock.c/h` - Общие часы воспроизведения и плавный догон live-потока по непрерывному запасу готового материала
**Файлы участников (src/member_files/)**
- `member_files.c/h` - Сервер и асинхронный клиент хранилища CHAT-группы: права доступа, запросы и события через защищённый router
- `protocol.c/h` - Wire-кодек файловых команд, снимки версий каталогов/файлов и токены временных загрузок
- `storage.c/h` - Серверные каталоги участников, квоты, файловые операции, временные загрузки и их очистка
- `transfer.c` - Передача файлов с CM handle, backpressure, дисковым worker, проверкой хеша и публикацией результата
- `disk.c/h` - Дисковые примитивы POSIX/Windows: безопасное разрешение путей без ссылок и I/O по 64-битному смещению
- `worker.c/h` - Единственный дисковый pthread с ограниченной очередью и возвратом результатов в uasync
- `private.h` - Внутреннее состояние запросов, передач, серверного хранилища и дискового worker
**Локальная сеть (src/local_transport/)**
- `local_transport.c/h` - Открытый UDP-транспорт LAN: обнаружение устройств, доставка сообщений с ACK/повторами и ограничения входящего потока
- `platform.c/h` - Снимок IPv4/IPv6 интерфейсов, broadcast/multicast-адреса, приём метаданных UDP и проверка локальности источника
- `protocol.h` - Wire-формат объявлений, сообщений и ACK: ID/эпохи устройств, ID запроса и проверка UTF-8 имени
**Libraries (lib/)**
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
- `ll_queue.c/h` - Lock-free linked list queue with callbacks, hash index, threshold waiter
- `memory_pool.c/h` - Fast object pool allocator (pre-allocated blocks)
- `ll_queue.c/h` - Двусвязная очередь одного uasync-потока с callbacks, хеш-индексом и ожиданием свободного места
- `memory_pool.c/h` - Пул объектов: ленивое выделение и повторное использование, до 64 свободных объектов в кеше
- `debug_config.c/h` - Debug logging system (levels, categories, dual output, runtime config)
- `timeout_heap.c/h` - Min-heap for timer management (used by u_async)
- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`)
- `serialize.c/h` - Binary serialization utilities
- `reed_solomon.c/h` - Кодирование и восстановление пачки данных Рида—Соломона с настраиваемым числом субблоков и избыточностью
- `video_plane.c/h` - Копирование YUV-плоскости с поворотом 0/90/180/270°, учётом stride и проверкой границ без выделения памяти
- `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
- `getmyip.c/h` - Get local IP / default route detection
- `getmyip.c/h` - Локальный IP, который ОС выберет для UDP к заданному адресу (bind/connect/getsockname)
- `myip.c` - My IP helper (no header)
- `socket_compat.c/h` - Cross-platform socket compatibility (Linux/FreeBSD/Windows)
- `platform_compat.c/h` - Cross-platform compatibility layer (byte order, time, random)
- `radix.c/h` - Radix tree for IP routing lookups
- `tcp_io.c/h` - TCP I/O abstraction (Windows IOCP, Linux epoll)
- `tcp_io.c/h` - Неблокирующий TCP поверх uasync/ll_queue: backpressure, FIN/close и callbacks
- `sqlite3.c/h` - SQLite3 amalgamation (embedded database)
- `opus_codec.c/h` - Opus audio codec wrapper
- `audio_compressor.c/h` - Audio dynamic range compressor
- `speex_aec.c/h` - Эхоподавление SpeexDSP: согласование playback/capture PCM через линию задержки и диагностика её заполнения
- `silero_vad.c/h` - Стриминговый VAD через ONNX Runtime: окна 512 отсчётов, 16 кГц моно, вероятность речи и сброс состояния
- `silero_vad_model.inc` - Встроенная ONNX-модель Silero VAD для silero_vad_create_default
- `json_flat.c/h` - Flat JSON parser
- `strbuf.c/h` - Безопасный растущий printf-буфер (замена snprintf-цепочек)
- `async_dns.c/h` - Асинхронный (неблокирующий) DNS-резолвер A-записей поверх uasync, адаптер над libdns
@@ -407,8 +580,20 @@ MEMBER_SYNC=27
- `dr_mp3.h` - MP3-декодер (single-header, на базе minimp3)
- `liblmdb/` - Встраиваемый key-value store LMDB
- `libopus/` - Встраиваемый кодек Opus
- `speexdsp/` - Встроенные исходники эхоканселлера SpeexDSP и KISS FFT
- `soundtouch-master/` - SoundTouch: изменение темпа аудио в jitter-буферах звонка и рации
- `wintun.h` - Wintun API header for Windows TUN driver
**Проверки (tests/)**
- `Makefile.am` - Состав тестовых целей, зависимости и отдельные проверки runner, service IDs и скрипта сборки
- `run_tests.py` - Запуск тестов ограниченным пулом с отдельными логами и последовательными барьерами для CPU/LAN-проверок
- `test_run_tests.py` - Проверки пула runner, эксклюзивного запуска, обработки ошибок и сигналов
- `test_service_ids.py` - Проверка уникальности ID транспортных и router-сервисов
- `test_build_script.py` - Регрессии выбора платформы, конфигурации и инкрементальной работы build.sh
- `stream_netns/` - Linux network namespaces: топологии, NAT/STUN-сценарии и проверка трансляций в изолированной сети
- `local_transport_ipv6.sh` - Проверка IPv6 link-local обнаружения и доставки в изолированной локальной сети
**Memory Pools in UTUN_INSTANCE:**
- `data_pool` — для данных пакетов (payload), используется input_queue/output_queue фрагментами
- `pkt_pool` — для struct ETCP_DGRAM (сетевые пакеты на отправку/приём)
@@ -416,7 +601,7 @@ MEMBER_SYNC=27
### Key Components
- **UASYNC:** One per thread. Async event loop (epoll/poll), timers via timeout_heap
- **LL_QUEUE:** Lock-free queue with auto-callback, hash index lookup, threshold waiter
- **LL_QUEUE:** Очередь с auto-callback, хеш-индексом и threshold waiter; не средство обмена между потоками
- **Memory Pool:** Fast allocation for hot-path objects (packets, inflight entries, fragments)
- **ETCP:** TCP-like reliable protocol with encryption, multi-link, load balancing
- **Secure Channel:** AES-CCM + X25519 key exchange, nonce-based encryption, pubkey obfuscation
@@ -432,6 +617,8 @@ MEMBER_SYNC=27
- Очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой.
- Порог задаётся через `queue_set_threshold(q, max_packets, max_bytes)`.
- Используй `queue_waiter_wait` для ожидания освобождения очереди до заданного порога.
- Handle ожидания обнуляется целиком, живёт до callback/отмены и отменяется до освобождения владельца.
Callback может быть синхронным. `queue_entry_free` не освобождает dgram — его освобождают отдельно.
### Чтение из очереди
- Используй `queue_set_callback`: при вызове callback обработай один или несколько элементов, потом вызови `queue_resume_callback`.
@@ -451,7 +638,8 @@ MEMBER_SYNC=27
## Config Rules
- В серверном конфиге только собственные ключи и нет секций `[client]`
- В клиентском конфиге есть собственные ключи и pubkey каждого сервера в секции `[client]`
- Chat-настройки хранятся в секции `[chatserver]` (принимается также `[chat]`). Парсер вызывает `chat_setting_set(key, value)` для каждого ключа — неизвестные ключи вызывают ошибку. Пример:
- Chat-настройки хранятся в `[chatserver]` (также `[chat]`). Парсер обрабатывает общие поля секции,
затем вызывает `chat_setting_set_state(&cfg->global.chat_settings, key, value)`; неизвестные ключи логируются как ошибка. Пример:
```ini
[chatserver]
storage_autoload=1
@@ -474,7 +662,8 @@ MEMBER_SYNC=27
Если видишь спам-логи - подумай как их выборочно отключить чтобы не мешали.
Для отладки добавляй в DEBUG_CATEGORY_DEBUG диагностические сообщения где надо. И включи эту категорию в настройках. Убирай только после проверки (путём запуска) когда все ошибки устранены.
сообщение выводится по или - либо debug_set_level(DEBUG_LEVEL_TRACE) - выводится ВСЁ независимо от настроек по категориям. Тоесть берется max(global level, category lavel)
Явный уровень категории заменяет глобальный. DEBUG_LEVEL_NONE наследует глобальный уровень;
DEBUG_LEVEL_DISABLED полностью отключает категорию, в том числе при глобальном TRACE (см. debug_should_output).
Твой бич - ты постоянно гадаешь и анализируешь код. Что приводит к снежному кобу ошибок и неверных гипотез. 10 раз повторяю - только логи логи логи и никакого гадания. Лоооги!!! правильные логи покажут всё с предельной точностью. Вся суть отладки - информативные логи
И смотри логи. не задавливай их grep-ом. лучше больше. единственное с чем борись - это бесполезные спам логи.
@@ -484,7 +673,8 @@ MEMBER_SYNC=27
Но полезные смотри всегда, и всегда оставляй логи которые выводятся нечасто.
Частые логи - это трафик которые >100 раз повторяются. Но которые мало раз обязательно оставлять и выводить.
можешь в тесте включить логи в нужный интервал чтобы не спамить. debug_set_level(DEBUG_LEVEL_TRACE) и выключить debug_set_level(DEBUG_LEVEL_NONE)
В тесте можно временно поднять нужные категории до TRACE, сохранив прежние уровни и восстановив их после участка.
Одного debug_set_level(DEBUG_LEVEL_NONE) недостаточно для отключения категорий с явно заданным уровнем.
Если видишь проблему и ее решение неочевидно то выстраивай диагностику вокруг неё пока не будет очевидно где и что происходит не так. Это базовое и обязательное требование к отладке.
Подробные логи ты должен выводить не обрезая всегда и анализировать. Если лог большой - выведи в файл и анализируй файл.
Рассуждение должно быть примерно таким:
@@ -526,7 +716,7 @@ MEMBER_SYNC=27
- Нельзя ничего восстанавливать из репозитория не спрашивая
- Для отладки не printf а DEBUG_*
- Запрещено прямое включение arpa/inet.h, sys/time.h, unistd.h — использовать ../lib/platform_compat.h + socket_compat.h
- Перед сборкой всегда make clean
- Меняй режим и флаги сборки через `./build.sh`: он отслеживает конфигурацию и очищает объекты только при необходимости.
- Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства
## Написание кода
@@ -535,12 +725,17 @@ MEMBER_SYNC=27
- Обязательно наличие подробной диагностики в функциях кода которые не сильно спамят (не часто вызываются).
- Для каждого отладочного вывода подумай какая из доступной информация будет полезна чтобы можно было наиболее завершенно оценить состояние алгоритма и состояний влияющих на алгоритм.
## Оформление кода
- в .c: перед каждой функцией - краткое но понятное описание что делает функция (кроме совсем простых)
- в .h: в начале - описание модуля - для чего предназначен, как пользоваться, нюансы. Далее структуры с комментариями, далее публичные функции с описаниями (что делает, как пользоваться, какие аргументы, возврат, нюансы работы)
## chatgui (GUI Chat Client)
Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отдельный проект внутри репозитория. Не связан с autotools-сборкой utun.
Chatgui (бинарник `vibechat`) — десктопный чат на Qt 6 (Qt 5 fallback), отдельная CMake-сборка общего ядра.
Корневой `build.sh` дополнительно вызывает её, если уже существует `tools/chatgui/build/CMakeCache.txt`.
В chat gui интегрированы библиотеки utun. Чат и библиотеки работают в разных потоках. Поэтому нужно использовать семафоры, сокеты или другие механизмы синхронизации (uasync_post, uasync_memsync, uasync_get_wakeup_fd)
GUI работает в Qt-потоке, ядро и чат — в uasync-потоке. Команды передаются через
`gui_bridge_post_uasync()` / `uasync_post()`, события возвращаются через Qt-сигналы.
База данных sqlite в chatgui: можно писать (update) из потока utun / instance. можно только читать из gui потока.
@@ -548,38 +743,45 @@ Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отде
- **Язык:** C++20
- **Фреймворк:** Qt 6 (предпочитаемый) или Qt 5.15+ (Widgets + Network)
- **Сборка:** CMake 3.16+
- **БД:** SQLite3 (WAL mode, встроенный `db/sqlite3.c` amalgamation)
- **БД:** SQLite3 (WAL mode, компилируется общий `lib/sqlite3.c`)
- **Анимации:** rlottie (C API) + zlib (gzip-декомпрессия TGS)
### Сборка
```bash
# Установка зависимостей
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev libssl-dev libx11-dev libxfixes-dev libpipewire-0.3-dev
# или qtbase5-dev для Qt 5 fallback
# Сборка
cd tools/chatgui && mkdir -p build && cd build
cmake .. && make -j4
./chatgui
./vibechat
```
### Конфиг и логи чатгуи
- **Конфиг:** `tools/chatgui/build/chatgui.cfg` (в `.gitignore`, содержит приватные ключи)
- **Конфиг:** `vibechat.cfg` рядом с бинарником (обычно `tools/chatgui/build/vibechat.cfg`, содержит приватные ключи)
- Секция `[gui]`: `debug_file`, `debug_level`, `debug_categories=cat=level,...`
- Секция `[control]`: `ip`, `port` для подключения etcpmon
- Секция `[ntp]`: синхронизация времени
- **Лог:** `tools/chatgui/build/chatgui.log` (путь задаётся в конфиге `debug_file`)
- **БД чата:** `tools/chatgui/build/chat_data/` (SQLite, путь из `db_path` в конфиге)
- **БД чата:** `chats.db` в каталоге `[gui] db_path` (по умолчанию `chat_data/` рядом с бинарником)
### Структура файлов (src/)
| Файл | Назначение |
|------|-----------|
| `main.cpp` | Точка входа, инициализация QApplication |
| `mainwindow.h/cpp` | Главное окно, QSplitter (ChannelList \| MessageList \| AccountList), трей |
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
| `messagelist.h/cpp` | QStandardItemModel, stub-данные, инициализация анимаций, hover→activate |
| `main.cpp` | Точка входа, QApplication, язык интерфейса и запуск единственного экземпляра профиля |
| `single_instance.h/cpp` | Блокировка каталога профиля и передача запроса показа окна уже работающему экземпляру |
| `gui_language.h` | Выбор языка интерфейса независимо от регионального формата дат и чисел |
| `mainwindow.h/cpp` | Главное окно, каналы/DM, трей, события ядра, звонки, локальные приглашения и окна трансляций |
| `chatview.h/cpp` | Список сообщений: фон, hover, выделение текста и прокрутка истории мышью/клавиатурой |
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
| `messagefeedback.cpp` | Действия MessageList: создание опроса, голосование, выбор реакции и показ результатов |
| `attachment_key_ui.cpp` | Список запросов на расшифровку групповых вложений и решения автора о выдаче ключа |
| `message_links.h/cpp` | Разбор границ utun:// и HTTP/HTTPS ссылок для подсветки и обработки нажатий в сообщении |
| `message_notification.h/cpp` | Уведомление о сообщении без захвата фокуса, переход в беседу и скрытие после активности пользователя |
| `user_activity_monitor.h/cpp` | Ожидание первой активности через системный idle time либо события приложения |
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
| `emojitabbar.h/cpp` | Кастомный QTabBar для эмодзи-панели |
@@ -595,33 +797,99 @@ cmake .. && make -j4
| `invitedialog.h/cpp` | Диалог приглашения пользователя |
| `invite_link.h/cpp` | Логика invite-ссылок |
| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) |
| `dminvitedialog.h/cpp` | Выбор канала для приглашения собеседника из DM с сопоставлением результата по request_id |
| `settingsdialog.h/cpp` | Окно настроек |
| `creategroupdialog.h/cpp` | Диалог создания группы |
| `channelsettingsdialog.h/cpp` | Диалог настроек канала |
| `renamedialog.h/cpp` | Диалог переименования |
| `networksettingspage.h/cpp` | Страница сетевых настроек |
| `addressprofiletext.h` | Читаемое представление mapping, доступности входящих и источника NAT-измерения |
| `nodespage.h/cpp` | Таблица узлов + дамп в настройках |
| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) |
| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках |
| `storagesettingspage.h/cpp` | Страница настроек хранилища |
| `soundsettingspage.h/cpp` | Страница настроек звука |
| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств |
| `sound_manager.h/cpp` | Управление звуками/уведомлениями |
| `audiorecorder.h/cpp` | Запись голосовых сообщений (захват аудио) |
| `voicemessageencoder.h/cpp` | Кодирование голосовых сообщений (Opus) |
| `sound_manager.h/cpp` | Общий miniaudio-контекст, уведомления/PCM, микширование голоса, маршруты устройств и восстановление |
| `audio_device.h/cpp` | Жизненный цикл miniaudio-устройства, прогресс callbacks, пауза и повтор запуска после сбоя |
| `audio_buffer_tuning.h` | Подбор размера аппаратного аудиобуфера по повторным XRUN с сохранением настройки при reopen |
| `audio_capture.h/cpp` | Совместное использование одного микрофона трансляцией и рацией, подписки PCM и смена профиля захвата |
| `audio_capture_lease.h` | Владение захватом, исключающее одновременную работу publisher, звонка и записи голосового сообщения |
| `audio_event_queue.h` | Ограниченная очередь PCM/команд с несколькими producers и резервом для управляющих событий |
| `audio_frame_ring.h` | Предвыделенное SPSC-кольцо кадров переменной длины под mutex; при переполнении удаляет старейший |
| `voice_audio_io.h/cpp` | Workers захвата/AEC и воспроизведения; упорядочивание PCM/PTT/mute и итоговый render-reference |
| `voice_capture_clock.h` | Непрерывные PTS захваченного PCM по позиции сэмплов с плавной поправкой аппаратного времени |
| `voice_playback_buffer.h` | Адаптивный запас готового PCM: увеличение после недостачи и уменьшение при стабильном выводе |
| `audiorecorder.h/cpp` | Захват голосовых сообщений, worker обработки PCM, компрессор, индикатор уровня и передача записи задаче |
| `voicemessageencoder.h/cpp` | Выбор Opus preset и создание медиа-каталога; кодирование выполняет общий voice_file |
| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений |
| `callwindow.h/cpp` | Окно аудио/видеозвонка: управление, выбор камеры, локальное/удалённое видео и статистика |
| `call_audio_engine.h/cpp` | Desktop-контур звонка: захват, VoiceAudioIo, общий выход SoundManager и завершение аудиосессии |
| `call_video_engine.h/cpp` | Desktop-видеозвонок: камера, TX/RX workers, обмен с ядром и показ кадров по аудиочасам |
| `call_video_encoder.h/cpp` | Кодер звонка: согласованный формат, смена битрейта с drain прежнего кодера и подтверждение настройки |
| `call_video_decoder.h/cpp` | FFmpeg-декодер H.264/H.265 с выбором GPU/CPU и сохранением PTS при задержанном выходе |
| `call_camera_capture.h/cpp` | Управление отдельным процессом камеры, каталог устройств/режимов и получение кадров |
| `call_camera_worker.cpp` | Процесс Qt-захвата камеры: настройка устройства, отбор кадров и выдача пикселей через shared memory |
| `call_camera_source.h` | Интерфейс backend камеры для процесса захвата и подстановки устройств в тестах |
| `call_camera_protocol.h` | Локальный протокол управляющих команд, каталога устройств и FRAME/ACK процесса камеры |
| `call_camera_buffer.h` | Владение shared-memory буфером пикселей между процессами захвата и GUI |
| `call_camera_rate.h` | Временное отключение снижения FPS из-за автоэкспозиции V4L2 с восстановлением настройки камеры |
| `call_video_cadence.h` | Отбор исходных кадров под заданный FPS без накопления ошибки дробного периода |
| `call_video_capture_clock.h` | Привязка времени камеры к монотонным часам с коррекцией задержки первого callback |
| `call_video_queue.h` | Ограниченная SPSC-очередь кадров между video workers без mutex и выделений памяти |
| `call_video_wake.h` | Пробуждение video worker по событию или абсолютному монотонному deadline на POSIX/Windows |
| `radio_audio_engine.h/cpp` | Desktop-контур рации: независимые захват/прослушивание, PTT/VAD и подключение к общему выходу |
| `radiopanel.h/cpp` | Панель активной рации: выбор аудиоустройств, VAD, PTT и состояние передачи из ядра |
| `radiosettingspage.h/cpp` | Настройки рации и запись глобальных сочетаний PTT |
| `ptt_hotkey_manager.h/cpp` | Глобальный PTT: физические клавиши Windows/X11, системный портал Wayland |
| `ptt_key_chord.h` | Хранение физических сочетаний с отдельными левыми/правыми модификаторами и запись одновременного нажатия |
| `ptt_portal.h/cpp` | D-Bus-сессия GlobalShortcuts для Wayland, настройка сочетаний и обработка нажатий/отпусканий |
| `media_blocks.h/cpp` | Чтение/сборка фрагментированных медиафайлов |
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
| `videoplayer_window.h/cpp` | Окно просмотра видео |
| `live_video_encoder.h/cpp` | Общий FFmpeg-кодер live-видео для звонка/трансляции, GPU/CPU, KEY-конфигурация и capture PTS |
| `video_frame_image.h` | Выделение выровненного RGB-буфера FFmpeg и передача владения QImage без копирования |
| `stream_publish_engine.h/cpp` | Публикация камеры/экрана и Opus: workers кодирования, локальный preview и завершение через общий publisher |
| `stream_publish_window.h/cpp` | Настройки и управление трансляцией; закрытие окна сохраняет работающий engine |
| `stream_portal_capture.h/cpp` | Захват экрана Wayland через ScreenCast portal/PipeWire, выбор монитора и режима курсора |
| `stream_screen_cursor.h/cpp`, `stream_screen_cursor_win.cpp` | Наложение системного курсора на кадр выбранного экрана средствами X11/Windows |
| `stream_video_modes.h` | Выбор размеров/FPS камеры и пропорционального масштабирования экрана для трансляции |
| `stream_qp_control.h` | Плавная корректировка потолка QP по фактическому выходному видеобитрейту |
| `stream_sources.h` | Разбор снимка активных источников канала из CHAT_EVT_STREAM_SOURCES |
| `streamplayer_window.h/cpp` | Просмотр live-потока: отдельные audio/video workers, смешивание PCM и синхронизация по часам устройства |
| `streamgrid_window.h/cpp` | Сетка трансляций канала и разворачивание плитки с сохранением плееров; своя плитка использует локальный preview |
| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) |
| `qrcode_utils.h/cpp` | Утилиты для QR-кодов |
| `debug_ui.h` | Отладочный UI |
### Другие поддиректории
- `db/` — SQLite3 amalgamation (`sqlite3.c/h`), `db_manager.h/cpp` (схема: nodes, node_addresses, channels, msg_<channel>, peers_<channel>, local_identity, accounts, ui_state)
- `transport/` — интеграция с uTun: `gui_bridge.h/gui_bridge_impl.cpp` (Qt signal-based обмен), `node_config.h/cpp` (конфиг узла), `config_updater.h/cpp` (правка конфига), `utun_node.h/cpp` (обёртка узла), `miniaudio_impl.c` (single-header miniaudio в отдельной единице трансляции). Chat-логика (`chat_core`, `chat_sync` и т.д.) компилируется напрямую из `src/chat/`, роутинг — из `src/routing_layer/` (см. `libutun/CMakeLists.txt`)
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
- `transport/` — интеграция GUI с ядром; файлы перечислены ниже
- `src/avatars/` — `avatarmanager.h/cpp`: загрузка/публикация аватаров участников через member_files, подписанные ссылки и очередь передач;
`avatarcache.h/cpp`: дисковый кеш, подготовка изображений и очистка; `avatarcrop.h/cpp`: выбор области;
`avatargallery.h/cpp`: просмотр аватаров пользователя; `groupavatarmanager.h/cpp`: подготовка и кеш подписанных аватарок каналов
- `src/member_files/` — `browser.h/cpp`: браузер серверных файлов группы; `batch.h/cpp`: последовательные операции над файлами/деревьями,
фоновый локальный обход, загрузка/скачивание и отмена
- `libutun/CMakeLists.txt` — состав и зависимости CMake-сборки общего ядра из `src/` и `lib/`
- `CMakeLists.txt` — приложение vibechat, Qt-зависимости, ресурсы и цели GUI-тестов
- `tests/` — проверки аудиотракта, PTT, видеозвонка, захвата камеры/экрана, трансляций, файлового браузера и GUI-компонентов
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
- `zxing-cpp/` — встроенная библиотека QR-кодов; `third_party/qhotkey/` — исходники QHotkey
### Интеграция с ядром (transport/)
| Файл | Назначение |
|------|-----------|
| `gui_bridge.h` / `gui_bridge_impl.cpp` | Команды GUI → uasync и события ядра → Qt-сигналы |
| `utun_node.h/cpp` | Запуск и остановка ядра/чата в выделенном uasync-потоке |
| `node_config.h/cpp` | Загрузка и сохранение конфигурации узла |
| `config_updater.h/cpp` | Изменение настроек в конфиге |
| `miniaudio_impl.c` | Реализация single-header miniaudio в отдельной единице трансляции |
| `audio_diagnostics.h/cpp` | Диагностика этапов аудио и событий backend; записывает метаданные без PCM |
| `stream_bridge.h/cpp` | Потокобезопасный мост к общим publisher/viewers: session IDs, каталог источников, запросы батчей и пробуждение workers |
### Система анимированных эмодзи
@@ -715,18 +983,33 @@ void lottie_animation_destroy(Lottie_Animation *anim);
---
## Key Documentation Files
- `/doc/etcp_protocol.txt` - ETCP протокол (формат кодограмм, ACK, handshake, keepalive)
- `/doc/etcp_arch.md` - ETCP архитектура
- `/doc/etcp_config.txt` - Конфигурация ETCP
- `/doc/etcp_router_arch.md` - ETCP роутер архитектура
- `/doc/route_p2pconn.txt` - Route peer-to-peer соединения
- `/src/route_bgp.txt` - BGP обмен маршрутами (дизайн-документ)
- `/doc/chat_join_test.md` - ТЗ теста join-протокола (chat_join + дерево приглашений)
- `/doc/chat_join_test_tasks.md` - Список задач по тесту join (текущий статус, пошагово)
- `doc/service_lifecycle.md` — ядро, независимые сервисы, владение ресурсами и остановка
- `doc/node_snapshot.md` — подписанная запись узла, timestamp, SQLite и обновление NCD
- `doc/etcp_protocol.txt` — ETCP: кодограммы, ACK, handshake, keepalive
- `doc/etcp_arch.md` — архитектура ETCP
- `doc/etcp_router_arch.md` — архитектура маршрутизатора
- `src/chat/chat_join.h` — нормативный протокол добавления участника
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
- `doc/call_video.txt` — видеотрек звонка: возможности кодеков, транспорт, битрейт, аудиочасы и показ кадров
- `doc/stream.txt` — трансляции: RS-пачки, выдачи/relay, выбор поставщиков, медиакурсоры и платформенные плееры
- `doc/stun_nat.md`, `doc/nat_detection.txt` — STUN-измерения и определение доступности рабочих UDP-сокетов
- `doc/member_files.txt` — файловое хранилище участников, права, wire-команды и передачи
- `doc/group_avatar.txt` — подписанная аватарка канала, хранение и репликация
- `doc/encrypted_attachments.md` — контейнер группового вложения, запросы ключа и ручная выдача доступа
- `doc/pm_invites.md`, `doc/message_links.md` — приглашения из личной беседы и ссылки внутри сообщений
- `doc/proxy_protocol.md` — TCP/UDP/ICMP-прокси, управление окнами, half-close и проверки
- `doc/lwip_nuances.md` — особенности адаптированного lwIP TCP-стека
- `doc/linux_audio_recovery.md` — диагностика аудиотракта Linux, контракты владения и восстановление устройств
- `src/media_delivery/file_transfer.h`, `src/dm/dm_media.h` — контракты передачи файлов и E2E-вложений DM
- `src/call/call_audio.h`, `src/radio/radio_audio.h` — API общего голосового стека и правила потоков
- `tools/chatcli_commands.txt` — команды headless CLI, в том числе DM, звонки и рация
## Список задач по проекту
Список задач — `/doc/tasks.md` (текущая работа, берём по одной задаче сверху).
Список задач — `doc/tasks.md` относительно корня репозитория (текущая работа, берём по одной задаче сверху).
При нахождении попутных багов/fail/флаков тестов добавляем задачу (либо фиксим сразу если баг простой).
Как сделали - помечаем [+] выполнено.
@@ -734,18 +1017,47 @@ void lottie_animation_destroy(Lottie_Animation *anim);
**Расположение:** `tools/chatgui-android/`
Android-версия чатгуи — P2P чат на STCP (TCP), UI на Jetpack Compose (Kotlin).
C-ядро: `libutun_lite` (выборочная компиляция нужных .c из `lib/` и `src/`).
Android-версия чатгуи — P2P чат на общем ETCP/STCP-ядре, UI на Jetpack Compose (Kotlin).
C-библиотека `utun_lite` собирает `lib/` и `src/` по `libutun_lite/utun_sources.cmake`;
`instance_lite` запускает ядро и чат в отдельном потоке, без UTUN-сервиса.
Сборка: CMake (headless, Linux) + Gradle/NDK (Android APK).
- 'fw' - собрать и обновить chatgui-android на всех подключенных телефонах (clean + сборка + install)
**Подробная инструкция:** `tools/chatgui-android/AGENTS.md`
**Chat-модули:** все файлы из `src/chat/` (описаны выше в секции «Chat») компилируются в `libutun_lite`.
### Основные файлы native-слоя
Пути относительны к `tools/chatgui-android/`; UI и Android-сервисы описаны в локальном `AGENTS.md`.
| Файл | Назначение |
|------|-----------|
| `libutun_lite/utun_sources.cmake` | Общий список исходников ядра для Android и headless |
| `libutun_lite/CMakeLists.txt` | Сборка native-библиотеки и зависимостей |
| `libutun_lite/instance_lite.c/h` | Запуск ядра/чата по INI-тексту и управление отдельным uasync-потоком |
| `libutun_lite/utun_config_api.c/h` | Провайдер конфигурации: callbacks Kotlin и значения для native-слоя |
| `libutun_lite/standby.c/h` | Фоновый duty-cycle, таймеры активных интервалов и ожидание пробуждения |
| `libutun_lite/invite_link_c.c/h` | Старая копия C-кодека; .c исключён из UTUN_SOURCES, рабочий код — src/chat/invite_link.c/h |
| `libutun_lite/attachment_sender.c/h` | Отправка файлов в канал через chat_msg_submit и канальный медиа-индекс |
| `libutun_lite/photo_sender.c/h` | Отправка фотографий в канал с MIME-типом и размерами изображения |
| `libutun_lite/video_sender.c/h` | Отправка видео через общий механизм подготовки вложений |
| `libutun_lite/voice_recorder.c/h` | Накопление PCM записи, компрессор и отправка голоса через общий attachment_send |
| `libutun_lite/stream_bridge.c/h` | Android-мост к publisher/viewers: session IDs, готовые access units/Opus и ограниченные батчи треков |
| `jni_bridge/android_jni_bridge.c/h` | JNI-команды и callbacks между Kotlin и C-ядром, события и аудио API |
| `jni_bridge/android_call_output_jni.c` | Вывод PCM звонка через AAudio с аппаратным тактированием и снимками позиции устройства |
| `jni_bridge/android_call_video_jni.c` | JNI видеозвонка: capabilities/формат, обмен кадрами с call_video_port, playout и поворот YUV |
| `jni_bridge/android_stream_jni.c` | JNI трансляций: publisher/viewer API, Opus/SoundTouch, готовый PCM и часы воспроизведения |
| `jni_bridge/android_udp_log.c/h` | Потокобезопасный буфер UDP-логов с flush-таймером в потоке ядра |
| `headless/headless_main.c` | Отдельный каркас control-loop; сам не запускает ядро/чат |
| `headless/headless_control.c/h` | Каркас control-сокета; часть команд — заглушки, рабочий API — src/chat/chat_headless_control.c/h |
| `app/src/main/cpp/CMakeLists.txt` | NDK-сборка библиотеки приложения |
| `libutun_lite/tests/test_standby.c` | Проверка duty-cycle и жизненного цикла standby |
## Runtime
- Запуск utun от root (для tun): `/home/vnc1/proj/utun3/utun_start.sh`
- Стоп utun: `sudo /home/vnc1/proj/utun3/utun_stop1.sh`
- Логи: `utun.log` (stdout), `utun_err.log` (stderr)
- После autotools-сборки бинарник — `src/utun`. Запуск из корня с TUN: `sudo ./src/utun -f -c utun.cfg`.
- Старый `utun_start.sh` ожидает `./utun` в корне. `utun_stop.sh` делает `killall -9 utun`, а не штатную остановку одного экземпляра.
- `-f` оставляет процесс на переднем плане; остановка — SIGINT/SIGTERM. Пути конфигурации, PID и лога задаются через `-c`, `-p`, `-l`.
- `utun_start.sh` перенаправляет stdout в `utun.log`, stderr в `utun_err.log`.
- Тестовые логи: `tests/logs/`
## Git Conventions
@@ -756,19 +1068,19 @@ C-ядро: `libutun_lite` (выборочная компиляция нужны
## Quick Start for New Features
1. Add new source file to `src/Makefile.am` under `utun_SOURCES`
2. Add test file to `tests/Makefile.am` under `check_PROGRAMS`
2. Add test target and its sources/dependencies to `tests/Makefile.am` under `check_PROGRAMS`
3. Use existing patterns from similar modules
5. Run `./check.sh` after changes
4. Run `./check.sh` after changes
5. Commit with descriptive message in appropriate language
## chatgui: отправка сообщений через xdotool
1. **Запуск:** `setsid env DISPLAY=:1.0 QT_ACCESSIBILITY=1 ./chatgui & disown` (иначе SIGTERM убьёт процесс при таймауте bash)
2. **Окно:** `0x4600006 "Chat"` (WM_CLASS "chatgui"), 900×600. Не путать с `0x4800001` (10×10, временное)
3. **Фокус:** клик в область InputBar (x=400, y=570 относительно окна) перед вводом
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`
5. **Проверка:** `sqlite3 chats.db "SELECT ... FROM msg_ch_GENERAL"` — сообщения в per-channel таблицах
6. **MCP computer use** не видит chatgui — нужен `qt5-at-spi` bridge (нет в репах, ставить из исходников Qt)
1. **Запуск:** из каталога сборки `setsid env QT_ACCESSIBILITY=1 ./vibechat & disown` с DISPLAY текущей X11-сессии.
2. **Окно:** найти актуальный ID через `xdotool search --onlyvisible --class vibechat`; ID и размеры окна меняются между запусками.
3. **Фокус:** активировать найденное окно и выбрать InputBar; координаты определять по текущему расположению интерфейса.
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`, где WID заменён найденным ID.
5. **Проверка:** через `chats.db` из каталога `db_path`; таблицы сообщений называются `msg_<числовой channel_id>`.
6. Доступность UI-автоматизации зависит от текущей X11/Wayland-сессии и настроек Qt accessibility; старые ID окон и DISPLAY не переносить.
---
Эта инструкция имеет приоритет над инструкцией opencode.
-89
View File
@@ -1,89 +0,0 @@
# Задача: BGP NODEINFO теряется при флапающем реконнекте (chatgui member online не возвращается)
## Кратко
После рестарта Android-пира и реконнекта локальный chatgui показывает мембера **offline** и не возвращает **online**.
Корневая причина — **не в chat/member коде**, а в ETCP: BGP-пакет NODEINFO (347 байт) расшифровывается, но **не доходит до BGP-обработчика** `topo_group_receive_cbk()`.
Нужно: **точно локализовать точку потери NODEINFO в receive-пути ETCP и починить её.**
## Что уже сделано (исправлено и работает)
1. `tools/chatgui/src/mainwindow.cpp` — **исправлен** `onMemberUpdatedCallback`/`onMemberRemovedCallback`: проверка `len < 1+64+1+79` отбрасывала все события с channel_id короче 64 симв. Теперь `len < 1` + точная проверка. (Это была причина «не обновляется вообще».)
2. `src/chat/chat_member.c/h` — добавлен `chat_core_member_online(ch_id,node_id)` = присутствие в `topo_group` (`topo_node_find_by_id`). Используется в `chat_core_get_member_list`/`get_single_member`. Убран `nodes.online` из SQL и блок `connected→2`.
3. `src/chat/chat_core.c`, `src/chat/chat_sync.c`, `src/chat/member_sync.c/h` — удалён онлайн-статус из БД + merkle-online gossip (`member_sync_set_online`, `_ms_apply_update`, `push_update`).
4. `src/chat/chat_status.c` + `tools/chatgui/src/accountlist.cpp` — флаг «bgp present» (0x08) для detail-панели.
5. `tools/chatgui/src/nodespage.cpp` — убрана колонка Online из диагностики.
Эти правки корректны; статус работает на чистом connect/disconnect/стабильном реконнекте.
## Найденная причина (подтверждено логами)
- Android **шлёт** свой NODEINFO: `send_nodeinfo: node 1657b3ea8281c0a8 ver=2` (bgp-debug на Android).
- Локально пакет **расшифровывается**: `decrypt: code=01 dlen=347 plen=350` (347-байтный BGP-пакет, `code=0x01` = `ETCP_ID_TOPO_ENTRY`).
- Но `BGP recv NODEINFO nid=1657` (строка 193 в `topo_group.c`) **не появляется** — до обработчика пакет не доходит.
- При этом маленький `TABLE_COMPLETE` (10 байт) **доходит** (виден в логе как «mislabeled» `nid=aaaaaaaaaadeadbe` — это строка 193 логирует не-NODEINFO пакет как NODEINFO, garbage-поля).
- Признак: в стабильных рестартах (18:18/18:20) узел **добавляется** каждый раз, в флапающем (18:07) — нет. Т.е. потеря привязана к флапу (TCP-линки + ETCP reinit).
## Receive-путь ETCP (где искать потерю)
```
decrypt (etcp_connections.c:2262, лог "decrypt: code=.. dlen=..")
→ if (link_state==CONNECTED) etcp_conn_input(pkt)
else memory_pool_free(pkt) ← добавлен WARN "decrypted pkt DROPPED"
→ etcp_conn_input (etcp.c:1506, "RX pkt dlen=%d")
→ нормализатор (reassembly) → int_queue
→ etcp_int_recv (etcp_api.c) ← добавлен WARN "int_recv BGP id=0x01 len=.."
→ dispatch по route id (0x01) → topo_group_receive_cbk (topo_group.c:193 "BGP recv NODEINFO")
```
## Уже добавленный дебаг (для локализации)
1. `src/transport_layer/etcp_connections.c` (после дешифровки):
```c
} else {
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] decrypted pkt DROPPED: link_state=%d code=0x%02x dlen=%u",
link->etcp->log_name, link->link_state, pkt_code, pkt->data_len);
memory_pool_free(e_sock->instance->pkt_pool, pkt);
}
```
2. `src/transport_layer/etcp_api.c` (`etcp_int_recv`, после чтения `id`):
```c
uint8_t id = e->dgram[0];
if (id == 0x01) DEBUG_WARN(DEBUG_CATEGORY_ETCP, "int_recv BGP id=0x01 conn=%s len=%zu", conn->log_name, e->len);
```
3. Android: `tools/chatgui-android/libutun_lite/instance_lite.c` добавлено
`debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);` (пересобрать APK: `cd tools/chatgui-android && ./build.sh`).
4. chatgui конфиг `tools/chatgui/build/vibechat.cfg` — включены `bgp=debug`, `chat_sync=debug`, `member_sync=debug`, `debug=debug`.
Как читать (по таймстампу флапа):
- есть `decrypt code=01 dlen=..` + `DROPPED` → потеря в проверке `link_state==CONNECTED`;
- есть `decrypt` + нет `DROPPED` + нет `int_recv BGP` → потеря в `etcp_conn_input`/нормализаторе;
- есть `int_recv BGP` + нет `BGP recv NODEINFO` → потеря в BGP-обработчике.
## Задача для агента
1. Пересобрать chatgui (`cd tools/chatgui && cmake --build build -j4`) и Android APK (`cd tools/chatgui-android && ./build.sh`).
2. Запустить chatgui (DISPLAY=:0, `./vibechat`), воспроизвести флап реконнекта:
```bash
adb shell am force-stop com.utun.chat && adb shell am start -n com.utun.chat/.MainActivity
```
(повторить несколько раз; флап ловится не каждый раз).
3. В логах (`tools/chatgui/build/chatgui.log`) найти потерю NODEINFO и **точно определить точку** по таблице выше.
4. Починить первопричину (варианты):
- буферизовать расшифрованный пакет до `LINK_STATE_CONNECTED` вместо `memory_pool_free`;
- либо гарантированный BGP-retry (после conn UP, если узел пира не в `group->nodes` через N мс — повторно `topo_group_send_table_request`).
5. Убрать весь диагностический мусор:
- WARN "DROPPED" (etcp_connections.c), WARN "int_recv BGP" (etcp_api.c);
- DEBUG-логи в `chat_member.c` (`on_member_props_changed`), `chat_sync.c` (`cs_on_peer_status_changed`), `memberlistmodel.cpp`;
- `instance_lite.c` BGP-debug;
- `vibechat.cfg` — вернуть категории в закомментированное состояние.
6. Прогнать: connect→online, disconnect→offline, реконнект (флап)→online. `./check.sh` (или `cd src && make -j4`).
## Воспроизведение / ключевые артефакты
- Пир: Android SM-A525F (`com.utun.chat`), node_id `0x1657b3ea8281c0a8`.
- Локальный: chatgui (`tools/chatgui/build/vibechat`), node_id `0x24cd036a6e659b9a`.
- Канал: `ch_id=7206723622466219923`, `group_id=0x64036cf3a783ef93`.
- Лог chatgui: `tools/chatgui/build/chatgui.log` (debug_file из конфига).
- Лог Android: `adb logcat -d | grep -iE "send_nodeinfo|NOT in registry|NODEINFO|DROPPED|int_recv"`.
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+39
View File
@@ -0,0 +1,39 @@
uTun licensing
==============
The original uTun source code, including its command-line, desktop and Android
applications, is licensed under the GNU General Public License, version 3 only
(GPL-3.0-only), with the additional permission for Wintun below.
uTun is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
version 3 of the License.
uTun is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
The complete GNU GPL version 3 is provided in LICENSE. Copyright remains with
the respective authors. Third-party code and binaries retain their own licenses
and copyright notices; see THIRD_PARTY_NOTICES and the notices in their files.
Additional permission under GNU GPL version 3 section 7
------------------------------------------------------
If you modify this Program, or any covered work, by linking or combining it with
Wintun (or a modified version of that library), containing parts covered by the
terms of the Wintun Prebuilt Binaries License, the licensors of this Program
grant you additional permission to convey the resulting work.
This permission applies to the original uTun code. It does not change the
license of Wintun or grant rights to modify its prebuilt binaries. Wintun must
be used and distributed in accordance with its own license, through its
Permitted API. No exception is granted for any other library.
Distribution
------------
Include LICENSE, LICENSING and THIRD_PARTY_NOTICES with source and binary
distributions. For GPL-covered binaries, also provide Corresponding Source
as required by GNU GPL version 3; including these documents does not replace
that requirement. Preserve upstream notices when updating bundled libraries.
+16
View File
@@ -3,6 +3,22 @@
# Order matters: lib -> src -> tests (tests depend on src objects)
SUBDIRS = lib lib/libopus src tests
# Полный комплект лицензий устанавливается вместе с бинарником и входит в make dist.
licensedir = $(datadir)/licenses/utun
dist_license_DATA = LICENSE LICENSING THIRD_PARTY_NOTICES
EXTRA_DIST = readme.md \
doc/nat_detection.txt \
doc/stun_nat.md \
doc/node_snapshot.md \
tinycrypt/LICENSE \
tools/chatgui/zxing-cpp/LICENSE \
tools/chatgui/zxing-cpp/core/src/libzueci/LICENSE \
tools/chatgui/third_party/qhotkey/LICENSE \
tools/chatgui-android/app/src/main/cpp/onnxruntime/LICENSE \
tools/chatgui-android/app/src/main/cpp/onnxruntime/ThirdPartyNotices.txt \
tools/chatgui-android/app/src/main/cpp/openssl/LICENSE.txt
# Clean build directory
clean-local:
-rm -rf build/
+2
View File
@@ -1,3 +1,5 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
# План: эпоха (reset_id) для ETCP Router
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою
+11786
View File
File diff suppressed because it is too large. Load diff
+9 -3
View File
@@ -153,10 +153,14 @@ general=info # общая информация
```ini
[firewall]
allow=all # разрешить всё (bypass)
allow=192.168.1.0/24 # разрешить подсеть
allow=192.168.1.10 # разрешить весь IP
allow=10.0.0.1:80 # разрешить конкретный IP:порт
```
Фильтр основного TUN проверяет IP и TCP/UDP-порт назначения одинаково в обоих направлениях.
Без разрешений весь трафик этого интерфейса запрещён; ответы требуют отдельных разрешений.
CIDR-подсети не поддерживаются. Ошибочное правило отменяет загрузку нового конфига.
### 3.7. `[nat]` — EIM NAT
```ini
@@ -553,11 +557,13 @@ dump=trace
```ini
[firewall]
allow=all # всё разрешено
allow=10.0.0.0/8 # разрешить подсеть
allow=10.0.0.5 # разрешить весь IP
allow=192.168.1.5:22 # разрешить IP:порт
```
По умолчанию без секции `[firewall]` — фильтрация выключена.
Без секции `[firewall]` весь входящий и исходящий трафик основного TUN запрещён.
`allow=IP` и `allow=IP:0` разрешают все протоколы IP; `allow=IP:port` — TCP/UDP указанного порта.
Для фрагментов IPv4 нужен весь IP либо `allow=all`. NAT и прокси используют отдельные TUN интерфейсы.
### 7.3. Проброс портов
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+10 -4
View File
@@ -1,4 +1,5 @@
@echo off
setlocal EnableExtensions
echo uTun Build Script for MSYS2 UCRT64
echo.
@@ -10,11 +11,11 @@ rem export https_proxy="http://192.168.29.1:9999"
REM Default MSYS2 path
set MSYS2_PATH=C:\msys64
set "MSYS2_PATH=C:\msys64"
REM Use environment variable if set
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
REM Check if msys2_shell.cmd exists
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
@@ -22,10 +23,15 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
echo Please install MSYS2 from https://www.msys2.org/
echo or set MSYS2_ROOT environment variable.
pause
popd
exit /b 1
)
REM Launch MSYS2 UCRT64 shell and run build script
echo Starting MSYS2 UCRT64 environment...
rem call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -c "bash build.sh > build_win.log 2>&1"
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh 2>&1 | tee build_win.log"
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --installer 2>&1 | tee build_win.log"
set "BUILD_RESULT=%ERRORLEVEL%"
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_win.log.
popd
endlocal & exit /b %BUILD_RESULT%
+208 -195
View File
@@ -1,208 +1,221 @@
#!/bin/bash
# Build script for uTun (supports both Linux and Windows/MSYS2)
# Usage: ./build.sh [options]
# Options:
# -h, --help Show this help
# --clean Clean before build
# --full Full rebuild (autoreconf + configure + make)
# --asan Build with AddressSanitizer (debug)
# --hardened Build with compiler security hardening flags
# -j[N] Build with N parallel jobs (default: 4)
#!/usr/bin/env bash
# Инкрементальная сборка Linux, FreeBSD и Windows/MSYS2; флаги задаются через configure.
set -euo pipefail
cd "$(dirname "$0")"
set -e # Exit on error
# Detect platform
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$MSYSTEM" ]]; then
IS_WINDOWS=1
echo "Detected Windows/MSYS2 environment"
else
IS_WINDOWS=0
echo "Detected Linux environment"
fi
# Parse arguments
MODE=normal
CLEAN=0
FULL_REBUILD=0
USE_ASAN=0
USE_HARDENED=0
JOBS="-j4"
MAKE_ARGS=""
HARDEN_FLAGS=""
FULL=0
BUILD_GUI=1
BUILD_INSTALLER=0
JOBS=4
MAKE_ARGS=()
MAKE_SETTINGS=()
STATE=.utun-build-state
while [[ $# -gt 0 ]]; do
case $1 in
case "$1" in
-h|--help)
echo "Usage: ./build.sh [options]"
echo "Options:"
echo " -h, --help Show this help"
echo " --clean Clean before build"
echo " --full Full rebuild (autoreconf + configure + make)"
echo " --asan Build with AddressSanitizer"
echo " --hardened Build with compiler security hardening flags"
echo " -j[N] Build with N parallel jobs (default: 4)"
exit 0
;;
--clean)
CLEAN=1
shift
;;
--full)
FULL_REBUILD=1
shift
;;
--asan)
USE_ASAN=1
shift
;;
--hardened)
USE_HARDENED=1
shift
;;
-j*)
JOBS="$1"
shift
;;
*)
MAKE_ARGS="$MAKE_ARGS $1"
shift
;;
esac
done
# Check required tools
if ! command -v gcc &> /dev/null; then
echo "Error: gcc not found."
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain"
else
echo "Please install build-essential or gcc"
fi
exit 1
fi
# Clean if requested
if [ "$CLEAN" -eq 1 ]; then
echo "Cleaning build..."
make clean 2>/dev/null || true
fi
# Full rebuild: regenerate build system
if [ "$FULL_REBUILD" -eq 1 ]; then
echo ""
echo "======================================"
echo "FULL REBUILD: autoreconf"
echo "======================================"
echo ""
# Check for autotools
if ! command -v autoreconf &> /dev/null; then
echo "Error: autoreconf not found."
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
else
echo "Install autotools: apt-get install autoconf automake libtool"
fi
exit 1
fi
# Clean generated files
echo "Cleaning generated files..."
make distclean 2>/dev/null || true
rm -rf build/
# Regenerate build system (root Makefile is hand-written, not generated)
echo ""
echo "Running autoreconf -fi..."
autoreconf -fi 2>&1 | tee autoreconf.log
echo ""
echo "======================================"
echo "Build system regenerated successfully"
echo "======================================"
echo ""
else
# Quick/incremental build: generate configure if needed
if [ ! -f ./configure ]; then
echo "Configure script not found. Generating..."
if ! command -v autoreconf &> /dev/null; then
echo "Error: autoreconf not found."
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
else
echo "Install autotools: apt-get install autoconf automake libtool"
echo "Usage: ./build.sh [--clean] [--full] [--asan|--hardened] [--no-gui] [--installer] [-jN] [make arguments]"
echo " --clean Rebuild objects without forcing configure"
echo " --full Regenerate, configure and rebuild everything"
echo " --asan AddressSanitizer; skip tests"
echo " --hardened Enable compiler security hardening"
echo " --no-gui Skip the separately configured ChatGUI"
echo " --installer Build Windows ChatGUI in Release and create its Inno Setup installer"
echo " -jN, -j N Parallel jobs (default: 4)"
echo " CC/CFLAGS/CPPFLAGS/LDFLAGS/LIBS assignments are passed to configure"
exit 0 ;;
--clean) CLEAN=1 ;;
--full) FULL=1 ;;
--no-gui) BUILD_GUI=0 ;;
--installer) BUILD_INSTALLER=1 ;;
--asan|--hardened)
selected_mode=${1#--}
if [[ "$MODE" != normal && "$MODE" != "$selected_mode" ]]; then
echo "ERROR: --asan and --hardened cannot be combined" >&2; exit 1
fi
exit 1
MODE=$selected_mode ;;
-j)
[[ $# -gt 1 ]] || { echo "ERROR: -j requires a positive job count" >&2; exit 1; }
shift; JOBS=$1 ;;
-j*) JOBS=${1#-j} ;;
CC=*|CFLAGS=*|CPPFLAGS=*|LDFLAGS=*|LIBS=*) export "$1" ;;
CC+=*|CFLAGS+=*|CPPFLAGS+=*|LDFLAGS+=*|LIBS+=*)
echo "ERROR: use NAME=value for configure flags; append assignments are not supported" >&2; exit 1 ;;
*)
MAKE_ARGS+=("$1")
[[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;;
esac
shift
done
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; exit 1; }
if [[ "$BUILD_INSTALLER" = 1 ]]; then
[[ "$BUILD_GUI" = 1 && "$MODE" = normal ]] || { echo "ERROR: --installer requires GUI and normal build mode" >&2; exit 1; }
bash tools/chatgui/package_win.sh --check
fi
PLATFORM=$(uname -s)
MAKE_TOOL=${MAKE:-make}
if [[ "$PLATFORM" = FreeBSD ]]; then
MAKE_TOOL=${MAKE:-gmake}
export CC="${CC:-clang}"
fi
MAKE_PATH=$(command -v "$MAKE_TOOL") || { echo "ERROR: GNU make not found: $MAKE_TOOL (FreeBSD: pkg install gmake)" >&2; exit 1; }
MAKE_ID=$("$MAKE_TOOL" --version) || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
[[ "$MAKE_ID" = *'GNU Make'* ]] || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
export MAKE="$MAKE_TOOL"
if command -v sha256sum >/dev/null; then
HASH_COMMAND=(sha256sum)
elif command -v sha256 >/dev/null; then
HASH_COMMAND=(sha256 -r)
elif command -v shasum >/dev/null; then
HASH_COMMAND=(shasum -a 256)
else
echo "ERROR: SHA-256 utility not found: sha256sum, sha256 or shasum is required" >&2; exit 1
fi
read -r -a COMPILER <<< "${CC:-gcc}"
COMPILER_PATH=$(command -v "${COMPILER[0]}") || { echo "ERROR: compiler not found: ${CC:-gcc}" >&2; exit 1; }
COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify compiler: ${CC:-gcc}" >&2; exit 1; }
# Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов.
hash_files() {
local result
result=$({ printf '%s\0' "$@"; "${HASH_COMMAND[@]}" "$@"; } | "${HASH_COMMAND[@]}")
printf '%s\n' "${result%% *}"
}
BUILD_DIRS=(lib lib/libopus src tests)
AUTOTOOLS_INPUTS=(configure.ac Makefile.am lib/Makefile.am lib/libopus/Makefile.am src/Makefile.am tests/Makefile.am)
AUTOTOOLS_OUTPUTS=(configure aclocal.m4 config.h.in Makefile.in lib/Makefile.in lib/libopus/Makefile.in src/Makefile.in tests/Makefile.in)
CONFIG_OUTPUTS=(config.status config.h Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile)
shopt -s globstar nullglob
for macro in m4/**/*.m4; do AUTOTOOLS_INPUTS+=("$macro"); done
GENERATION_INPUT=$(hash_files "${AUTOTOOLS_INPUTS[@]}")
GENERATION_OUTPUT=missing
CONFIG_OUTPUT=missing
missing_generation=0
missing_configuration=0
for file in "${AUTOTOOLS_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_generation=1; done
for file in "${CONFIG_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_configuration=1; done
if [[ "$missing_generation" = 0 ]]; then GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}"); fi
if [[ "$missing_configuration" = 0 ]]; then CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}"); fi
FFMPEG_PACKAGES=(libavformat libavcodec libavutil libswscale libswresample)
FFMPEG_OPTION=--without-ffmpeg
FFMPEG_ID=unavailable
FFMPEG_CFLAGS=""
FFMPEG_LDFLAGS=""
if command -v pkg-config >/dev/null && pkg-config --exists "${FFMPEG_PACKAGES[@]}"; then
FFMPEG_OPTION=--with-ffmpeg
FFMPEG_ID=$(pkg-config --modversion "${FFMPEG_PACKAGES[@]}")
FFMPEG_CFLAGS=$(pkg-config --cflags "${FFMPEG_PACKAGES[@]}")
FFMPEG_LDFLAGS=$(pkg-config --libs-only-L "${FFMPEG_PACKAGES[@]}")
fi
request_sum=$({
printf '%s\0' "$MODE" "$PLATFORM" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_PATH" "$COMPILER_ID"
printf '%s\0' "$MAKE_PATH" "$MAKE_ID" "${HASH_COMMAND[@]}"
printf '%s\0' "${CFLAGS+x}" "${CFLAGS-}" "${CPPFLAGS+x}" "${CPPFLAGS-}" "${LDFLAGS+x}" "${LDFLAGS-}"
printf '%s\0' "${LIBS+x}" "${LIBS-}" "${MAKEFLAGS-}" "${DEBUG_FLAGS+x}" "${DEBUG_FLAGS-}"
printf '%s\0' "$FFMPEG_OPTION" "$FFMPEG_ID" "$FFMPEG_CFLAGS" "$FFMPEG_LDFLAGS"
printf '%s\0' "${MAKE_SETTINGS[@]}"
hash_files build.sh
} | "${HASH_COMMAND[@]}")
CONFIG_REQUEST=${request_sum%% *}
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
if [[ -f "$STATE" ]]; then
if ! {
read -r OLD_GENERATION_INPUT; read -r OLD_GENERATION_OUTPUT
read -r OLD_CONFIG_REQUEST; read -r OLD_CONFIG_OUTPUT
} < "$STATE"; then
echo "WARN: incomplete build state; adopting the build with a clean rebuild"
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
fi
fi
REGENERATE=0
RECONFIGURE=0
if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then
echo "INFO: regenerating Autotools: full=$FULL"
if [[ "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" ]]; then echo "INFO: Autotools descriptions changed or unrecorded"; fi
if [[ "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then echo "INFO: generated Autotools files changed or missing"; fi
command -v autoreconf >/dev/null || { echo "ERROR: autoreconf is required to regenerate the build" >&2; exit 1; }
mkdir -p m4
autoreconf -fi 2>&1 | tee autoreconf.log || { echo "ERROR: autoreconf failed; see autoreconf.log" >&2; exit 1; }
REGENERATE=1
fi
if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then
echo "INFO: configuring: mode=$MODE regenerated=$REGENERATE"
if [[ "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" ]]; then echo "INFO: compiler, flags or optional dependencies changed or unrecorded"; fi
if [[ "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then echo "INFO: configuration files changed or missing"; fi
CONFIG_ARGS=("$FFMPEG_OPTION" --disable-hardening)
if [[ "$MODE" = asan ]]; then
export CFLAGS="${CFLAGS-} -g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie"
export LDFLAGS="${LDFLAGS-} -fsanitize=address -no-pie"
if [[ "$PLATFORM" = FreeBSD ]]; then export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=0}"; fi
elif [[ "$MODE" = hardened ]]; then
CONFIG_ARGS+=(--enable-hardening)
fi
if [[ "$FFMPEG_OPTION" = --with-ffmpeg ]]; then
export CPPFLAGS="${CPPFLAGS-} $FFMPEG_CFLAGS"
export LDFLAGS="${LDFLAGS-} $FFMPEG_LDFLAGS"
fi
./configure "${CONFIG_ARGS[@]}" 2>&1 | tee configure.log || { echo "ERROR: configure failed; see configure.log" >&2; exit 1; }
RECONFIGURE=1
fi
# Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем.
MISSING_DEPS=0
for directory in "${BUILD_DIRS[@]}"; do
while read -r keyword depfile marker; do
[[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue
depfile=${depfile//\$\(DEPDIR\)/.deps}
object=${depfile/.deps\//}
object=${object%.Po}
[[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue
dependency_line=""
if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi
if [[ "$dependency_line" != *:* ]]; then
echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile"
MISSING_DEPS=1
break
fi
autoreconf -fiv
fi
fi
# Auto-detect FFmpeg (optional video transcoding). If libav libraries are
# available, build with --with-ffmpeg; configure gracefully skips it otherwise.
FFMPEG_OPT=""
if command -v pkg-config >/dev/null 2>&1 && \
pkg-config --exists libavformat libavcodec libavutil libswscale libswresample 2>/dev/null; then
FFMPEG_OPT="--with-ffmpeg"
echo "FFmpeg detected — building with video transcoding"
done < "$directory/Makefile"
done
if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then
echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS"
"$MAKE_TOOL" clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; }
else
echo "FFmpeg not found — building without video transcoding"
echo "INFO: configuration unchanged; incremental build (mode=$MODE)"
: > build.log
fi
# Generate Makefiles if needed
if [ ! -f Makefile ]; then
echo ""
echo "Running ./configure..."
if [ "$USE_ASAN" -eq 1 ]; then
./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie" $FFMPEG_OPT
elif [ "$USE_HARDENED" -eq 1 ]; then
./configure --enable-hardening $FFMPEG_OPT
echo "INFO: compiling: mode=$MODE jobs=$JOBS"
if [[ "$MODE" = asan ]]; then
echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests"
MAKE_ARGS+=("SUBDIRS=lib lib/libopus src")
fi
"$MAKE_TOOL" "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; }
# Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты.
GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}")
CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}")
state_tmp="$STATE.tmp.$$"
trap 'rm -f "$state_tmp"' EXIT
printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp"
mv -f "$state_tmp" "$STATE"
if [[ "$BUILD_GUI" = 1 && ( "$BUILD_INSTALLER" = 1 || -f tools/chatgui/build/CMakeCache.txt ) ]]; then
command -v cmake >/dev/null || { echo "ERROR: ChatGUI exists but cmake is not available" >&2; exit 1; }
echo "INFO: building ChatGUI with its CMake configuration: jobs=$JOBS"
if [[ "${MSYSTEM:-}" = UCRT64 ]]; then
GUI_ARGS=("-j$JOBS")
[[ "$BUILD_INSTALLER" != 1 ]] || GUI_ARGS+=(--installer)
(cd tools/chatgui && bash build.sh "${GUI_ARGS[@]}") || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
else
./configure $FFMPEG_OPT
cmake --build tools/chatgui/build --parallel "$JOBS" || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
fi
fi
# Build (all .o files go to build/ directory, binary copied to project root)
echo ""
echo "Compiling with $JOBS..."
BUILD_SUCCESS=0
if [ "$USE_ASAN" -eq 1 ]; then
echo "(AddressSanitizer enabled — lib/ and src/ only, skipping tests)"
AF="-fsanitize=address -fno-omit-frame-pointer -g -Og"
AL="-fsanitize=address"
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
BUILD_SUCCESS=1
elif [ "$USE_HARDENED" -eq 1 ]; then
echo "(Hardening flags enabled)"
HF="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
HL="-Wl,-z,relro -Wl,-z,now -pie"
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
(cd tests && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "tests build failed"; exit 1; }
BUILD_SUCCESS=1
else
make $JOBS $MAKE_ARGS 2>&1 | tee build.log
if [ ${PIPESTATUS[0]} -eq 0 ]; then
BUILD_SUCCESS=1
fi
fi
if [ $BUILD_SUCCESS -eq 1 ]; then
echo ""
echo "Build completed successfully!"
echo "Binary: utun"
# Rebuild chatgui (CMake project) if transport sources changed
if [ -f tools/chatgui/build/CMakeCache.txt ] && command -v cmake &> /dev/null; then
echo ""
echo "======================================"
echo "Rebuilding chatgui (CMake)"
echo "======================================"
(cd tools/chatgui/build && cmake --build . -j4 2>&1) || echo "WARNING: chatgui build failed"
fi
else
echo ""
echo "ERROR: Build failed!"
echo "Check build.log for details"
exit 1
fi
echo "Build completed successfully!"
echo "Binary: src/utun"
+24 -136
View File
@@ -1,144 +1,32 @@
#!/bin/sh
# Build script for uTun on FreeBSD (full rebuild)
# Usage: ./build_bsd_full.sh [options]
# Options:
# -h, --help Show this help
# --asan Build with AddressSanitizer (clang)
# --hardened Build with compiler security hardening flags (clang)
# -j[N] Build with N parallel jobs (default: sysctl hw.ncpu)
# Полная сборка FreeBSD: зависимости и общий инкрементальный скрипт.
set -e
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
case " ${*} " in
*" --help "*|*" -h "*)
echo "Usage: ./build_bsd_full.sh [--asan|--hardened] [-jN] [build.sh arguments]"
echo "Installs missing build tools and runs build.sh --full"
exit 0 ;;
esac
# Parse arguments
USE_ASAN=0
USE_HARDENED=0
JOBS=""
MAKE_ARGS=""
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
echo "Usage: ./build_bsd_full.sh [options]"
echo "Options:"
echo " -h, --help Show this help"
echo " --asan Build with AddressSanitizer"
echo " --hardened Build with compiler security hardening flags"
echo " -j[N] Build with N parallel jobs (default: sysctl hw.ncpu)"
exit 0
;;
--asan)
USE_ASAN=1
shift
;;
--hardened)
USE_HARDENED=1
shift
;;
-j*)
JOBS="$1"
shift
;;
*)
MAKE_ARGS="$MAKE_ARGS $1"
shift
;;
esac
need_install=0
for tool in bash gmake autoreconf aclocal automake libtoolize pkg-config; do
command -v "$tool" >/dev/null 2>&1 || need_install=1
done
if [ -z "$JOBS" ]; then
if command -v nproc >/dev/null 2>&1; then
JOBS="-j$(nproc)"
else
JOBS="-j$(sysctl -n hw.ncpu)"
fi
if [ "$need_install" -eq 1 ]; then
echo "INFO: installing missing FreeBSD build dependencies (requires root)"
pkg install -y bash gmake autoconf automake libtool pkgconf openssl || {
echo "ERROR: cannot install FreeBSD build dependencies" >&2; exit 1;
}
fi
# Проверка и установка зависимостей
need_install=""
for cmd in autoreconf aclocal automake libtoolize; do
if ! command -v "$cmd" >/dev/null 2>&1; then
need_install=1
break
fi
jobs_found=0
for argument do
case "$argument" in -j*) jobs_found=1 ;; esac
done
if [ -n "$need_install" ]; then
echo "Устанавливаются необходимые пакеты (нужен root)..."
pkg update
pkg install -y autoconf automake libtool pkgconf openssl
fi
# Предпочитаем GNU make (gmake), но fallback на bmake — он собирает
# autotools-генерённые Makefile данного проекта. gmake ставится отдельно:
# pkg install gmake (нужен root)
GMAKE="$(command -v gmake || command -v make)"
echo "Используем make: $GMAKE"
mkdir -p m4
echo "=== Полная пересборка uTun ==="
# 1. Очистка
echo "[1/4] Очистка..."
$GMAKE distclean 2>/dev/null || true
rm -f config.h config.status config.log
rm -f configure.log autoreconf.log build.log
# 2. Генерация configure
echo "[2/4] Автогенерация..."
autoreconf -fi 2>&1 | tee autoreconf.log
# Проверяем, появился ли configure
if [ ! -f configure ]; then
echo "Ошибка: autoreconf не создал configure."
exit 1
fi
chmod +x configure
# 3. Конфигурация
echo "[3/4] Конфигурация..."
# FFmpeg (опционально) — автоопределение через pkg-config
FFMPEG_OPT=""
if command -v pkg-config >/dev/null 2>&1 && \
pkg-config --exists libavformat libavcodec libavutil libswscale libswresample 2>/dev/null; then
FFMPEG_OPT="--with-ffmpeg"
echo "FFmpeg detected — building with video transcoding"
else
echo "FFmpeg not found — building without video transcoding"
fi
if [ "$USE_ASAN" -eq 1 ]; then
echo "(AddressSanitizer enabled — clang, lib/ + src/ only)"
./configure --prefix=/usr/local CC=clang \
CFLAGS="-g -O1 -fsanitize=address -fno-omit-frame-pointer" \
LDFLAGS="-fsanitize=address" $FFMPEG_OPT 2>&1 | tee configure.log
elif [ "$USE_HARDENED" -eq 1 ]; then
echo "(Hardening enabled — clang-safe flags)"
./configure --prefix=/usr/local CC=clang \
CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE" \
LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie" $FFMPEG_OPT 2>&1 | tee configure.log
else
./configure --prefix=/usr/local $FFMPEG_OPT 2>&1 | tee configure.log
fi
# 4. Сборка
echo "[4/4] Сборка..."
if [ "$USE_ASAN" -eq 1 ]; then
(cd lib && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../build.log)
(cd lib/libopus && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../../build.log)
(cd src && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../build.log)
else
$GMAKE $JOBS $MAKE_ARGS 2>&1 | tee build.log
fi
echo
echo "=== Готово ==="
echo "Бинарный файл: src/utun"
if [ "$USE_ASAN" -eq 1 ]; then
echo
echo "Запуск с ASAN (detect_leaks=0 обязателен на FreeBSD):"
echo " ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log"
if [ "$jobs_found" -eq 0 ]; then
jobs=$(sysctl -n hw.ncpu) || { echo "ERROR: cannot determine CPU count" >&2; exit 1; }
set -- "-j$jobs" "$@"
fi
echo "INFO: running shared build script for FreeBSD"
exec bash "$SCRIPT_DIR/build.sh" --full "$@"
+1 -1
View File
@@ -70,7 +70,7 @@ echo "Building utun.exe..."
cd src
EXE_NEEDS_REBUILD=0
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_padding.c transport_layer/reality_record.c transport_layer/reality_io.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
for src in $CORE_SOURCES; do
if [ -f "$src" ]; then
+11 -5
View File
@@ -1,4 +1,5 @@
@echo off
setlocal EnableExtensions
echo uTun Full Build Script for MSYS2 UCRT64
echo =========================================
echo This will regenerate build system with autoreconf
@@ -11,11 +12,11 @@ rem export https_proxy="http://192.168.29.1:9999"
REM Default MSYS2 path
set MSYS2_PATH=C:\msys64
set "MSYS2_PATH=C:\msys64"
REM Use environment variable if set
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
REM Check if msys2_shell.cmd exists
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
@@ -23,11 +24,16 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
echo Please install MSYS2 from https://www.msys2.org/
echo or set MSYS2_ROOT environment variable.
pause
popd
exit /b 1
)
REM Launch MSYS2 UCRT64 shell and run build script with --full flag
echo Starting MSYS2 UCRT64 environment for FULL REBUILD...
echo Running: bash build.sh --full
echo Running: bash build.sh --full --installer
echo.
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full 2>&1 | tee build_full.log"
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full --installer 2>&1 | tee build_full.log"
set "BUILD_RESULT=%ERRORLEVEL%"
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_full.log.
popd
endlocal & exit /b %BUILD_RESULT%
+1 -1
View File
@@ -16,7 +16,7 @@ set "TOTAL=0"
if not exist "%LOGS_DIR%" mkdir "%LOGS_DIR%"
:: List of all test executables
set "TEST_LIST=test_etcp_crypto test_crypto test_etcp_two_instances test_etcp_simple_traffic test_etcp_minimal test_etcp_100_packets test_pkt_normalizer_etcp test_pkt_normalizer_standalone test_etcp_api test_ll_queue test_ecc_encrypt test_intensive_memory_pool test_memory_pool_and_config test_packet_dump test_u_async_comprehensive test_u_async_performance test_debug_categories test_config_debug test_route_lib test_bgp_route_exchange bench_timeout_heap bench_uasync_timeouts"
set "TEST_LIST=bench_timeout_heap bench_uasync_timeouts test_etcp_minimal test_u_async_performance test_packet_dump test_config_debug test_debug_categories test_memory_pool_and_config test_etcp_crypto test_route_lib test_crypto test_ecc_encrypt test_ll_queue test_intensive_memory_pool test_pkt_normalizer_standalone test_etcp_two_instances test_u_async_comprehensive test_etcp_simple_traffic test_pkt_normalizer_etcp test_etcp_100_packets test_etcp_api test_bgp_route_exchange"
echo Running tests...
echo.
+18 -6
View File
@@ -1,6 +1,10 @@
#!/bin/bash
#!/usr/bin/env bash
set -eo pipefail
cd "$(dirname "$0")"
MAKE_TOOL=${MAKE:-$(command -v gmake || command -v make)}
TIMEOUT_TOOL=$(command -v timeout || command -v gtimeout) || {
echo "ERROR: timeout or gtimeout is required for the test runner" >&2; exit 1;
}
LAST=0; [ -f .check_time ] && read -r LAST < .check_time
TMO=$(( LAST + 30 ))
@@ -8,21 +12,29 @@ TMO=$(( LAST + 30 ))
START=$(date +%s)
TMP=$(mktemp)
timeout $TMO make check -j4 > "$TMP" 2>&1 || {
"$TIMEOUT_TOOL" "$TMO" "$MAKE_TOOL" check -j4 > "$TMP" 2>&1 || {
tail -40 "$TMP"; rm -f "$TMP"
[ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2
exit 1
}
grep -E '\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
grep -E '\[INFO|\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
rm -f "$TMP"
# Интеграционный тест tcp_proxy_full (требует sudo; без passwordless sudo — пропускаем)
echo ""
if [ "$(id -u)" -eq 0 ]; then
make -C tests check-proxy
if [ "$(uname -s)" != Linux ]; then
echo "[SKIP] tcp_proxy_full integration tests require Linux networking tools"
elif [ "$(id -u)" -eq 0 ]; then
"$MAKE_TOOL" -C tests check-proxy
"$MAKE_TOOL" -C tests check-burst
"$MAKE_TOOL" -C tests check-load
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
echo "=== tcp_proxy_full integration test (sudo) ==="
sudo -n make -C tests check-proxy
sudo -n "$MAKE_TOOL" -C tests check-proxy
echo "=== tcp_proxy_full burst test (sudo) ==="
sudo -n "$MAKE_TOOL" -C tests check-burst
echo "=== tcp_proxy_full load test (sudo) ==="
sudo -n "$MAKE_TOOL" -C tests check-load
else
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
fi
+34
View File
@@ -0,0 +1,34 @@
@echo off
setlocal
pushd "%~dp0"
if errorlevel 1 (
echo [ERROR] Cannot open the script directory.
pause
exit /b 1
)
set "MSYS2_PATH=C:\msys64"
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
set "PROBE_LOG=%CD%\cc-probe.log"
set "PROBE_RESULT=1"
> "%PROBE_LOG%" echo [INFO] MSYS2: %MSYS2_PATH%
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
>> "%PROBE_LOG%" echo [ERROR] msys2_shell.cmd not found. Set MSYS2_ROOT to the MSYS2 directory.
goto finish
)
echo [INFO] Checking the UCRT64 C compiler and Opus build...
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "bash ./check_cc.sh" >> "%PROBE_LOG%" 2>&1
set "PROBE_RESULT=%ERRORLEVEL%"
:finish
>> "%PROBE_LOG%" echo [INFO] MSYS2 shell exit code: %PROBE_RESULT%
type "%PROBE_LOG%"
echo.
echo [INFO] Log: "%PROBE_LOG%"
if not "%PROBE_RESULT%"=="0" echo [ERROR] Compiler or Opus check failed. See the log above.
if /i not "%~1"=="--no-pause" pause
popd
exit /b %PROBE_RESULT%
+150
View File
@@ -0,0 +1,150 @@
#!/bin/bash
set -uo pipefail
# Проверяем базовую компиляцию, затем Opus отдельно и через существующую CMake-сборку.
probe_cc="${UTUN_PROBE_CC:-/ucrt64/bin/cc.exe}"
printf '[INFO] MSYSTEM=%s\n[INFO] PATH=%s\n[INFO] Compiler=%s\n' "${MSYSTEM:-unset}" "$PATH" "$probe_cc"
if [ ! -x "$probe_cc" ]; then
printf '[ERROR] Compiler is missing or not executable: %s\n' "$probe_cc"
exit 1
fi
probe_dir=$(mktemp -d "${TMPDIR:-/tmp}/utun-cc-probe.XXXXXX") || {
printf '[ERROR] Cannot create the temporary probe directory.\n'
exit 1
}
trap 'rm -rf -- "$probe_dir" || printf "[WARN] Cannot remove probe directory: %s\n" "$probe_dir"' EXIT
printf '[INFO] Temporary directory: %s\n' "$probe_dir"
# Каждый этап сохраняет собственный код завершения, включая молчаливые сбои.
probe_step() {
local step="$1" result
shift
printf '\n[INFO] %s\n[INFO] Directory: %s\n[INFO] Command:' "$step" "$PWD"
printf ' %q' "$@"
printf '\n'
"$@"
result=$?
printf '[INFO] %s exit code: %d\n' "$step" "$result"
if [ "$result" -ne 0 ]; then
printf '[ERROR] Failed stage: %s\n' "$step"
fi
return "$result"
}
printf '#include <stdio.h>\nint main(void) { puts("utun compiler probe OK"); return 0; }\n' > "$probe_dir/probe.c" || {
printf '[ERROR] Cannot write the probe source: %s/probe.c\n' "$probe_dir"
exit 1
}
probe_step 'Compiler version' "$probe_cc" --version || exit $?
probe_step 'Compile' "$probe_cc" -v -std=c99 -c "$probe_dir/probe.c" -o "$probe_dir/probe.o" || exit $?
probe_step 'Link' "$probe_cc" -v "$probe_dir/probe.o" -o "$probe_dir/probe.exe" || exit $?
probe_step 'Run' "$probe_dir/probe.exe" || exit $?
printf '\n[SUCCESS] Compile, link and run completed.\n'
probe_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || {
printf '[ERROR] Cannot locate the repository directory.\n'
exit 1
}
probe_opus="$probe_root/lib/libopus"
probe_includes=()
for probe_include in '' include celt silk silk/float silk/fixed src; do
probe_includes+=("-I$probe_opus/$probe_include")
done
for probe_source in laplace bands; do
probe_step "Compile Opus $probe_source.c" "$probe_cc" -v -DOPUS_BUILD -DUSE_ALLOCA -DVAR_ARRAYS "${probe_includes[@]}" \
-O3 -DNDEBUG -std=gnu99 -MD -MF "$probe_dir/$probe_source.d" \
-c "$probe_opus/celt/$probe_source.c" -o "$probe_dir/$probe_source.o" || exit $?
done
probe_build="$probe_root/tools/chatgui/build"
if [ ! -f "$probe_build/CMakeCache.txt" ]; then
printf '[ERROR] Existing ChatGUI build cache not found: %s/CMakeCache.txt\n' "$probe_build"
exit 1
fi
if ! command -v cmake >/dev/null 2>&1; then
printf '[ERROR] cmake is not available in PATH.\n'
exit 1
fi
probe_step 'CMake cache settings' sed -n -E \
'/^CMAKE_(C_COMPILER|C_COMPILER_LAUNCHER|C_FLAGS(_(DEBUG|RELEASE|MINSIZEREL|RELWITHDEBINFO))?|MAKE_PROGRAM|GENERATOR|BUILD_TYPE):/p' \
"$probe_build/CMakeCache.txt" || exit $?
# Повторяем Windows-команду без преобразования аргументов MSYS2; добавляем только GCC -v.
if [ "${MSYSTEM:-}" = UCRT64 ]; then
probe_ninja=$(sed -n 's/^CMAKE_MAKE_PROGRAM:[^=]*=//p' "$probe_build/CMakeCache.txt")
probe_ninja=${probe_ninja%$'\r'}
probe_ninja=$(cygpath -u "$probe_ninja") || exit $?
"$probe_ninja" -C "$probe_build" -t commands opus_internal > "$probe_dir/ninja-commands.txt"
probe_result=$?
if [ "$probe_result" -ne 0 ]; then
cat "$probe_dir/ninja-commands.txt"
printf '[ERROR] Cannot read Ninja commands, exit code: %d\n' "$probe_result"
exit "$probe_result"
fi
probe_command=''
while IFS= read -r probe_line; do
probe_line=${probe_line%$'\r'}
case "$probe_line" in
*' -DOPUS_BUILD '*' -c '*'/celt/bands.c'|*' -DOPUS_BUILD '*' -c '*'/celt/bands.c"')
probe_command="$probe_line"; break ;;
esac
done < "$probe_dir/ninja-commands.txt"
if [ -z "$probe_command" ]; then
printf '[ERROR] Cannot find the Opus bands.c compile command in Ninja.\n'
exit 1
fi
probe_command=${probe_command/ -DOPUS_BUILD/ -v -DOPUS_BUILD}
printf '[INFO] Native command: %s\n' "$probe_command"
printf '@echo off\r\n%s\r\nexit /b %%errorlevel%%\r\n' "$probe_command" > "$probe_dir/replay.cmd" || {
printf '[ERROR] Cannot write the native compiler replay script.\n'
exit 1
}
probe_cmd=$(cygpath -u "${COMSPEC:-C:/Windows/System32/cmd.exe}") || exit $?
probe_replay=$(cygpath -w "$probe_dir/replay.cmd") || exit $?
# CMD сохраняет полный Windows-код завершения cc1; Bash оставляет только младший байт.
probe_cc1=$("$probe_cc" -print-prog-name=cc1) || { printf '[ERROR] Cannot locate cc1.\n'; exit 1; }
probe_cc1=${probe_cc1%$'\r'}
probe_cc1=$(cygpath -u "$probe_cc1") || { printf '[ERROR] Cannot resolve the cc1 path.\n'; exit 1; }
probe_cc1_native=$(cygpath -w "$probe_cc1") || { printf '[ERROR] Cannot resolve the native cc1 path.\n'; exit 1; }
printf '@echo off\r\n"%s" --version\r\nset "UTUN_CC1_RESULT=%%errorlevel%%"\r\n' "$probe_cc1_native" \
> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot write the cc1 probe script.\n'; exit 1; }
printf 'echo [INFO] cc1 Windows exit code: %%UTUN_CC1_RESULT%%\r\nexit /b %%UTUN_CC1_RESULT%%\r\n' \
>> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot finish the cc1 probe script.\n'; exit 1; }
probe_cc1_script=$(cygpath -w "$probe_dir/cc1.cmd") || { printf '[ERROR] Cannot resolve the cc1 script path.\n'; exit 1; }
for probe_cwd in "$probe_root" "$probe_build"; do
cd -- "$probe_cwd" || { printf '[ERROR] Cannot open probe directory: %s\n' "$probe_cwd"; exit 1; }
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 before runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script"
# При ошибке продолжаем сравнение каталогов и DLL, затем повторяем исходный сбой.
done
cd -- "$probe_root" || { printf '[ERROR] Cannot open the repository directory.\n'; exit 1; }
probe_objdump="${probe_cc%/*}/objdump.exe"
"$probe_objdump" -p "$probe_cc1" > "$probe_dir/cc1-imports.txt" || {
printf '[ERROR] Cannot inspect cc1 imports.\n'; exit 1;
}
printf '\n[INFO] cc1 DLL imports:\n'
sed -n '/DLL Name:/p' "$probe_dir/cc1-imports.txt" || { printf '[ERROR] Cannot read cc1 imports.\n'; exit 1; }
source "$probe_root/tools/chatgui/compiler_runtime.sh"
for probe_dll in "$probe_build/"*.dll; do
[ -f "$probe_dll" ] || continue
chatgui_is_compiler_runtime_dll "${probe_dll##*/}" || continue
probe_system_dll="${probe_cc%/*}/${probe_dll##*/}"
[ -f "$probe_system_dll" ] || continue
cmp -s "$probe_dll" "$probe_system_dll"
probe_result=$?
case "$probe_result" in
0) printf '[INFO] DLL matches UCRT64: %s\n' "${probe_dll##*/}" ;;
1) printf '[WARN] DLL differs from UCRT64: %s\n' "${probe_dll##*/}"
probe_step 'DLL hashes' sha256sum "$probe_dll" "$probe_system_dll" || exit $? ;;
*) printf '[ERROR] Cannot compare DLL: %s, exit code: %d\n' "$probe_dll" "$probe_result"; exit "$probe_result" ;;
esac
done
probe_step 'Refresh compiler runtime DLLs' chatgui_refresh_compiler_runtime "$probe_build" "${probe_cc%/*}" || exit $?
cd -- "$probe_build" || { printf '[ERROR] Cannot open the ChatGUI build directory.\n'; exit 1; }
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 after runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script" || exit $?
MSYS2_ARG_CONV_EXCL='*' probe_step 'Replay Ninja bands.c command with GCC verbose output' \
"$probe_cmd" /d /c call "$probe_replay" || exit $?
fi
probe_step 'Build Opus with CMake' cmake --build "$probe_build" --target opus_internal --parallel 1 --verbose || exit $?
printf '\n[SUCCESS] Standalone Opus compilation and CMake Opus build completed.\n'
+63 -5
View File
@@ -62,11 +62,24 @@ AC_ARG_ENABLE([hardening],
[enable_hardening=no])
if test "x$enable_hardening" = "xyes"; then
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat=2"
compiler_version=$($CC --version 2>&1) || AC_MSG_ERROR([Cannot identify compiler for hardening])
case "$compiler_version" in
*clang*) ;;
*) HARDEN_CFLAGS="$HARDEN_CFLAGS -fstack-clash-protection -Wformat-overflow=2" ;;
esac
case "$host" in
*mingw* | *msys* | *cygwin*)
HARDEN_LDFLAGS="-Wl,--dynamicbase,--nxcompat,--high-entropy-va"
;;
*)
HARDEN_CFLAGS="$HARDEN_CFLAGS -fPIE"
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
;;
esac
CFLAGS="$CFLAGS $HARDEN_CFLAGS"
LDFLAGS="$LDFLAGS $HARDEN_LDFLAGS"
AC_MSG_NOTICE([Hardening flags enabled: $HARDEN_CFLAGS])
AC_MSG_NOTICE([Hardening flags enabled: CFLAGS=$HARDEN_CFLAGS LDFLAGS=$HARDEN_LDFLAGS])
fi
# ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS ====================
@@ -95,6 +108,19 @@ AC_MSG_RESULT([$os_freebsd])
AM_CONDITIONAL([OS_FREEBSD], [test "x$os_freebsd" = "xyes"])
# ==================== DETECT LINUX ====================
AC_MSG_CHECKING([for Linux])
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
#ifndef __linux__
#error "Not Linux"
#endif
]], [[ return 0; ]])],
[os_linux=yes],
[os_linux=no])
AC_MSG_RESULT([$os_linux])
AM_CONDITIONAL([OS_LINUX], [test "x$os_linux" = "xyes"])
# Set TUN libs based on platform
if test "x$os_windows" = "xyes"; then
TUN_LIBS="-liphlpapi -lws2_32 -ladvapi32 -lbcrypt -ldbghelp"
@@ -165,6 +191,37 @@ AM_CONDITIONAL([HAVE_FFMPEG], [test "x$with_ffmpeg" = "xyes"])
AC_SUBST([FFMPEG_LIBS])
AC_SUBST([FFMPEG_CFLAGS])
# ==================== SILERO VAD (OPTIONAL, requires ONNX Runtime) ====================
AC_ARG_WITH([silero-vad],
AS_HELP_STRING([--with-silero-vad], [Enable Silero voice activity detector (requires ONNX Runtime)]),
[with_silero_vad=$withval],
[with_silero_vad=no])
if test "x$with_silero_vad" = "xyes"; then
AC_CHECK_HEADER([onnxruntime/onnxruntime_c_api.h], [
save_LIBS="$LIBS"
LIBS="-lonnxruntime $LIBS"
AC_CHECK_LIB([onnxruntime], [OrtGetApiBase], [
SILERO_VAD_LIBS="-lonnxruntime"
AC_DEFINE([HAVE_SILERO_VAD], [1], [Define to enable Silero VAD via ONNX Runtime])
AC_MSG_NOTICE([Silero VAD enabled (ONNX Runtime)])
], [
AC_MSG_WARN([libonnxruntime not found, disabling silero-vad])
with_silero_vad=no
])
LIBS="$save_LIBS"
], [
AC_MSG_WARN([onnxruntime/onnxruntime_c_api.h not found, disabling silero-vad])
with_silero_vad=no
])
fi
if test "x$with_silero_vad" != "xyes"; then
SILERO_VAD_LIBS=""
fi
AM_CONDITIONAL([HAVE_SILERO_VAD], [test "x$with_silero_vad" = "xyes"])
AC_SUBST([SILERO_VAD_LIBS])
# Checks for header files, typedefs, functions...
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h netinet/in.h stdint.h stdlib.h string.h sys/ioctl.h sys/socket.h unistd.h])
AC_TYPE_SIZE_T
@@ -191,10 +248,11 @@ AC_OUTPUT
# Summary
echo "
Configuration summary:
----------------------
---------------------
Host: ${host}
Windows: ${os_windows}
Prefix: ${prefix}
Compiler: ${CC}
CFLAGS: ${CFLAGS}
"
Silero VAD: ${with_silero_vad}
"
+3 -3
View File
@@ -197,14 +197,14 @@ SRC_SOURCES=(
routing_layer/route_lib.c routing_layer/route6_lib.c topo_bgp.c routing_layer/route_ping.c
routing_layer/topo_node.c topo_node_lmdb.c routing_layer/route_connectivity.c routing_layer/conn_mgr.c
routing_layer/routing.c tun_if.c tun_route.c tun_linux.c tun_freebsd.c tun_windows.c
transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
transport_layer/etcp_debug.c transport_layer/etcp_dump.c transport_layer/secure_channel.c transport_layer/crc32.c
transport_layer/stcp_link.c transport_layer/stcp.c transport_layer/stcp_server.c transport_layer/stcp_client.c
transport_layer/pkt_normalizer.c transport_layer/packet_dump.c transport_layer/etcp_api.c
control_server.c
firewall.c eim_nat.c nat_transport.c transport_layer/dummynet.c
etcp_router.c route_crypto.c
proxy/udp_proxy.c proxy/socks_proxy.c
proxy/udp_proxy.c proxy/socks_proxy.c proxy/http_proxy.c
lwip_tcp/lwip_pbuf.c
)
@@ -272,7 +272,7 @@ echo ""
if [ -f "$SCRIPT_DIR/lib/wintun.dll" ]; then
echo "Run on Windows:"
echo " 1. Install Wintun driver (https://www.wintun.net)"
echo " 2. Copy utun.exe + wintun.dll to the same directory"
echo " 2. Copy utun.exe + wintun.dll + LICENSE + LICENSING + THIRD_PARTY_NOTICES to the same directory"
echo " 3. Run: utun.exe -f -p utun.pid -l utun.log"
else
echo "Run on Windows:"
+1524
View File
File diff suppressed because it is too large. Load diff
+335 -69
View File
@@ -1,77 +1,160 @@
# P2P аудио-звонок пользователю — архитектура
# P2P аудио- и видеозвонок пользователю — архитектура
Статус: спроектировано, ждёт реализации.
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
## 1. Область
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
Аудио и видео 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)**.
Модуль оперирует `group_id` + `peer_node_id`. Для `dm:<conv_id>` функция `dm_call_route`
выбирает общую CHAT-группу с живым BGP-маршрутом к собеседнику; отдельная DM-группа не создаётся.
Аудио и видео используют отдельные каналы `etcp_router`; возможности видеокодеков
согласуются в активном звонке (см. [call_video.txt](call_video.txt)). Работает desktop↔desktop и desktop↔android
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
установка соединения (conn_mgr) → аудиопоток → HANGUP.
аудиопоток и опциональное видео через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
### Входящие вызовы и телефонная занятость
Android хранит пять независимых настроек: показывать статус звонка, предупреждать вызывающего,
отклонять при телефонной занятости, отклонять все обычные звонки, отклонять все приглашения в рацию.
Они передаются маской `CALL_POLICY_*` через `[chat] incoming_call_policy` при старте,
а при изменениях — вместе с живым состоянием телефона через JNI в uasync.
Первые три настройки используют `READ_PHONE_STATE`; запреты всех входящих разрешения не требуют.
Наблюдатель учитывает все активные SIM, набор номера, удержание и входящий телефонный вызов.
Без разрешения или без телефонной службы состояние неизвестно, а не свободно.
Приоритет: запрет типа вызова → автоматический отказ при телефонной занятости → предупреждение с выбором.
Отказ при занятости возвращает `CALL_REASON_PHONE_BUSY`/`RADIO_CALL_PHONE_BUSY`.
Предупреждение переводит попытку в `WAIT_DECISION`: у вызываемого нет события INCOMING, рингтона и регистрации в Telecom.
Вызывающий видит «Абонент разговаривает по телефону», «Всё равно позвонить» и «Завершить».
Продолжение подтверждается повторяемой командой внутри той же попытки; освобождение телефона само не запускает звонок.
Повторные пакеты не создают повторных входящих событий. При изменении политики перед продолжением запрет проверяется снова.
Предел всей попытки — 60с от начала, включая выбор; продолжение deadline не меняет.
У рации первый ответ и подтверждение продолжения ограничены 2с; сообщение о занятости является ответом, но не RINGING.
Рация, уже включённая у адресата в указанном канале, возвращает `RADIO_CALL_ALREADY_ACTIVE`:
вызывающий получает уведомление, входящий вызов не показывается, его собственная рация не включается.
Этот ответ описывает существующую подписку и не создаёт нового входящего вызова даже при включённом запрете приглашений.
Принятие обычного приглашения в рацию по-прежнему сначала включает местную подписку, затем отправляет ACCEPT.
Android обычного звонка показывает своё уведомление после `onShowIncomingCallUi` и обрабатывает отказ Telecom.
## 2. Разбиение файлов (отдельный каталог `src/call/`)
| Файл | Язык | Сборки | Назначение |
|---|---|---|---|
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x34` |
| `src/call/call_proto.h` | C | везде | wire-формат сигналинга (`0x36`) и аудио (`0x3F`), subcmd |
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
| `src/call/audio_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
| `src/call/call_video.h/c`, `call_video_transport.h/c` | C | везде | формат видеотрека, согласование возможностей и передача кадров отдельными каналами |
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
Новая категория логирования: `DEBUG_CATEGORY_CALL = 31` (добавить в `debug_config.h`,
`DEBUG_CATEGORY_COUNT` → 32).
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
Desktop направляет сообщения FFmpeg и Qt в общий `debug_file` (по умолчанию `chatgui.log`).
Начальная проверка codec пишет краткий итог на info, детали и ожидаемые отказы — на debug:
`[gui] debug_categories=call=debug,video=debug`. Повторы FFmpeg внутри пробы сворачиваются со счётчиком;
рабочие ошибки параллельного звонка сохраняют error/warn. Информационный stderr libva отключён по умолчанию;
для диагностики драйвера можно явно запустить с `LIBVA_MESSAGING_LEVEL=2`.
Отсутствующая desktop-запись приложения создаётся в XDG data applications для регистрации в portal.
## 3. Протокол (svc_id 0x34, первый байт payload = subcmd)
Формат приёма (как media_delivery, одноуровневый): `entry->dgram[0]=svc_id`,
`entry->dgram[1]=subcmd`, `entry->dgram[2..]=payload`.
## 3. Протокол
Каналы роутера независимы:
| svc_id | Константа | Назначение |
|---|---|---|
| `0x36` | `ETCP_RT_ID_CALL` | сигналинг звонка |
| `0x3D` | `ETCP_RT_ID_CALL_VIDEO_CONTROL` | согласование и восстановление видеотрека |
| `0x3E` | `ETCP_RT_ID_CALL_VIDEO` | видеокадры |
| `0x3F` | `ETCP_RT_ID_CALL_AUDIO` | аудиокадры `CALL_MEDIA` |
Формат приёма сигналинга и аудио: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
Сигналинг (`0x36`):
```
CALL_INVITE 0x01 {call_id:8, group_id:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
CALL_RINGING 0x02 {call_id:8}
CALL_ACCEPT 0x03 {call_id:8, agreed_tracks}
CALL_ACCEPT 0x03 {call_id:8}
CALL_DECLINE 0x04 {call_id:8, reason:1}
CALL_BUSY 0x05 {call_id:8}
CALL_CANCEL 0x06 {call_id:8}
CALL_HANGUP 0x07 {call_id:8, reason:1}
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload}
CALL_END_ACK 0x08 {call_id:8}
CALL_PHONE_BUSY 0x09 {call_id:8}
```
Аудио (`0x3F`):
```
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload}
```
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
- `group_id` — CHAT-группа; для DM-звонка выбирается общая группа участников с живым BGP-маршрутом.
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
- Сигналинг, аудио и видео: обязательные `ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT` между конечными узлами.
- AES-128-CCM шифрует тело, Ed25519 подписывает заголовок роутера и шифртекст. Посредники не имеют ключа для расшифровки.
- Ключ получается из постоянных X25519-идентичностей собеседников; forward secrecy отсутствует.
- Маска защиты задаётся через `etcp_router_require_crypto`: открытые/частично защищённые данные отбрасываются
на конечном узле до изменения состояния приёма, очередей и `receive_ready`; отправка без требуемых флагов возвращает ошибку.
Пустые служебные пакеты роутера не являются медиаданными. Перехода на открытое медиа при ошибках нет.
- Каждый пакет получает случайный nonce(13) от OpenSSL CSPRNG; ошибка RNG прекращает шифрование.
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет.
Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt).
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
`capture_epoch` различает разрывы захвата; `codec_delay_samples` передаёт Opus lookahead.
Старый формат не поддерживается; медиа принимается только от пира текущего звонка.
## 4. Машина состояний
```
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → CONNECTING(ACCEPT, conn_mgr_open) → ACTIVE → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept, conn_mgr_open) → ACTIVE → ENDED
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → ENDED
```
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
Таймауты:
- ring-timeout 45с (нет ответа → CANCEL);
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
- **кодограмма INVITE 300мс** — вызывающий шлёт её весь период вызова
(`OUTGOING` + `RINGING`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
держит маршрут живым;
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
- ring-timeout 60с от начала, включая ожидание решения вызывающего (нет ответа → CANCEL);
- `CONN_EVENT_DOWN/TIMEOUT` CM не завершают звонок: роутер использует доступные пути;
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
Завершающие кодограммы HANGUP/CANCEL/DECLINE повторяются каждые 300мс до END_ACK,
не дольше 20с. Это необходимо при смене эпохи роутера: старые очереди удаляются,
даже если отправитель ранее получил успешный результат постановки команды в очередь.
Медиа и их локальные очереди освобождаются сразу, ENDED публикуется один раз.
В реестре до ACK/таймаута остаётся только завершённая сессия с CM handle и таймером.
Получатель подтверждает также повторные команды после удаления сессии, без повторного ENDED.
Команда и ACK проверяются по подписи, шифрованию, размеру, call_id, пиру и группе существующей сессии.
ACK не подтверждается. Новый звонок имеет другой call_id и не затрагивается поздними командами старого.
При одновременном завершении стороны независимо подтверждают команды друг друга.
После ACK сессия удаляется на ближайшем tick завершения; при удалённом завершении CM удерживается
ещё 500мс для отправки ACK. Остановка ядра отменяет таймер без повторных отправок.
Запоздалые кодограммы вызова при идущем разговоре (`ACTIVE`) игнорируются.
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`;
`CONN_EVENT_UP` → ACTIVE. Медиа идёт `etcp_route_send` (после подъёма прямого линка
роутер идёт напрямую — RTT минимален).
CM handle инициирует оптимальное подключение и удерживается до завершения звонка.
Медиа всегда идёт через `etcp_route_send`: роутер сам выбирает лучший путь и fallback.
Звонок не хранит ETCP_CONN и не завершается при потере отдельного транспорта.
Очистка записи CM отсоединяет handles и отменяет отложенные UP; handle закрывает владелец.
Закрытие CM handle не посылает DISCONNECT удалённым владельцам. Механизм NCD не меняется.
## 5. Отправка медиа — backpressure через локальный буфер
@@ -87,49 +170,213 @@ NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
В сессии:
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью
~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
15с (750 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=0)`;
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=1)` (после проверки места);
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход.
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости).
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в
`media_delivery.c:stream_send_chunk_cb`).
## 6. Thread-модель (single-writer)
## 6. Потоки и жизненный цикл
| Объект | Владелец |
|---|---|
| `call_session` (состояние, encoder, decoder, tx_q, таймеры) | uasync-поток |
| Opus encoder (send) | аудио-поток (encode) |
| Opus decoder (recv) | uasync-поток (`decode → ajb_push`) |
| `ajb` push | uasync-поток |
| `ajb` pull + SoundTouch | аудио-поток |
| `call_tones` | аудио-поток |
`call_session`, CM handle, очередь TX и таймеры принадлежат uasync-потоку.
Аудиопоток кодирует PCM и постит `call_send_media`; приём в uasync кладёт Opus в `call_jitter`.
Декодер, SoundTouch и генератор тонов принадлежат аудиопотоку.
Кольцо кодированных кадров и снимок статистики защищены мьютексом; статистика не читает SoundTouch из другого потока.
`CallTimeline` хранит фактическое соответствие входных участков SoundTouch выходному PCM.
Платформа публикует позицию воспроизведения с локальной монотонной меткой; `call_audio_get_clock`
возвращает исходный PTS слышимого участка. Локальная тишина и тоны удерживают PTS.
Невалидная/устаревшая отметка устройства не разрешает продвижение видео. Контракт и ограничения — `call_video.txt`.
Поток данных:
- TX: аудио-поток кодирует → `call_send_encoded(call_id, opus, len)` → `uasync_post` →
uasync: `tx_q` + `call_tx_flush` (backpressure).
- RX: uasync принимает `CALL_MEDIA` → decode → `ajb_push`; аудио-поток `call_pull_pcm` → `ajb_pull` (+ тоны).
GUI запускает аудиоустройство по CALL_ACCEPTED. При CALL_ENDED начинается финальный тон;
устройство останавливается перед `call_audio_release`. Явное завершение звонка прекращает
воспроизведение накопленной речи и включает тон завершения — на обеих платформах.
Android останавливает гудки до настройки звонкового аудиотракта. После выбора маршрута
capture/play потоки независимо создают и запускают AudioRecord/AudioTrack; каждый сразу обслуживает своё направление.
`start()` подтверждает запуск подготовки, ошибки устройств передаются владельцу на main через `onFailure`.
Ошибка любого направления завершает звонок. Общий `stop()` отменяет подготовку, разблокирует I/O и ждёт оба потока
до освобождения Android-устройств и native-аудиостека. Отдельные locks защищают подготовку/остановку каждого устройства.
Замер AEC планируется через 300мс после готовности обоих направлений и не задерживает воспроизведение.
При наличии Telecom Connection режимом и выходом звонка управляет только Telecom.
ACTIVE запрашивается до создания аудиоустройств; AudioManager/прямой preferred device не дублируют выбор.
На API 34+ маршрут запрашивается через доступный CallEndpoint, на старых — через Connection.setAudioRoute.
Подтверждённый маршрут приходит из callback системы, отдельно от желания пользователя; ошибка запроса логируется.
Если Connection ещё нет, используется AudioManager. Поздняя регистрация передаёт управление Telecom,
снимая заявку приложения на режим и прямые устройства. Рация сохраняет отдельный AudioManager-путь.
Реальное время запуска/переключения зависит от Android и требует проверки на устройстве.
Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте,
событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается.
INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута,
создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления.
Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину.
Эффективный рабочий буфер AudioTrack на всех поддерживаемых API при старте запрашивается размером
два блока PCM (40мс); на API 31+ стартовый порог следует фактически принятому размеру.
Ёмкость выделенной памяти может быть больше. Ограничение повторяется по callback фактической
смены выхода на main, под trackLock; callback старого AudioTrack игнорируется и снимается при stop.
Подачу PCM задаёт свободное место в AudioTrack, без дополнительного таймера 20мс.
Частичная запись удерживает остаток одного блока до полной передачи; следующий pull ждёт.
Сетевой запас остаётся в адаптивном JB, его рост и SoundTouch-догон не изменены.
Лог `playback buffering` показывает причину настройки, запрошенный/принятый размер, порог и ёмкость;
ежесекундный `audio summary` — queueUs, текущий порог и underruns; новый episode underrun имеет уровень WARN.
Это очередь AudioTrack, а не полная сквозная задержка и не сетевой jitter reserve.
Ранее на Samsung startFrames=1920 был принят, но без ограничения рабочего буфера
written-played оставался 120–160мс; одного стартового порога недостаточно.
Замер Samsung подтвердил постоянную очередь 40мс. При прежнем параллельном управлении маршрутом
AudioTrack пересоздавался, очередь временно достигала 160мс, а ожидание свободного места — 328–336мс.
При старте AudioTrack.play занимал 1080мс. После перехода к единственному владельцу Telecom
в проверенном звонке старт занял 62мс, очередь оставалась не более 40мс и при смене выхода.
Переключение всё ещё останавливало потребление на 293–328мс; этот всплеск принимает сетевой JB.
`ajb` = mutex-кольцо PCM (гранулярность 20ms/50 ops в сек — мьютекс бесплатен);
SoundTouch инстанс заперт на pull-стороне.
На desktop `AudioDiag: capture clock` при call=debug выводит секундную сводку исходных backend-блоков:
число блоков/сэмплов, накопленную последовательность, диапазон ошибки относительно конца предыдущего блока,
latency PulseAudio, число скачков >2мс, hole-сэмплы и блоки без достоверного измерения.
Первый скачок за секунду содержит обе latency/поправки конвертера, expected/actual PTS и монотонное время.
Backend публикует только числовой снимок в ограниченное кольцо; форматирование выполняет reporter.
Исходный backend-снимок остаётся наблюдением. В capture worker позиция PCM задаёт непрерывные PTS,
а backend медленно корректирует их привязку: постоянная времени 2с, предел 0.5% (100мкс на 20мс PCM).
Скорректированное время используется и для Opus, и для выбора AEC-референса. Сам PCM не изменяется.
Разрыв sequence, смена route и скачок исходных меток >20мс по-прежнему сбрасывают capture/AEC и эпоху;
порог 2мс в call_audio не увеличивается. Секундный `voice-io: capture clock` показывает phase/error/adjustment.
Тесты проверяют десять минут дрейфа +/-300ppm, скачки оценки до 14мс, разбиение чанков 25мс на Opus 20мс,
сохранность PCM, явную потерю, смену устройства и большой разрыв исходных часов.
Счётчик sequence описывает полученные backend-сэмплы; отсутствие потерь очереди само по себе
не доказывает непрерывность физического захвата. Проверять вместе с hole/overflow и route-событиями.
**Контракт жизненного цикла:** GUI стартует аудио-устройство по `CALL_ACCEPTED`,
останавливает по `CALL_ENDED` и только после остановки (join аудио-потока) вызывает
`call_release(call_id)` (post в uasync) → uasync освобождает session/encoder/decoder/jb.
Пока устройство работает — `call_pull_pcm`/`call_send_encoded` не гоняются с освобождением.
Для захвата звонка desktop явно запрашивает период 20мс вместо дефолта PulseAudio 25мс;
фактический период сообщает `audio: call-capture initialized`. Сборщик по-прежнему принимает произвольные чанки.
Секундные замеры задержки при `call=debug`:
- `voice-io: capture latency`: возраст первого сэмпла перед native feed (включает сам блок 20мс),
подготовка/AEC в worker и вызов native feed; диапазоны min/avg/max в микросекундах.
- `voice-io: playback latency`: приблизительный остаток PCM после mix, включая частичный блок,
длительность pull и недостающие сэмплы. Это очередь приложения, отдельно от backend.
- `AudioDiag: playback clock`: latency backend, задержка конвертера/cache и максимальный размер
backend-порции в микросекундах. При measured=0 нулевые границы не означают нулевую задержку.
- Windows `AudioDiag: WASAPI queue`: текущий padding shared-mode, отдельный stream latency,
converter/cache, размер callback и незавершённого mapped buffer. Диапазоны — min/avg/max в мкс;
`padding_samples=0` означает отсутствие пригодного замера, а не пустую очередь.
Снимок снимается перед заполнением client PCM; запись и форматирование выполняет reporter.
`AudioDiag: audio timing` для стадий `callback`/`write` при `call=debug` показывает интервалы
callback и время блокирующей передачи в WASAPI. HRESULT ошибок и потери диагностической очереди логируются.
Снимки только наблюдают: Windows-аудиочасы остаются `callback-estimate` (`measured=0`).
- `video playout`: показанные кадры, `speed_ppm`, диапазон A/V-фазы, интервалы показа,
опоздание worker и ожидания аудиочасов. Опоздание кадра не вызывает пропуск; догон до 1.6x.
- Desktop `video RX worker`: число декодированных кадров, максимальное время декодирования,
возраст кадра после декодирования и опоздание пробуждения worker после sleep. Снимок аудио
содержит `audio_measured`, PTS и позиции played/written. Все эти строки относятся к `call=debug`;
`video=trace` для этой диагностики не требуется.
- `video GUI delivery`: доставленные изображения, ожидание уведомления, промежуток доставки,
`paint_wait_max_us` до подтверждения рисования. В GUI один ожидающий кадр, замены нет.
- `video port TX paused/resumed`: сохранённый хвост пачки, первый ID, длительность ожидания
и число повторов. Busy сохраняет encoded-кадры и не запрашивает KEY.
- `video port RX full/drained`: заполнение и полная разгрузка encoded FIFO перед возобновлением
приёма. Hysteresis переносит ограничение до новых входов sender encoder, освобождая запас для догона.
- `call audio TX timing`: ожидание g_mtx, обработка/кодирование под ним, ожидание posted task;
post включает ожидание lock при выполнении задачи, его разрешение 100мкс. На Android этот же
замер обработки включает программный AEC, на desktop AEC уже выполнен в capture worker.
- `call audio TX queue`: число ожидающих кадров, возраст старейшего и avg/max ожидания кадров,
переданных роутеру за интервал. Отправка роутеру не является подтверждением доставки пиру.
- Android `audio capture latency`: возраст первого сэмпла перед native feed и число измеренных
привязок; дополнительные поля старта устройств разделяют min-buffer query, constructor, routing и buffer setup.
Возраст PCM содержит часть измеренной обработки: эти числа нельзя механически складывать.
PTS/возраст используют локальный MONOTONIC, интервалы внутри native — локальный get_time_us/get_time_tb;
эти временные базы и часы разных устройств напрямую не вычитаются. Все сводки формируются вне device callback.
Период захвата 20мс и новые измерения требуют контрольного звонка; дополнительная desktop PCM-очередь,
жизненный цикл подготовки Android и таймер показа видео этим изменением не менялись.
## 7. Адаптивный джиттер-буфер (SoundTouch)
На Android `CallPlaybackBuffer` подбирает рабочую очередь отдельно от JB/SoundTouch:
после 30с наблюдаемого продвижения playback head без underrun пробует уменьшить её на один
блок PCM (960 сэмплов, 20мс), до одного блока или фактического минимума устройства.
При росте AudioTrack.underrunCount добавляет один блок в пределах выделенной ёмкости.
Интервал следующей пробы удваивается (60/120/240/300с), чтобы не повторять частые неудачные уменьшения.
После старта, запроса/подтверждения маршрута и увеличения даются 2с на стабилизацию;
после уменьшения первый underrun учитывается немедленно. Отсутствие продвижения head или
пропуск наблюдений >1с обнуляет стабильный участок. Сброс системного счётчика учитывается отдельно.
Фактическая смена выхода начинает подбор с 40мс заново. Непринятая меньшая заявка фиксирует
нижнюю границу до следующего выхода; ошибки API выключают подбор с WARN/ERROR, сохраняя воспроизведение.
Reporter управляет размером под trackLock независимо от фильтра debug; playback не берёт этот lock.
Нет flush/recreate, частичная запись PCM сохраняется. После смены размера/выхода через секунду
повторно измеряется задержка для AEC. Ожидание полного буфера и недостача сети не увеличивают его размер.
Лог настройки содержит reason, previous/requested/actualFrames и прирост underruns;
достижение ёмкости предупреждает не чаще раза в 10с. Принятие и стабильность 20мс ещё требуют
проверки на Samsung; буферы HAL и Bluetooth могут добавлять собственную задержку.
На desktop адаптация устройства ещё не реализована: выход принадлежит SoundManager,
изменение backend-буфера должно выполняться его владельцем, а не отдельным звонком.
Буфер ≤ **1000 мс**, target playout ~60 мс; при росте буфера плавно tempo → **1.6x**.
Диагностика Android при `call=debug`: playback сохраняет один числовой снимок на блок (20мс)
в кольце 512 элементов, без форматирования/вывода. Отдельный reporter раз в 200мс забирает записи;
раз в секунду выводит `audio summary` с min/avg/max очередей, задержек pull/write и опоздания потока,
целевым запасом/разбросом доставки JB, темпом, недостающим PCM и отдельными счётчиками JB/AudioTrack.
Снимок JB не сбрасывает события для CALL_STATS. encodedMs — очередь Opus после pull,
processingMs — оставшийся PCM SoundTouch в длительности исходной речи; reserveMs зависит только от доставки.
Reporter опрашивает AudioTimestamp, routedDevice, underrunCount и размеры буфера не чаще раза в 200мс.
Playback читает только опубликованный неизменяемый снимок. Его begin/end и возраст deviceAgeUs
отличают задержку опроса от задержки аудио; stampQueryUs/routeQueryUs/propertiesQueryUs показывают стоимость запросов.
Android API может удерживать общие внутренние locks даже при вызове из reporter: перенос убирает
прямое ожидание диагностического запроса в playback, но сам по себе не доказывает отсутствие влияния на write/head.
frame/time аппаратного timestamp и время начала опроса печатаются отдельно;
при недоступности стоит stampOk=0. Времена trace — MONOTONIC; очередь устройства — written-played.
Эти показатели нельзя складывать с возрастом AudioTimestamp как независимые задержки.
В playback отдельно измеряются writeCallUs (только вызовы write), headCallUs (чтение позиции),
progressCallUs (публикация позиции ядру), jbSnapshotUs (нативный снимок), writeSpanUs (весь цикл записи)
и blockUs (pull до формирования строки). При частичных записях длительности вызовов суммируются за блок.
pullGapExcessUs — превышение интервала между началами соседних pull над 20мс;
включает ожидание устройства, а не только задержку планировщика (прежнее поле wakeLateUs удалено).
Старое поле writeUs включало чтение позиции и прогресс: его нельзя трактовать как длительность одного AudioTrack.write.
Stop дожидается reporter, включая непрерываемый Android-запрос, до освобождения AudioTrack.
`audio detail` сохраняет 2с до и 3с после старта, фактической смены routedDevice или новой аномалии:
очередь устройства >100мс в течение секунды, отсутствие продвижения head 100мс при наличии данных,
либо новый episode underrun (затухает через 3с без повторений). Порог очереди — диагностический,
не настройка JB и не изменение воспроизведения. События внутри окна объединяются, между выгрузками
не менее 10с; продолжающаяся аномалия не создаёт повторных дампов. Восстановление логируется INFO.
Подробные строки содержат по четыре снимка и заголовок с порядком колонок; при stop окно может быть неполным.
Переполнение кольца при задержке reporter имеет отдельный WARN lost=N. Аудиоданные не сохраняются.
Выключение call=debug прекращает сбор/выгрузку; буфер и reporter освобождаются с playback worker.
Сравнивать сводки следует по call ID и monoUs; рост сетевого запаса сам по себе не является ошибкой.
## 7. Буфер звонка и восстановление
`call_jitter` независим от `radio_jitter`: рация сохраняет свои правила задержки и сброса.
Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса.
Целевой запас — `clamp(max-min, 60, max_reserve_ms)` мс по изменению задержки доставки:
на входящем кадре накапливается разность интервалов приёма и отправки, десять секундных интервалов хранят её экстремумы.
Отправитель уже передаёт timestamp в CALL_MEDIA; смещение часов пиров исключается, переполнение uint32 учитывается.
Подготовка местного аудиоустройства и ускоренное воспроизведение очереди не изменяют оценку доставки.
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь.
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория `call=debug`.
После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x
без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом.
Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.
Роутер сохраняет последовательность и неподтверждённые данные при смене пути.
Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок.
Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные.
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
## 8. Тоны и диагностика
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
- **Исходящий звонок — две фазы с разными гудками** (у вызывающего):
- фаза 1 «пытаемся связаться» (`OUTGOING`: INVITE отправлен/ретранслируется, маршрут
устанавливается) — гудок «соединение» (короткие бипы);
- фаза 2 «звоним» (`RINGING`: получен RINGING, у пира идёт вызов) — длинные гудки (ringback).
Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через `SoundManager`
(`call_connecting`/`call_ringing`), Android — цикличные MP3 через `CallRingback`/`MediaPlayer`.
Переключение — по `CHAT_EVT_CALL_RINGING`,
остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup.
- **Начальное ожидание первого PCM** → тишина без сигнала потери звука. Подготовка удалённого микрофона
и начальное накопление jitter не считаются перебоем. Существующий таймаут 20с без медиа действует с ACTIVE.
- **Недобор PCM ≥500мс после начала воспроизведения** → в `call_audio_pull_pcm` воспроизводится
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл.
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. До порога — тишина при нехватке PCM.
При возобновлении PCM сигнал останавливается; одного входящего пакета без достаточного запаса недостаточно.
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
@@ -142,32 +389,50 @@ CHAT_EVT_CALL_RINGING 33 [call_id:8]
CHAT_EVT_CALL_ACCEPTED 34 [call_id:8]
CHAT_EVT_CALL_ENDED 35 [call_id:8][reason:1]
CHAT_EVT_CALL_DECLINED 36 [call_id:8][reason:1]
CHAT_EVT_CALL_STATS 37 [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2]
CHAT_EVT_CALL_STATS 39 [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2]
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
```
## 10. Публичный API (call.h)
## 10. Основные функции публичного API
```
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
void call_accept(call_id); // callee
void call_decline(call_id);
void call_hangup(call_id);
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
// аудио-поток (общий процессинг в C-ядре):
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
void call_release(call_id); // post → uasync: освободить аудио-объекты
`call.h`: сигналинг и отправка готового Opus выполняются в uasync-потоке.
Из других потоков команды передаются через `uasync_post`/мост GUI и соответствующие `call_*_trampoline`.
`ch_id` — идентификатор CHAT-группы либо `dm:<conv_id>`; `out_call_id` — значение, заранее сгенерированное вызывающим.
```c
void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id);
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
const struct call_capture_time* timing);
```
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
`call_audio.h`: движок сам кодирует PCM и ставит отправку Opus в uasync.
Захват и воспроизведение используют по одному аудиопотоку на направление.
```c
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us);
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us);
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
void call_audio_release(uint64_t call_id);
```
`feed_pcm` принимает ровно 960 отсчётов; `feed_prepared_pcm` — 960 отсчётов после платформенного AEC
и маску mute. `call_audio_release` вызывается в GUI-потоке после остановки обоих направлений I/O.
Создание движка — `call_audio_start`, для подготовленного PCM — `call_audio_start_prepared`.
Видео настраивается через `call_video_configure`, кадры отправляются через `call_video_send_frame`
в uasync-потоке; полный контракт — в `call_video_transport.h`.
`call_init / call_destroy` вызываются из `chat_service_start / chat_service_stop` в `utun_instance.c`:
инициализация после `dm_core_init`, освобождение перед `dm_core_destroy`.
## 11. Платформенный I/O
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
(capture через VoiceAudioIo → `call_audio_feed_prepared_pcm`, `call_audio_pull_pcm` → playback) + окно звонка (UI)
+ события в `gui_bridge_impl.cpp`.
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
@@ -184,7 +449,8 @@ void call_release(call_id); // post → uasync: о
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
`LANGUAGES C CXX`, те же файлы.
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
- `src/transport_layer/etcp_api.h`: `ETCP_RT_ID_CALL=0x36`, `ETCP_RT_ID_CALL_VIDEO_CONTROL=0x3D`,
`ETCP_RT_ID_CALL_VIDEO=0x3E`, `ETCP_RT_ID_CALL_AUDIO=0x3F`.
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
## 13. Порядок задач
+201 -66
View File
@@ -1,93 +1,228 @@
# DM — прямой p2p чат между двумя пользователями группы
# DM: доставка сообщений и файлов
## 1. Цель
Личные беседы принадлежат `src/dm/`. Общие CHAT-группы используются для знакомства,
проверки ролей и маршрутизации; группа не становится идентичностью беседы.
Возможность начать личный чат с любым пользователем доступных групп.
Пока target offline сообщения временно хранятся на storage-узлах.
## Реализовано
## 2. Ключевые решения
- История и постоянная исходящая очередь сообщений в общей SQLite ядра.
- Доставка через живой маршрут router, включая BGP-транзит.
- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон.
- Приём после проверки подписи и расшифровки, подтверждение только после commit.
- Доставка с суперузла после возвращения получателя, независимо от отправителя.
- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов.
- Подготовка, шифрование, проверка и durable публикация медиа в рабочих потоках.
- Прямая передача файлов либо custody на отдельном M, доставка после restart и подтверждённое удаление.
- TTL и квота M; вложения и статусы доставки в Qt и Android.
- Голосовые Opus и видео MP4 с зашифрованными метаданными; общий ввод и отображение с каналами.
- Отдельная DM-подсистема (НЕ канал: без TOPO_GROUP/member_sync/merkle).
- Идентичность детерминированная: одна ветка на пару, согласование без handshake.
- E2E-шифрование содержимого сразу.
- Offline-хранение: dm_mailbox на storage-узлах с флагом dm_storage.
- Доставка: fallback — сперва проверка наличия узла в BGP-группе; доступен →
прямой send; нет связи → storage.
## Идентичность и шифрование
## 3. Идентичность и ключи (детерминированные)
```text
conv_hash = SHA256("utun_dm_v" || min(node_a,node_b) || max(node_a,node_b))
conv_id = native_u64(conv_hash[0:8]) & 0x7FFFFFFFFFFFFFFF
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
msg_nonce = SHA256(conv_id || author || seq)[..13]
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
```
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
shared = X25519(my_x25519_priv, peer_x25519_pub)
content_key = SHA256(shared || "utun_dm_content")
Префикс conv_hash — 9 ASCII-байт без NUL. ID узлов подаются в хеш в порядке
байтов хоста; native_u64 читает первые 8 байт хеша в том же порядке.
Нулевой conv_id заменяется на 1.
Обе стороны выводят одинаковые значения из своего privkey и pubkey пира
(известен из общей группы). Смена x25519 у пира = новая ветка.
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID.
## 4. Модель данных (SQLite, общий chats.db)
Каноническое тело сообщения:
dm_conversations(conv_id TEXT PK, peer_node_id, peer_x25519 BLOB, peer_ed25519 BLOB,
peer_name TEXT, last_out_seq INT, last_in_seq INT, created_at INT, last_ts INT)
dm_messages(conv_id TEXT, dir INT, seq INT, ts INT, author INT, ct TEXT,
data BLOB, sig BLOB, PRIMARY KEY(conv_id,dir,seq))
dm_mail(recipient INT, sender INT, conv_id TEXT, seq INT, ts INT, data BLOB, sig BLOB,
ttl INT, PRIMARY KEY(recipient,sender,seq))
```text
[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64]
```
Два независимых направленных потока (A→B, B→A) с монотонным seq.
Без chain-хеша: для 2 пиров достаточно seq + catch-up по диапазону.
Автор подписывает все поля до sig. conv связывает автора с получателем.
Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`.
В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID.
Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64.
## 5. Доставка
```text
[kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice]
```
1. Проверка доступности: peer присутствует в BGP-группе (общая группа).
2. Доступен → прямой send (сервис ETCP_RT_ID_DM = 0x34), ждём DM_ACK{conv_id,seq}.
Нет ACK за таймаут → fallback в mailbox.
3. Нет связи → MAIL_PUT на 1..N storage-узлов.
4. Catch-up при connect: DM_HELLO{conv_id,last_out_seq,last_in_seq} → пир досылает
(in_seq+1 .. out_seq).
5. Dedup: вставка только при seq > last_in_seq.
Поля метаданных многобайтной длины имеют big-endian формат. kind: 1=file,
2=voice, 3=video. Имя — basename до 255 байт, без разделителей, управляющих символов
и некорректного UTF-8. FILE имеет нулевые duration/dimensions; VOICE — положительную
длительность и 100 символов уровней waveform; VIDEO — длительность и размеры.
Длительность ограничена 24 часами, размеры — 16384. Метаданные проверяются до
локального commit и до подтверждения получателем; S/M видят только ciphertext.
## 6. E2E-шифрование
Медиа ciphertext — последовательность порций с отдельным tag каждой порции.
Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256
ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все
tags, точный размер и хеш; владелец публикует временный файл только после успеха
и собственного durable сохранения.
data_enc = nonce(13B, счётчик по seq) || AES-CCM_ct || tag
## Владение и данные
secure_channel уже умеет AES-CCM. Storage/relay видят только шифротекст.
- `dm_core`: беседы, история, outbox и подтверждение получателя.
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
- `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети.
- `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений.
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
- `attachment`: формат, валидация и тип метаданных, без сети и БД.
- `voice_file`: общий Opus-кодек отправки; `attachment_send` готовит файл в worker
и передаёт результат отдельно в `chat_core` либо `dm_media`.
## 7. Storage-узлы (dm_storage)
```text
dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name,
group_id, last_out_seq, last_in_seq, created_at, last_ts)
dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
dm_outbox(conv_id, seq, body, retry_at)
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
```
- Новый флаг dm_storage=yes в adm_tags (блок B владельца), распространяется
существующим member_sync без нового синка.
- DM читает adm_tags мемберов общих групп (peers_<ch>) и кэширует storage-узлы.
- Протокол mailbox (сервис ETCP_RT_ID_DM_MAILBOX = 0x35):
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox —
одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть.
Приём, создание/обновление беседы и история — также одна транзакция.
MAIL_PUT {recipient,sender,conv_id,seq,ts,data,sig}
MAIL_PULL {recipient, per-sender cursor}
MAIL_ACK {up_to_seq}
Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq —
статистика максимального номера, не курсор подтверждения: seq=99 после seq=100
сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
TTL 7 дней + лимит объёма, dedup по (recipient,sender,seq).
У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи
через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется
backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры,
но сохраняет БД. При старте очередь продолжает работу.
## 8. Старт и anti-spam
## Подтверждения и протокол
- dm_start(peer): создать беседу (вывести conv_id/ключи), обеспечить прямое
соединение (переиспользовать chat_sync_connect_node/node_conn_direct).
- Пир автопринимает на первом DM_HELLO/сообщении, только если отправитель в
общей группе.
Сервис `ETCP_RT_ID_DM=0x34`: MSG=1, RECEIPT=2.
## 9. Файлы и интеграция
```text
["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64]
```
| Файл | Назначение |
|------|-----------|
| src/dm/dm_core.c/h | беседы, send/recv, таблицы, bind 0x34, conn-события, fallback |
| src/dm/dm_crypto.c/h | derive + AES-CCM |
| src/dm/dm_mailbox.c/h | storage-узел, push/pull/ack, bind 0x35 |
Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта.
Отправитель удаляет outbox только после проверки подписи и точного хеша тела.
История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию.
Плюс:
Сервис `ETCP_RT_ID_DM_MAILBOX=0x35`:
- src/chat/chat_member.c — бит CHAT_MEMBER_FLAG_DM_STORAGE + парсинг dm_storage.
- utun_instance.c — dm_core_init/destroy.
- chat_core — API dm_start/send/list + события CHAT_EVT_DM_*.
- headless CLI — dm list/send/messages.
- src/Makefile.am — новые источники.
```text
PUT=1 [recipient:8][message]
DELIVER=2 [message]
RECEIPT=3 [receipt:136]
STORED=4 [conv:8][seq:8]
```
## 10. Сервисы ETCP
Все пакеты идут с подписью router; unsigned control отклоняется.
PUT принимается только от автора, при роли supernode=yes у локального узла
в общей группе автора и получателя. Роль берётся из подписанного adm_tags;
node_type=4 — производная сетевой классификации, для авторизации не используется.
storage=yes само по себе не даёт права хранить сообщения.
ETCP_RT_ID_DM = 0x34
ETCP_RT_ID_DM_MAILBOX = 0x35
STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся
у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется
по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция.
Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу.
dm_mail_receipts содержит только служебные квитанции; доставленное содержимое
с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и
диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется.
TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа.
## Доставка медиа по pm.txt
1. Общая передача байтов: явные peer/group/file/size, backpressure и CM handle
на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT.
Используется `ETCP_RT_ID_FILE_TRANSFER=0x37`. Один callback читает/отправляет
не более 8 КиБ, продолжение через `call_soon`; inflight ограничен 16 пакетами.
2. Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание,
временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне
группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются.
3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M.
M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK;
поздний STORED не возрождает завершённую доставку.
5. TTL медиа `dm_media_ttl_sec` по умолчанию 604800 с; квота `dm_media_storage_mb`
по умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при
недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока.
6. Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт
media_async с живыми owner-контекстами. Группы уничтожаются после owners.
Control `ETCP_RT_ID_DM_MEDIA=0x38`: OFFER, STORE, STORED, RECEIPT, DELETE,
DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в `dm_media.h` и реализациях owners.
Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author,
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`,
`dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M
учитываются при удалении; повторный STORE не продлевает TTL.
Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений,
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`;
Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет
оригинал; «как видео» в Qt использует общий FFmpeg worker и метаданные конечного MP4.
Android записывает MP4 через CameraX; импорт MP4 проходит через платформенный
MediaMetadataRetriever в IO. Другие контейнеры на Android отправляются как файлы.
Голосовая запись отдаёт worker собственный PCM либо компрессор без копирования
в GUI/uasync; worker выполняет flush, Opus-кодирование и вычисление waveform.
Кодирование Qt и Android использует один `voice_file`. Адресат фиксируется до
записи, выбора URI и подготовки; завершение не читает текущую беседу GUI.
Подготовленный PM-файл и Android cache удаляются после копирования, ошибки или отмены;
пользовательский исходник Qt остаётся во владении пользователя.
Событие 46 `[target_len:1][target][job_UUID:16][state:1]` сообщает начало (0),
готовность (1) и ошибку (2). Target канала — числовой ID, PM — `dm:<conv_id>`.
Подготовка, доставка и воспроизведение имеют независимые состояния. UI отображает
только задания текущей беседы. Состояния файла и delivery receipt приходят в JSON
41; `content_type`, duration, dimensions и waveform преобразуются в общие роли
сообщения. Канальные скачивания вызываются контроллером только для каналов;
PM-файлы доставляются автоматически через `dm_media`.
Android использует общие `ConversationComposer`/`ConversationMessages` для обеих
бесед; Qt — `InputBar` и `MessageDelegate`. Превью Qt создаются в общем пуле
с двумя workers; декодирование длинного Opus-файла выполняется вне GUI. Оба плеера
проверяют заголовок, пакеты и end marker; Qt ограничивает PCM буфер 256 МиБ.
## Проверки
`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
неверные author/UUID/hash, обрезание и лишние байты.
`test_dm_e2e`: реальные A–C–B соединения и BGP; нет физического A–B линка.
Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue,
ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT.
Первый сценарий передаёт 40 offline-сообщений (больше порции pump) и перезапускает источник с pending outbox; второй перезапускает
суперузел и доставляет первое сообщение при выключенном источнике.
`test_dm_media`: реальные A/B/S/M с раздельными ролями supernode и storage;
прямая передача, custody, restart S/M при выключенном A, точный файл на B,
удаление копии и тела S, поздние STORE/STORED, TTL, отказ по квоте, неверная квитанция,
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
Голосовое передаётся напрямую; метаданные видео сохраняются после offline custody
и restart S/M при выключенном источнике.
`test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.
Destroy отменяет уже posted completion, callback вызывается ровно один раз.
`test_attachment`: строгий формат, повреждения и граничные длины, реальное
кодирование/декодирование 50 Opus-кадров, длительность и waveform.
`test_attachment_send`: выбранная беседа после смены текущего target, освобождение
PCM в worker, остановка до completion без регистрации и удаление отклонённого cache.
Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder.
Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` —
117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS.
Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`.
В тесте worker максимальный интервал heartbeat составил 10,3 мс.
Интерактивная проверка GUI на телефоне и десктопе не выполнялась.
+118
View File
@@ -0,0 +1,118 @@
# Зашифрованные вложения группового чата
В desktop chatgui после выбора файла или перетаскивания в чат рядом с его именем
появляется флажок «Зашифровать вложение». Он применяется к выбранному файлу,
изображению или видео. Удаление, замена файла и переход в другую беседу сбрасывают
флажок. Выбор фиксируется вместе с адресатом при отправке, до подготовки.
В DM флажок скрыт: используется существующее автоматическое E2E-шифрование лички.
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
название «Зашифрованное вложение» вместо manifest.
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
передаётся через обычные media_index/media_delivery. UUID транспортного медиа
и crypto_UUID имеют разные назначения: первый идентифицирует сетевые блоки,
второй — контейнер и ключ. Повтор доставки передаёт прежний контейнер.
## Подписанное сообщение и контейнер
В подписанном сообщении `ct=application/x-utun-encrypted`, базовое имя тела —
`<crypto_UUID:32hex>.uenc`. Остальные поля manifest описывают шифротекст:
размер, SHA256 всего контейнера, UUID и подписи блоков. Ключ, исходное имя,
MIME, waveform и остальные медиаметаданные в историю канала не записываются.
Контейнер:
```text
magic "UTENC001":8
crypto_UUID:16
plaintext_size:8BE
encrypted_metadata_size:2BE
SHA256(encrypted_file_body):32
encrypted_metadata:variable
encrypted_file_body:variable
```
Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`.
Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт.
Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт
с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`;
формат существующего DM при их выделении не менялся.
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
все tags файла, точный размер и hash тела. Результат пишется во временный файл
и публикуется после успешной проверки и durable rename. Все длительные файловые
операции и криптография выполняются в media_async.
## Хранение и разрешения
Локальные таблицы не регистрируются в db_sync или Merkle:
- `attachment_keys`: ключ автора/получателя, связь с общим message_id,
crypto_UUID, hash и пути двух копий, расшифрованные метаданные и состояние.
- `attachment_key_requests`: один запрос на `(channel,message_id,peer)`,
направление, решение и дата создания.
- `attachment_key_outbox`: команда, адресат и время следующей попытки.
Шифротекст хранится в `media/<channel>/<crypto_UUID>.uenc`, локальная открытая
копия — в `decrypted/<channel>/<crypto_UUID>.file`. В media_files и объявлениях
держателей остаётся только шифротекст, включая backfill после restart.
Квота медиакеша учитывает обе копии; очистка кеша не удаляет ключ. При повторном
открытии удалённая открытая копия восстанавливается с сохранённым ключом.
После полной проверенной загрузки обычный клиент автоматически запрашивает
ключ автора. Фоновая копия узла с подписанной ролью `storage=yes` запрос не
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
Сетевая классификация `peers.storage` для этого не используется.
В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения.
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
и подтверждённая доставка ключа; отказ также сохраняется.
## Адресный протокол
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x42`. Каждый пакет обязан иметь
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
```text
magic "AK01":4 | command:1 | message_id:32 | crypto_UUID:16 | cipher_hash:32
GRANT дополнительно содержит key:32
REQUEST=1, REQUEST_ACK=2, GRANT=3, DENY=4, RESULT_ACK=5
```
Автор принимает запрос только на собственное опубликованное сообщение с
сохранённым ключом. Участие запрашивающего проверяется по полному подписанному
мемберу, join и дереву приглашений; удалённые мемберы отклоняются. Проверка
повторяется при решении и перед каждой отправкой GRANT. Получатель принимает
ответ только от автора, по своему существующему запросу и точной идентичности
контейнера. Конфликтующие ключи и результаты отклоняются.
REQUEST_ACK отправляется после сохранения запроса. GRANT/DENY появляются
в outbox только после сохранения решения. Полученный ключ сохраняется до
RESULT_ACK и до запуска расшифровки. Повторы возвращают прежнее решение и не
создают новых строк. Таймер обрабатывает ограниченную порцию очереди с
backpressure и повтором через 5 секунд; restart продолжает очереди.
Для доставки нужен живой маршрут обеих сторон: офлайн-mailbox этого сервиса
не реализован. Запрос ждёт появления автора; решение ждёт появления получателя.
Уже выданный ключ невозможно отозвать у получателя. Защита содержимого не
скрывает автора, канал, время и размер шифротекста. Автоматическая публикация
транскрипции зашифрованного голосового в канал не запускается.
## Проверки
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
лишние байты и отсутствие частичного plaintext после отказа.
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
восстановление удалённого plaintext без нового согласия и удаление канала.
+5 -1
View File
@@ -1,4 +1,8 @@
Архитектура etcp:
> Исторический документ: описания NAT, модулей и ограничения обмена маршрутами ниже не отражают текущую реализацию.
> Актуальные правила группового обмена маршрутами — в [topo_group.h](../src/routing_layer/topo_group.h),
> модель узла — в [node_snapshot.md](node_snapshot.md), протокол роутера — в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md).
Архитектура etcp:
ETCP устанавливает только одно подключение между двумя узлами. Это подключение может добавлять-убирать каналы связи (динамически) без разрыва соединения.
Также каналы связи мониторятся и трафик распределяется между доступными каналами. Если канал определяется как недоступен - он исключается из обмена.
+178
View File
@@ -0,0 +1,178 @@
# Разбор полного прогона 2026-09-27
Исходный результат `./check.sh` вне песочницы: 92 passed, 5 failed, 1 skipped (98 тестов).
Песочница запрещает сетевые сокеты; её результаты не использовались для оценки протокола.
Промежуточный прогон: 96 passed, 1 failed, 1 skipped; оставался `test_chat_join_e2e`.
После KEY_REGISTER_ACK и исправления невыровненных чтений повторный полный
`make check -j4`: **97 passed, 0 failed, 1 skipped (98)**.
Лог: `/tmp/utun-ack-final-check.log`. Исходное падение отдельного root-теста и
последующая доработка описаны ниже. После исправления autosockets общий набор дал
97 PASS / 1 SKIP, а `check-proxy`, `check-burst`, `check-load` прошли:
`/tmp/root-fix-full-check.log`.
`test_etcp_lifecycle`, `test_etcp_link_stress`, `test_conn_mgr_phases` дополнительно
прошли ASan/UBSan/LeakSanitizer. Инструментированы тесты и изменённые транспортные/
маршрутные модули, остальные объекты библиотек взяты из обычной сборки.
Логи: `/tmp/ncd-late-asan.log`, `/tmp/link-role-asan.log`, `/tmp/cm-phases-asan.log`.
## Исправленные причины
- `test_conn_mgr_phases`: после TIMEOUT фазы DIRECT NCD навсегда подавлял UP,
хотя conn_mgr сохранял handle для REVERSE. В логе физический ETCP уже UP,
но conn_mgr продолжал DIRECT_REQ до своего таймаута. TIMEOUT теперь обозначает
истечение срока первой попытки, CLOSED — окончательное завершение. Удерживаемый
handle получает поздний UP. Отдельный lifecycle-регрессионный тест воспроизводит
TIMEOUT → INIT/UP (одно уведомление) → DOWN → UP; до исправления он падал.
- `test_etcp_link_stress`: ожидал reset всей сессии при коллизии дополнительного
линка. Проверка заменена на сохранение эпох, отсутствие reset и дублей линков,
доставку и ACK сообщений через новый линк. Прежний тест забирал сырые фрагменты
вместо normalizer и мог считать служебные пакеты полезным трафиком. Теперь он
использует отдельный ETCP service и проверяет каждый байт и порядок сообщений.
Эта проверка выявила ошибку реализации: при принятии встречного INIT сохранённый
исходящий линк не менял `is_server`. Роль обновляется при отправке INIT_RESPONSE.
- `test_bgp_route_exchange`: после разрыва B–C recovery успешно строил C–A,
нарушая предположение теста о фиксированной цепочке. Теперь A и C принимают
только ключ B: все прежние проверки withdraw/restore сохранены, обход запрещён
условиями тестовой сети. Автоматический обход отдельно проверяет `test_topo_recovery`.
- `test_tcp_io`: адрес 192.0.2.1:81 в окружении устанавливал TCP-соединение,
поэтому connect timeout закономерно не срабатывал. Теперь используется локальный
listener с заполненной accept-очередью, проверяется ETIMEDOUT. Перед уничтожением
uasync тест завершает отложенное освобождение TCP и проверяет баланс таймеров.
Диагностика тестов включается `UTUN_TEST_DEBUG=1`; обычный запуск не включает DEBUG.
Остаточные DATA после удаления соединения могут быть undecryptable и сами по себе
не означают ошибку транспорта.
## Исправлено: гонка регистрации приглашения
`test_chat_join_e2e`, happy path A != C, падал из-за отсутствия подтверждения KEY_REGISTER.
Раньше `chat_invite_build_link()` вызывал `chat_join_register_key()` и сразу возвращал ссылку.
Успех `etcp_route_send()` означает постановку в очередь. Он не подтверждает обработку
ключа посредником C. Параллельное подключение J–C может завершиться раньше доставки A–C.
Доказательство: `/tmp/join-e2e-debug.log`, один ключ `90c4b081833450a0`:
- 01:39:02.437851 — A пишет `join-key registered`, фактически ключ только поставлен в отправку.
- 01:39:02.446527 — C отвергает JOIN_INFO_REQ: ключ ещё неизвестен.
- 01:39:02.449724 — C сохраняет этот ключ, но J уже получил терминальный ERROR.
Теперь C отвечает KEY_REGISTER_ACK только после сохранения ключа. A сопоставляет
channel/target/key и выдаёт ссылку после успешного ACK и локального сохранения ключа.
Отрицательный ACK завершает запрос ошибкой. Чужие, неподписанные, незашифрованные,
повторные и запоздалые подтверждения не могут завершить другой запрос.
Контракт асинхронных API `chat_join_register_key` / `chat_invite_build_link`:
NULL означает ошибку запуска без callback; принятый запрос даёт ровно один callback.
Обычное завершение происходит после возврата API, включая self. Cancel/destroy
завершают запрос синхронно с CANCELLED. Handle недействителен после callback.
Для удалённого узла дедлайн 5 секунд; доставку и повтор потерянных пакетов обеспечивает
надёжный router. Повторная регистрация того же ключа тем же инвайтером обновляет TTL;
замена инвайтера для существующего ключа запрещена. Отмена может оставить ключ на C
до истечения TTL, но A не авторизует по нему вход, поскольку локально такой ключ
сохраняется только при успешном завершении.
GUI получает request_id в событии и игнорирует устаревшие результаты. Новый запрос
отменяет предыдущее ожидание GUI. Headless связывает ответ с JSON id и отменяет все
ожидания отключившегося клиента. Ни один из путей не запускает вложенный event loop.
Дополнительные исправления:
- Устранён double-free при ошибке `etcp_route_send`: пакет уже освобождён роутером.
- Headless прекращает разбор пакета после `quit`; последующая команда не создаёт
запрос на уже закрытом клиенте. При destroy отменяются cleanup_timer и запросы,
удаляются клиентские сокеты и освобождается контекст.
- UBSan обнаружил невыровненное чтение group_id из `d + 1` в chat_sync. Оно и три
аналогичных диагностических чтения заменены на memcpy.
- Тест chat_join теперь останавливает вручную запущенный member_sync;
проверяется баланс выделения/освобождения таймеров.
Проверки:
- `test_chat_join`: 31/31, включая задержанный/отсутствующий ACK, таймаут,
неверные channel/peer/key/flags/размер/status, дубликат, отказ хранения из-за
конфликта инвайтера, отмену, shutdown, параллельные ключи и ошибку отправки.
- `test_chat_join_e2e`: 3/3 (A≠C, A=C, неверный ключ). C принудительно откладывает
регистрацию на 200 мс; проверяется отсутствие ссылки до обработки регистрации.
Дополнительно проверяются подавление старого GUI-запроса и реальный headless
control socket: параллельные ответы, валидность ссылок, quit и команда после него.
- ASan/UBSan: оба теста проходят. LeakSanitizer и баланс таймеров проверены в
test_chat_join; E2E использует fork/_exit, поэтому сам по себе не проверяет утечки
при завершении дочерних процессов. Инструментированы изменённые chat-модули и
transport/routing, остальные объекты взяты из обычной библиотеки.
Логи: `/tmp/utun-ack-join-asan.log`, `/tmp/utun-ack-e2e-asan.log`.
## Отдельный root-тест auto_socket_dynamic
Запуск в `sudo unshare --net` с поднятым loopback закончился TIMEOUT через 30 секунд.
Лог: `/tmp/utun-ack-root-checks.log`. Это не PASS основного набора: в нём тест пропущен.
В конфиге теста включён auto_sockets, но не отключён стандартный фильтр
`auto_socket_skip_no_default_route=1`. Dummy-интерфейсы теста не имеют default route,
а `auto_socket.c:reconcile_iface()` исключает такие интерфейсы. Лог показывает
`no links created`, затем TIMEOUT. Исправленный тест создаёт собственный network
namespace, поднимает loopback и добавляет default route только клиентским dummy.
Фильтр остаётся включённым; отдельно проверяется исключение серверного dummy,
иначе его сокет скрывал бы полный обрыв клиентской сети.
Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor
в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе
не отменялись все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках
трафика освобождался entry без queue_dgram_free. Теперь у каждого таймера свой slot,
waiters отменяются до teardown, payload освобождается, deferred cleanup завершается
до проверки баланса таймеров, сокетов и `u_get_allocated_count()`.
Новый генератор шлёт 1024-байтовые пакеты независимо в обе стороны, по одному через
threshold waiter, с бюджетом 64 пакета на tick. Проверяются содержимое, строгий seq,
ограниченность TX/RX очередей, реальное срабатывание backpressure, остановка
генераторов при полном обрыве и полное опустошение после восстановления. NCD handle
удерживает логическую сессию: смена интерфейса не является разрешением терять её данные.
Остатки после действительно удалённого соединения — отдельный допустимый случай.
Прогон нагрузки обнаружил ошибки, скрытые прежними проверками:
- `tcp_socket_add` не сохранял `netif_index`: монитор принимал interface-bound сокет
за сокет default route и менял его адрес при событиях другого интерфейса.
- Удаление TCP модели искало её по имени уже удалённого интерфейса и порту из БД.
Теперь запись интерфейса хранит имя и прямую ссылку на модель; один путь закрывает
listener, линки и модель. Портовые записи удаляются с сохранённым именем.
- `tcp_socket_remove` не закрывал линки, созданные вне NCD. Теперь закрывает все
связанные линки до освобождения модели; root-тест создаёт такой линк отдельно.
- Поиск UDP модели не исключал TCP. Добавлена проверка типа сокета.
- При selective ACK sender мог выдавать новые seq за границей `MAX_INFLIGHT_SIZE`
относительно cumulative ACK. Теперь генерация seq останавливается на границе,
cumulative ACK возобновляет очередь даже при отсутствии уже selectively-acked записей.
Тесты проверяют блокировку, возобновление и uint32 wrap. ACK не может подтвердить
ещё не назначенный seq; декодирование старшего байта seq/ACK не делает signed shift.
- Timestamp Q16 умножался как signed int и переполнялся на циклических/отрицательных
смещениях. Перевод выполняется в uint32. Jitter раньше всегда получал нулевую разницу:
предыдущий RTT читался после перезаписи; теперь берётся до неё, EWMA использует int64.
- UBSan выявил невыровненные canary/указатели в `mem.c` и `memory_pool.c`. Метаданные
читаются/пишутся через memcpy; покрыты размеры блоков 1..17 и повторное использование пула.
- Повторный общий прогон поймал `test_etcp_connect`: PID-порты пересекались с занятым
UDP-портом (`EADDRINUSE`). Теперь сервер bind-ится на порт 0, клиент получает фактический
порт через getsockname; собственного алгоритма поиска свободного порта нет.
Root ASan/UBSan/LSan: PASS, 153335 / 175912 пакетов, reinit=0, все очереди пусты,
баланс памяти/таймеров/сокетов совпал (`/tmp/root-fix-asan.log`). Инструментированы
тест, ETCP/session/router/NCD/autosocket/STCP/normalizer, очереди, event loop и аллокаторы;
прочие объекты архивов обычные. Unit-логи: `/tmp/root-session-asan.log`,
`/tmp/root-memory-asan.log`. LSan запускается вне sandbox: под ptrace он не работает.
Финальная чистая обычная сборка: `./check.sh` — **97 PASS / 0 FAIL / 1 SKIP**;
proxy, burst и load — PASS (`/tmp/root-fix-final-check.log`). Отдельный root-тест —
PASS, 188069 / 191552 пакета, все очереди пусты, reinit=0, баланс ресурсов совпал
(`/tmp/root-test-final.log`). После добавления drain отложенной очистки
`test_etcp_connect` повторно прошёл без остаточных таймеров (`/tmp/root-connect-final.log`).
Подробные socket/connection INFO включаются только через `UTUN_TEST_DEBUG=1`.
`ENETUNREACH` непосредственно при принудительном удалении IP/интерфейса ожидаем до
обработки netlink; тест требует последующей доставки всех данных. Ошибки выхода
за receive window недопустимы и после исправления sender gate не наблюдаются.
## Попутная проблема диагностики
`lib/debug_config.c:debug_parse_config()` требует `:`, хотя документация описывает `=`.
Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории
теперь являются индексами. Исправление в этот набор не включено; тесты используют
`debug_set_category_level()` напрямую.
+132 -39
View File
@@ -41,6 +41,12 @@ inflight - две очереди (ll_queue):
- send_hist[8] - история через какие линки отправлялся (индекс линка)
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
input_queue даже если подтверждённые записи уже удалены по selective ACK.
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
optimal_inflight расчитывается из RTT и bandwidth линка.
bandwidth по каждому линку адаптивно подстраиваем:
@@ -59,8 +65,12 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных.
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка):
Для DATA/ACK после этих 3 байт добавляется обязательный заголовок сессии:
[0x09][sender_epoch(8 BE)][receiver_epoch(8 BE)], затем идут секции из etcp.c.
Итого шифруется: 3 + 17 + data_len байт для DATA/ACK;
для остальных кодограмм — 3 + (data_len - noencrypt_len) байт.
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
а) Подтверждения (ACK) - заголовок 0x01:
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
@@ -108,82 +118,165 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
- клиент отправляет init запрос и выставляет таймаут
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03).
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04).
- каждая сторона сообщает в INIT собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
только подходящий CONFIRM сбрасывает поток и заменяет эпоху пира, сохраняя собственную.
- повтор INIT и коллизия встречных подключений сами по себе не сбрасывают поток.
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT_REQUEST (0x02);
если link_state=2 (reconnect) — INIT_REQUEST_NOINIT (0x04). Коды INIT не заменяют подтверждение смены эпохи.
link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
**** Формат кодограмм для etcp_connections.c/h ****
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется.
Типы пакетов:
0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии)
0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом)
0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса)
0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса)
0x02 — ETCP_INIT_REQUEST (начальный handshake линка)
0x03 — ETCP_INIT_RESPONSE (вариант ответа INIT)
0x04 — ETCP_INIT_REQUEST_NOINIT (восстановление линка, также разрешение коллизии)
0x05 — ETCP_INIT_RESPONSE_NOINIT (вариант ответа INIT)
0x06 — ETCP_PING (проверка доступности)
0x07 — ETCP_PONG (ответ на пинг)
0x08 — ETCP_KEEPALIVE
0x09 — ETCP_SESSION_DATA (обёртка секций DATA/ACK с эпохами)
0x0a — ETCP_SESSION_HELLO
0x0b — ETCP_SESSION_CHALLENGE
0x0c — ETCP_SESSION_CONFIRM
0x0d — ETCP_TCP_ADDRESS (STCP: code + family(4/6) + observed IP(16), без порта)
Варианты ответа 0x03/0x05 не определяют смену эпохи: клиент в обоих случаях обрабатывает reset_id одинаково.
1) Init запрос (0x02 / 0x04):
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)]
[recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)]
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [keepalive(2 BE)]
[recovery(2 BE)] [link_id(1)] [socket_id(1)] [only_local(1)] [type(1)]
[src_ipv4(4)] [src_port(2 BE)] [collision(1)] [ed25519_pubkey(32)] [device_type(1)]
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
- code: 0x02 (со сбросом) или 0x04 (без сброса)
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
- code: 0x02 или 0x04, вариант handshake линка
- node_id: 64 бита, идентификатор узла (big-endian)
- session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian)
- mtu_local: uint16_t локальный MTU
- keepalive_interval: uint16_t интервал keepalive в мс
- recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100)
- local_link_id: uint8_t (0-255), назначается отправителем
- sock_id: uint8_t, уникальный ID сокета отправителя
- reset_id: uint64_t, собственная локальная эпоха потока клиента (big-endian)
- mtu: uint16_t локальный MTU
- keepalive: uint16_t интервал keepalive в мс
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
- link_id: uint8_t (0-255), назначается отправителем
- socket_id: uint8_t, уникальный ID сокета отправителя
- only_local: uint8_t, флаг "только локальные подключения"
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
в noencrypt_len байтах (не попадает под AES-шифрование)
Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт.
Между нодами только одно подключение, но можно добавлять каналы.
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
2) Init подтверждение (0x03 / 0x05):
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)]
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)]
[random_padding...]
- code: 0x03 (со сбросом) или 0x05 (без сброса)
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [link_id(1)]
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2 BE)]
[ed25519_pubkey(32)] [device_type(1)] [keepalive(2 BE)] [random_padding...]
Фиксированное тело до padding — 64 байта (ETCP_INIT_RESPONSE_PKT).
- code: 0x03 или 0x05, вариант ответа INIT; сам по себе не сбрасывает поток клиента
- node_id: 64 бита, big-endian
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
- mtu_local: uint16_t локальный MTU сервера
- local_link_id: uint8_t идентификатор канала сервера
- remote_socket_id: uint8_t (копируется из запроса)
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
- mtu: uint16_t локальный MTU сервера
- link_id: uint8_t идентификатор канала сервера
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
- peer_port: 2 байта (порт клиента как его видит сервер)
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер, в сетевом порядке)
- peer_port: uint16_t (порт клиента как его видит сервер, big-endian)
При IPv6 оба поля нулевые.
- ed25519_pubkey: публичный ключ Ed25519 сервера, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- keepalive: uint16_t интервал keepalive сервера в мс
- random_padding: случайные байты
3) PING (0x06):
[0x06] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...]
[0x06] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией).
Используется для проверки доступности узла и NAT-тестирования.
4) PONG (0x07):
[0x07] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...]
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Ответ на PING. nonce копируется из запроса для сопоставления.
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
socket_id(1), generation(8, BE), round(8, BE).
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
При получении init получатель пакета должен:
- reset ETCP_LINK с этим ip_port если он есть
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение.
При получении INIT получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
приём кодограмм из UDP выглядит так:
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
Keepalive: пакет без секций (только timestamp+flag_up в шифрованном заголовке, data_len=0).
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
Padding keepalive, PING/PONG и STCP-handshake:
- ETCP_CONN хранит собственный диапазон min/max и независимый таймер для всех своих линков.
- UTUN_INSTANCE хранит отдельный диапазон и таймер для PING/PONG без существующего ETCP_CONN.
- При создании владельца и каждом обновлении: min=rand(0,30), max=min+rand(32,128), границы включительные.
- Следующее обновление назначается через rand(50,300) секунд. При закрытии владельца таймер отменяется.
- Каждый пакет независимо выбирает длину rand(min,max); padding заполняется нулями ДО шифрования.
- UDP PING/PONG: padding шифруется перед CRC/tag; открытый salt+pubkey остаётся в конце датаграммы.
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
После handshake CTR непрерывно продолжается через padding и DATA, без повторной инициализации на кадр.
- Padding не должен превышать 158 байт. Для UDP длина ограничивается оставшимся MTU;
если не помещается даже min, отправка завершается ошибкой без обрезания пользовательских данных.
- Диагностика диапазонов/обновлений/PING/PONG: etcp=debug; keepalive: keepalive=debug.
Клиент: если все линки =down то начинается процедура восстановления связи:
- клиент посылает init без сброса (0x04), link_state=2
- сервер как получает init проверяет session_id:
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
**** Подтверждение транспортной сессии (etcp_session.c/h) ****
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
uses the same control protocol on every initialized UDP or TCP link, bypassing the
normalizer and reliable DATA/ACK queues. A stream reset does not generate physical DOWN/UP.
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with
an epoch mismatch are discarded before stream state or link liveness is updated.
Control frames (all 25 bytes after timestamp/flag):
0a HELLO | sender_epoch | receiver_epoch | 0
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
The first nonzero peer epoch from a physical handshake is stored directly if unknown.
HELLO and a change to an already known epoch advertised by INIT only request a challenge. Only a
CONFIRM for the current local epoch and pending random challenge commits a peer
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
Both sides may reset concurrently. Loss of any control frame is recovered by a
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes
the connection; an unconfirmed candidate alone is discarded without closing a
working session. Close cancels the timer before destroying links.
TCP may initially receive peer epoch zero because the server creates ETCP_CONN
only after STCP handshake. It completes the same session confirmation before INIT/UP.
REINIT notifies stream consumers without tearing down physical ownership. The
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
its previous control messages may have been discarded by the stream reset.
+2
View File
@@ -1,3 +1,5 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
# ETCP Router Architecture
## Структуры данных
+81
View File
@@ -0,0 +1,81 @@
Аватарка CHAT-группы
===================
Модуль src/chat/group_avatar.c. Картинка — бинарный BLOB до 16384 байт в общей
SQLite каждого участника: channels.avatar_data/version/hash/signature.
Storage-сервер, каталоги файлов и пользовательские квоты не используются.
При старте добавляются отсутствующие колонки одной транзакцией, записи/ключи
каналов сохраняются. channel_put использует UPSERT и сохраняет аватарку,
UI-состояние и локальные приватные ключи; смена публичных ключей существующего
channel_id запрещена. Удаление группы удаляет и её аватарку.
Право публикации — приватный Ed25519 ключ группы плюс собственное действующее
членство. creator UID/admin-флаг не заменяют ключ. group_avatar_set подписывает:
ASCII "group-avatar-v1" без NUL, group:u64 LE, version:u64 LE, size:u16 LE, bytes.
Версия — max(NTP microseconds/1000, previous+1), ограничена INT64_MAX.
SHA256 считается по картинке; при одинаковой версии побеждает больший SHA256.
Обе конкурирующие записи должны иметь правильную подпись группы; конфликт — WARN.
Публикация одинаковой картинки не создаёт новую версию. Удаление — подписанная
запись с size=0 и новой версией; подпись/версия сохраняются, старый пир не может
восстановить удалённую картинку. Чтение проверяет сохранённый хеш и подпись.
Транспорт
---------
ETCP_RT_ID_GROUP_AVATAR=0x3A, router wire version=1. Отправитель/группа берутся
из проверенного router-заголовка. Все пакеты подписаны ключом узла и зашифрованы
end-to-end; в DATA дополнительно передаётся подпись группы над всей картинкой.
Обмен только между READY-соседями CHAT-группы с действующим подписанным членством.
Любой участник пересылает сохранённую запись, владелец ключа может быть offline.
Все числа протокола LE. Общая часть, 44 байта:
version:u8=1, command:u8, avatar_version:u64, total_size:u16, SHA256:32.
STATE=1 объявляет локальную версию/хеш; начальное отсутствие — version=0, hash=0.
GET=2 запрашивает актуальную запись. Объявление не меняет БД без полной DATA.
DATA=3 добавляет group_signature:64, offset:u16, block_size:u16, bytes.
DATA header=112 байт. Картинка разбивается на блоки по 4096 байт; последний
короче. Полная картинка 16 КиБ — ровно четыре DATA. Удаление — один DATA без bytes.
Порядок блоков гарантирует router. Сборка проверяет offset, постоянные version,
total_size/hash/signature и допустимые размеры. До полной сборки прежняя картинка
остаётся в БД. После SHA256/Ed25519 вся запись заменяется одним UPDATE, затем
CHANNEL_UPDATED доставляет изменение GUI и STATE распространяется соседям.
Промежуточные блоки не публикуются, ACK/retransmit каждого блока — только router.
Отправка заполняет router до нижнего порога send_q count<=0 и ждёт send_ready;
планировщик отправляет максимум четыре пакета за callback. Snapshot отправителя
не меняется при новом локальном обновлении. Максимум 16 tx и отдельно 16 rx
сборок на экземпляр, каждая до 16 КиБ. receive_ready при заполнении rx сохраняет
пакет в router recv_q без продвижения ACK; освобождение слота возобновляет приём.
Несовпадение эпох/закрытие router отменяет незавершённую сборку и начинает сверку.
Эпоха нового контекста берётся из текущей READY-сессии, не сбрасывает первый приём.
Обнаружение соседей/таймауты — таймер 1 с; STATE также раз в 30 с. Нет прогресса
15 с — отмена незавершённого snapshot, повтор запроса/сверки. Остановка сервиса
отменяет таймеры/waiters/call_soon до удаления группы/ядра.
ChatGUI
-------
Настройки группы: Upload group avatar/Remove group avatar, предпросмотр и статус.
Кнопки доступны только при локальном приватном ключе. Применение сразу, отдельно
от Save локальных настроек. Worker обрезает центральный квадрат, создаёт JPEG
128×128, снижает качество/размер до лимита 16 КиБ. Вход ограничен 16 мегапикселями.
GUI отправляет байты в uasync; ядро повторно проверяет размер/членство/ключ.
SQLite — постоянный кеш. GroupAvatarManager хранит QImage до 32 МиБ, ключ кеша:
group/version/hash. Декодирование в отдельном worker, проверка SHA256, JPEG и
квадратных размеров до 128×128. Поздний результат старой версии не заменяет
новую. Готовая картинка обновляет только строку группы; без аватарки — инициалы.
Приватные ключи/байты картинки не выводятся в диагностику.
Проверки
--------
test_group_avatar: настоящие два ядра/UDP/router, 16 КиБ четырьмя блоками,
атомарность сборки, hash/signature/cross-group отказ, приостановка receive,
перезапуск канала, stale/conflict, редактор с ключом вне creator UID, подписанное
удаление, повторное описание группы и restart БД. Schema failure откатывает
частичные ALTER, сохраняет прежние данные/ключи, повторное добавление идемпотентно.
test_group_avatars (Qt): нормализация, лимит/отображение, кеш, права кнопок и отказ
ядра без ключа, удаление. chat=debug показывает состояния и каждый 4КиБ блок;
chat=info — публикации и жизненный цикл. Ошибки содержат group/peer/version/offset.
+528
View File
@@ -0,0 +1,528 @@
# Linux: диагностика и восстановление аудио
Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
аудита; раздел «Реализация» описывает действующие контракты и проверки.
## Вывод
До исправлений тракт был некорректен при сбоях. Самое существенное: воспроизведение
звонка зависит от наличия микрофонных кадров в duplex-ring. Его опустошение
останавливает вызов пользовательского callback, оставляя хвост выходного буфера
незаполненным. Восстановление jitter-буфера не исправляет остановившееся устройство.
Отдельного управляющего механизма восстановления устройств не было.
В реализации восстановлены три гарантии: каждый запрос output полностью заполнен; обработка
одного callback всегда ограничена; остановка/пересоздание устройства выполняются
вне аудиопотока и не уничтожают сетевую сессию звонка.
## Что установлено по работающим клиентам
В локальном старом логе звонка `132e4a9698f47bdf` с Samsung SM_A525F наблюдались
паузы callback примерно 0,5–2,6 секунды при зарегистрированной обработке 0–2 мс.
Входящий буфер desktop достигал 15 секунд, росли потери, включалось ускорение 160%.
На Samsung захват продолжал давать около 50 кадров/с, playback регистрировал
underrun и тоны разрыва. RTT порядка 1–4 мс не объясняет эти паузы сам по себе.
Это признаки проблемы обслуживания локального аудио, но ещё не доказательство
конкретной причины зависания PulseAudio. Старое измерение исключало время
синхронных DEBUG-вызовов между концом обработки и обновлением timestamp.
Новая диагностика охватывает весь callback, включая эти логи.
Работающий GUI не перезапускался, поэтому новый инструмент ещё не записал этот
сбой на реальном звонке. Лог Samsung сохранён в `/tmp/sm-audio-current.log`.
## Найденные дефекты и ограничения до исправлений
| Место | Поведение и последствия | Необходимая доработка |
| --- | --- | --- |
| `lib/miniaudio.h`, `ma_device__handle_duplex_callback_playback()` | При пустом capture-ring `acquire_read` возвращает success с нулём кадров. Выполняется `break`, output остаётся частично незаполненным, call callback не вызывается. PulseAudio затем отправляет весь mapped buffer. | Обеспечить полный output и продолжение playback при отсутствии capture. Нельзя делать commit_read для синтетических кадров. |
| `ma_device_on_write__pulse()` | Успешная запись с нулём обработанных кадров не уменьшает остаток цикла. Возможно бесконечное вращение с блокированием того же PA mainloop, включая capture. | Немедленно выходить при отсутствии продвижения, возвращать управление mainloop. Логировать состояние и запрос, не пытаться записывать до успеха в этом callback. |
| `ma_device_on_read__pulse()` | PA hole отбрасывается без подачи соответствующего количества нулевых кадров. Из capture timeline исчезает интервал, duplex-ring может опустеть. | Сохранять длительность hole тишиной либо явно обозначать discontinuity независимому capture-тракту. |
| `lib/speex_aec.c`, `speex_aec_feed_playback()` | Обрабатывает только остаток одного 960-sample кадра, отбрасывая хвост входного блока. Контракт разрешает произвольный count. | Циклом потреблять весь count, складывать все полные кадры, сохранять остаток. |
| `CallAudioEngine::setPlaybackDeviceIndex()` | После неудачного пересоздания `m_active` остаётся true при отсутствии устройства; `start` того же звонка возвращает успех. | Разделить желание продолжать звонок и работоспособность устройства. Ошибка запуска переводит устройство в recovering. |
| `RadioAudioEngine::teardownDevice()` | Если playback init успешен, а start нет, uninit пропускается: он зависит от флага started. Объект удаляется вместе с незавершёнными ресурсами. | Разделить initialized и started; каждый успешный init обязательно завершать uninit. |
| `MainWindow::~MainWindow()` / `SoundManager::shutdown()` | Общий context освобождается вместе с engine, но звонок и рация здесь явно не останавливаются. Их singleton-объекты могут оставлять живые устройства. `m_context` также не обнуляется. | Сначала остановить все зависимые устройства и дождаться callbacks, затем освобождать общий context. |
| `AudioRecorder` | Callback выбирает глобальный `g_recorder`, игнорируя `pUserData`. Второй recorder, в том числе тест микрофона, заменяет получателя. Флаги/уровень/вектор waveform используются из GUI и аудиопотока без согласованной синхронизации. | Использовать `pUserData`; передавать уровни атомарно, waveform — безопасным snapshot; буфер записи должен иметь однозначного владельца. |
До исправлений первые два дефекта воспроизводились стендом без аппаратуры:
duplex оставлял sentinel в output при starvation, а PA-write без продвижения
требовал внешней остановки. AEC из 1200 + 720 samples сохранял один кадр вместо
двух. Теперь эти проверки требуют полного output, самостоятельного возврата
из callback и двух сохранённых AEC-кадров; результаты приведены ниже.
Capture-AEC также обрабатывает не более одного кадра за вызов. В звонке его
вызывают ровно с 960 samples, поэтому текущий call feed этот случай не нарушает.
При расширении API нельзя объявлять произвольный count без согласованного размера
выходного буфера. `speex_aec_reset()` уже существует: новый API для сброса самого
AEC не нужен, но нужен согласованный сброс всего локального voice-тракта.
## Чистота реализации
Этот раздел фиксирует исходный аудит; изменения владения и ограничение работы
callback описаны в разделе «Реализация».
Нормальная структура уже есть: GUI владеет устройствами, сервисный слой кодирует
Opus и обрабатывает jitter, сетевой поток доставляет пакеты. Нет необходимости
переписывать весь стек или заводить новый backend.
Однако границы владения не завершены: context фактически принадлежит engine
звуковых уведомлений, а используется независимыми устройствами звонка/рации.
Флаги active/started/initialized местами смешивают три разных состояния.
Выбор input применяется к recorder, звонок использует default input, рация —
default устройства. Output звонка и SoundManager тоже выбираются отдельно.
Индекс перечисления не является устойчивой идентичностью устройства после hotplug.
В radio capture fallback `useDefault` фактически повторяет ту же попытку:
`pDeviceID` и так NULL в обеих ветках. Callbacks рации запускаются раньше
подготовки capture-буфера и установки VAD; порядок запуска нужно привести к
«сначала состояние и буферы, затем устройства», чтобы исключить гонки при старте.
Callback не является строго realtime-safe. Capture удерживает `g_mtx` во время
AEC/AGC/Opus; отправка выделяет память и вызывает `uasync_post`. Playback вызывает
SoundTouch и код с возможным расширением контейнеров, mutex и синхронными логами.
Приём пакетов тоже может держать mutex при работе с jitter и логировании.
Плохие Opus-кадры могут последовательно выбираться из большой очереди в одном
pull: нужен предел работы на callback. Новая диагностика покажет, где это
действительно нарушает deadline. Перенос всего DSP в ещё один worker заранее
увеличит число очередей и точек синхронизации без установленной необходимости.
AEC получает reference только звонка. Звуки отдельного SoundManager в него не
попадают; delay по умолчанию фиксирован, не вычисляется по фактической задержке
устройств. Это ограничение качества эхоподавления, а не установленная причина
текущего зависания.
Jitter умеет повторно накопить резерв после сетевого underrun, но диапазон tempo
ограничен 1,0–1,6. Ускорение убирает избыток, а замедления для компенсации более
медленных часов отправителя нет. При устойчивом отрицательном дрейфе резерв будет
снова опустошаться. Это отдельная задача длительных звонков: после восстановления
устройств проверить наклон глубины буфера и при необходимости добавить небольшую
компенсацию дрейфа в обе стороны, не смешивая её с быстрым catch-up после паузы.
Принудительные 480 frames задают период 10 мс при 48 кГц. Собственный PA default
miniaudio — 25 мс с комментарием о проблемах PipeWire при периодах меньше ~20 мс.
Период backend и Opus-кадр 20 мс должны оставаться независимыми. Увеличивать
период до исправления AEC нельзя: 1200-sample блоки уже воспроизводят потерю хвоста.
После исправления разумно сначала проверить default backend и измерить задержку.
## Предлагаемое восстановление без большого усложнения
### 1. Сначала восстановить инварианты обработки и владения
Исправить полный output, zero-progress, AEC accumulation и uninit при неудачном
start. Перед освобождением context явно остановить recorder, звонок, рацию и
прочие его устройства. До остановки отменять recovery timer; после join можно
сбрасывать voice-ресурсы. Проверять возвраты feed/start/init и логировать причину.
Самый небольшой вариант — оставить один duplex-device: при нехватке capture
подать в call callback нулевой input, полностью сформировать playback и не
продвигать реальный capture-ring за синтетические samples. Это чинит непосредственный
дефект, но сохраняет общий жизненный цикл input/output и синтетический TX по
playback-clock.
Для устойчивости к исчезновению микрофона предпочтительнее два устройства
capture/playback внутри существующего `CallAudioEngine`, как уже сделано в рации.
Capture только накапливает 960 samples и вызывает feed; playback независимо
вызывает pull и всегда заполняет output. Это два handle с одним владельцем,
без нового DSP-worker или дополнительной PCM-очереди. Потребуется уточнить
потоковый контракт voice-слоя: один capture-writer и один playback-writer,
согласованный доступ к AEC, остановка обоих до release. Текущий `g_mtx` уже
защищает AEC, но остальные общие поля нужно проверить перед разделением.
### 2. Разделить краткий underrun и неисправность устройства
| Событие | Действие |
| --- | --- |
| Нет сетевого PCM, callback продолжает приходить | Тишина/существующий gap tone; jitter автоматически накапливает данные. Устройство не пересоздавать. |
| Один backend underrun, новые кадры принимаются | Подать следующий полностью заполненный буфер; собрать счётчик. Не сбрасывать AEC и не запускать restart на каждый xrun. |
| Hole/переполнение capture | Учесть потерянную длительность; краткую потерю пережить, длительную отметить discontinuity для AEC. |
| Поток failed/stopped или нет продвижения кадров длительное время | Перейти в recovering и пересоздать затронутое устройство вне callback. |
| Suspend/resume ОС | Учитывать suspend как отдельную причину, восстановление проверять после resume; не запускать частый restart-loop во время сна. |
| Потерян общий backend/context | Однократно остановить всех его пользователей, восстановить владельца, затем требуемые устройства. |
Один owner timer и состояния `Stopped / Running / Recovering` достаточны.
`wanted` хранит желание пользователя продолжать; generation меняется при stop,
новом звонке и смене устройства. Каждая отложенная попытка проверяет generation
и wanted — законченный звонок никогда не возрождается.
Callbacks только обновляют атомарные счётчики успешно обработанных кадров и
признак backend failure. Watchdog проверяет фактический прогресс отдельно по
направлениям. Тишина в PCM не означает остановку устройства. Reporter диагностической
очереди не должен становиться watchdog: его события могут теряться.
Начальные значения для проверки: timer 200 мс, подозрение при отсутствии прогресса
500 мс после startup grace; повторные попытки через 250/500/1000 мс с потолком
2 секунды. Это предлагаемые настройки, не доказанные универсальные пределы.
Suspend и фактический согласованный backend period должны учитываться отдельно.
Нельзя обещать bounded recovery, пока init/stop/uninit сами могут зависнуть:
сначала устранить spin-loop и проверить время backend-операций. Если они реально
блокируют GUI, вынести только управление устройствами в один owner-thread,
сохранив тот же автомат состояний.
На пересоздании сбросить capture-accumulator, AEC delay/filter и локальный PCM/
stretch, сохранив call ID и сетевую сессию. После длительной аппаратной паузы
устаревшую речь нужно явно отбросить до актуального резерва jitter, иначе
15 секунд накопленного звука при скорости 160% разгружаются примерно 25 секунд.
Этот сброс применять только при аппаратной discontinuity; обычный сетевой jitter
не должен автоматически приводить к отбрасыванию очереди.
Устройство хранить по backend ID либо как default, не по индексу. При его
исчезновении попробовать default и явно сообщить о смене; выбранное устройство
можно вернуть при появлении. Нужны ошибки с ID/именем/направлением и результатом
попытки. Для PA-ошибок следующий уровень диагностики — stream/context state и
`pa_context_errno`, потому что один `-1` от API не объясняет причину.
### 3. Проверить сценарии, а не только успешный старт
После реализации добавить проверки: capture starvation 20/500 мс и возврат;
hole; AEC блоки, пересекающие границу кадра; zero writable; ошибка записи;
исчезновение выбранного устройства и появление default; suspend/resume;
перезапуск звукового сервера; stop во время backoff; закрытие GUI при активном
звонке/рации. Проверять полный output, bounded очереди и работу callback,
возобновление прогресса, отсутствие callbacks после release и отсутствие
повторного старта после пользовательского stop.
## Как читать добавленную диагностику
В GUI-категориях включить `call=debug` либо задать отдельную строку в INI:
```ini
[debug]
call=debug
```
Обычные сводки идут на DEBUG, аномалии — WARN/ERROR; miniaudio info перенесён
на DEBUG, подробный backend log — TRACE. Все устройства используют категорию
call, включая recorder, radio и sound-engine; role и ID различают их в строках.
Не включать глобальный trace для всего проекта при измерении аудио.
Reporter раз в секунду печатает stage, tid, callbacks/s, requested frames/s,
максимальный интервал между входами, полное wall-time обработки и thread CPU-time,
незавершённый вход и время с последнего входа. `work_max` и `cpu_max` — отдельные
максимумы за окно, они не обязательно относятся к одному вызову.
| Stage | Что измеряется |
| --- | --- |
| loop | Один `pa_mainloop_iterate`, включая ожидание и вложенные callbacks. Долгое время само по себе не доказывает тяжёлый DSP. |
| read / write | Полный backend read/write callback. Frames/s здесь — запрошенные, не гарантированно обработанные кадры. |
| map | `writable_size` и `begin_write`; пустой/ошибочный выход тоже закрывает измерение. |
| submit | Вызов `pa_stream_write`. |
| duplex | Перенос capture-ring в duplex-output; detail содержит глубину capture-ring. |
| callback | Полный miniaudio data callback, включая код приложения и его синхронные логи. |
| feed / pull | Capture DSP/encode/send и playback jitter/stretch/tone в звонке. |
`unfilled_frames` — незаполненный output, `hole_frames` — потеря capture timeline,
`capture_drop_frames` — переполнение capture-ring, `no_progress` — PA-write без
продвижения. `backend_underruns` — отдельный callback самого PA-stream; это не
то же самое, что пустой сетевой jitter. `backend playback started/resumed`
показывает фактическое начало/возобновление playback. Notification содержит
числовые miniaudio type/state.
Первое событие каждого типа за окно имеет tid и CLOCK_MONOTONIC timestamp;
повторения суммируются. Для задержавшихся потоков reporter читает `/proc` wchan
и накопленные schedstat CPU/ready-wait/slices. Сравнивать соседние snapshots:
сами totals не являются длительностью текущей паузы. CPU-time намного меньше
wall-time означает ожидание/вытеснение; wchan помогает отличить poll от mutex/
I/O wait. Это снимок в момент отчёта, не исторический стек зависшего вызова.
Producer не выделяет память, не ждёт mutex reporter и не пишет диагностические
логи: ограниченная MPSC-очередь 8192 события, не более четырёх CAS-попыток.
Reporter не разыменовывает ma_device/pa_stream. Переполнение логируется как
`diagnostic events lost`; такое окно нельзя считать полным. Инструментация
добавляет получение часов/TID и атомарные операции, поэтому overhead ненулевой.
Частые события call jitter/AEC/TX теперь публикуются счётчиками и выводятся из
uasync вне аудиомьютекса. Часть существующих логов рации остаётся синхронной;
тракт не объявляется строго realtime-safe.
После close устройство удаляется из reporter: для устройств короче секунды
доступны lifecycle и первые аномалии, но не полная секундная сводка.
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
## Реализация (2026-10-02)
`SoundManager` владеет общим контекстом и конечными выходами. На каждый фактический
выход приходится один микшер: PCM сообщений/видео, заранее декодированные сигналы,
готовый голос звонка и рации. После суммирования float PCM ограничивается, и один
и тот же stereo буфер отправляется устройству и AEC соответствующего микрофона.
Звук другого выхода в референс не входит. Default/явный backend ID объединяются;
PulseAudio также публикует идентификатор фактического маршрута. После server move
совпавшие выходы объединяются в GUI только после подтверждения фактического
маршрута; предположение о default не используется вместо PA snapshot. При временной коллизии дополнительный выход
отдаёт тишину до объединения. Изменение списка источников выполняется после stop
callback, с сохранением backend stream и выбранного сервером маршрута.
Устройства сохраняются как backend ID вместо индекса. При init/start failure
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется;
вернуться к нему можно повторным выбором или при следующем recovery. Автоматический
опрос hotplug для возврата на первоначальный выбор не добавлен.
`AudioDevice` — владелец одного miniaudio device в GUI. Успешный init всегда имеет
парный uninit, включая ошибку start. Callback и его состояние готовятся до start.
Close останавливает/join-ит device worker перед освобождением callback. Отдельный
атомарный счётчик кадров контролирует прогресс. Таймер 200 мс проверяет stopped
и отсутствие продвижения 1 с; suspend продлевает grace, отдельный xrun не вызывает
restart. Backoff: 250/500/1000/2000 мс, сброс после продвижения.
Miniaudio не добавляет накопитель фиксированного периода: `noFixedSizedCallback`
включён, hardware chunks могут иметь любую длину. В PulseAudio физическое начало
блока определяется по timing snapshot с AUTO_TIMING_UPDATE/INTERPOLATE_TIMING.
Capture: monotonic now минус latency read index, без повторного вычитания длины
блока. Playback: now плюс latency write index. Учитываются задержка converter и
остающийся input cache. Один backend dispatch имеет один snapshot, позиции
последующих client callbacks продвигаются по числу кадров. В отсутствие snapshot
используется оценка по callback; источник времени и physical route логируются.
PA hole отдельно помечен как отсутствующий capture, а не как реальная тишина.
`VoiceAudioIo` создаётся только для звонка и рации. Capture и RX обрабатывают
независимые потоки. Hardware capture только копирует PCM в фиксированную MPSC-очередь;
RX worker выполняет decode/SoundTouch/тоны и поддерживает до 40 мс готового голоса.
В сервисном слое рации вычисление Silero держит только TX mutex: общий mutex
отпущен, поэтому VAD не блокирует RX; lifetime и порядок PCM/PTT сохранены.
Hardware output смешивает готовый PCM. Opus, AEC, VAD, AGC и растущие буферы записи
не исполняются в аппаратных callbacks. Обработка самого backend, PCM/resampler
узлов miniaudio и атомарные уведомления остаются в callback; строгая hard realtime
гарантия не заявляется.
Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. Кадр AEC —
960 отсчётов на канал; 1/2 канала микрофона и 2 независимых канала референса.
Противофазное стерео не усредняется. История рендера содержит до 1,92 с PCM и
физические timestamps. Reference выбирается по времени микрофона; небольшое
различие часов компенсируется интерполяцией только референса. Реальная тишина
сохраняется как валидный PCM; при отсутствующем reference AEC пропускает микрофон
и не адаптируется. Потеря reference, смена маршрута/поколения и скачок часов
сбрасывают фильтр. Возраст capture-очереди ограничен 100 мс от enqueue, отдельно
от собственной задержки backend. Потери и очередь видны в сводках категории `aec`.
Mute применяется после AEC/AGC: фильтр продолжает получать настоящий микрофон,
а Opus получает нули только в отмеченной части кадра. Desktop запускает core
через `call_audio_start_prepared`: повторного core AEC нет. Raw API с core AEC
сохранён для Android/headless; смешивать политики в активном звонке нельзя.
У рации один автомат передачи: manual PTT или VAD. VAD читает непрерывный очищенный
сигнал до AGC, включая время ручного PTT. Передача использует отдельный накопитель
Opus-кадра; границы PTT внутри AEC-кадра сохраняются. При закрытии серии хвост
дополняется нулями только перед Opus и помещается в очередь перед FIN. Короткий
capture tail при stop проходит без AEC, а не дополняет непрерывный фильтр. Разрыв
захвата завершает старый сетевой хвост и сбрасывает VAD, исключая склейку через
потерянные отсчёты. Capture restart завершает принятую серию; новое прослушивание
того же канала отменяет старые BEGIN/media. FIN проверяет токен своего capture/burst
и поколение экземпляра ядра, поэтому не закрывает более новую серию и не обращается
к освобождённому ядру даже при повторном использовании его адреса.
TX ограничен 8 media-задачами, возраст — 200 мс. У рации дополнительно ограничено
16 control-задач; FIN выделяется заранее вместе с BEGIN. Если control-очередь
занята, новый BEGIN откладывается до освобождения; это логируется. Очередь PCM
резервирует 8 слотов для управляющих сообщений; producer не меняет consumer и
публикует слот только после копирования. Capture callback никогда не ждёт слот.
Отказ постановки GUI-команды завершает capture; таймер запускает recovery.
`AudioRecorder` получает raw microphone без AEC. Его worker обрабатывает AGC,
накапливает запись и публикует атомарный уровень/длительность. Переполнение или
ошибка обработки отклоняют запись с логом. Meter-only использует тот же capture,
но не сохраняет PCM и не создаёт голосовое сообщение. Stop: close device, drain
и join worker, затем передача владельца записи задаче attachment. Настройки
компрессора в ходе записи применяются к следующей.
PCM воспроизводится через независимые handles. Сообщение, видео и тест динамика
не останавливают чужой PCM; sample rate принадлежит источнику. Seek пересоздаёт
узел и очищает его read/resampler cache, сохраняя handle и владельца PCM. Cursor
вычисляется по реально обработанным графом кадрам, а не по prefetched datasource.
Pause отсоединяет узел и ждёт начатое чтение графа; resume подключает тот же узел,
сохраняя его PCM и cache. Соседние проигрыватели продолжают работать.
У асинхронного открытия видео/голоса проверяется generation.
Потоковый AGC использует фиксированные кольца истории и PCM, нулевой lookahead
и непосредственный process_frame без растущего output. У записи lookahead явно
100 мс; растёт только сохраняемый результат. Push/flush/configure возвращают
ошибки; flush проверяется перед кодированием вложения.
Порядок остановки голосового направления: close capture → join capture worker →
detach output → join RX worker → release core. При failed context синхронный сигнал
сначала закрывает всех клиентов, затем освобождает context. Mixer, PCM handles,
call ID и группа переживают recovery. Stop выключает recovery; таймер завершения
звонка проверяет generation. MainWindow останавливает клиентов до SoundManager
и сетевого ядра. Сетевые операции и регистрация audio ops выполняются в uasync.
PulseAudio control-waits ограничены 1 с на операцию; при timeout operation
отменяется до возврата, исключая callback с просроченным stack pointer. Partial
init освобождает контекст/mainloop/события. Failed stream/context содержит PA
errno в логе. Zero writable/zero converter progress возвращают управление.
Последовательность нескольких операций init/uninit всё ещё может задержать GUI
дольше секунды; отдельный device owner-thread не добавлен.
## Проверки реализации
`vibechat` и аудиотесты собираются через CMake. Набор проверки:
```sh
ctest --test-dir tools/chatgui/build --output-on-failure \
-R 'test_(linux_audio|audio_timing|audio_diagnostics|audio_recovery|audio_event_queue|voice_audio_io|voice_tx|call_jitter|radio_audio|radio_jitter|voice_file)$'
make -C tests test_audio_compressor test_speex_aec
./tests/test_audio_compressor
./tests/test_speex_aec
```
Добавлены отдельные проверки MPSC (80000 сообщений, reserve/control, слот ещё не
опубликован), точных PCM/PTT/mute границ, короткого хвоста, независимого RX при
задержке capture worker, противофазного stereo AEC, старого физического timestamp
при свежем enqueue, совпадения реальных Opus-пакетов с правильно дополненным
хвостом, rapid PTT, capture restart, отмены старого прослушивания и bounded control.
Проверяются manual/VAD handover
без второй серии и продвижение настоящего сервисного RX при заблокированном VAD.
Timing test проверяет read-index timestamp, несколько converter callbacks одного
блока и метку hole. Recorder тестирует raw PCM и отсутствие вложения в meter-only.
Speex проверяет подавление эха и сохранение double-talk также для двух микрофонов
с двумя независимыми каналами reference. В детерминированном синтетическом тесте
остаточная энергия эха — около 0,009 исходной; корреляция near-end — около 0,95.
Эти величины относятся только к тесту, а не к реальным акустическим условиям.
В `/tmp/utun-audio-asan` проверяются пять тестов нового тракта с ASAN/UBSAN и
LeakSanitizer; LeakSanitizer запускается вне песочницы. MPSC отдельно проверяется
ThreadSanitizer. Изолированный интеграционный запуск:
```sh
dbus-run-session -- python3 tools/chatgui/tests/run_pulse_recovery.py
```
Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11,
ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS,
MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных
повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем
проигрывателе; она ждёт чтение узла, а не только меняет его флаг.
Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает
аппаратные monitors и использует два виртуальных sink. Проверяет объединение
voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка
при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность;
recorder и медиа продолжают работу. Системный аудиосервер не затрагивается.
Исторические проверки исходного recovery: полный `check.sh` — 119 passed,
0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя
async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка
на телефон не выполнялась.
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество
AEC в длительном реальном звонке этими проверками не подтверждены. Reference
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
## Период Linux/PulseAudio и сравнительный замер
`AudioDevice::requestLowLatency()` задаёт запрос 10 мс для capture звонка/рации.
Он применяется при каждом открытии, включая recovery и fallback.
Остальные backends сохраняют прежние параметры.
Общий и отдельный voice output сохраняют прежний период: запрос 10 мс
на проверенной системе не снизил измеренную задержку воспроизведения.
Кадры AEC/кодека остаются 20 мс; `noFixedSizedCallback` позволяет обслуживать
фактические порции backend без дополнительного накопления в miniaudio.
Это запрос задержки, а не гарантия размера callback. PulseAudio согласует
`tlength`, `fragsize`, `minreq`, `maxlength`, `prebuf`; PipeWire также ограничивает
quantum и минимальные порции. `AudioDiag` при init выводит внутренний PCM-формат
и согласованные параметры в миллисекундах (`-1` — значение по умолчанию).
Пересчёт использует внутренние rate/channels/format, а не формат приложения.
Секундные clock-отчёты содержат `latency_us`/`backend_us` и `block_us` в виде
`min/avg/max`. В задержку включены только измеренные timing snapshots;
`estimated_blocks`/`measured` позволяют отличить их отсутствие от нулевой задержки.
Для времени исполнения callback включать `call=trace`, для состояния AEC —
`aec=debug`. Логи агрегируются вне аппаратного callback.
Ручной режим существующего стенда использует настоящие устройства и голосовые
workers, не сохраняет микрофонный PCM и не отправляет его по сети. Выход звонка
без удалённого собеседника обслуживается тишиной; акустическое подавление эха
этот режим не измеряет. Аргументы: capture frames, playback frames, секунды,
необязательный режим `call` (по умолчанию), `radio` или `output`, затем путь лога.
Ноль frames оставляет стандартный период backend; override применяется только
в тестовом executable, до настоящего `ma_device_init`.
```sh
cmake --build tools/chatgui/build --target test_audio_recovery -j4
tools/chatgui/build/test_audio_recovery --latency 960 0 60 > /tmp/audio-baseline.log 2>&1
tools/chatgui/build/test_audio_recovery --latency 480 0 60 > /tmp/audio-capture10.log 2>&1
tools/chatgui/build/test_audio_recovery --latency 960 480 60 > /tmp/audio-playback10.log 2>&1
tools/chatgui/build/test_audio_recovery --latency 480 480 60 > /tmp/audio-both10.log 2>&1
```
Запуски последовательные: несколько потоков могут менять общий quantum сервера.
Сравнивать одинаковые маршруты и нагрузку, отдельно от старта/остановки и после
стабилизации адаптивного PCM-резерва. Проверять backend latency, возраст первого
сэмпла перед готовым TX-кадром, очередь после mix, starvation, holes, потери PCM,
AEC reference и событий диагностики. Затем повторить baseline и проверить
выбранный вариант под нагрузкой. Меньший возраст PCM в capture callback сам по
себе не доказывает такой же выигрыш до кодирования: worker собирает кадры 20 мс.
`test_voice_audio_io` дополнительно проверяет порции по 256 samples: порядок PCM,
временные метки, границы mute/PTT и позицию частичного playback. AEC проверяется
при разных порциях capture/reference (256/480 и 480/256) с противофазным stereo
reference. Очереди по-прежнему ограничены числом событий; уменьшение периода
не увеличивает их ёмкость в миллисекундах.
Сравнение 2026-10-15 на встроенном HDA, PipeWire 1.6.2, 48 кГц. Каждый вариант
работал 60 секунд; таблица содержит средние по callback/кадрам после 45 секунд
прогрева (PCM-резерв к этому моменту уменьшился до одного блока).
| Запрос capture / playback | Capture backend, мс | Первый сэмпл готового TX-кадра, мс | Playback backend, мс | PCM после mix, мс |
| --- | ---: | ---: | ---: | ---: |
| 20 мс / default 25 мс | 30,69 | 34,53 | 16,32 | 18,67 |
| 10 мс / default 25 мс | 16,02 | 29,79 | 16,15 | 18,67 |
| 20 мс / 10 мс | 31,51 | 34,97 | 18,09 | 24,00 |
| 10 мс / 10 мс | 16,13 | 29,66 | 18,46 | 24,00 |
Изменение capture даёт около 14,7 мс на границе backend и около 4,7 мс перед
передачей полного кадра в голосовой стек. Для playback сервер согласовал
`tlength=16ms`, `minreq=5.333ms` при запросе 10 мс; callback уменьшился с 512
до 256 samples, но выигрыша по задержке не получилось. Поэтому в рабочем коде
изменён только capture. Значения в столбцах не следует складывать как независимые
участки общей задержки: capture backend входит в возраст TX-кадра, а очередь после
mix снята уже после извлечения текущего выходного блока из очереди.
В этих четырёх прогретых окнах не было потерь capture/reference PCM, выходного
starvation, backend underrun/overflow или потери событий диагностики.
Отдельные отсутствующие AEC references были и в baseline (4 кадра за окно),
и в остальных вариантах (4–5). Это существующее ограничение привязки reference,
а не проверка акустического качества. Первоначальный output starvation и
временное увеличение PCM-резерва учитываются отдельно от стабильного окна.
Повтор baseline дал 30,76 мс capture backend и 34,99 мс возраста TX-кадра.
Двухминутный прогон выбранного capture 10 мс под нагрузкой четырёх CPU-процессов
дал после прогрева 15,81 и 28,55 мс соответственно; PCM после mix — 18,67 мс.
Потерь PCM, backend underrun/overflow и потери событий диагностики в этом окне
не было. За 75 секунд окна зарегистрировано 25 отсутствующих AEC references.
Отдельный минутный прогон рации: capture backend в среднем 15,48 мс, без потерь PCM.
Сборка `vibechat` и 6 аудиотестов прошли. Изолированный интеграционный тест
подтвердил объединение выходных маршрутов и восстановление capture/playback после
перезапуска тестового PulseAudio. Запускать весь стенд через `dbus-run-session`,
чтобы имя его PulseAudio не конфликтовало с сервером пользовательской сессии.
В fixture перед `radio_audio_init` создаётся `radio_instance`, освобождаемый после
`radio_audio_destroy`; это исключает ложную ошибку регистрации mute callback.
Полный `check.sh` вне sandbox: 156/156, 9/9 proxy-интеграций, burst и load — PASS.
В sandbox сетевые тесты не проходят из-за запрета сокетов; этот запуск не является
проверкой сетевых регрессий.
## Референсы
Первичные исходники и документация просмотрены 2026-10-01; ссылки на master могут
изменяться. Используем принципы, а не копируем целиком архитектуру библиотеки.
- [PipeWire audio-src-ring.c](https://docs.pipewire.org/audio-src-ring_8c-example.html):
callback читает доступные кадры, заполняет остаток тишиной и возвращает полный
буфер; пополнение инициирует через событие управляющего потока. Хороший минимальный
пример поведения при пустой PCM-очереди.
- [PulseAudio stream API](https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/stream.h):
отдельные underflow/overflow/started/suspended/moved callbacks; peek различает
пустую очередь и hole с ненулевой длиной. Нужно разделять эти состояния в логах.
- [PulseAudio pacat.c](https://github.com/pulseaudio/pulseaudio/blob/master/src/utils/pacat.c):
underflow callback сообщает событие без пересоздания stream. При failed context
утилита завершает работу — это пример диагностики, не готовая self-healing система.
- [Mozilla cubeb PulseAudio backend](https://github.com/mozilla/cubeb/blob/master/src/cubeb_pulse.c):
явные context/stream states, пересоздание ошибочного context при инициализации,
согласованное отключение callbacks и доступ под mainloop lock; minimum latency
25 мс. Это полезный референс владения и границ управления, а не основание
автоматически заимствовать всю библиотеку.
- [miniaudio](https://github.com/mackron/miniaudio/blob/master/miniaudio.h):
init/start/stop/uninit запрещены внутри data callback из-за deadlock. Просмотренный
upstream тоже содержит break при пустом duplex-ring: обновление библиотеки само
по себе не решает этот найденный дефект.
+380
View File
@@ -0,0 +1,380 @@
Файлы участников CHAT-группы
===========================
Модуль: src/member_files/. Desktop-браузер: tools/chatgui/src/member_files/.
Отдельный сервис router ETCP_RT_ID_MEMBER_FILES (0x39), отдельно от вложений
сообщений media_delivery и E2E-файлов DM. Серверы выбираются явно; между
серверными деревьями автоматической репликации нет.
Хранение и конфигурация
----------------------
Корень по умолчанию: <db_path>/member_files/ (без db_path — ./member_files/).
Публичное дерево: <корень>/<group_id>/<node_id>/<относительный путь>.
Временные файлы: <корень>/.uploads/<upload UUID в hex>.
При старте сервис создаёт/открывает корень в дисковом worker, удаляет старые
незавершённые temp и сканирует опубликованные файлы. До готовности отвечает BUSY.
Ошибка сканирования оставляет локальное хранилище недоступным до restart;
частичные счётчики клиенту не выдаются. Клиентские передачи к другим серверам продолжают работать.
Очистка кэша вложений media/ это дерево не обходит.
Настройки [chatserver] / [chat]:
member_files_enabled=1
member_files_user_quota_mb=1024
member_files_upload_ttl_sec=3600
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
по всем группам; корни групп, корни UID и .uploads исключены. Пустой опубликованный файл считается файлом.
Сохранённые файлы удалённых участников также занимают квоту.
При начале загрузки резервируется размер нового файла или положительный прирост при замене.
WRITE не изменяет used; COMMIT обновляет used на разницу размеров и освобождает резерв.
ABORT/Cancel/TTL освобождают резерв после удаления temp; REMOVE вычитает размер файла.
Переименование внутри UID не меняет размер и числа объектов. Новый COMMIT увеличивает files на 1;
замена существующего файла, включая нулевой размер, сохраняет files. MKDIR увеличивает directories на 1,
REMOVE уменьшает соответствующий счётчик. Для mkdir учитывается факт создания даже при последующей ошибке sync.
BEGIN/WRITE/ABORT/Cancel/TTL не увеличивают files: temp не опубликован.
Все изменения корректируют счётчики инкрементально, без обхода каталога пользователя;
QUOTA только читает их из памяти. LIST выполняет свой обычный обход для отображения записей, не пересчитывая статистику.
Повторы BEGIN/COMMIT/MKDIR/REMOVE не меняют учёт повторно.
Уже превышенная квота не удаляет данные: чтение, удаление и уменьшение файла доступны,
новый положительный прирост запрещён. Физический temp может занимать больше места,
чем резерв прироста; нехватка места файловой системы остаётся отдельной I/O-ошибкой.
Нехватка квоты/места возвращает NO_SPACE; опубликованные файлы не вытесняются.
Общая member_files_quota_mb удалена. Корень обслуживается только файловым сервисом;
ручные изменения требуют повторного сканирования через restart.
Сервер — действующий полный мембер группы с подписанным adm_tags.storage="yes".
Колонка peers.storage — производный сетевой приоритет, для полномочий не используется.
Каждый запрос и ответ подписаны Ed25519 и зашифрованы end-to-end средствами
route_crypto. group/src берутся только из проверенного заголовка router;
ETCP peer промежуточного хопа не считается автором запроса.
Читать могут действующие участники той же группы. Изменять дерево может только
src == owner. Удалённые записи и source=1 (плейсхолдеры) доступа не дают.
Открытый fd/UUID загрузки не отменяют повторную проверку членства.
Файлы удалённого участника сохраняются на диске, но через сервис не доступны.
Wire, версия 4
--------------
Все числа LE, без сырых структур и padding. Фиксированный заголовок 125 байт:
0 protocol_version:u8 = 4
1 kind:u8 (1=request, 2=response)
2 command:u8
3 status:u16 (request=0)
5 request_id:u64, ненулевой случайный ID
13 owner:u64 (UID каталога; 0 только для LIST виртуального корня группы)
21 offset:u64
29 size:u64
37 cursor:u64
45 version:16
61 upload:16
77 hash:32
109 mtime:u64, Unix seconds
117 type:u8 (1=file, 2=directory)
118 more:u8 (0/1)
119 path_len:u16
121 target_len:u16
123 data_len:u16
После заголовка: path[path_len], target[target_len], data[data_len], без NUL.
Длина пакета обязана точно совпадать с указанными размерами.
Путь максимум 1024 байта, компонент максимум 255 байт, максимум 60 компонентов.
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
Команды
---------------
1 LIST: owner/path/cursor/version. Первая страница cursor=0, version=0.
Ответ data — записи: name_len:u16,type:u8,size:u64,mtime:u64,version:16,name.
cursor — сколько записей уже выдано. more=1 требует следующую страницу с
возвращёнными cursor/version. При изменении снимка — STALE, начать сначала.
Размер каталога не ограничивает память страницы. Виртуальный корень группы
выдаёт UID действующих полных участников, даже если каталог ещё не создан.
Изменение namespace сервера консервативно инвалидирует дисковые LIST-курсоры.
2 STAT: owner/path. Ответ type/size/mtime/version. Не хеширует весь файл.
version — непрозрачный снимок identity/size/mtime/ctime и эпохи сервера.
Закрытие fd по таймауту токен не меняет; restart меняет дисковые токены.
3 READ: owner/path/version/offset/size (1..8192).
Ответ size — полный размер файла, data_len — фактически прочитано,
more — есть ли байты после ответа. offset==EOF возвращает 0 байт/more=0;
offset>EOF — INVALID_RANGE. Изменение файла между блоками — STALE.
Клиент не смешивает версии и публикует локальный файл только по завершении.
4 BEGIN_WRITE: owner/path/upload/size/hash/version.
upload — 16 случайных байт, size — конечный размер, hash — SHA256 содержимого.
version=0 означает создать новый файл, существующий возвращает EXISTS.
Для замены требуется версия из STAT/LIST. В один путь одна активная загрузка.
UUID привязан к group/src/owner/path; повтор BEGIN с теми же параметрами безопасен.
5 WRITE: owner/path/upload/offset/size/data. size==data_len, диапазон внутри
конечного размера. Можно писать в произвольном порядке и повторять блок.
ACK подтверждает запись в temp, а не durable публикацию всего файла.
6 COMMIT: owner/path/upload. Проверяет SHA256, fsync/_commit и ожидаемую
версию назначения, затем атомарно заменяет файл и синхронизирует namespace.
Прежний файл виден до публикации. Ответ содержит размер/версию/SHA256.
Незаписанные sparse-области читаются как нули; неправильное содержимое не
проходит SHA256. Пустые файлы поддерживаются. При I/O-ошибке во время
публикации результат может быть неопределённым; нужно повторно проверить STAT.
7 ABORT: owner/path/upload. Удаляет temp, освобождает резервацию.
Неизвестный UUID — OK; опубликованные файлы никогда не удаляет.
8 MKDIR: owner/path. Создаёт один каталог; родители должны существовать.
9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены
существующего назначения. Активная загрузка в затрагиваемом пути — BUSY.
10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления.
Непустой каталог возвращает DIRECTORY_NOT_EMPTY.
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
12 STREAM_READ: открытие потокового скачивания; owner/path/version/size/cursor,
upload — случайный токен клиента. Сервер сверяет снимок и размер перед началом.
13 STREAM_DATA: request_id — ID потока, owner/path/upload, offset/size/data.
kind=request для upload, response для download. Ответа на блок нет.
offset обязан равняться следующему принятому смещению; size==data_len, не более 8192.
14 STREAM_END: ID потока, owner/path/upload, offset==size==размер файла, SHA256.
Upload: клиент отправляет END после всех DATA, сервер отвечает END после COMMIT.
Download: сервер отправляет END после DATA и повторной проверки версии; клиент
проверяет SHA256 записанных данных, синхронизирует и публикует локальный файл.
Ошибка сервера приходит END.status!=OK; ошибка открытия — ответом на STREAM_READ/WRITE.
15 STREAM_CANCEL: клиент отменяет конкретный поток. Поздние DATA не публикуются.
16 QUOTA: owner=UID, path/target пустые, cursor=0, data_len=0.
Права чтения те же, что у STAT/LIST выбранного участника. Ответ data_len=40:
used:u64, limit:u64, reserved:u64, files:u64, directories:u64, LE.
Первые три значения в байтах, последние два — числа объектов. Снимок общий для UID во всех группах.
Не резервирует место и не занимает квитанцию мутации. used может превышать limit
после уменьшения лимита; сервер не выдаёт фиктивный нулевой размер при ошибке учёта.
Открытие потока — обычный повторяемый запрос с отдельным request_id. cursor связывает
его с DATA/END/CANCEL. Поток определяется group/peer/kind/ID; один ID не может
одновременно обозначать разные открытия. Каждый пакет по-прежнему подписан и зашифрован.
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
Статусы (0..14): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT,
DIRECTORY_NOT_EMPTY.
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
не дают гарантии того же результата; перед новой мутацией требуется STAT.
Потоки, таймауты и остановка
---------------------------
На экземпляр один дисковый pthread с очередью до 64 обычных задач. Сеть,
ACL, таймеры и state machines выполняются в uasync; open/stat/readdir/read/write,
resize/hash/sync/rename/remove/close — в дисковом потоке. GUI только передаёт
команды через bridge и получает события. Нет вызова дискового I/O из callback
таймера. Completion резервируется до постановки задачи; без polling workers.
Локальные файлы transfer и серверное дерево используют тот же дисковый поток.
Для обязательной очистки резервируется отдельная задача до открытия файла.
Обращение к своему серверу доставляется через отдельную ограниченную очередь
call_soon в тот же обработчик, без сетевого канала. ACL и дисковый поток общие;
сетевой recv не принимает unsigned даже при src == self.
Сервер держит до 64 открытых файлов. Каждый READ/WRITE/BEGIN продлевает idle.
После 10 секунд простоя fd закрывается на очередном дисковом sweep (таймер 1 с);
занятый дисковый поток откладывает sweep до освобождения. Закрытие не публикует
и не удаляет файл. Temp можно открыть заново до истечения upload TTL.
После restart незавершённые temp удаляются; опубликованные файлы остаются.
Лимиты: 16 клиентских запросов, 32 серверных задания/ответа, 16 временных
загрузок, 16 клиентских и 16 серверных потоков на экземпляр. Каждый поток имеет
кольцо 32 x 8192 байт (256 КиБ); worker обрабатывает до 16 блоков за задачу.
Чтение следующей порции перекрывается с отправкой уже подготовленной.
Все серверные и локальные I/O используют один FIFO worker, без блокировки uasync.
Отправитель добавляет пакеты, пока send_q router находится под нижним порогом
(сейчас count<=0), затем ждёт etcp_router_on_send_ready. Файловый слой не устанавливает
своё окно inflight и не повторяет DATA; транспортные ACK, retransmit и порядок — router.
Callback отправляет не более 8 блоков за запуск, уступая очередь другим потокам.
Параллельные файлы одного пира разделяют канал и FIFO/round-robin waiters.
Готовность приёмника проверяется router до доставки следующего seq. Заполненное
кольцо или очередь заданий оставляют пакет в recv_q: rx_seq/ACK не продвигаются.
Освобождение места вызывает отложенный resume; pump отменяется при reset/close.
ACK RECEIVE_BUSY (flags bit0) раз в 100 мс отличает ожидание диска от потери транспорта.
Отправитель приостанавливает новые DATA, а живые busy ACK продлевают таймаут router.
Буфер router ограничен его receive window. Локальная доставка использует такую же
проверку готовности и ограниченный FIFO, а не сеть или отдельные подтверждения.
При остановке сначала отменяются producers/requests и дожидаются все stream cleanup
continuations; только затем worker освобождает storage. Отмена после уже выполненной
публикации не удаляет опубликованный файл.
Transfer владеет CM handle до окончания/отмены; DOWN/TIMEOUT handle не освобождает.
quiesce запрещает новые команды, отменяет таймеры, waiter и запросы; destroy
вне callbacks дожидается только своих работ/completions и закрывает дисковый
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
Desktop
-------
Контекстное меню канала: Group files. Меню участника: Member files.
Окно: выбор сервера/UID, путь, Up/Refresh, таблица каталогов/файлов,
Upload/Download/New folder/Rename/Delete, прогресс и Cancel transfer.
Чужой UID доступен только для чтения. Автор может быть offline.
Open скачивает в выбранный пользователем локальный файл и открывает его
приложением ОС после завершения. Смена контекста отбрасывает поздние ответы.
Для выбранного UID строка хранилища показывает занято/всего, число файлов/каталогов и резерв текущих загрузок.
Точные байты доступны в tooltip. QUOTA запрашивается отдельно от LIST при выборе
сервера/UID, обновлении каталога, завершении передачи и каждые две секунды видимого окна.
Поздние ответы старого UID/сервера игнорируются. BUSY при стартовом сканировании
сохраняет Loading directory и повторяет LIST через секунду; новый переход отменяет повтор.
API параллельных передач
-----------------------
member_files_transfer_start(request, callback, arg) копирует request и возвращает
отдельный mf_transfer* на каждый файл. upload задаёт направление, group/server/owner
выбираются для каждой операции. id=0 выбирает случайный ID; id в callback — фактический.
NULL означает отказ запуска без callback; причина пишется в лог.
max_bytes=0 оставляет прежний лимит; ненулевое значение ограничивает размер файла.
Upload проверяет размер источника до хеширования, download — ответ STAT до открытия
локального temp. STREAM привязан к проверенным размеру/version; превышение — INVALID_RANGE.
callback(arg, mf_transfer_progress*) получает id/done/total/status/finished.
Прогресс заимствован до возврата callback. done для отправителя означает байты,
принятые router в очередь; успешный finished подтверждает завершение операции,
для upload — публикацию на сервере, для download — публикацию локального файла.
finished приходит ровно один раз, handle к этому моменту уже освобождён;
вызывающий обнуляет свой указатель в callback. member_files_transfer_cancel(handle)
отменяет только этот файл, ждёт его I/O и ставит CANCEL в очередь с backpressure.
Stop сервиса/группы завершает все принадлежащие ему handles.
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
Один локальный файл назначения не может одновременно принадлежать двум download.
Аватарки пользователей
---------------------
Относительный путь avatars/<sha256>.<расширение> находится внутри обычного дерева UID:
<корень>/<group>/<UID>/avatars/<sha256>.<расширение>. Дополнительного UID в API/пути нет.
Аватарка по умолчанию копируется в каждую группу, где выбранный сервер имеет
подписанную storage-роль. Ссылка каждого канала публикуется только после COMMIT
соответствующей копии; отсутствующий сервер в другом канале не создаёт битую ссылку.
Новое членство обнаруживается через снимки участников и повторяет публикацию.
Между серверами автоматического копирования нет.
userinfo содержит строковые avatar_server (16 hex цифр) и avatar_file (64 hex SHA256 + .json).
Оба поля отсутствуют при удалённой аватарке. Профиль подписан ключом участника;
смена имени/адресов сохраняет остальные плоские JSON-атрибуты. userinfo ограничен
1024 байтами UTF-8. Merkle, JOIN_INFO_RESP, CHANNEL_INVITE и JOIN_REQUEST используют
длину userinfo:u16 BE вместо прежнего u8. Bootstrap передаёт существующий подписанный
профиль инвайтера, не создавая более свежую запись только с именем. Обе стороны
нужно обновить: обратной совместимости нет.
Снимок участника CHAT_MEMBER_DISPLAY_SIZE теперь 158 байт: прежние 79 байт,
avatar_server:8LE, avatar_file:70 (с NUL), storage_role:1. storage_role обозначает
подписанную роль сервера, независимо от существующей колонки storage — сетевого
приоритета, который может быть 0 у сервера за NAT. Потребители используют общий
chat_member.h; локальное изменение профиля также вызывает MEMBER_UPDATED.
В Settings → Profile доступны Upload avatar, выбор storage-сервера, предпросмотр
и Remove avatar. Изменения применяются сразу, независимо от Save других настроек.
Перед загрузкой desktop/Android показывают редактор: неподвижная квадратная рамка,
перемещение фото и масштабирование (мышь/колесо или жесты). Отмена не публикует новую ссылку.
Worker принимает JPEG/PNG/WebP до 32 МиБ и 16 мегапикселей. Исходные байты, включая EXIF,
сохраняются без изменений. Выбранная область после EXIF-ориентации становится WebP lossy
256×256, quality=85, максимум 1 МиБ. Описание JSON (максимум 4096 байт): version=1,
original/preview — имена объектов; original_bytes/preview_bytes, width/height — размеры
ориентированного оригинала; x/y/w/h — нормализованный квадрат в его координатах.
Все объекты лежат в avatars/<SHA256 собственных байтов>.<jpg|png|webp|json>.
Порядок COMMIT: оригинал, превью, описание; затем подписанная ссылка на описание.
Повтор использует уже сохранённые неизменяемые объекты. Сервер проверяет имя/SHA256,
лимит объекта и запрещает замену под прежним именем и rename в/из avatars.
Файлы и каталог входят в обычные квоты. Прежние версии сохраняются на сервере;
владелец может удалить их через файловый браузер. Старые PNG-ссылки не поддерживаются.
Клик по аватарке сообщения/участника открывает галерею пользователя: LIST каталога
выбирает только описания, проходит все страницы cursor/version, сортирует mtime DESC,
при равенстве — по имени. Изначально выбрана текущая подписанная ссылка. Большое фото —
полный оригинал, до его загрузки показывается превью. Внизу пять квадратных позиций:
две предыдущие, выбранная по центру, две следующие; за границами списка позиции пустые.
Desktop: кнопки/клавиши ←/→; Android: кнопки/свайпы при исходном масштабе. Превью кликабельны.
Загружаются выбранный оригинал и видимые превью; закрытие отменяет запросы без других владельцев.
Для DM источник — общий канал, из которого берётся отображаемая аватарка.
AvatarManager общий для списка участников и сообщений, максимум две операции
одновременно, одинаковые хеши не скачиваются параллельно. Изменение своего профиля
имеет приоритет перед фоновыми загрузками. Для DM используются ссылки из общих
каналов; при отсутствии доступной ссылки остаются инициалы. Онлайн-статус списка
отображается отдельной точкой поверх фотографии. Поздний результат старой ссылки
пополняет кеш, но не заменяет картинку новой ссылки.
Кеш аккаунта: <db_path>/avatar_cache/<local_UID_hex>/<sha256>.<расширение>; без db_path
используется Qt AppLocalDataLocation. Проверяются SHA256 объектов, описание и WebP 256×256;
превышение/битая картинка не публикуются. Публикация кеша атомарна через QSaveFile,
незавершённые download/part очищаются при старте. Готовые файлы не требуют TTL/STAT:
смена содержимого меняет имя. Дисковый LRU — 100 МиБ, кеш QImage — 32 МиБ;
собственная аватарка и источники незавершённых upload сохраняются при вытеснении.
Дисковое I/O и декодирование выполняются вне GUI/uasync. Повторы неудачных загрузок:
5/10/20/40/80/160 секунд; новая ссылка сбрасывает задержку. RTT/online не меняют ссылку
и не вызывают повторной загрузки. Желаемая собственная ссылка/удаление сохраняются
атомарно одной строкой ui_state.avatar_default, незавершённая публикация повторяется
после запуска при наличии локального источника.
test_avatars проверяет нормализацию, хеш/размеры, очистку temp, настоящее storage
в двух каналах, публикацию, сохранение при смене имени, DM-кеш, restart без сети,
автоматический download после потери кеша и удаление ссылок. Также проверяются
max_bytes до открытия local temp и смена аватарки при задержанном download старой
версии: поздний результат не заменяет новый рисунок. test_member_adapter проверяет
подписанные профили 191/255/256/512/1023/1024 байт вместе с adm_tags до 8192.
Категория media=debug показывает discovery, cache hit/eviction, направление,
group/owner/server/hash, ошибки проверки и retry; media=info — передачи/публикации.
Проверки и диагностика
---------------------
tests/test_member_files: wire/UTF-8, произвольные блоки, повторы, EOF, версии,
короткая замена, SHA256, пустой файл, независимые квоты UID, общий учёт по группам,
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
fd idle и принадлежность work/done потокам.
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
проверка, отмена одного handle, restart router и destroy с активным server I/O.
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
test_services — stop/start общего чат-сервиса.
Диагностика файлового сервиса — категория media, маршрутизатора — etcp_route.
Настройка для полного трасса этих модулей:
[debug]
media=trace
etcp_route=trace
media=debug показывает запрос/ответ, ACL, очереди файлового сервиса, I/O и fd idle.
etcp_route=debug добавляет выбор маршрута, состояния канала и ретрансмиты.
Уровень trace дополнительно показывает каждый
read/write и передачу задач/завершений между дисковым потоком и uasync.
stream progress раз в секунду показывает ID/направление, done/total, число блоков
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
receive paused/resumed и peer receive busy без повторного сообщения на каждый блок.
quota scan started/ready показывают путь, число пользователей и время обхода;
quota updated показывает UID/операцию/used/reserved/files/directories; quota exceeded — used/reserved/growth/limit.
По request_id видны GUI post → core request → router seq → server rx → ACL →
disk queued/start/done → response tx → GUI result/bridge → directory ready.
PID создания worker, PID отправки задачи и startup PID позволяют однозначно
обнаружить fork после создания потока; несовпадение PID логируется как ошибка.
Ключи шифрования, plaintext файлов и сырые медиа в debug не выводятся.
Старые VAD/рация/GUI/вложения сообщения перенесены в vad/radio/chat/media/config;
ошибки и пользовательские info остаются в тематических категориях.
+36
View File
@@ -0,0 +1,36 @@
# Ссылки в сообщениях
Android и Qt распознают `http://`, `https://` и `utun://` в обычном тексте
личных и групповых сообщений. HTTP/HTTPS нечувствительны к регистру схемы.
Ссылки выделяются цветом и подчёркиванием; длинные веб-адреса переносятся по ширине.
Сокращённая строка структурированного PM-приглашения сохраняет полный `utun://` адрес.
Пробелы, управляющие символы, кавычки и угловые скобки отделяют ссылку от текста.
Конечные `. , ; : ! ?` и лишние закрывающие скобки удаляются из распознанного адреса.
Сбалансированные скобки в адресе, параметры, fragment и Unicode сохраняются.
Адреса без схемы не распознаются. Некорректные веб-адреса остаются обычным текстом.
Исходное сообщение в БД и протоколе не меняется.
Разборщики возвращают диапазоны UTF-16 и полные адреса:
`tools/chatgui/src/message_links.cpp`, Android `data/MessageLinks.kt`.
Подсветка и нажатие используют эти диапазоны. Android подставляет emoji placeholders
только в обычный текст между ссылками: emoji внутри адреса остаётся частью полного URL.
Qt использует один QTextLayout для размеров, отрисовки, выделения и координат нажатия.
Перетаскивание выделяет текст без открытия ссылки.
HTTP/HTTPS открываются из GUI системным обработчиком: Android `ACTION_VIEW`,
Qt `QDesktopServices::openUrl`. `utun://` запускает существующий JOIN.
Ошибки открытия показываются через перевод интерфейса и логируются.
Диагностика открытия содержит схему и host, без полного пути/query.
Общие случаи: `tools/chatgui/tests/message_links_cases.tsv`.
Qt `test_invite_links` проверяет разбор, перенесённые веб-ссылки, смесь схем,
нажатие, drag/copy и сокращённые приглашения; `test_emoji_playback` — жесты приглашений.
Android `tests/test_message_links.kt` проверяет тот же TSV на классах из APK-сборки:
```bash
cd tools/chatgui-android
./build.sh noinstall
kotlinc tests/test_message_links.kt -classpath app/build/tmp/kotlin-classes/debug -d /tmp/message-links-tests.jar
kotlin -classpath /tmp/message-links-tests.jar:app/build/tmp/kotlin-classes/debug Test_message_linksKt ../chatgui/tests/message_links_cases.tsv
```
+109
View File
@@ -0,0 +1,109 @@
Как работает детектор NAT
========================
Детектор выясняет две вещи: какой внешний IP:порт получает рабочий UDP-сокет
при обращении к разным IP и проходят ли к нему пакеты от незнакомого IP.
Проверяется именно сокет, через который работает приложение.
Есть быстрый путь без помощников: если рабочий сокет принял подключение
и локальный IP и фактический IP подключившегося узла публичные,
ему сразу ставится direct/open (UNCHANGED + OPEN). Это правило действует
для UDP и TCP, даже если с IP пира раньше уже был исходящий обмен.
Локальные подключения и SOCKS интернет-вход не подтверждают. Для TCP
фактический локальный IP должен совпадать с IP интерфейса сокета.
Нужны три узла: A проверяет себя, B наблюдает его внешний адрес,
C помогает проверить входящие пакеты. Команды и результаты передаются
по уже работающей сети uTun, в том числе через TCP или промежуточные узлы.
Сами тестовые пакеты идут напрямую по UDP.
Порядок проверки
---------------
1. A отправляет пакет узлу B. Тот сообщает, с какого IP:порта его получил.
Ответ идёт по управляющему соединению, поэтому блокировка прямого
UDP-ответа не мешает узнать внешний адрес A.
2. Через B узел A просит C отправить несколько пакетов на этот внешний адрес.
До этого A не должен отправлять ничего на IP узла C с проверяемого сокета.
Приложение хранит историю таких отправок и учитывает новые во время проверки.
На входящие тестовые пакеты A автоматически не отвечает.
3. Получатель проверяет настоящий адрес отправителя каждого тестового пакета.
Локальный или другой непубличный IP не подходит для интернет-проверки.
Для проверки незнакомого входа также не подходят IP узла B, собственный
внешний IP узла A и IP, на который A уже отправлял пакеты.
Если помощник не подходит, A выбирает другого. Разные узлы с одним IP
не считаются разными источниками. Проверяются также подлинность пакета
и принадлежность текущей попытке, чтобы чужой или старый пакет не влиял на результат.
4. Затем A сам отправляет пакет C и узнаёт, какой внешний IP:порт видит C.
Если входящие до этого не проходили и внешний адрес совпал с увиденным B,
C повторяет отправку с того же IP:порта на исходный внешний адрес A.
Так проверяется, открыл ли исходящий контакт путь для входящих.
Параллельный обмен с B подтверждает, что UDP-связь в целом работает.
5. A повторяет измерение внешнего адреса через B и C. Результат принимается,
только если адрес для каждого направления остаётся стабильным.
Сравниваются разные IP назначения; другие порты того же IP не проверяются.
Что означает результат
----------------------
strict — B и C стабильно видят разные внешние IP:порты узла A.
Единого внешнего адреса для подключения с любого IP нет.
eim open — B и C видят одинаковый внешний IP:порт, и пакет от C пришёл
ещё до исходящего контакта A с его IP.
eim filtered — внешний IP:порт одинаковый, серия пакетов до контакта не прошла,
а после исходящего контакта пакеты с того же адреса C начали приходить.
Это оценка: потерю UDP-пакетов нельзя абсолютно отличить от блокировки.
EIM? — два направления видят одинаковый внешний IP:порт, но незнакомый вход
проверить не удалось, например все доступные помощники уже были знакомы.
Это EIM с неизвестной доступностью входящих, без обещания открытого входа.
Такой же предварительный EIM? дают два STUN-сервера, видящие одинаковый внешний
IP:порт тестового сокета на том же локальном IP и интерфейсе. Поддержка запроса
с незнакомого IP для этого не нужна. Источник оценки — STUN, вход остаётся UNKNOWN.
Измерение рабочего сокета имеет приоритет; STUN-оценка действует до пяти минут
и обновляется автоматически. Публичный порт для подключения берётся только
из наблюдения рабочего сокета: внешний порт STUN в него не подставляется.
Если внешний IP:порт совпадает с локальным, отдельно отмечается отсутствие
трансляции адреса. Доступность входящих всё равно проверяется.
Если данных недостаточно, соответствующая часть результата остаётся UNKNOWN.
Известный тип NAT можно сохранить, даже если входящие проверить не удалось.
Таймаут и отсутствие подходящих узлов сами по себе не означают strict.
Если один пир уже увидел публичный IP:порт, отличный от локального, этот адрес
сразу сохраняется как NAT с type=UNKNOWN. Он публикуется отдельно от адреса
интерфейса и остаётся кандидатом для пробного подключения: одного наблюдения
недостаточно, чтобы обещать тот же адрес для других IP или открытый вход.
Уже знакомый помощник пригоден для измерения типа NAT, хотя проверить незнакомый
вход через него нельзя. Потеря результата входящих не останавливает сравнение
адресов через доступные публичные узлы; ожидание одного этапа ограничено по времени.
Измеренный mapping сохраняется сразу, а проверка незнакомого входа продолжается
через остальных помощников. Порядок обхода фиксирован на его начало; фактический
source всегда проверяется по актуальной истории. При паузе по общему бюджету
незавершённая попытка повторяется с новыми идентификаторами, без сброса обхода.
Если попытка уже получила полный бюджет, его исчерпание считается таймаутом:
обход переходит дальше, а не повторяет одного помощника бесконечно.
Старый сохранённый результат входящих не считается успехом новой проверки.
Соединения по LAN работают независимо и не сбрасывают интернет-профиль.
Если доступны только локальные узлы, измерить тип интернет-NAT для UDP нельзя.
STUN на другом порту даёт предварительную оценку типа NAT для того же IP/интерфейса.
Работа TCP не заменяет проверку UDP.
Для TCP определяется только наличие NAT. Непубличный локальный IP сразу означает
NAT с неизвестным типом. Если локальный IP публичный, достаточно одного публичного
TCP-пира: по готовому защищённому соединению он сообщает увиденный IP.
IP совпали — трансляция IP не обнаружена; отличаются — NAT есть, тип UNKNOWN.
Порты при сравнении не учитываются. Публичный внешний IP сохраняется с портом 0:
входящий порт неизвестен, поэтому NCD не использует эту запись для подключения.
Активных проб типа NAT и TCP-входа нет; OPEN устанавливается по принятому
публичному подключению и сохраняется при последующих совпавших наблюдениях IP.
Через SOCKS внешний IP прокси
не записывается как собственный адрес.
+96
View File
@@ -0,0 +1,96 @@
# Подписанная запись узла
`TOPO_NODE.timestamp` — версия всей подписанной записи, а не отдельного адреса.
Узел-владелец назначает Unix microseconds, строго больше предыдущей сохранённой
версии. Имя, тип, активность, адреса, метаданные сокетов, REALITY и подпись
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
Повторное BGP-объявление может обновить путь, но не содержимое общей записи.
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
только timestamp.
Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем
`flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком.
Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`;
они не входят в подпись общей записи и используются только для цвета трубки в GUI.
Без разрешения Android, при выключенной публикации и после ухода узла статус неизвестен.
Решение о начале вызова принимает адресат по локальному состоянию ОС, а не по BGP-подсказке.
Изменения публикуются обычной новой версией NODEINFO во всех CHAT-группах; отдельного опроса и таймера нет.
Профиль endpoint в SOCKMETA содержит независимые `mapping` и `inbound`, источник `source`.
ID и время измерения не передаются: последний успешный профиль действует до
явного изменения endpoint или подтверждённого нового результата. Повторная
проверка с тем же результатом не меняет NODEINFO. Все поля SOCKMETA — отдельные байты;
границы списков metadata/адресов/REALITY также входят в каноническую подпись.
Алгоритм и условия неопределённости описаны в `doc/nat_detection.txt`.
`source=ETCP` обозначает результат детектора рабочего endpoint. Одно UDP-наблюдение может дать
адрес `TOPO_ADDR_NAT` при `mapping=UNKNOWN`, `inbound=UNKNOWN`: это NAT-кандидат
для пробного подключения, а не адрес интерфейса или подтверждённый открытый вход.
Общий внешний адрес подтверждает EIM; strict не публикует универсальный NAT-адрес.
Source входит в подпись; SOCKMETA занимает 5 байт. `source=STUN` означает предварительный
EIM? для того же локального IP/netif с неизвестным входом; измерение ETCP приоритетно.
Владелец снимает STUN-оценку после TTL. Внешний адрес тестового порта не публикуется:
NAT-кандидат по-прежнему требует наблюдения рабочего сокета.
Для TCP достаточно одного публичного пира: внешний IP с неизвестным входящим
портом публикуется как `TOPO_ADDR_NAT` с `port=0`, `mapping=UNKNOWN`, `inbound=UNKNOWN`.
NCD пропускает нулевые порты. Непубличный локальный TCP IP сразу означает NAT;
совпавший с наблюдением публичный IP даёт UNCHANGED без подтверждения открытого входа.
Таблица `node_snapshots` хранит полную сериализованную запись без группового
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
источником для восстановления подписанной записи. При наличии сохранённого
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
ошибка сохранения не разрешает публикацию новой версии.
В проекции адресов сохраняются `nat_mapping`, `nat_inbound`, `nat_source`, `address_kind`.
Уникальность определяется endpoint и видом адреса,
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap,
старые профили сбрасываются, сериализованные snapshots инвалидируются.
Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация
не откатывала версию даже при коррекции часов. Чат и ключи миграция не изменяет.
Timestamp 0 обозначает bootstrap-сведения без подписи (конфиг, приглашение,
начальные адреса). Они могут помочь установить первое соединение, но не
заменяют принятую подписанную запись. По BGP принимаются подписанные записи
с ненулевым timestamp. Формат NODEINFO изменён без обратной совместимости.
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
Линки дополняются как при UP, так и до установления соединения; повторная
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
обслуживает соединение, и использует её при восстановлении.
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
свежая подписанная запись принимается целиком, старая уступает уже известной.
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
Конфигурационные подключения UTUN используют один адаптер
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
передаются NCD; адаптер не создаёт ETCP-линки. Новый набор handles открывается
до освобождения старого; при ошибке старые handles сохраняются. Конфигурация
удерживает один handle на клиента, независимо от количества его адресов.
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
WITHDRAW от отсоединённого UTUN-пира игнорируются, чтобы не вернуть его маршруты.
LEAVE ждёт backpressure через queue waiter и временно удерживает собственный
NCD handle. Закрытие транспорта отменяет ожидание; новое участие в группе
делает ожидающий LEAVE устаревшим, и он не отправляется.
Причина удаления пира передаётся явно: только потеря транспорта запускает
recovery каскадных маршрутов. Локальный/удалённый LEAVE и удаление битой
member-записи не должны восстанавливать намеренно закрытое участие.
REALITY использует полное SNI до 255 символов; размер поля в записи узла
согласован с транспортом. Если локального TCP listen-сокета нет, NCD может
создать исходящий TCP-линк с выбором локального адреса операционной системой.
+12
View File
@@ -0,0 +1,12 @@
Работа лички
1. отдели логику лички от логики групповых чатов. не смешивай.
Работа лички. сообщения и медиа доставляется напрямую если узел доступен через bgp.
если недоступен - то pending сообщения сохраняются на суперузле и pending media сохраняется на каком-либо медиаузле, а суперузлу сообщается где хранится медиа.
при появлении пира суперузел организует доставку медиа до получателя. после успешной доставки медиа/сообщений они удаляются с промежуточных узлов.
время хранения медиафайлов на промежуточных узлах ограничено - 1 неделя по умолчанию (задачется в конфиге этих узлов).
для доставки медиа поднимаем линк (conn_mgr).
и посмотри реализацию медиа для групповых чатов.
+69
View File
@@ -0,0 +1,69 @@
# Приглашение в группу через PM
В desktop chatgui и Android кнопка «+» доступна в строке другого участника
и в шапке личной беседы. Она открывает один диалог с фиксированным получателем
и выбором группы, в которой отправитель состоит. Группа не выбирается автоматически.
После нажатия «Пригласить» клиент создаёт свежую ссылку через `invite_build`,
ожидает подтверждения регистрации ключа и отправляет PM с `ct="invite"`.
Зашифрованное тело: `[group_name:UTF8][NUL][полная ссылка:UTF8]`, без конечного NUL.
Снимок для GUI содержит полную ссылку в `data` и название в `tags.group_name`.
GUI добавляет заголовок «Приглашение в группу «название»» из перевода интерфейса;
язык отправителя не фиксирует язык сообщения у получателя.
Успех означает сохранение сообщения и исходящей задачи в одной транзакции.
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
После успешной отправки открывается личная беседа с получателем.
Срок действия ключа — **10 минут с момента последней успешной регистрации на каждом узле**.
Повторная регистрация того же ключа для того же канала тем же инвайтером продлевает срок.
Connection-узел сохраняет ключ при обработке регистрации, инвайтер — после её подтверждения;
при совпадении этих ролей запись одна. Для JOIN ключ должен действовать у обоих.
Получение PM срок не продлевает. Длительность указана в диалогах отправки и подключения. Поздняя offline-доставка
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
без обратного отсчёта и вычисления оставшегося времени.
Нажатие на `utun://` в сообщении запускает существующий JOIN и показывает его результат.
Если клиент уже состоит в группе, открывается эта группа. Ссылка приглашения отображается
одной строкой с многоточием; нажатие и копирование используют полный адрес.
В Android долгое нажатие копирует ссылку, в desktop выделение сокращённой строки
копирует ссылку целиком без запуска JOIN. Обычные текстовые PM с вручную
вставленными ссылками сохраняют текстовое отображение.
## Контракт операции
`dm_invite_trampoline` выполняется в uasync-потоке. Запрос содержит `request_id`,
ID получателя, выбранную группу и исходную общую группу для создания PM.
Из шапки существующей PM исходная группа может быть пустой.
На instance допускается одно ожидание регистрации приглашения в PM.
Повтор во время ожидания возвращает BUSY. Закрытие диалога отменяет только
соответствующий `request_id`; уже отправленное сообщение не отзывается.
Отмена и остановка DM завершают принятый запрос ровно один раз.
`CHAT_EVT_DM_INVITE_RESULT` содержит 28 байт:
`[request_id:u64][result:i32][peer:u64][conv_id:u64]` в порядке байтов ядра.
Desktop и Android сопоставляют результат с запросом и получателем.
Ошибки регистрации или сохранения сообщения оставляют диалог открытым для повторной попытки.
JOIN во вторую группу может использовать уже открытый транспорт общей группы.
Поэтому обработчики JOIN_REQUEST и JOIN_READY явно планируют member_sync,
а JOIN_READY обновляет кеш каналов до запуска синхронизации.
## Проверка и диагностика
- `tests/test_dm_invite`: настоящая UDP-доставка ссылки в другую группу, JOIN,
репликация участника и отправка группового сообщения после вступления;
отмена, BUSY, неверная группа, rollback при ошибке commit и остановка DM;
передача названия с Unicode, кавычками и переводом строки.
- `tests/test_chat_join`, `tests/test_chat_join_e2e`, `tests/test_dm_e2e`:
существующие сценарии регистрации ключей, JOIN и PM-доставки.
- Qt `test_invite_links`: одна строка приглашения на разных ширинах, полный адрес
при нажатии и копировании, переносы двух обычных ссылок, их полное содержимое при нажатии,
выделение текста и выбор получателя кнопкой в строке участника.
- Qt `test_emoji_playback`: перетаскивание ссылки не активирует подключение.
Для сетевого теста подробности включаются через `UTUN_TEST_DEBUG=1`.
В приложении полезны категории `dm=debug`, `chat_sync=debug`, `member_sync=debug`.
Операция пишет request, peer, group, результат и TTL; сообщения диагностики
новой операции не выводят саму invite-ссылку.
+61
View File
@@ -0,0 +1,61 @@
# Proxy: потоки TCP и датаграммы
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
## TCP
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
- DATA (3): от 1 до 4096 байт.
- CLOSE (4), ERROR (5): прекращение потока.
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
не является подтверждением потребления данных конечным TCP.
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
или backpressure роутера. Закрытие одного направления не прекращает другое.
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
накопленные HTTP headers/body передаются частями размером DATA.
## UDP и ICMP
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
## Проверки и ограничения
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
Windows/FreeBSD в этом прогоне не проверялись.
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
+337
View File
@@ -0,0 +1,337 @@
# Жизненный цикл ядра и сервисов
CLI выполняет daemon fork до создания `UASYNC`, SQLite, сокетов и фоновых потоков.
Сервисы и их workers создаются уже в дочернем процессе. Родитель ждёт подтверждения
успешной инициализации и записи PID-файла; ошибка старта возвращается вызывающему.
`test_daemon_worker` проверяет настоящий запуск без `-f`, наличие дискового потока,
штатный SIGTERM и ненулевой статус launcher при ошибке PID-файла.
Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks
останавливаемых модулей. GUI передаёт команды в этот поток.
```c
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config);
if (!inst || utun_core_start(inst) < 0) /* обработать ошибку */;
if (chat_service_start(inst) < 0) /* обработать ошибку */;
if (utun_service_start(inst) < 0) /* обработать ошибку */;
chat_service_stop(inst); /* UTUN продолжает работать */
chat_service_start(inst); /* загружает сохранённые каналы */
utun_service_stop(inst); /* чат продолжает работать */
utun_instance_destroy(inst);
```
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при
`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора.
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN.
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
```ini
[global]
tun_enabled=yes
tun_ifname=vibechat
tun_ip=10.42.0.1
[routing]
my_subnet=10.42.0.1/32
route_subnet=10.42.0.0/24
```
Адреса и маршруты выбираются под свою сеть; изменения применяются после перезапуска.
На Windows нужна `wintun.dll` рядом с `vibechat.exe` и запуск с правами администратора.
CMake копирует DLL и лицензии из `lib/`; Windows-упаковка явно включает DLL, проверяет
её архитектуру x64 и зависимости, поскольку `LoadLibrary` не отражается в импортах EXE.
На Linux необходимы доступ к `/dev/net/tun` и права на настройку интерфейса и маршрутов.
Успех запуска требует успешной инициализации всех обязательных ресурсов: заданных
listen-сокетов, мониторинга, TUN и всех `route_subnet` при включённом TUN. Частичный
набор сокетов или маршрутов — ошибка запуска. `utun_started` устанавливается только
после завершения всех шагов. Системные маршруты хранятся отдельным списком успешно
созданных записей; stop и откат удаляют только этот список, в обратном порядке.
Маршрут, на добавлении которого ядро вернуло ошибку, не становится собственностью
UTUN и при откате не удаляется. Ошибка одного удаления не прекращает очистку остальных.
`auto_sockets=android` выбирает внешнее управление сетью: frontend регистрирует
ConnectivityManager до запроса запуска и передаёт конфигурацию сокетов через JNI.
Оба нативных монитора ОС в этом режиме не создаются. Рабочие listen-сокеты по-прежнему
должны открыться полностью. Android подтверждает запуск по `SERVICE_STARTED`, а не
по успешному созданию потока; после события передаётся свежий снимок адресов.
| Владелец | Ресурсы |
|---|---|
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
`:memory:` данные сохраняются только до уничтожения ядра.
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
не разрывает соединение, нужное другой группе. При остановке группы отменяются
её планировщики, запросы, таймеры и ожидания очередей; отправляется LEAVE,
закрываются логические каналы маршрутизатора и удаляется её ожидающий транзит
на общих соединениях. Транзит других групп сохраняется. Отложенный LEAVE может временно
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе.
ETCP удаляет транспорт после 30 секунд непрерывного DOWN только при отсутствии локальных NCD handles.
Первоначальное ожидание UP тоже учитывает владение. Повторный DOWN не продлевает
таймаут; восстановление UP отменяет его. Если срок истёк при наличии handles, транспорт
сохраняется, а исходный deadline остаётся. Освобождение последнего handle во время DOWN
оставляет транспорт до этого deadline либо запускает удаление в следующем callback,
если срок уже истёк. Внешние ETCP refs задерживают только освобождение памяти.
Явный force-close и отмена неудачной попытки последнего владельца закрывают сразу.
Входящий UP без локальных handles сохраняется: он может быть нужен пиру. CLOSE/KEEP_ALIVE
привязан при запуске ядра, поэтому мягкое закрытие работает и на чисто входящем узле.
На CLOSE при наличии handles отвечаем KEEP_ALIVE; без них закрываем транспорт.
KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE обычно занимает 5 секунд,
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
завершает ранее запрошенное мягкое закрытие.
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
отправки срок жизни пира не продлевают. Пробуждение сразу сокращает deadline до normal timeout.
После истечения последнего линка транспорт
становится DOWN, затем может удаляться через 30 секунд, когда локальных NCD handles нет. `test_etcp_server_lifecycle`
проверяет сроки, несколько путей, восстановление, владение, сон и CLOSE/KEEP_ALIVE
с управляемыми часами и контролем освобождения памяти/таймеров.
Остановка чата сначала прекращает внешние команды и фоновые задания, затем
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
транскодирование или распознавание речи может задержать stop; принудительного
прерывания этих библиотек нет.
Владелец принимает не более 16 задач, включая уже завершённые workers до доставки
completion. Отказ запуска/неверные аргументы дают синхронный done с -1, остановка
владельца — с `MEDIA_ASYNC_CANCELLED`; при NULL done только логируется ошибка.
Примитивы `media_async.h` синхронные: отмена задачи не откатывает выполненные ими
действия. `ma_copy_file()` публикует копию внутри worker после проверенных записи и
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.h`.
Подготовка файлов в `src/video/video.h` выполняет remux подходящих H.264/AAC либо
перекодирование в MP4 H.264/YUV420P и AAC. Worker пишет уникальный соседний временный
файл, завершает декодеры/ресемплер/FIFO/кодеры, проверяет trailer, закрытие и метаданные.
Конечное имя заменяется только успешным completion в uasync. При отмене, включая
отмену уже подготовленного результата до доставки completion, временный файл удаляется,
старое назначение сохраняется. Совпадающие исходник/назначение и файловые алиасы
отклоняются; исходный файл должен оставаться неизменным до done. В процессе занята
одна задача до completion; атомарный захват исключает гонку между экземплярами ядра.
Перед done занятость освобождается, поэтому callback может запустить следующую задачу.
JPEG использует те же правила файлов, но извлекается синхронно в отдельном frontend worker.
Probe также синхронен и не обещает ограниченного времени чтения.
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
части против вытеснения. После FIN publisher может снять своё владение, сохранив
завершённый источник для оставшихся локальных readers. Последний close освобождает
источник, индекс и кольцо. Новая публикация отсоединяет readers прежней эпохи.
Android preview собственного эфира не создаёт reader: Camera2 пишет прямо в TextureView
и encoder Surface. UI attach/detach меняет лишь camera session; audio/encoder/origin сохраняются.
UI, ожидающие команды camera handler и каждая camera session удерживают preview target отдельно.
TextureView возвращает false при destroy и снимает ссылку UI; Surface/SurfaceTexture освобождаются
после закрытия последней session. CameraDevice.onClosed снимает также оставшиеся ссылки своих
sessions, если Android не прислал отдельные session.onClosed. Повторные callbacks безопасны.
Capture stop запрещает attach и снимает своё владение target; camera HandlerThread завершается
после camera close и освобождения outputs всех созданных sessions.
Отказ preview output возвращает encoder-only session и не завершает сам эфир.
При stop/destroy отменяются delivery и media-index callbacks до освобождения store;
destroy отсоединённого cursor не обращается к store. Frontend прекращает audio callback
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
PTS показа; audio cursor сохраняет временной lookahead. Viewers готовят ограниченный батч
на запрос: отдельно задаются число видеокадров, audio lookahead и число всех записей.
Desktop и Android принимают до 16 записей и обрабатывают треки по их готовности.
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека, не ожидая другого трека;
общий END завершает вход, а окно ждёт опустошения обоих выходов. GAP/CLOSED отменяют старый батч.
Получение батча погашает запрос, включая повторные обращения до pull; скрытого чтения следующего батча нет.
Callback готовности только будит desktop worker и не вызывает API ядра/bridge. Stop снимает callback
под mutex viewer до освобождения его контекста; detach будит ожидающие workers до уничтожения manager.
На Android signal принадлежит audio worker и живёт до stream_stop, join video worker и close mixer input;
ядро только уведомляет его. Surface освобождается после join video worker.
В wait не удерживаются mutex bridge/ядра. Расход PCM будит тот же signal, уведомления сливаются без потери пробуждения.
GUI и audio callback будят worker при движении часов/освобождении места; сна между готовыми пакетами нет.
Каждый reader имеет отдельные audio/control и video workers. Audio worker читает батчи и готовит
до 250ms PCM через Opus/SoundTouch, не удерживая блокировку consumer. Запас пополняется при наличии
данных без обязательного prefill. SPSC передаёт PCM вместе с отображением исходных PTS; mixer/audio
callback только копирует и смешивает. Короткая блокировка защищает output map/clock и lifetime,
в ней нет декодирования, time-stretch или MediaCodec/FFmpeg. EOF продолжает prepare до опустошения DSP.
Android video worker получает ограниченный mailbox (два кадра) и держит один рабочий input и один готовый output MediaCodec; свободный
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
Если первый доступный звук позже KEY, cursor отсчитывает стартовый audio lookahead от
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
Смена revision заменяет только video decoder и его диагностические измерения;
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
Reset увеличивает video generation; результаты прежнего поколения не публикуются. Video worker
после EOF остаётся доступен для reset до явного stop. Audio clock после старта не ждёт видео:
просроченные изображения декодируются для сохранения references, но не показываются.
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
конец GUI требует также показа оставшихся изображений и завершения audio tail.
Отсутствие прогресса decoder при наличии работы имеет отдельную диагностику и срок 10s.
ACK-история принадлежит ядру и обновляется только при первом найденном подтверждении
пакета. REINIT/close соединения не стирает уже подтверждённые за последнюю секунду байты.
Общий `stream_publisher` Android/desktop владеет отдельным 100ms таймером TX-снимков;
close/detach отменяют его в uasync до уничтожения instance. Bridges копируют снимок
под mutex, frontend не обращается к живым очередям транспорта. Android экран подписан
на отдельный StateFlow статистики; desktop обновляет статистику отдельно от сырого превью.
Publisher не регистрирует stream event callback и не подменяет callback viewer.
Bridge удерживает внешний mutex на время producer/open/stop и destroy общего publisher;
actor не вызывает frontend callbacks под своим mutex. Reserved post, media timer,
stats timer и stream handle освобождаются до контекста publisher.
Desktop `MainWindow` владеет `StreamPublishEngine`, а окно управления лишь отображает
его состояние. Закрытие окна сохраняет эфир, новый запуск запрещён до конца FIN drain.
Qt capture callback сохраняет последний `QVideoFrame` для preview и ограниченно ставит кадр
в очередь отдельного долгоживущего image worker. Он конвертирует `QVideoFrame` в `QImage`;
video worker получает готовый image и не создаёт Qt Multimedia TLS, которое могло задержать его exit на Windows.
Video/audio workers выполняют encode/DSP вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять
видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture
обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback.
Stop запрещает producers, закрывает Qt capture и подписку на аудиозахват. Отдельный join worker
дожидается полного выхода video/audio threads, включая native TLS Qt; GUI продолжает обрабатывать события.
После join отправляется FIN с drain 2s; закрытие последнего микрофона может продолжаться параллельно.
Microphone lease и owner сохраняются до завершения обоих путей. Новый start ждёт также FIN drain.
При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync
закрывает оставшийся publisher. Destructor ждёт workers в локальном event loop без пользовательского ввода,
чтобы thread cleanup мог обратиться к GUI; start и внешние сигналы engine в это время запрещены.
Workers никогда не держат borrowed instance/handle.
Microphone lease публикации исключает call/recorder; рация согласует вход через `AudioCapture`.
Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы.
Подключение/отключение рации не останавливает устройство трансляции: атомарный слот и счётчик readers
дожидаются только callback удаляемого потребителя. Последняя подписка закрывает устройство.
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
На Android `MediaAudioController` владеет общим выбором устройства, AudioManager и одним аппаратным reader.
Маршрутизация использует `CallAudioRouter`; AudioTrack рации и микшера просмотра регистрируются у одного владельца.
`MicrophoneCapture` раздаёт независимые ограниченные очереди постоянного PCM 48k stereo; моно апмиксуется.
После подключения публикации встроенный микрофон использует CAMCORDER, сохраняемый до последней подписки.
Смена профиля заменяет только аппаратный AudioRecord: камера, видеокодер и source_epoch продолжают работать.
Общая сетка по capture timestamps имеет запас 200ms; пропуски заполняются тишиной, старый PCM не переносится вперёд.
Stop публикации удаляет её подписку до join Opus worker и FIN, сохраняя захват для оставшейся рации.
PTT и PCM рации проходят через одну очередь. Последняя подписка завершает общий reader;
контроллер закрывает аппаратный вход, сохраняя выходы. Перед Telecom все владельцы снимаются,
а `MediaAudioController.awaitIdle()` дожидается освобождения входа и глобального маршрута.
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
не завершает новую сессию. Причина ошибки сохраняется в окне после окончания drain.
Desktop-звонок и рация проверяют поступление корректного PCM, включая первый callback.
Секунда без данных отключает TX и фиксирует понятную ошибку в GUI; цифровая тишина является данными.
RX продолжает работать, рация снимает PTT, автоматического переоткрытия неисправного входа нет.
Выбор другого микрофона или новая сессия снимают ошибку. Неисправное устройство закрывается
в отдельном потоке; stop не присоединяет уже запущенный close worker. Новый захват ждёт его завершения.
В звонке microphone lease удерживается до конца закрытия даже после stop. Общий AudioCapture
рации сначала отсоединяет callback потребителя; последний владелец освобождает backend в worker.
Сброс контекста и уничтожение владельца дожидаются worker до освобождения miniaudio-контекста.
Штатное закрытие исправного устройства сохраняет синхронный порядок.
`AudioDevice` подстраивает буфер по подтверждённым backend XRUN: WASAPI DATA_DISCONTINUITY
на входе, ALSA EPIPE, PulseAudio capture hole/overflow и playback underflow. Notification
только увеличивает атомарный счётчик, решение и лог выполняются в GUI recovery timer.
Исходные параметры, включая Linux capture period=10мс, сохраняются до повторных сбоев.
Первая секунда после start исключена; нужны ошибки в двух опросах за 5с. Период увеличивается
на 5мс, не чаще раза в 5с, до общей запрошенной ёмкости 100мс с учётом числа периодов.
Фактический размер согласует backend; исходный или уже достигнутый больший размер не уменьшается.
При достижении предела повторные сбои логируются без новых перезапусков. Подобранный запрос
сохраняется при recovery, сбрасывается при смене устройства/сессии; стабильный звук не вызывает reopen.
Подстройка микрофона закрывает устройство асинхронно, останавливая TX worker и сохраняя RX.
Общая capture-сессия отсоединяет callbacks до close и заново подготавливает потребителей после init.
Новая подписка во время close откладывается, stop удаляет её без ожидания backend.
Первое переоткрытие после подстройки не использует backoff; при ошибке открытия действует обычный retry.
В desktop-видеозвонке Windows/Linux камера живёт в отдельном процессе того же
`vibechat --call-camera-worker`. Служебный вход выполняется до конфига, БД и
single-instance lock. Перечисление устройств, Qt/V4L2, преобразование и освобождение
аппаратных кадров происходят только там. GUI получает собственный `QImage`; TX не
обращается к драйверу. Смена codec не перезапускает захват и не ждёт следующего GOP.
Пиксели одного кадра передаются через mapping (Windows — pagefile-backed объект ОС,
Linux — временный файл), метаданные/команды — через локальный сокет.
Worker записывает mapping до FRAME; следующий write разрешён
только после ACK, который GUI посылает после копирования в свою память. Общего mutex
нет. В worker ожидает только последний raw-кадр; поколения отсекают старые результаты.
Preview обновляется по готовности, TX получает condition-variable wake. PTS сохраняет
время захвата; кадры старше 250мс отбрасываются до encode и перед send. Отброшенный
encoded frame требует следующего KEY, поскольку encoder уже продвинул reference chain.
Отключение камеры и удаление окна не ждут worker. Независимый Session сохраняет
QProcess и mapping до finished, затем удаляет себя после отсоединения владельца.
Зависший stop получает kill через 1.5с, отсутствие прогресса — через 5с; таймеры
живут в родительском процессе. При выходе приложения kill отправляется без wait;
GUI не уничтожает живой QProcess. При отказе новый процесс запускается лишь по
явной команде пользователя и после finished прежнего. Лог категории call показывает
последнюю стадию enumerate/start/convert/release/stop и возраст кадра.
`test_call_camera_capture` проверяет настоящую IPC-цепочку с подставным устройством:
блокировки start/map/stop, поздние кадры после смены поколения, задержку 720p,
повтор после отказа, отсутствие рестарта камеры при смене codec, удаление окна,
выход приложения и независимое освобождение mapping в каждом процессе.
Статус транспорта UP подтверждает восстановление физического пути. READY сессии
группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно
его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи
защищают от сообщений старой сессии и не задают срок жизни маршрута.
Проверки: `test_services` проверяет независимый stop/start, общий NCD в CONNECTING,
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену workers,
заблокированного медиа-потока, транзита и ожиданий заменённой очереди. `test_chat_join_e2e`
проверяет join и распространение membership между узлами без UTUN-сервиса.
В Android звонковой сессией владеет `CallController` уровня приложения; события
ядра и команды Telecom обрабатываются один раз в main thread, без повторного
проигрывания событий при создании ViewModel. UI получает `StateFlow` и передаёт
пользовательские команды. `CallService` владеет `CallAudioEngine` и частичным
wake lock: перед исходящим вызовом или принятием входящего запускается foreground
режим `phoneCall|microphone`, затем сигналинг и аудио. Завершение сначала
останавливает I/O и дожидается аудиопотоков, затем освобождает native-аудио,
wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выполняется тот же
порядок; новый звонок разрешается после `SERVICE_STARTED` нового ядра.
Активность ядра задаётся явно: видимая Activity или незавершённый звонок удерживают
ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После
завершения звонка в фоне возвращается standby; экран может гаснуть во время
разговора независимо от частичного wake lock сервиса.
Если работают рация, публикация или просмотр трансляции, исходящий Android-звонок
требует подтверждения их остановки; входящий показывает предупреждение до принятия.
После подтверждения/принятия `CallController` выключает участие в рации, завершает
публикацию и просмотр, асинхронно ждёт освобождения их аудио и только затем запускает
сигналинг и звонковое аудио. Foreground-сервис резервируется сразу из видимой Activity.
Отмена во время освобождения не запускает устаревший звонок.
До окончания звонка повторное включение рации/трансляции заблокировано.
После закрытия звонкового аудио и Telecom Connection ранее включённая рация
возвращается в прежний канал; удержания PTT сброшены. Удалённый канал и остановленное
ядро не восстанавливаются. Публикация и просмотр трансляции автоматически не возобновляются.
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно.
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
+1295
View File
File diff suppressed because it is too large. Load diff
+104
View File
@@ -0,0 +1,104 @@
# Независимая STUN-диагностика NAT
Модуль `src/transport_layer/stun_nat.c/h` измеряет NAT на уникальных путях существующих IPv4 UDP-сокетов.
Ключ группы — семейство, фактический IP и netif; имена и рабочие порты не участвуют.
Для каждой группы создаётся один собственный неблокирующий UDP-сокет с тем же локальным IP,
привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535.
Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения.
STUN не меняет исходный ETCP fd, его порт и обработчики. Через `stun_nat` API модуль
`nat_detection` получает предварительную оценку для рабочих сокетов того же IP/netif
с совпадающими mark/FIB/device. Если два разных IP видят одинаковый стабильный внешний
IP:порт и есть трансляция адреса, рабочий профиль получает EIM? с source=STUN и inbound=UNKNOWN.
Это действует и для обычных серверов без запроса с незнакомого IP. Filtering тестового порта
не переносится; измерение mapping рабочего сокета всегда имеет приоритет.
Оценка снимается после TTL, отключения STUN, смены пути или противоречащего внешнего IP.
Тестовый mapped endpoint не копируется в `nat_addr` и NODEINFO: адрес для подключения
должен быть получен наблюдением самого рабочего сокета. Детектор описан в `doc/nat_detection.txt`.
## Конфигурация
```ini
[stun]
enabled=yes
server=stun-a.example.org:3478
server=stun-b.example.org:3478
# Дополнительные строки — резервные серверы.
[debug]
nat=debug
```
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
## Результат
`stun_nat_get_result(instance, "имя исходного сокета", &result)` копирует локальный снимок:
Имя сначала разрешается в текущий рабочий сокет, затем в группу. API ядра
`stun_nat_get_socket_result(instance, socket, &result)` дополнительно проверяет
текущие IP/netif и параметры маршрутизации. Несколько рабочих портов получают общий снимок.
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
- `mapping`: UNKNOWN, MATCH, DEPENDENT, EIM, ADM, APDM;
- `filtering`: UNKNOWN, EIF, ADF, APDF;
- причины неполного результата, статус, время измерения и обнаруженная поддержка RFC 5780.
- ключ группы, generation, evidence_id, measured_at_us, количество рабочих сокетов и признак refresh.
Обычный STUN даёт MATCH при одинаковых адресах и DEPENDENT при разных. MATCH не доказывает EIM,
а обычные Binding-ответы не позволяют определить filtering. Первоначальный сервер опрашивается повторно:
смена его mapping во время проверки инвалидирует классификацию как нестабильную.
## RFC 5780
Корректные OTHER-ADDRESS и RESPONSE-ORIGIN включают автоматическую попытку Behavior Discovery.
Нужны две публичные IP и два порта на серверной стороне, доступные во всех четырёх комбинациях.
1. Обычный Binding получает внешний адрес и альтернативный endpoint.
2. CHANGE-REQUEST с флагами IP+port проверяет EIF.
3. Если ответ не пришёл, контрольный Binding проверяет живость сервера и стабильность mapping.
4. CHANGE-REQUEST только port различает ADF и APDF; после таймаута снова выполняется контрольный Binding.
5. Перед публикацией ADF/APDF альтернативные endpoints проверяются обычными Binding-запросами.
Недоступный альтернативный endpoint даёт UNKNOWN, а не ложную строгую фильтрацию.
6. После filtering проверяется mapping: альтернативный IP с первоначальным портом, затем при необходимости
альтернативный IP с альтернативным портом. Сравнение различает EIM, ADM и APDM.
7. Второй сервер и повтор первого проверяют независимую пару и стабильность результата.
Все filtering-тесты выполняются до открывающих фильтр отправок на альтернативные endpoints.
Собственные предыдущие STUN-отправки учитываются: при пересечении адресов следующего сервера
сомнительный тест filtering пропускается. ERROR 420 и игнорирование CHANGE-REQUEST дают UNKNOWN.
Аутентификация STUN не реализована: требующий её сервер фиксируется как ошибка, затем используется резервный.
Запрос содержит случайный 96-битный transaction ID и FINGERPRINT. Проверяются источник ответа,
transaction ID, границы/выравнивание атрибутов, XOR-MAPPED-ADDRESS, RESPONSE-ORIGIN и FINGERPRINT при его наличии.
Повторы одной транзакции сохраняют ID: RTO от 500 мс удваивается, всего семь отправок, последнее ожидание 8 секунд.
Новые транзакции всех проб ограничены десятью в секунду; ожидания не блокируют event loop.
## Жизненный цикл и проверки
Ядро запускает модуль при `utun_core_start`; остановка уничтожает его до сетевых ресурсов и config.
Перезагрузка конфига отменяет старые DNS/таймеры и создаёт новый модуль.
Раз в три секунды выполняется локальная сверка групп: новые/изменённые проверяются,
группа удаляется после исчезновения последнего рабочего сокета. Смена имени/представителя
или удаление дубликата не теряет кэш. Несовпадение mark/FIB/device внутри одного ключа
даёт UNKNOWN с причиной incompatible routing policy.
TTL результата 300 секунд, refresh 180 секунд; неполные/неудачные проверки повторяются через 30 секунд.
Во время штатного refresh доступен прежний результат до TTL; его возраст не обновляется.
Использованные тестовые порты не переиспользуются в течение жизни ядра, включая reload STUN.
История портов освобождается только при окончательном destroy ядра.
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
Публичные операции выполняются в uasync-потоке экземпляра.
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
DEBUG содержит этапы, transaction ID, повторы, RTT, DNS и очистку; WARN/ERROR описывают аномалии.
Linux-тест `test_stun_nat` использует реальные локальные UDP-сокеты и ускоренные часы:
обычный STUN, все классы RFC 5780, недоступные альтернативы, потеря запроса, неверные/поздние ответы,
резервные серверы, DNS, нестабильный mapping, отмена и повторная проверка, изменение состава сокетов.
Сценарии проверяют сохранность рабочего fd/порта, предварительный EIM?, приоритет ETCP,
снятие оценки после TTL, совпадение IP/netif и освобождение таймеров.
Ссылки: [RFC 5780](https://www.rfc-editor.org/rfc/rfc5780), [STUN RFC 8489](https://www.rfc-editor.org/rfc/rfc8489).
+361 -111
View File
@@ -2,136 +2,386 @@
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
## Текущие задачи
[ ] **Звонок (P2P аудио)** — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный
каталог `src/call/`. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с.
Android: переключение источника (трубка/громкая/гарнитура).
Архитектура: `/doc/chat_call_arch.md`. Статус: спроектировано, ждёт реализации.
[+] **Android: приём рации через SCO после отпускания PTT** — исправлено 22.10.2026:
Общий MediaAudioController сохраняет разговорный тракт для SCO-only выхода, даже без подписчиков микрофона.
AudioTrack рации снова использует USAGE_VOICE_COMMUNICATION: Android учитывает playback как разговорную активность.
Добавлены логи фактического routedDevice/mode/SCO и аппаратная проверка после закрытия последнего input.
Samsung + C1 PRO: PHONE → Bluetooth → PHONE, 35с приёма без микрофона и холодный запуск SCO без PTT прошли.
На Q8 прошли общий захват/переключения/сохранение playback. JVM 145/145, сборки ядра и APK успешны.
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
[ ] **Android: просмотр трансляции через SCO без работающей рации** — воспроизведено 22.10.2026:
Media-выход с USAGE_MEDIA после закрытия микрофона теряет SCO: mode=0, sco=false,
хотя общий контроллер продолжает владеть выходом и сам больше не закрывает разговорный тракт.
AudioService учитывает VOICE_COMMUNICATION playback/record для активности MODE_IN_COMMUNICATION.
Повтор: adb -s RZ8R72MG64B shell am instrument -w -e output media com.utun.chat/com.utun.chat.data.MediaAudioProbe.
Радио с USAGE_VOICE_COMMUNICATION проходит ту же проверку, media-вариант её не проходит.
Сохранить media-назначение/громкость обычных выходов; для SCO переключать аппаратный AudioTrack на VOICE_COMMUNICATION.
При пересоздании только аудиовыхода сохранить недописанный PCM, учесть уже записанный буфер и непрерывный played;
видео и его кодеки не перезапускать. Проверить оба направления смены выхода и длительный playback без микрофона.
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`):
- `chat_setting` → `inst->chat_settings` (`struct chat_setting_state`, state-level API для парсера конфига + instance-level для runtime).
- `chat_event` → `inst->chat_event_handler` (обработчик теперь получает `inst`).
- `chat_core` (`g_cc`) → `inst->chat_core` (`struct chat_core_ctx*`).
- `chat_sync` (`g_cs`) → `inst->chat_sync`.
- `member_sync` callback-списки (`g_props_cbks`/`g_apply_cbks`) → в `chat_core_ctx`.
- `chat_join` (`g_keys`) → в `chat_core_ctx`.
- `chat_msg` media-счётчики/backfill → в `chat_core_ctx`.
- `dm_core` (`g_dm`) → `inst->dm`; `dm_mailbox` (`g_mb`) → `inst->dm_mailbox`.
- `chat_headless_control` (`g_hc`) → `inst->headless`.
- `chat_whisper` — сигнатуры `init/available/trigger` принимают `inst`.
- API протащено через `inst` (chat_core_*/dm_*), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser).
- `test_dm_e2e.c` переписан на однопоточный: один `uasync`, три `utun_instance` (A,B,C), master state-machine, без fork. Фаза 2 (B offline) — `utun_instance_destroy(B1)` + пересоздание B2.
- Попутно исправлен 1-байтовый overflow в `dm_mailbox.c: mb_route_send` (`u_malloc(1+body_len)` → `1+1+body_len`).
[+] **media_async: файловые примитивы и точные контракты .h** — исправлено 22.10.2026:
ma_copy_file проверяет существование/идентичность, сохраняет один regular-файл при ./ или hardlink;
ссылки назначения отклоняются, копирование идёт через соседний temp с проверенными записью/close/publish.
Общие примитивы lib/file_io используются также video, сохраняя публикацию видео только в completion.
ma_uuid возвращает ошибку RNG и обнуляет выход; media_index не записывает результат при отказе media/block UUID.
ma_file_size и переменные вызывающего кода — int64_t; проверяются смещения, число блоков и размеры выделений.
Обход передаёт ошибки, пропускает ссылки/reparse points, проверяет длину/глубину; Windows использует wide API.
Контракты .h и service_lifecycle описывают синхронные отказы, лимит 16 до completion и отсутствие отката work при отмене.
Неверные аргументы дают -1, closing/destroy — CANCELLED; повторный вход в destroy диагностируется и отклоняется.
Новый test_media_primitives проверяет сохранность при I/O/fdopen ошибках, RNG/seek без записей в БД,
sparse-размеры на границах 2/4 ГиБ, ошибки обхода, ссылки/глубину и ownership задач с внедрением ошибок.
Все шесть тестов media_primitives/video/media_index/attachment_send/services/dm_worker прошли ASAN/UBSan с detect_leaks=1.
Общий check.sh: 176/176 и proxy/burst/load. Windows runtime/сборка не проверялись: кросс-компилятора в окружении нет.
Логи: /tmp/media-primitives-full-check-final.log, /tmp/media-primitives-final.log, /tmp/media-asan-test_*.log.
Статус сборки: `make -C src` (libutun.a + бинарник utun) и `make -C tests` — OK.
[ ] **media_index_commit: не обрезать длинный location** — статическая проверка 22.10.2026:
Относительный dest_path записывается через snprintf в location[512] без проверки длины;
register_async допускает dest_path до 1023 байт, а media_base может не сокращать его до 511 байт.
При таком пути в БД попадёт обрезанное имя. Проверить сценарий на реальном вложенном пути,
затем отклонять непредставимый location до INSERT либо согласовать размеры со всеми читателями.
Найдено при сверке вызывающего кода media_async; воспроизведение и исправление отложены.
### Осталось (по порядку)
[+] **test_stream_service: убрать зависимость bootstrap-проверки от гонки TCP-handshake** — исправлено 22.10.2026:
Падение на строке 409: REINIT=0 при probes=1/subflows=1/failures=0 и доставленной первой пачке.
Встречный TCP-линк успевает сообщить peer_reset_id до ETCP CONFIRM: начальная эпоха уже известна,
поэтому etcp_session_receive не вызывает REINIT. При другом порядке событий REINIT=1 и тест проходит.
Холодная подписка проверяет одну пробу/выдачу и доставку без ошибок, число начальных REINIT диагностическое.
Отдельная автоматическая подписка удерживает INFO_RESP, выполняет настоящий REINIT и продолжает ту же пробу.
Проверяются новые epoch/nonce, отсутствие лишних проб/CLOSED и пустой локальный буфер перед получением DATA.
Ручные проверки старых ответов и исходного deadline сохранены; ожидания привязаны к поколениям и событиям.
Добавлен --bootstrap-only: 8 повторов с ускоренными и 2 с обычными часами прошли.
Полный test_stream_service прошёл ASAN/UBSan с detect_leaks=1; check.sh — 175/175 и proxy/burst/load.
Изолированная потеря pending-пробы вызвала ожидаемый отказ новой автоматической проверки; мутация убрана.
Логи: /tmp/stream-service-original-failure.log, /tmp/stream-bootstrap-repeat-*.log,
/tmp/stream-service-asan-final.log, /tmp/stream-bootstrap-mutant.log, /tmp/stream-bootstrap-full-check-final.log.
[+] **test_dm_e2e: зависание в utun_instance_destroy(B1)** (фаза P_B_OFFLINE) — сделано.
Причин было несколько (не одна):
1. `lib/u_async.c`: если ближайший таймер уже истёк на входе в `uasync_poll`,
`get_next_timeout` возвращал {0,0}, `timeout_ms=-1` → `epoll_wait(-1)` блокировался
навсегда, а `process_timeouts` (вызывается только после epoll_wait) не запускался.
Фикс: `if (timeout_ms == -1 && heap не пуст) timeout_ms = 0`.
2. `routing_layer/etcp_router.c`: `router_send_conn` с group_id=0 (глобальная
маршрутизация DM/mailbox) не находил маршрут (`topo_groups_find(0)==NULL`).
Фикс: fallback на `instance_find_conn(inst, remote)` при group==NULL.
3. `dm/dm_core.c`: `dm_on_conn_status` PULL-ил только беседы с подключившимся пиром;
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения
PULL для всех бесед, чей пир не подключён напрямую.
4. `dm/dm_mailbox.c`: не было ACK после PULL_RESP → storage не чистил dm_mail.
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK.
5. `utun_instance.c`: `dm_core_init` ставил deliver-cb до `dm_mailbox_init` →
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core.
`test_dm_e2e` проходит (несколько прогонов), `test_dm` тоже.
[+] **video: сохранность файлов, JPEG и подготовка MP4** — исправлено 22.10.2026:
Одинаковые файлы/алиасы и недопустимые пути отклоняются без изменения исходника.
JPEG и MP4 пишутся в эксклюзивный временный файл; запись, drain, trailer и close проверяются.
Только успешный completion публикует результат; отмена до callback сохраняет старое назначение.
Превью получает задержанные B-кадры. MOV/H.264/AAC проходит remux в MP4; остальные профили
перекодируются с audio FIFO, сохранением временного смещения и хвоста ресемплера.
Атомарный global slot удерживается до completion, ошибки имеют точную фазу и контекст в логе.
Контракты описаны в src/video/video.h и doc/service_lifecycle.md.
test_video проверяет реальные кадры/сигнал, 1080p→720p, PCM/MP3/AAC, resample 12345→12000 Гц,
файловые ошибки, отмену, повтор из callback и конкурентные start; test_video_no_ffmpeg — стабы.
Оба теста, test_services и test_attachment_send прошли ASAN/UBSan с detect_leaks=1.
Отдельная конфигурация всего ядра/тестов без FFmpeg собрана, оба video-теста прошли.
Логи: /tmp/video-asan-final.log, /tmp/video-services-asan.log, /tmp/video-attachment-send-asan.log,
/tmp/video-without-ffmpeg-build-final.log; общий прогон: /tmp/video-full-check-final.log.
[+] **chatgui-android: переезд на per-instance API** — сделано.
`instance_lite.c/h` (+`instance_lite_get_instance()`), `jni_bridge.c`, `standby.c`,
`headless_control.c`, `voice_recorder.c`, `attachment_sender.c`, `video_sender.c`,
`photo_sender.c` — все call-сайты переведены на `inst`. `libutun_lite` компилируется,
`jni_bridge.c` — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.)
[+] **tcp_io: владение connect-таймером и повторный FIN** — 21.10.2026:
Замена таймаута сохраняет старый таймер при ошибке создания нового; <=0 отменяет таймер.
Возвращаемые ошибки и диапазон INT_MAX/10 проверяются API и TCP-прокси; connect/error/destroy отменяют таймер.
FIN различает fin_pending и fin_local, ошибка shutdown идёт в on_error без ложного on_fin_sent.
Уточнены отложенное освобождение, синхронные callbacks очередей и платформенный выбор IPv6/маршрута.
Проверено: 15 сценариев test_tcp_io, test_tcp_io_flush, test_proxy_regressions и test_socks_http_proxy
(16 загрузок с побайтной сверкой). test_tcp_io и flush также прошли ASAN с detect_leaks=1 без ошибок.
Логи: /tmp/tcp-io-fix-unit.log, /tmp/tcp-io-fix-asan-unit.log, /tmp/tcp-io-fix-asan-flush.log,
/tmp/tcp-io-fix-proxy-regressions.log, /tmp/tcp-io-fix-socks-http.log.
[+] **test_chat_join: segfault** (регресс рефакторинга) — сделано.
`chat_join` хранил `join_keys` в `chat_core_ctx` (CC(inst)), а тест использует chat_join
без chat_core → NULL-deref. Фикс: `join_keys` вынесен в `UTUN_INSTANCE.join_keys`
(у chat_join собственный init/destroy).
[ ] **Desktop publisher: проверить восстановление после недопустимого FPS в Xvfb-тесте** — 20.10.2026:
Широкий screenAndWindowCapture прошёл закрытие управления с active=1 и продолжением публикации,
затем не получил видео revision>=3 после запроса fps=61 и возврата engine.config().video.fps к 15.
Перед этим применены VP9/rotation=90; worker продолжает кодировать generation=4.
Причина и связь с UI-изменениями не установлены; отдельная проверка закрытия управления PASS.
Лог /tmp/desktop-stream-controls-xvfb.log, тест tools/chatgui/tests/test_stream_publish.cpp.
[ ] **BGP-гонка: дропнутый REQUEST_TABLE** (отдельная задача, как договорились).
[ ] **Проверить одновременную собственную и удалённую трансляции в multiview** — 20.10.2026:
Собственный preview и удалённые readers по отдельности проверены; их совместный живой сценарий в одном канале не проверялся.
На desktop/Android проверить переход в управление своей публикацией и возврат, fullscreen/Back/Esc,
сохранение удалённых readers и общего звука, отсутствие местного звука и остановку собственной публикации.
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка
«conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки
`topo_group_send_table_request()`.
[ ] **Multiview desktop: terminal CLOSED оставляет аудиовход и timer** — проверка 20.10.2026:
fail(reason, false) ставит complete=true/failed=false, но tick снимает audio только по ended/failed.
После STREAM_CLOSE_GROUP воспроизведено complete=1, failed=0, attached=1, timer=1.
При valid=0 каталог сохраняет плитки, поэтому состояние живёт до ручного закрытия окна.
Отделить завершение от разрешения retry и освобождать output при любом terminal CLOSED.
Временный regression-проверочный стенд и лог: /tmp/multiview-review/test_stream_view.cpp,
/tmp/multiview-review/lifecycle.log. Исходники реализации при проверке не менялись.
**Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк,
затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего
переподключение не стартует. Поэтому `return` в дедупе — правильный.
[ ] **Проверить ожидание запросов ключей в test_encrypted_attachment** — 21.10.2026:
До rebase исправлений control_server: check.sh вне sandbox 165/166; отдельный повтор также
phase=5/heartbeats=12001, ожидание двух pending incoming-запросов после перезапуска автора.
Логи: /tmp/control-server-full-check.log, /tmp/control-server-attachment-recheck.log;
артефакты общего прогона: /tmp/utun_encrypted_attachment_6JMO0c. Код вложений в этой работе не менялся.
**В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу
(NODEINFO/узлы группы). Сценарий «разнесённого старта»:
1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn`
→ `topo_group_send_table_request`, `topo_group.c:602`).
2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка).
C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G →
дропает запрос.
3. C наконец создаёт G (`topo_groups_create_group` → обход connections →
`topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`.
A отвечает своей таблицей → **C узнаёт A**.
4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G.
Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость).
[ ] **Разобрать нестабильную загрузку storage-узлов в test_media_delivery_chat** — 19.10.2026:
В том же общем прогоне storage st1/st2 не собрали файл, n2 исчерпал попытки блока 0 (15/19 внутренних проверок).
Отдельный повтор прошёл 19/19. Причина не установлена; сравнить маршруты и готовность storage под общей нагрузкой.
Логи: tests/logs/test_media_delivery_chat.log, /tmp/phone-busy-media-recheck.log.
Повторилось 21.10 при аудите ID сервисов: 164/165, те же 15/19 внутренних проверок.
Отдельный запуск и 16 параллельных повторов прошли. Причина остаётся открытой;
тест теперь сохраняет подробный media/router лог и БД при отказе в /tmp/utun_mdc_*/.
Логи /tmp/service-ids-full-check.log, /tmp/service-ids-media-chat-debug.log, /tmp/service-ids-media-chat-stress/.
**Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно
идемпотентно `topo_group_send_table_request(group, conn)`:
```c
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
topo_group_send_table_request(group, conn); /* повторно, идемпотентно */
DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name);
return;
}
```
`topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE,
пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден.
[ ] **Разобрать нестабильное ожидание ACK в test_local_transport** — 19.10.2026:
Общий check.sh: 161/162, chat_adapter упал на строке 173 при ожидании LOCAL_DELIVERY_FORBIDDEN для request=508.
В логе получатель дважды отклонил приглашение и отправил ACK; отправитель до проверки получил только late ACK request=507.
Отдельный повтор прошёл. Причина не установлена; проверить доставку UDP и шаги ускоренных часов под параллельной нагрузкой.
Логи: /tmp/config-name-local-transport-failure.log, /tmp/config-name-local-transport-retry.log.
Ещё один общий прогон 19.10: 161/162, chat_adapter упал на строке 145 при ожидании доставки request=501.
Получатель принял приглашение и дважды отправил ACK; до проверки в логе отправителя нет приёма этого ACK. Отдельный повтор прошёл.
Логи: /tmp/utun-stream-preview-local-transport-failure.log, /tmp/utun-stream-preview-local-transport-retry.log.
**Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за
детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE —
поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный
воспроизводимый тест для этой гонки пока не написан.
[ ] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — 17–20.10.2026:
test_stream_view::realH264RenderingAndClose нестабильно получает silenceGaps=3–4 вместо 2.
Зафиксированы pcm_short с got=96/480 и 288/480; видео продолжало выводиться.
Сбой повторялся с общей нагрузкой и после её завершения; изолированные повторы проходили.
Проверить актуальные PCM workers/часы под нагрузкой; единичный PASS не подтверждает устранение.
Логи: /tmp/stream-zoom-full-view-audio-gap.log, /tmp/stream-zoom-full-view.log,
/tmp/multiview-review/desktop-review.log, /tmp/multiview-review/audio-original.log.
[ ] **«packet undecryptable» на линках C** (вторичное). A, узнав B через BGP, через
`topo_group_connect` пытается поднять прямой A↔B линк (`seskey 4777737b`), но B не
настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением
(не пытаться соединяться с узлами, для которых нет `[client]`/ncd-конфигурации,
либо обрабатывать неудачу без спама).
[ ] **Устранить коллизию порта при перезапуске узла в test_dm_e2e** — 16.10.2026:
Общий check.sh при проверке Windows-курсора: 155/156, test_dm_e2e упал в phase=9 на создании B2.
Повторный bind 127.0.0.1:33843 вернул Address already in use; отдельный повтор теста проходит.
Проверить владение/резервирование порта между остановкой B и созданием B2 при параллельном прогоне.
Логи: /tmp/win-stream-cursor-dm-e2e-failure.log, /tmp/win-stream-cursor-dm-e2e-retry.log.
[+] **Обновить GUI call-сайты `tools/chatgui` (Qt, desktop)** — сделано.
- В `chat_core.h` вынесены trampoline-структуры из `.c`: `update_my_name_arg`,
`save_ui_state_arg`, `chat_setting_arg` (были в chat_profile.c/chat_core.c).
- `gui_bridge.h/impl`: `gui_bridge_set_inst()`/`gui_bridge_get_inst()` (+`g_inst`).
- `utun_node.cpp`: `gui_bridge_set_inst(m_instance)`, `chat_event_set_handler(m_instance,…)`,
`chat_core_sync_my_addresses(m_instance)`.
- Обновлены ~45 call-сайтов в 16 файлах (`mainwindow`, `messagelist`, `messagedelegate`,
`memberlistmodel`, `memberpropsdialog`, `accountlist`, `invitedialog`, `inviteby`,
`joindialog`, `settingsdialog`, `channelsettingsdialog`, `soundsettingspage`,
`audiodevicesettingspage`, `storagesettingspage`, `statuspage`, `connmonitorwindow`).
- Сборка `cmake --build . --target vibechat` — OK.
Попутно: `chat_sync_connect_from_invite` писал `cs->pending_*` на вызывающем (GUI) потоке —
перенёс в `cm_invite_trampoline` (uasync-поток), убрав гонку; заодно исправил утечку
`inv->addrs_data` и добавил null-check `cs` в трамплине.
[ ] **Проверить Windows-трансляцию на исходной машине после исправлений playback/stop** — 16.10.2026:
Цикл ожидания позднего аудио исправлен; join и закрытие микрофона выполняются асинхронно,
QVideoFrame::toImage() вынесен в долгоживущий image worker. Linux/Xvfb-регрессии проходят.
Осталась свежая трансляция Windows → Samsung и stop после пересборки Windows: исходный Qt backend ещё не проверен.
На RYS HFR USB2.0 Camera проверить сообщение об отсутствии PCM, отзывчивость при завершении WASAPI,
отказ/согласие на CPU после отказа NVENC. Причина frames=0 микрофона пока не установлена;
подробный AudioDiag реализован только для Linux. libx265 3440×1440 на исходном CPU выдаёт около 2–3fps.
Логи: 4.log, /tmp/stream-viewer-new-run.log, /tmp/late-audio-test.log,
/tmp/stream-persistent-image-xvfb.log, /tmp/stream-audio-stop-full-check.log.
[+] **Прогон всех тестов** — сделано. `make clean && make -j4` OK; все тесты проходят
(кроме `test_auto_socket_dynamic` — пропуск «requires root»). `test_dm_e2e` стабилен
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов,
`router_no_route`), не влияет на результат — стоит разобрать отдельно.
[ ] **Проверить появление значка свежей трансляции на SM после сна через colo2** — 16.10.2026:
Пробуждение и перезапуск SM во время сна уже проверены на обновлённом colo2:
новый линк снимает прежний таймер, Merkle завершается без ожидания полного сна 120с.
Осталась визуальная проверка значка новой desktop-трансляции после возврата SM в foreground;
в прежнем прогоне активных источников не было (count=0).
Логи: /tmp/stream-wake-live-{sm,colo2}-*.log.
[ ] **Проверить остаточные паузы видеозвонка на актуальном playout** — 15.10.2026:
После исправления TX flush живой звонок 12694689407894286842 дал SM 29.87fps без drops,
desktop 29.91fps/3 late drops за последние 20с; отдельные промежутки SM доходили до 63.8мс.
В старой трассе краткие invalid audio clock сбрасывали якорь, точная причина invalid не была видна.
После переработки playout/ACK и чтения аудиочасов 19–20.10 повторить оба направления на устройствах,
сопоставить gap_pts_us/gap_rx_us/gap_dispatch_us и причины недоступности часов, если паузы сохраняются.
Текущий контракт и результаты поздних проверок: doc/call_video.txt.
[ ] **Доступность разговорного динамика в UI звонка** — при подключённой проводной гарнитуре Samsung убирает
EARPIECE из Telecom endpoints, но кнопка остаётся доступной по наличию физического встроенного устройства.
Нажатие создаёт отложенный запрос, который исполняется после отключения гарнитуры. Согласовать UI с доступными endpoints.
Также выбор проводной гарнитуры в CallScreen сейчас запрашивает BLUETOOTH_CONNECT при отсутствии этого разрешения;
проверку разрешения нужно привязать к выбранному типу гарнитуры.
[ ] **Ручная приёмка видеозвонков Android и ChatGUI после последних исправлений** — 14–20.10.2026:
Сигналинг, CAPS, реальные codecs/камера/render и A/V подключены; живые звонки уже проводились.
Осталась полная проверка после изменений Full HD/50–60fps, HEVC High tier, Surface ACK и фоновых codec probes:
Windows ↔ Samsung/Q8, камеры и оба направления, первый и повторный звонок, смена камеры/разрешения/кодека/битрейта,
A/V, завершение и освобождение Surface/видеопорта. Аппаратные probes не заменяют эту проверку.
Архитектура, контракты и выполненные проверки: doc/call_video.txt, doc/chat_call_arch.md.
[ ] **Тесты: нестабильное завершение test_stcp_traffic** — 14.10.2026:
В полном прогоне после NCD CLOSE server получил 1029466/1048576 bytes (done=0), client — 1048576 (done=1).
Упала проверка test_stcp_traffic.c:216; отдельный повтор прошёл. Причина не установлена, сетевой код в задаче zoom не менялся.
Проверить завершение двусторонней передачи относительно закрытия handle; первичный лог сохранён: /tmp/stream-zoom-stcp-first.log.
[ ] **Тесты: диагностировать нестабильную проверку TX snapshot в test_android_stream** — 14.10.2026:
В полном прогоне один раз упала строка 177: snapshot успешен и 0 < video_bytes < 500000 после test_clock_run_for(ua, 1000).
Сам publisher обновляет snapshot таймером 1000; текущий CHECK не выводит result/sample_us/video_bytes.
Отдельный повтор и следующий полный прогон прошли. Добавить значения и проверить ожидание конкретного снимка;
причина не установлена, Kotlin-аудиовывод этот C-тест не вызывает.
Логи прогонов: /tmp/stream-audio-check-host.log, /tmp/stream-audio-native-retry.log, /tmp/stream-audio-check-final.log.
[ ] **Общий AGC: клиппинг после резкого роста сигнала при max_gain=45dB** —
на SM при проверке CAMCORDER с настройками рации rise=6/release=30 наблюдались
4581 обрезанный отсчёт за интервал около 1s. Это общий алгоритм radio/stream,
плавное снижение gain может не успеть за скачком входа. Проверить ограничение пиков
без щелчков и ухудшения адаптации. Лог: `/tmp/stream-camcorder-sm-live.log`.
[ ] **Desktop: выход после SIGTERM оставляет GUI после остановки ядра** — 13.10.2026:
при обновлении live-плеера старый vibechat завершил ядро/worker, но процесс остался
в QCoreApplication::exec с работающим audio output. В логах также DM SELECT после
уничтожения SQLite (`bad parameter or other API misuse`). Проверить отмену Quit
в MainWindow::closeEvent (всегда ignore), порядок уведомлений и отключение GUI от БД.
Снимок: `/tmp/stream-desktop-shutdown-backtrace.log`, лог vibechat 13:04:09.
[ ] **Стриминг SM → colo2 → desktop: восстановление частично остановившихся субпотоков** —
13.10.2026: помощь подключилась за ~8 с, все 8 субпотоков через colo2 приняты с общего индекса 5844.
Позже шесть субпотоков перестали продвигаться около seq=111625; четыре restart не восстановили
просмотр, desktop завершился по таймауту в 11:10:48 (colo2: 12:10:48). На SM публикация продолжилась,
colo2 снял свою подписку через 3 с. В том же интервале сервер видит RX timeout/переподключения TCP к SM;
первопричина остановки выдач пока не установлена: stream на colo2 наследовал error, детальных логов не было.
Повторить с заранее включённой диагностикой stream/etcp на SM и colo2, проверить upstream handles,
аренды, позиции и повторное согласование. Логи: `/tmp/stream-live-gui-final.log`,
`/tmp/stream-live-colo2-final.log`, `/tmp/stream-live-sm-final.log`.
Повтор 13.10 со stereo: до 15:20 desktop показывал ~30fps, затем video/audio очереди опустели;
decodeFps упал до 7–18 и viewer завершился по таймауту в 15:21:13 после пяти restart.
SM при этом сохранял ~30fps без dropped. Новые логи: `/tmp/stream-stereo-desktop-delivery-stall.log`,
`/tmp/stream-stereo-colo2-delivery-stall.log`, `/tmp/stream-stereo-sm-delivery-stall.log`.
[ ] **Android: отмена асинхронного запуска до готовности ядра** — при проверке
мониторинга 12.10.2026 обнаружено: `NativeLib.stop()` пропускает native stop,
если `isReady()==false`, а `instance_lite_stop()` возвращается при `g_inst==NULL`.
Поэтому запрос stop до завершения конструктора может не отменить запускаемый
поток. Согласовать отмену с состоянием потока и единым сигналом завершения;
проверить stop до SERVICE_STARTED, отказ конструктора и последующий start.
[ ] **Android: повторные отказы persisted node snapshots** — замечено 12.10.2026
на устройстве D2AR10P3600820 при проверке live media. В сохранённой БД ряд узлов
регулярно получает `invalid persisted snapshot` в `topo_node_sqlite_snapshot_load`,
затем NCD сообщает `node not found`; повторные попытки засоряют лог.
Лог: `/tmp/stream-av-device-initial.log`. Причина пока не установлена: текущий
текст объединяет отказ deserialize, несовпадение ID и отказ verify. Разделить
диагностику этих ветвей и проверить повторные попытки загрузки отвергнутого снимка.
Существующую БД устройства при проверке стриминга не очищали.
[ ] **STUN IPv6** — отдельный этап: IPv6 endpoint/config/DNS и XOR-MAPPED,
scope/netif, тесты IPv6/NAT66. IPv4-оценки не переносить на IPv6 или TCP.
[ ] **check.sh: изоляция сетевых integration-тестов** — после основного набора
автоматически запускает check-proxy/burst/load при доступном sudo. Скрипты
меняют rp_filter, таблицу 100 и iptables главного namespace; cleanup выставляет
rp_filter=1 вместо сохранённого значения. Перенести тесты в отдельный namespace
и проверять неизменность родительской сети, как в tests/stream_netns.
[ ] **Стриминг: оставшиеся длительные и масштабные сетевые проверки** — 10–12.10.2026:
Реализация ядра, публикации, приёма и автоматического выбора поставщиков завершена; контракт — doc/stream.txt.
Есть 16-процессный namespace-стенд с NAT/reverse и выбором UDP/TCP: tests/stream_netns/README.md.
Осталось подтвердить покрытие длительной нагрузки, смены адресов/мультилинка, одновременной перестройки
нескольких ретрансляторов, нескольких автоматических источников/групп и mobile standby на устройстве
(doc/stream.txt, раздел 16). Сохранённые проверки отдельных этапов не закрывают всю матрицу.
[ ] **Member files: исчерпание кеша результатов массовых изменений** — 10.10.2026:
Воспроизведено test_member_files_browser::recursiveDeletion(server-busy): из 76 объектов удаляются 64,
следующая команда получает MF_BUSY, остаются 12. storage.c: STORE_RECEIPTS=64, RECEIPT_TB=1200000 (120 секунд).
Лимит общий для успешных COMMIT/MKDIR/RENAME/REMOVE; затрагивает также рекурсивное копирование.
Нужен отдельный этап управления временем жизни receipts с сохранением идемпотентности повторных запросов;
простое вытеснение старых записей нарушает гарантию повторного ответа. После исправления изменить регрессию
на успешное удаление более 64 объектов и проверить повторные запросы/потерю ответа.
[ ] **Desktop: провалы вероятности VAD при компрессии** — диагностика 09.10.2026:
Воспроизведение пользователя: длительная гласная, затем временная нечувствительность; ближе к концу лога несколько повторов.
Новый лог 14:32:14–14:34:38: на 14:34:07–18 и 14:34:20–38 все 556 окон с RMS > -30dBFS имеют p < 0.5;
PCM непрерывен, errors=0, RX-gate не мешает, clips=5 не растёт. Сигнал не исчезает, порог остаётся 0.5.
--probe-stream: C-обёртка + реальный AGC + две Silero на одинаковом PCM, отличие — один reset состояния второй модели.
Синтезированный flite ah растянут до 24.576с: без AGC/клиппинга p первой секунды следующей речи 0.027 против 0.674 после reset.
При AGC 1dB (clips=0) — 0.012 против 0.662. Влияние истории модели подтверждено, провал на десятки секунд целиком не воспроизведён.
Лог стенда: /tmp/vad-vowel-context.log; запуск: test_silero_vad lib/silero_vad.onnx --probe-stream /tmp/vad-vowel-context48.pcm 993.
Вход стенда: mono s16le/48кГц, reset_window — индекс окна 512/16кГц; режим проверяет без AGC, 1dB и 15dB.
Автоматические сбросы в рабочем VAD не добавлялись; test_silero_vad PASS.
Уточнение: низкая вероятность держится десятки секунд, затем распознавание восстанавливается; краткий клиппинг это не объясняет.
Локальный лог 14:00:10–14:00:58: 15 сбросов AEC при пропусках референса, 16 clipped samples в VAD; vad DEBUG не был включён.
Добавлены секундные уровни raw/clean/reference и причина bypass/reset AEC; отдельное предупреждение о клиппинге VAD.
Секундный отчёт захвата работает также без PCM: возраст кадра, очередь, максимальная задержка и предупреждение о простое.
TRACE каждого окна содержит DC, число нулей и gain вместе с вероятностью/уровнем/паузой между окнами; видны сбросы и RX-gate.
В локальном build/vibechat.cfg включены vad=trace, aec/radio/call=debug, general=info; после диагностики вернуть обычные уровни.
Стенд --probe повторяет одинаковый PCM без reset: AGC 15/30dB после паузы обрезает пики (468/5625 samples на чистой записи).
Полного провала всей фразы почти до нуля стенд не воспроизвёл; нужен повтор живой речи с новыми логами для связи с AEC/AGC.
Алгоритмы обработки сигнала пока не менялись; исправление причины выбирается по результатам повтора.
[ ] **Linux headless CMake: актуализировать зависимости общего ядра** — 07.10.2026:
чистая сборка tools/chatgui-android/build_headless останавливается на lib/speex_aec.c:
speex/speex_echo.h не найден. utun_setup_sources включает этот исходник, но UTUN_INCLUDE_DIRS не содержит
lib/speexdsp/include. Проверить полный состав Speex/voice зависимостей отдельного каркаса;
основная Autotools C-сборка и Android NDK-сборка проходят.
[ ] **Завершить проверку реакций и голосов на SM** — 07.10.2026:
Исправления подписей корней и их сохранения реализованы, проверены тестами; local и копия SM проверены.
Последующие APK уже устанавливались на SM, поэтому прежнее ожидание подключения ADB неактуально.
Осталась явная проверка реакции/голоса на устройстве с текущей БД и сборкой.
Разовый скрипт и резервная копия подписей: /home/user/utun2-backups/root-certificates-20261007.
[ ] **rlottie: разбор предупреждений GCC 15 в импортированных исходниках** — 05.10.2026:
при сборке `tools/chatgui/rlottie_src` GCC сообщает `-Wuninitialized` в
`VArenaAlloc::reset()`: fFirstBlock/fFirstSize/fFirstHeapAllocationSize читаются
после явного вызова деструктора. Проверить сохранение значений до разрушения
и повторный reset с реальными выделениями. Также проверить `-Wfree-nonheap-object`
в `vcow_ptr`: статическая модель default_s сохраняет базовую ссылку.
Лог: `/tmp/utun-rlottie-vendor-build.log`. Сборка библиотеки и подключение её
экспортированного CMake-пакета к ChatGUI на Linux прошли; Windows ещё не проверен.
[ ] **ChatGUI Windows: подтверждение обновления runtime компилятора** — 05.10.2026:
`cc-probe.log`: cc1 запускается из корня, но в `tools/chatgui/build` завершается
с `0xC0000139`; локальные DLL GCC/math/runtime отличаются от UCRT64.
Перед обеими точками сборки добавлено обновление существующих копий этих DLL;
раскладка FFmpeg использует для них UCRT64. Проверены обновление, повторный вызов,
сохранение кодеков и ошибки. Требуется Windows-прогон `check_cc.bat`: cc1 после
обновления, повтор команды Ninja и сборка Opus должны завершиться с кодом 0.
[ ] **ChatGUI Windows: обновление остальных DLL приложения после обновления MSYS2** — 05–06.10.2026:
Пропуск раскладки при наличии основного комплекта уже убран: зависимости проверяются на каждой сборке,
Qt DLL/плагины и собственный комплект FFmpeg копируются заново; runtime GCC обновляется отдельно.
Осталось определить обновление прочих уже существующих транзитивных DLL: рекурсивный проход
по-прежнему копирует только отсутствующие файлы. Сохранить приоритет собственной сборки FFmpeg
и проверить запуск приложения после обновления MSYS2.
[ ] **Android: время жизни config provider** — 03.10.2026: `utun_bridge_init()`
передаёт адрес локального `utun_config_provider_t` в `utun_config_set_provider()`,
который хранит указатель. После возврата из init он недействителен; перед
использованием `cfg_get_*` обеспечить постоянное хранение provider.
[ ] **Общий лимит check.sh после clean** — 01.10.2026: расчёт по времени предыдущего
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
до `test_uasync_socket_race`. Повторный запуск на готовой сборке с большим лимитом
прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов
либо учитывать clean при расчёте.
[ ] **Повторно проверить паузы desktop-аудио при звонке с SM** — 01.10.2026:
В звонках 939a3ea36f3eeac3/b2a423ed2d36ed83 частота callback падала до 4–40/с,
входящий PCM продолжал приходить, очередь росла до нескольких секунд без dropped.
Диагностика backend, интервалов callback и capture/playback уже добавлена; позднее менялись аудиовывод и его буферы.
Нужен повтор на текущей сборке с CALL_STATS и временными метриками, чтобы проверить сохранение исходного сбоя.
Сетевую задержку оценивать отдельно: в одном из прежних звонков DIRECT RTT был 2–3мс.
[ ] **Измерить оставшуюся задержку запуска аудио SM после исправлений** — 01.10.2026:
В исходном звонке 2b9ad4e21a54d8ba первый capture приходил через 1140мс от startAudio;
router.start=781мс, startRecording → первый кадр=328мс, AudioTrack.play=327мс.
Независимый запуск capture/playback, отмена частичного старта и подробные замеры уже реализованы;
позднее добавлен выбор AAudio/AudioTrack. Осталось измерить текущую сборку на SM:
callbackMs/btProfileMs/applyMs/refreshMs, первый AudioRecord.read и первая запись playback,
начальное накопление очереди. Старые длительности не считать характеристиками текущей реализации.
[ ] **Гонка invite-файла в test_chat_join_e2e** — `wait_file()` проверяет только
существование файла, а `wf()` создаёт его до записи. Читатель иногда получает
пустую ссылку и ошибку префикса `utun://`. Публиковать готовый файл атомарно.
[+] **Linux ONNX Runtime: повторная регистрация схем при запуске теста** — проверено 21.10.2026:
Причина в системной сборке `1.23.2+dfsg-6ubuntu1`: Runtime вручную регистрирует схемы,
затем динамическая `libonnx.so.1` повторяет регистрацию в `OpSchemaRegistry::map()`.
Подтверждено GDB (`/tmp/silero-schema-gdb.log`) и исходниками обеих библиотек.
Текущая Linux x64 CMake-сборка переключена на [официальный Runtime 1.23.2](https://github.com/microsoft/onnxruntime/releases/tag/v1.23.2).
Архив `onnxruntime-linux-x64-1.23.2.tgz` распакован в `.cache/onnxruntime/`;
SHA-256: `1fa4dcaef22f6f7d5cd81b28c2800414350c10116f5fdd46a2160082551c5f9b`.
Для существующего include `onnxruntime/onnxruntime_c_api.h` создана ссылка
`.cache/onnxruntime/include/onnxruntime` → `../onnxruntime-linux-x64-1.23.2/include`.
В CMake явно заданы абсолютные пути: `ONNXRUNTIME_INCLUDE_DIR=<repo>/.cache/onnxruntime/include`,
`ONNXRUNTIME_LIB=<repo>/.cache/onnxruntime/onnxruntime-linux-x64-1.23.2/lib/libonnxruntime.so`.
Выбор сохраняется в `tools/chatgui/build/CMakeCache.txt`; после удаления cache его нужно повторить.
`ldd` подтверждает официальный Runtime без отдельной `libonnx`; весь `test_silero_vad` проходит без `Schema error`.
Лог проверки: `/tmp/silero-runtime-test.log`, полный вывод — `tools/chatgui/build/Testing/Temporary/LastTest.log`.
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:`, документация
описывает `=`; преобразование битовой маски осталось после перехода enum на индексы.
[Разбор](etcp_full_suite_findings.md).
[ ] **Повторно проверить crypto-шум при автоподключении в сценарии A–C–B**:
В старом test_dm_e2e попытка прямого A↔B через topo_group_connect давала packet undecryptable примерно раз в секунду.
Наблюдение относилось к прежним настройкам приёма и архитектуре подключений; устранение не подтверждено.
Воспроизвести на текущих NCD/ETCP и установить точную причину по логам до изменения политики подключения.
Отсутствие [client] само по себе не считать основанием запрета: CHAT использует собственных NCD-владельцев.
[ ] **lwip/proxy: утечка pcb в FIN_WAIT_2** — `tcp_shutdown(pcb,0,1)` (half-close по FIN от
exit) не ставит `TF_RXCLOSED`, поэтому таймаут FIN_WAIT_2 в `tcp_slowtmr` не срабатывает:
если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash).
Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).
## Открытые флаки
[ ] **test_etcp_transport: таймаут восстановления REALITY-сессии** — 18.10.2026:
в общем `./check.sh` сценарий `reality=1 simultaneous=0 restart=0` завершился
`Session recovery timed out: phase=1 stream=15 control=6`. Перед таймаутом —
`record socket read failed: err=104`, клиент ждал server-handshake.
Отдельный повтор того же бинарника прошёл. Причина нестабильности пока не установлена;
изменения Android JNI/UI транспортные исходники не затрагивали.
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present Silero Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39 -2
View File
@@ -15,6 +15,8 @@ libuasync_a_SOURCES = \
socket_compat.h \
platform_compat.c \
platform_compat.h \
file_io.c \
file_io.h \
getmyip.c \
getmyip.h \
swm_min.c \
@@ -25,6 +27,10 @@ libuasync_a_SOURCES = \
radix.h \
serialize.c \
serialize.h \
video_plane.c \
video_plane.h \
reed_solomon.c \
reed_solomon.h \
json_flat.c \
json_flat.h \
tcp_io.c \
@@ -40,17 +46,48 @@ libuasync_a_SOURCES = \
dns.c \
dns.h \
async_dns.c \
async_dns.h
async_dns.h \
speex_aec.c \
speex_aec.h \
speexdsp/mdf.c \
speexdsp/fftwrap.c \
speexdsp/kiss_fft.c \
speexdsp/kiss_fftr.c
if HAVE_SILERO_VAD
libuasync_a_SOURCES += \
silero_vad.c \
silero_vad.h
endif
libuasync_a_CFLAGS = \
-D_ISOC99_SOURCE \
-DDEBUG_OUTPUT_STDERR \
-DSQLITE_THREADSAFE=1 \
-DDNS_RANDOM=RAND_bytes \
-DFLOATING_POINT \
-DUSE_KISS_FFT \
-DEXPORT= \
-g \
-I$(top_srcdir)/src \
-I$(top_srcdir)/lib \
-I$(top_srcdir)/lib/libopus/include
-I$(top_srcdir)/lib/libopus/include \
-I$(top_srcdir)/lib/speexdsp \
-I$(top_srcdir)/lib/speexdsp/include
if HAVE_SILERO_VAD
libuasync_a_CFLAGS += -DHAVE_SILERO_VAD
endif
# Вшитая модель Silero VAD (включается из silero_vad.c)
EXTRA_DIST = silero_vad_model.inc \
COPYING.silero-vad \
wintun-LICENSE.txt \
wintun-api-LICENSE.txt \
speexdsp/COPYING \
liblmdb/LICENSE \
liblmdb/COPYRIGHT \
soundtouch-master/COPYING.TXT
# Clean build directory
clean-local:
+1 -1
View File
@@ -251,7 +251,7 @@ static void adns_rearm(struct adns_query* q) {
}
q->cur_fd = -1;
if (fd >= 0) {
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, q);
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, "dns_query", q);
if (q->socket_id) q->cur_fd = fd;
else DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_add_socket_t(%d) failed", fd);
}
+7 -3
View File
@@ -34,23 +34,27 @@ struct adns_result {
struct sockaddr_in addrs[ADNS_MAX_ADDRS]; /* AF_INET, sin_port = 0 */
};
/* res заимствован до возврата callback; handle запроса к этому моменту уже освобождён. */
typedef void (*adns_done_cb)(const struct adns_result* res, void* arg);
struct adns_opts {
struct sockaddr_in server; /* sin_family != 0 → использовать этот сервер (иначе системные) */
int timeout_ms; /* таймаут на сервер, по умолчанию 2000 */
int timeout_ms; /* Таймаут на сервер в мс; <=0 → 2000, округляется вверх до целых секунд. */
int max_attempts; /* попыток на сервер, по умолчанию 2 */
};
struct adns_query; /* opaque */
/* Запускает асинхронный A-запрос. Коллбэк вызывается в потоке event loop.
Возвращает handle (NULL при немедленной ошибке: невалидные аргументы/нет памяти). */
* Возвращает handle, автоматически освобождаемый до callback либо при отмене.
* NULL: неверные аргументы, нехватка памяти, отсутствие DNS-серверов, ошибка создания
* резолвера, постановки запроса или регистрации отложенного запуска; callback не вызывается. */
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
const struct adns_opts* opts,
adns_done_cb cb, void* arg);
/* Отмена ожидающего запроса: коллбэк НЕ вызывается. Только из потока loop. */
/* Отмена ожидающего запроса: освобождает handle, коллбэк НЕ вызывается. Только из потока loop.
* После завершения/отмены handle недействителен; NULL допустим. */
void adns_cancel(struct adns_query* q);
/* Системные DNS-серверы (IPv4). Возвращает количество; out[].sin_port = htons(53).
+183 -222
View File
@@ -3,223 +3,144 @@
#include "mem.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
#define BLOCK_LEVELS_CHUNK 64
#define PENDING_CHUNK 32
#include <stdint.h>
/* История и задержанные блоки ограничены окном настройки. Только запись накапливает output. */
struct audio_compressor {
audio_compressor_config_t cfg;
int enabled;
int block_samples;
int lookback_blocks;
int lookahead_blocks;
float rise_factor_per_block;
float max_gain;
float gain_smoothed;
float* block_levels;
size_t block_levels_count;
size_t block_levels_cap;
struct pending_block {
int16_t* samples;
size_t count;
size_t level_index;
}* pending;
size_t pending_count;
size_t pending_cap;
int16_t* accumulator;
size_t accum_count;
int enabled, configured, failed, block_samples, lookback_blocks, lookahead_blocks;
float rise_factor_per_block, release_factor_per_block, max_gain, gain_smoothed;
float* block_levels;
size_t levels_cap, blocks_seen;
int16_t* pending;
size_t pending_cap, pending_head, pending_count, accum_count;
int16_t* output;
size_t output_size;
size_t output_cap;
int dbg_counter;
size_t output_size, output_cap;
uint32_t clip_count;
unsigned dbg_counter;
};
/* Начальное усиление в середине диапазона в дБ. */
static float initial_gain(float max_gain) { return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f; }
struct audio_compressor* audio_compressor_create(void) {
struct audio_compressor* ac = u_calloc(1, sizeof(*ac));
if (!ac) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: OOM");
return NULL;
}
if (!ac) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: state allocation failed"); return NULL; }
ac->enabled = 1;
ac->gain_smoothed = 1.0f;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: ok");
return ac;
}
void audio_compressor_destroy(struct audio_compressor* ac) {
if (!ac) return;
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
u_free(ac->pending);
u_free(ac->block_levels);
u_free(ac->accumulator);
u_free(ac->output);
u_free(ac);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor_destroy");
}
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg) {
if (!ac || !cfg) return;
ac->cfg = *cfg;
if (ac->cfg.sample_rate <= 0) ac->cfg.sample_rate = 48000;
if (ac->cfg.channels <= 0) ac->cfg.channels = 1;
if (ac->cfg.block_duration_ms <= 0) ac->cfg.block_duration_ms = 20;
if (ac->cfg.lookback_ms <= 0) ac->cfg.lookback_ms = 200;
if (ac->cfg.lookahead_ms <= 0) ac->cfg.lookahead_ms = 100;
if (ac->cfg.target_level <= 0.0f) ac->cfg.target_level = 0.25f;
ac->block_samples = ac->cfg.sample_rate * ac->cfg.block_duration_ms / 1000 * ac->cfg.channels;
ac->lookback_blocks = ac->cfg.lookback_ms / ac->cfg.block_duration_ms;
ac->lookahead_blocks = ac->cfg.lookahead_ms / ac->cfg.block_duration_ms;
ac->rise_factor_per_block = powf(ac->cfg.rise_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/sec target=%.0fdBFS",
ac->cfg.sample_rate, ac->cfg.channels, ac->cfg.block_duration_ms,
ac->cfg.lookback_ms, ac->cfg.lookahead_ms, ac->cfg.max_gain_db,
ac->cfg.rise_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
u_free(ac->pending); u_free(ac->block_levels); u_free(ac->output); u_free(ac);
}
void audio_compressor_reset(struct audio_compressor* ac) {
if (!ac) return;
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
ac->pending_count = 0;
ac->block_levels_count = 0;
ac->accum_count = 0;
ac->output_size = 0;
ac->gain_smoothed = 1.0f;
ac->dbg_counter = 0;
ac->pending_head = ac->pending_count = ac->accum_count = ac->blocks_seen = ac->output_size = 0;
ac->gain_smoothed = initial_gain(ac->max_gain);
ac->clip_count = ac->dbg_counter = 0;
ac->failed = 0;
}
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) {
if (!ac) return;
ac->enabled = enabled ? 1 : 0;
/* Ноль lookahead означает отсутствие задержки; defaults остальных полей задаются явно здесь. */
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* config) {
if (!ac || !config) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: missing configuration"); return -1; }
audio_compressor_config_t cfg = *config;
if (!cfg.sample_rate) cfg.sample_rate = 48000;
if (!cfg.channels) cfg.channels = 1;
if (!cfg.block_duration_ms) cfg.block_duration_ms = 20;
if (!cfg.lookback_ms) cfg.lookback_ms = 200;
if (!cfg.target_level) cfg.target_level = 0.25f;
if (!cfg.rise_rate_per_sec) cfg.rise_rate_per_sec = 4.0f;
if (!cfg.release_rate_per_sec) cfg.release_rate_per_sec = cfg.rise_rate_per_sec * 5.0f;
if (cfg.sample_rate < 8000 || cfg.sample_rate > 192000 || cfg.channels < 1 || cfg.channels > 2 ||
cfg.block_duration_ms < 1 || cfg.block_duration_ms > 1000 || cfg.lookback_ms < 0 || cfg.lookback_ms > 5000 ||
cfg.lookahead_ms < 0 || cfg.lookahead_ms > 5000 || !isfinite(cfg.target_level) || cfg.target_level <= 0 ||
!isfinite(cfg.max_gain_db) || cfg.max_gain_db < 0 || cfg.max_gain_db > 60 ||
!isfinite(cfg.rise_rate_per_sec) || cfg.rise_rate_per_sec <= 0 ||
!isfinite(cfg.release_rate_per_sec) || cfg.release_rate_per_sec <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid rate=%d ch=%d block=%d back=%d ahead=%d",
cfg.sample_rate, cfg.channels, cfg.block_duration_ms, cfg.lookback_ms, cfg.lookahead_ms);
return -1;
}
int block = cfg.sample_rate * cfg.block_duration_ms / 1000 * cfg.channels;
int back = cfg.lookback_ms / cfg.block_duration_ms, ahead = cfg.lookahead_ms / cfg.block_duration_ms;
size_t levels_cap = (size_t)back + ahead + 2, pending_cap = (size_t)ahead + 2;
float* levels = u_calloc(levels_cap, sizeof(float));
int16_t* pending = u_calloc(pending_cap * (size_t)block, sizeof(int16_t));
if (!levels || !pending) {
u_free(levels); u_free(pending);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: buffers allocation failed");
return -1;
}
u_free(ac->block_levels); u_free(ac->pending);
ac->cfg = cfg; ac->block_samples = block;
ac->lookback_blocks = back; ac->lookahead_blocks = ahead;
ac->block_levels = levels; ac->levels_cap = levels_cap;
ac->pending = pending; ac->pending_cap = pending_cap;
ac->rise_factor_per_block = powf(cfg.rise_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
ac->release_factor_per_block = powf(cfg.release_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
ac->max_gain = powf(10.0f, cfg.max_gain_db / 20.0f);
ac->configured = 1;
audio_compressor_reset(ac);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "compressor: configured rate=%d ch=%d ahead=%dms history=%zu pending=%zu",
cfg.sample_rate, cfg.channels, cfg.lookahead_ms, levels_cap, pending_cap);
return 0;
}
int audio_compressor_is_enabled(const struct audio_compressor* ac) {
return ac ? ac->enabled : 0;
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source) {
if (!source || !source->configured) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: cannot clone configuration"); return NULL; }
struct audio_compressor* copy = audio_compressor_create();
if (copy && audio_compressor_configure(copy, &source->cfg) != 0) { audio_compressor_destroy(copy); return NULL; }
if (copy) copy->enabled = source->enabled;
return copy;
}
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { if (ac) ac->enabled = !!enabled; }
int audio_compressor_is_enabled(const struct audio_compressor* ac) { return ac && ac->enabled; }
/* Одна оценка для всех каналов сохраняет баланс стерео. */
static float compute_block_level(const int16_t* samples, size_t count) {
double sum_sq = 0.0;
float peak = 0.0f;
for (size_t i = 0; i < count; i++) {
float v = (float)samples[i] / 32768.0f;
sum_sq += (double)v * v;
float av = fabsf(v);
if (av > peak) peak = av;
if (fabsf(v) > peak) peak = fabsf(v);
}
float rms = (float)sqrt(sum_sq / (double)count);
return (rms * 2.0f + peak) / 2.0f;
return (2.0f * sqrtf((float)(sum_sq / count)) + peak) / 2.0f;
}
static void ensure_output_cap(struct audio_compressor* ac, size_t needed) {
size_t want = ac->output_size + needed;
if (want <= ac->output_cap) return;
size_t new_cap = ac->output_cap ? ac->output_cap * 2 : 4096;
while (new_cap < want) new_cap *= 2;
int16_t* tmp = u_realloc(ac->output, new_cap * sizeof(int16_t));
if (!tmp) return;
ac->output = tmp;
ac->output_cap = new_cap;
}
static void append_output(struct audio_compressor* ac, const int16_t* samples, size_t count) {
ensure_output_cap(ac, count);
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
}
static void ensure_block_levels_cap(struct audio_compressor* ac) {
if (ac->block_levels_count < ac->block_levels_cap) return;
size_t new_cap = ac->block_levels_cap ? ac->block_levels_cap * 2 : BLOCK_LEVELS_CHUNK;
float* tmp = u_realloc(ac->block_levels, new_cap * sizeof(float));
if (!tmp) return;
ac->block_levels = tmp;
ac->block_levels_cap = new_cap;
}
static void add_block_level(struct audio_compressor* ac, float level) {
ensure_block_levels_cap(ac);
ac->block_levels[ac->block_levels_count++] = level;
}
static void ensure_pending_cap(struct audio_compressor* ac) {
if (ac->pending_count < ac->pending_cap) return;
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(*tmp));
if (!tmp) return;
ac->pending = tmp;
ac->pending_cap = new_cap;
}
static void process_pending_block(struct audio_compressor* ac);
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
if (!ac || !samples || count == 0) return;
if (!ac->enabled) {
append_output(ac, samples, count);
return;
/* Выход записи растёт только вне аппаратного callback; OOM переводит запись в failed. */
static int reserve_output(struct audio_compressor* ac, size_t count) {
if (count > SIZE_MAX / sizeof(int16_t) - ac->output_size) goto failed;
size_t want = ac->output_size + count;
if (want <= ac->output_cap) return 0;
size_t capacity = ac->output_cap ? ac->output_cap : 4096;
while (capacity < want) {
if (capacity > SIZE_MAX / sizeof(int16_t) / 2) { capacity = want; break; }
capacity *= 2;
}
size_t remaining = count;
const int16_t* src = samples;
while (remaining > 0) {
size_t need = (size_t)ac->block_samples - ac->accum_count;
size_t take = remaining < need ? remaining : need;
size_t new_cnt = ac->accum_count + take;
int16_t* tmp = u_realloc(ac->accumulator, new_cnt * sizeof(int16_t));
if (!tmp) return;
ac->accumulator = tmp;
memcpy(ac->accumulator + ac->accum_count, src, take * sizeof(int16_t));
ac->accum_count = new_cnt;
src += take;
remaining -= take;
if (ac->accum_count == (size_t)ac->block_samples) {
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
}
}
while (ac->pending_count > (size_t)ac->lookahead_blocks)
process_pending_block(ac);
int16_t* samples = u_realloc(ac->output, capacity * sizeof(int16_t));
if (!samples) goto failed;
ac->output = samples; ac->output_cap = capacity;
return 0;
failed:
ac->failed = 1;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: recording output allocation failed samples=%zu", count);
return -1;
}
static void process_pending_block(struct audio_compressor* ac) {
if (ac->pending_count == 0) return;
size_t idx = ac->pending[0].level_index;
size_t win_start = idx >= (size_t)ac->lookback_blocks ? idx - (size_t)ac->lookback_blocks : 0;
size_t win_end = idx + (size_t)ac->lookahead_blocks;
if (win_end >= ac->block_levels_count) win_end = ac->block_levels_count - 1;
float envelope = 0.0f;
for (size_t i = win_start; i <= win_end; i++) {
if (ac->block_levels[i] > envelope) envelope = ac->block_levels[i];
/* Общий алгоритм усиления для записи с lookahead и потокового кадра без lookahead. */
static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t count, size_t idx) {
size_t begin = idx >= (size_t)ac->lookback_blocks ? idx - ac->lookback_blocks : 0;
size_t end = idx + ac->lookahead_blocks;
if (end >= ac->blocks_seen) end = ac->blocks_seen - 1;
float envelope = 0;
for (size_t i = begin; i <= end; i++) {
float level = ac->block_levels[i % ac->levels_cap];
if (level > envelope) envelope = level;
}
if (envelope < 0.0001f) envelope = 0.0001f;
@@ -227,66 +148,106 @@ static void process_pending_block(struct audio_compressor* ac) {
float G_prev = ac->gain_smoothed;
if (G_raw > ac->gain_smoothed) {
/* сигнал стал тише — плавно поднимаем gain (атака) */
float max_rise = ac->gain_smoothed * ac->rise_factor_per_block;
ac->gain_smoothed = G_raw < max_rise ? G_raw : max_rise;
} else {
ac->gain_smoothed = G_raw;
/* сигнал стал громче — плавно опускаем gain (release), без мгновенной ступеньки
* (мгновенный дроп давал клик на границе 20мс-блока). */
float min_fall = ac->gain_smoothed / ac->release_factor_per_block;
ac->gain_smoothed = G_raw > min_fall ? G_raw : min_fall;
}
if (ac->gain_smoothed > ac->max_gain) ac->gain_smoothed = ac->max_gain;
float gain_db = 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f);
float prev_db = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f);
if (ac->dbg_counter % 25 == 0 || fabsf(gain_db - prev_db) > 3.0f) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB",
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB clip=%u",
idx, ac->block_levels[idx % ac->levels_cap], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count);
}
ac->dbg_counter++;
for (size_t i = 0; i < ac->pending[0].count; i++) {
float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed;
if (v > 32767.0f) v = 32767.0f;
if (v < -32768.0f) v = -32768.0f;
ac->pending[0].samples[i] = (int16_t)(int)v;
for (size_t i = 0; i < count; i++) {
float v = (float)samples[i] * ac->gain_smoothed;
if (v > 32767.0f) { v = 32767.0f; ac->clip_count++; }
if (v < -32768.0f) { v = -32768.0f; ac->clip_count++; }
samples[i] = (int16_t)(int)v;
}
append_output(ac, ac->pending[0].samples, ac->pending[0].count);
u_free(ac->pending[0].samples);
/* shift remaining pending blocks down */
if (ac->pending_count > 1)
memmove(ac->pending, ac->pending + 1, (ac->pending_count - 1) * sizeof(*ac->pending));
ac->pending_count--;
}
void audio_compressor_flush(struct audio_compressor* ac) {
if (!ac) return;
if (!ac->enabled) return;
/* Слить один готовый блок записи. История хранится по абсолютному номеру в кольце. */
static int drain_block(struct audio_compressor* ac, size_t count) {
if (reserve_output(ac, count) != 0) return -1;
int16_t* samples = ac->pending + ac->pending_head * ac->block_samples;
apply_gain(ac, samples, count, ac->blocks_seen - ac->pending_count);
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
ac->pending_head = (ac->pending_head + 1) % ac->pending_cap;
--ac->pending_count;
return 0;
}
if (ac->accum_count > 0) {
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
/* Ровно один кадр, без output-накопителя и выделений памяти. in/out могут совпадать. */
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count) {
if (!ac || !ac->configured || ac->failed || !in || !out || count != (size_t)ac->block_samples ||
ac->lookahead_blocks || ac->pending_count || ac->accum_count) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid streaming frame count=%zu", count);
return -1;
}
if (ac->pending_count > 0)
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %zu pending blocks (gainSmoothed=%.1fdB)",
ac->pending_count, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f));
while (ac->pending_count > 0)
process_pending_block(ac);
memmove(out, in, count * sizeof(int16_t));
if (ac->enabled) {
size_t idx = ac->blocks_seen++;
ac->block_levels[idx % ac->levels_cap] = compute_block_level(in, count);
apply_gain(ac, out, count, idx);
}
return 0;
}
const int16_t* audio_compressor_output(const struct audio_compressor* ac) {
return ac ? ac->output : NULL;
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
if (!ac || !ac->configured || ac->failed || (!samples && count)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid recording input"); return -1;
}
if (!ac->enabled) {
if (reserve_output(ac, count) != 0) return -1;
if (count) memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
return 0;
}
while (count) {
size_t take = (size_t)ac->block_samples - ac->accum_count;
if (take > count) take = count;
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
memcpy(block + ac->accum_count, samples, take * sizeof(int16_t));
samples += take; count -= take; ac->accum_count += take;
if (ac->accum_count == (size_t)ac->block_samples) {
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, ac->accum_count);
ac->accum_count = 0; ++ac->pending_count;
if (ac->pending_count > (size_t)ac->lookahead_blocks && drain_block(ac, ac->block_samples) != 0) return -1;
}
}
return 0;
}
size_t audio_compressor_output_size(const struct audio_compressor* ac) {
return ac ? ac->output_size : 0;
int audio_compressor_flush(struct audio_compressor* ac) {
if (!ac || !ac->configured || ac->failed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid flush"); return -1; }
if (!ac->enabled) return 0;
size_t tail = ac->accum_count;
if (tail) {
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, tail);
ac->accum_count = 0; ++ac->pending_count;
}
while (ac->pending_count) {
size_t count = tail && ac->pending_count == 1 ? tail : (size_t)ac->block_samples;
if (drain_block(ac, count) != 0) return -1;
}
return 0;
}
const int16_t* audio_compressor_output(const struct audio_compressor* ac) { return ac ? ac->output : NULL; }
size_t audio_compressor_output_size(const struct audio_compressor* ac) { return ac ? ac->output_size : 0; }
void audio_compressor_clear_output(struct audio_compressor* ac) { if (ac) ac->output_size = 0; }
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) { return ac ? ac->clip_count : 0; }
float audio_compressor_gain_db(const struct audio_compressor* ac) {
return ac && ac->configured && ac->enabled ? 20.0f * log10f(ac->gain_smoothed) : 0.0f;
}
+26 -4
View File
@@ -1,3 +1,9 @@
/* audio_compressor — адаптивное усиление int16 PCM (моно/стерео) с общей огибающей каналов.
* create → configure → обработка → destroy; объект использует один владелец, блокировок нет.
* Запись: push принимает произвольные порции, flush завершает хвост и lookahead; output принадлежит объекту.
* Поток: process_frame обрабатывает ровно sample_rate*block_duration_ms/1000*channels отсчётов,
* требует нулевого lookahead и пустого входного накопителя; in/out могут совпадать.
* count/output_size — interleaved отсчёты, не байты и не кадры на канал. Выключение даёт исходный PCM. */
#ifndef AUDIO_COMPRESSOR_H
#define AUDIO_COMPRESSOR_H
@@ -18,24 +24,40 @@ typedef struct {
int lookahead_ms;
float max_gain_db;
float rise_rate_per_sec;
float target_level;
float release_rate_per_sec; /* скорость плавного снижения gain (0 = авто: rise*5) */
float target_level; /* Линейная амплитуда огибающей, не дБ; 0 задаёт default 0.25. */
} audio_compressor_config_t;
struct audio_compressor* audio_compressor_create(void);
/* Новый пустой compressor с теми же настройками; позволяет передать запись worker без копии PCM. */
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source);
void audio_compressor_destroy(struct audio_compressor* ac);
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
/* Потоковый кадр без lookahead: результат немедленный, память не выделяется; 0 / -1. */
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count);
void audio_compressor_reset(struct audio_compressor* ac);
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled);
int audio_compressor_is_enabled(const struct audio_compressor* ac);
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
void audio_compressor_flush(struct audio_compressor* ac);
/* push копирует вход; push/flush возвращают 0 / -1. Ошибка накопления требует reset перед повтором. */
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
int audio_compressor_flush(struct audio_compressor* ac);
/* Указатель действителен до изменения/освобождения выходного буфера; вызывающий его не освобождает. */
const int16_t* audio_compressor_output(const struct audio_compressor* ac);
size_t audio_compressor_output_size(const struct audio_compressor* ac);
/* Сбросить накопленный выход (output_size=0) после слива — для потокового режима.
* НЕ трогает gain_smoothed (адаптивная громкость сохраняется). */
void audio_compressor_clear_output(struct audio_compressor* ac);
/* Число сэмплов, обрезанных по потолку int16 (клиппинг) с момента create/reset. */
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac);
/* Текущее фактическое усиление в дБ; выключенный compressor возвращает 0. */
float audio_compressor_gain_db(const struct audio_compressor* ac);
#ifdef __cplusplus
}
#endif
+42 -28
View File
@@ -132,6 +132,12 @@ static const struct {
{"video", DEBUG_CATEGORY_VIDEO},
{"reality", DEBUG_CATEGORY_REALITY},
{"dm", DEBUG_CATEGORY_DM},
{"call", DEBUG_CATEGORY_CALL},
{"radio", DEBUG_CATEGORY_RADIO},
{"vad", DEBUG_CATEGORY_VAD},
{"aec", DEBUG_CATEGORY_AEC},
{"stream", DEBUG_CATEGORY_STREAM},
{"discovery", DEBUG_CATEGORY_DISCOVERY},
{"all", DEBUG_CATEGORY_ALL},
{NULL, DEBUG_CATEGORY_NONE}
};
@@ -266,6 +272,10 @@ void debug_set_masks(debug_category_t category_idx, debug_level_t level) {
debug_set_category_level(category_idx, level);
}
void debug_enable_timestamp(int enable) {
g_debug_config.timestamp_enabled = !!enable;
}
void debug_enable_function_name(int enable) {
g_debug_config.function_name_enabled = enable;
}
@@ -317,15 +327,24 @@ void debug_disable_file_output(void) {
}
}
/* Reopen log file (append mode) for SIGHUP. Complete operation with full error logging. */
/* Переоткрыть лог в append-режиме; при ошибке открытия сохранить прежний поток. */
int debug_reopen_log(void) {
if (!g_debug_config.output_file) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "No log path for reopen");
return -1;
}
const char* path = g_debug_config.output_file;
debug_disable_file_output();
debug_enable_file_output(path, 0); // append
FILE* new_file = fopen(path, "a");
if (!new_file) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot reopen log '%s': %s", path, strerror(errno));
return -1;
}
FILE* old_file = g_debug_config.file_output;
g_debug_config.file_output = new_file;
if (old_file && fclose(old_file) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot close previous log '%s': %s", path, strerror(errno));
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Log reopened: %s", path);
return 0;
}
@@ -385,13 +404,15 @@ void debug_output(debug_level_t level, debug_category_t category_idx,
va_start(args, format);
/* Add timestamp with microseconds: hh:mm:ss-xxx.yyy */
struct timeval tv;
utun_gettimeofday(&tv, NULL);
time_t tv_sec = (time_t)tv.tv_sec;
struct tm* tm_info = localtime(&tv_sec);
char time_str[32];
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
if (g_debug_config.timestamp_enabled) {
struct timeval tv;
utun_gettimeofday(&tv, NULL);
time_t tv_sec = (time_t)tv.tv_sec;
struct tm* tm_info = localtime(&tv_sec);
char time_str[32];
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
}
/* Add thread marker if set */
if (g_debug_config.thread_marker > 0) {
@@ -454,7 +475,7 @@ void debug_set_log_hook(debug_log_hook_fn hook) {
g_log_hook = hook;
}
/* Parse debug configuration from string (e.g., "ll_queue:debug,uasync:info,memory:warn,etcp:error") */
/* Настройка отдельных категорий: "sys=debug,etcp=error"; разделитель ':' также допустим. */
int debug_parse_config(const char* config_string) {
if (!config_string || !*config_string) {
return 0;
@@ -462,12 +483,10 @@ int debug_parse_config(const char* config_string) {
char* str = u_strdup(config_string);
if (!str) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to allocate debug configuration copy");
return -1;
}
debug_category_t categories = 0;
debug_level_t level = DEBUG_LEVEL_NONE;
char* token = strtok(str, ",");
while (token) {
/* Trim leading spaces */
@@ -475,15 +494,16 @@ int debug_parse_config(const char* config_string) {
token++;
}
char* colon = strchr(token, ':');
if (!colon) {
char* separator = strpbrk(token, "=:");
if (!separator) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Missing '=' or ':' in debug setting '%s'", token);
u_free(str);
return -1;
}
*colon = '\0';
*separator = '\0';
char* category_name = token;
char* level_name = colon + 1;
char* level_name = separator + 1;
/* Trim leading spaces in level_name */
while (isspace(*level_name)) {
@@ -491,22 +511,16 @@ int debug_parse_config(const char* config_string) {
}
debug_category_t cat = get_category_by_name(category_name);
if (cat == DEBUG_CATEGORY_NONE) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown debug category '%s'", category_name);
if (cat <= DEBUG_CATEGORY_NONE || cat >= DEBUG_CATEGORY_COUNT) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown or unsupported debug category '%s' (expected an individual category)", category_name);
u_free(str);
return -1;
}
debug_level_t lev = debug_level_from_name(level_name);
/* Set per-category level */
int idx = 0;
while (!(cat & (1ULL << idx)) && idx < DEBUG_CATEGORY_COUNT) {
idx++;
}
if (idx < DEBUG_CATEGORY_COUNT) {
g_debug_config.category_levels[idx] = lev;
}
debug_set_category_level(cat, lev);
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Applied debug setting: %s=%s (category=%d, level=%d)", category_name, level_name, cat, lev);
token = strtok(NULL, ",");
}
+49 -24
View File
@@ -1,6 +1,12 @@
/**
* Runtime debug configuration system
* Provides flexible control over debug output without recompilation
* Управление диагностикой: debug_config_init → уровни категорий и настройки вывода → DEBUG_*.
* NONE наследует общий уровень, DISABLED отключает категорию. Имена настроек — category=level.
* Открытый файл имеет приоритет над консолью; hook получает сообщения независимо от выбора выхода.
* Уровни: disabled, none, error, warn, info, debug, trace; отдельный уровень категории заменяет общий, не max().
* Пример: debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_parse_config("etcp=trace,config=info").
* DEBUG_* фильтруют до форматирования; прямой debug_output фильтр не проверяет.
* Строка с префиксами ограничена буфером 4096 байт и обрезается; каждый вывод fflush.
* Модуль и смена глобальных настроек/файлов не синхронизированы; параллельный доступ согласует владелец.
*/
#ifndef DEBUG_CONFIG_H
@@ -70,19 +76,25 @@ typedef int debug_category_t;
#define DEBUG_CATEGORY_VIDEO 29 // Video module (probe/transcode)
#define DEBUG_CATEGORY_REALITY 30 // REALITY TLS camouflage
#define DEBUG_CATEGORY_DM 31 // DM — direct p2p chat between two users
#define DEBUG_CATEGORY_COUNT 32 // Total number of categories
#define DEBUG_CATEGORY_CALL 32 // Call — P2P audio call (signaling + media)
#define DEBUG_CATEGORY_RADIO 33 // Radio — PTT walkie-talkie multicast (voice)
#define DEBUG_CATEGORY_VAD 34 // VAD — Silero voice activity detector (ONNX Runtime)
#define DEBUG_CATEGORY_AEC 35 // AEC — acoustic echo cancellation (SpeexDSP)
#define DEBUG_CATEGORY_STREAM 36 // Подписанные потоки, FEC, буферы и выбор поставщиков
#define DEBUG_CATEGORY_DISCOVERY 37 // LAN UDP: discovery and delivery
#define DEBUG_CATEGORY_COUNT 38 // Total number of categories
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
/* Debug configuration structure */
typedef struct {
debug_level_t level; // Global debug level (default: ERROR)
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Per-category levels (0 = disabled)
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Уровень категории: NONE наследует общий, DISABLED выключает
int timestamp_enabled; // Include timestamps in output
int function_name_enabled; // Include function names
int file_line_enabled; // Include file:line info
const char* output_file; // NULL = stdout, otherwise file path
const char* output_file; // Заимствованный путь для reopen; строка должна жить, пока поле её хранит
// Dual output support
// Выход: файл, иначе консоль при console_enabled != 0
FILE* file_output; // File handle for log file (NULL if not open)
int console_enabled; // Enable console output
int thread_marker; // Thread identification (0=none, e.g. 2 for uasync)
@@ -91,38 +103,47 @@ typedef struct {
/* Global debug configuration */
extern debug_config_t g_debug_config;
/* Initialize debug system with default settings */
/* До использования: ERROR, категории NONE, консоль stdout, timestamp/function/file:line включены.
* Повторный init сбрасывает поля, но не закрывает уже открытый файл; перед ним закрыть файл отдельно. */
void debug_config_init(void);
/* Set debug level */
// итоговый level = max (global level - здесь задается, category level)
// Общий уровень применяется к категориям с уровнем NONE.
void debug_set_level(debug_level_t level);
/* Set debug level for specific category (0 = disabled, otherwise uses that level) */
/* Уровень категории заменяет общий; NONE наследует его, DISABLED подавляет вывод. */
void debug_set_category_level(debug_category_t category, debug_level_t level);
/* Enable/disable specific categories */
/* enable фиксирует текущий общий уровень; disable возвращает к наследованию (NONE).
* Для полного выключения использовать debug_set_category_level(cat, DEBUG_LEVEL_DISABLED). */
void debug_enable_category(debug_category_t category);
void debug_disable_category(debug_category_t category);
void debug_set_categories(debug_category_t categories);
/* Set masks directly */
/* Установка уровня одной категории (аргумент categories — индекс, не битовая маска). */
void debug_set_masks(debug_category_t categories, debug_level_t level);
/* Configure output options */
/* Configure output options */
/* Ненулевое enable включает временную метку, 0 отключает. */
void debug_enable_timestamp(int enable);
void debug_enable_function_name(int enable);
void debug_enable_file_line(int enable);
void debug_set_output_file(const char* file_path);
/* Открыть ненулевой file_path в append-режиме, только если файл ещё не открыт; путь заимствован. */
void debug_set_output_file(const char* file_path);
/* Dual output configuration */
/* Заменить файл: truncate != 0 → обрезать, иначе append. NULL ничего не меняет.
* Путь заимствован; успешное открытие также выключает console_enabled. */
void debug_enable_file_output(const char* file_path, int truncate);
/* Закрыть файл; прежнее значение console_enabled не восстанавливается. */
void debug_disable_file_output(void);
/* Переоткрыть текущий путь в append-режиме: 0 — успех, -1 — ошибка с диагностикой.
* При ошибке открытия сохраняет прежний поток; console_enabled не меняет. */
int debug_reopen_log(void);
/* Вывод в консоль разрешён только при отсутствии открытого файла. */
void debug_enable_console(int enable);
// IP address to string (static buffer, single-threaded)
// IP/sockaddr в строку: результат возвращается по значению, освобождать не требуется.
typedef struct {
char str[54]; // INET6_ADDRSTRLEN(45) + 6chars (:port) + \0
} ip_str_t;
@@ -130,7 +151,7 @@ typedef struct {
ip_str_t ip_to_str(const void *addr, int family);// big endian addr
ip_str_t sockaddr_storage_to_str(const struct sockaddr_storage *addr);// автоопределение v4 или v6
// hex dump в лог
// Hex dump: первые min(len, 128) байт; при len > 128 добавляет "...". Применяется фильтр уровня/категории.
void log_dump(int level, int category, const char* prefix, const uint8_t* data, size_t len);
/* Check if debug output should be shown for given level and category */
@@ -139,13 +160,13 @@ int debug_should_output(debug_level_t level, debug_category_t category_idx);
/* Get current debug level */
debug_level_t debug_get_level(void);
/* Get category name by index */
/* Заимствованное имя категории; для неизвестного индекса возвращает "UNKNOWN". */
const char* debug_get_category_name(debug_category_t category_idx);
/* Get category by name string */
/* Поиск без учёта регистра: индекс, ALL для "all", NONE при NULL/неизвестном имени. */
debug_category_t get_category_by_name(const char* name);
/* Get level by name string with error reporting */
/* Поиск без учёта регистра: NULL → NONE; неизвестное имя логируется и даёт INFO (fallback). */
debug_level_t debug_level_from_name(const char* name);
/* Set category level using text names (main API) */
@@ -157,16 +178,20 @@ int debug_apply_category_config(const char* category_name, const char* level_nam
/* Apply global debug level from text name */
void debug_apply_global_level(const char* level_name);
/* Format and output debug message (internal use by macros) */
/* Форматирует сообщение printf-формата с необязательными метками; фильтрацию выполняет вызывающий.
* Время календарное, локальное, hh:mm:ss-миллисекунды.микросекунды; положительный thread_marker выводится отдельно. */
void debug_output(debug_level_t level, debug_category_t category_idx,
const char* function, const char* file, int line,
const char* format, ...);
/* Log hook — if set, called with every debug message (for JNI/bridge integration) */
/* Hook вызывается синхронно для форматированного сообщения до записи в выход; NULL снимает hook.
* category/message заимствованы на время вызова, message указывает на стековый буфер; для хранения скопировать. */
typedef void (*debug_log_hook_fn)(int level, const char* category, const char* message);
void debug_set_log_hook(debug_log_hook_fn hook);
/* Parse debug configuration from string (e.g. "etcp=trace,config=info") */
/* Настройка отдельных категорий: "etcp=trace,config=info" (также ':' вместо '=').
* NULL/пустая строка ничего не меняет. Возвращает 0 при успехе, -1 при ошибке;
* настройки до ошибочного элемента остаются применёнными. */
int debug_parse_config(const char* config_string);
/* Convenience macros for debug output */
@@ -195,8 +220,8 @@ int debug_parse_config(const char* config_string);
debug_output(DEBUG_LEVEL_TRACE, category, __FUNCTION__, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
} } while(0)
/* Backward compatibility - default to ERROR level if not specified */
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_ALL, fmt, ##__VA_ARGS__)
/* Сообщение без явной категории: ERROR в категории GENERAL, с обычной фильтрацией. */
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, fmt, ##__VA_ARGS__)
#ifdef __cplusplus
}
-112
View File
@@ -1,112 +0,0 @@
# debug_config — Runtime-система отладочного логирования
## 1. Назначение
Модуль обеспечивает гибкое управление отладочным выводом без перекомпиляции. Позволяет:
- Задавать **глобальный уровень** логирования (error/warn/info/debug/trace).
- **Покатегорийно** включать/выключать или переопределять уровень (26 категорий: etcp, crypto, tun, routing, bgp, bbr, …).
- Вести **двойной вывод**: в консоль (stdout) и/или в файл.
- Настраивать формат сообщений (временные метки с микросекундами, имя функции, файл:строка).
- Парсить конфигурационную строку вида `"etcp=trace,config=info"` из конфиг-файла.
Логика вывода: итоговый уровень = **max(глобальный уровень, уровень категории)**. Сообщение печатается, если его уровень ≤ итогового.
## 2. Как пользоваться
### 2.1. Инициализация
```c
#include "debug_config.h"
debug_config_init(); // уровень по умолчанию: ERROR, консоль включена
debug_set_level(DEBUG_LEVEL_INFO); // поднять глобальный уровень до INFO
debug_enable_file_output("/tmp/utun.log", 1); // писать в файл (truncate=1)
debug_apply_category_config("etcp", "trace"); // для ETCP-категории — всё
```
### 2.2. Макросы логирования
```c
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "Decrypt failed: len=%d", len);
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "Retransmit timeout, seq=%u", seq);
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port);
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Route added: %s -> %s", dst, gw);
DEBUG_TRACE(DEBUG_CATEGORY_TUN, "Packet recv: %zu bytes", n);
```
Макросы делают проверку `debug_should_output()` **перед** вызовом `debug_output()`, поэтому дорогой `vsnprintf` не вызывается, если вывод не нужен.
### 2.3. Конфигурационная строка
Формат: `"категория=уровень,категория=уровень,..."`. Парсится вызовом `debug_parse_config()`. Пример из конфиг-файла:
```ini
debug = etcp=trace,config=info,crypto=error
```
Категории: `none, uasync, ll_queue, connection, etcp, crypto, memory, timing, config, tun, routing, timers, normalizer, bgp, socket, control, dump, traffic, debug, general, nat, keepalive, etcp_route, bbr, etcp_dump, connectivity, all`.
Уровни: `disabled, none, error, warn, info, debug, trace`.
**Важно:** старый формат `ll_queue:debug` (двоеточие) тоже работает через `debug_parse_config`, но в конфиге рекомендуется `=`.
### 2.4. Двойной вывод (консоль + файл)
- По умолчанию вывод в **stdout**.
- `debug_enable_file_output(path, truncate)` — перенаправляет вывод в файл, консоль отключается.
- `debug_disable_file_output()` — закрывает файл.
- `debug_reopen_log()` — переоткрывает лог-файл (используется по SIGHUP для ротации).
- `debug_enable_console(1/0)` — ручное управление консольным выводом.
### 2.5. Вспомогательные утилиты
```c
log_dump(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DUMP, "payload", data, len); // hex-дамп (до 128 байт)
ip_str_t s = sockaddr_storage_to_str(&addr); // "192.168.1.1:8080" или "[::1]:443"
ip_str_t s = ip_to_str(&addr, AF_INET); // "192.168.1.1" (без порта)
```
### 2.6. Нюансы
- **Потокобезопасность:** модуль не потокобезопасен. Несколько потоков, одновременно вызывающих макросы, могут перемешать вывод (но не упадут). Файловый вывод использует `fflush()` после каждой строки.
- **Буфер:** размер строки лога ограничен 4096 байт (`BUFFER_SIZE`), длинные сообщения обрезаются.
- **`debug_set_output_file`** против `debug_enable_file_output`: первая вызывается один раз (кто первый — того и тапки), вторая позволяет переоткрыть файл.
- **Флаг `-f` (foreground):** в foreground-режиме вывод идёт в stdout, в daemon-режиме — в файл.
## 3. API
| Функция/макрос | Назначение |
|---|---|
| `debug_config_init()` | Инициализация: глобальный уровень ERROR, консоль включена, метки времени/функций/файлов — да |
| `debug_set_level(level)` | Задать глобальный уровень (ERROR..TRACE) |
| `debug_set_category_level(cat, level)` | Задать уровень конкретной категории (0 = использовать глобальный) |
| `debug_set_category_level_by_name("etcp", "trace")` | То же, но именами строк |
| `debug_apply_category_config("etcp", "trace")` | Установить уровень категории с логированием результата |
| `debug_apply_global_level("info")` | Установить глобальный уровень по имени строки |
| `debug_parse_config("etcp=trace,config=info")` | Разобрать конфигурационную строку |
| `debug_should_output(level, cat)` | Проверить, нужно ли выводить сообщение данного уровня/категории |
| `debug_output(level, cat, func, file, line, fmt, ...)` | Форматировать и вывести сообщение (вызывается макросами) |
| `debug_enable_file_output(path, truncate)` | Включить вывод в файл (консоль отключается) |
| `debug_disable_file_output()` | Закрыть файл вывода |
| `debug_reopen_log()` | Переоткрыть лог-файл (для SIGHUP/ротации) |
| `debug_enable_console(1/0)` | Включить/выключить консольный вывод |
| `debug_get_level()` | Получить текущий глобальный уровень |
| `get_category_by_name("etcp")` | Получить ID категории по имени строки |
| `debug_level_from_name("trace")` | Получить уровень по имени строки |
| `debug_get_category_name(cat)` | Получить имя категории по ID |
| `log_dump(level, cat, prefix, data, len)` | Hex-дамп данных (первые 128 байт) в лог |
| `ip_to_str(addr, family)` | IP-адрес → строка (без порта). Статический буфер |
| `sockaddr_storage_to_str(addr)` | sockaddr_storage → "ip:port" (IPv6 в скобках). Статический буфер |
| `DEBUG_ERROR/WARN/INFO/DEBUG/TRACE(cat, fmt, ...)` | Макросы логирования с проверкой `debug_should_output()` |
| `DEBUG_OUTPUT(fmt, ...)` | Устаревший макрос совместимости (ERROR, категория ALL) |
**Глобальная переменная:** `g_debug_config` (тип `debug_config_t`) — хранит всё состояние системы логирования.
**Структура `debug_config_t`:**
- `level` — глобальный уровень (по умолчанию ERROR)
- `category_levels[26]` — уровни по категориям (NONE = использовать глобальный, DISABLED = никогда не выводить)
- `timestamp_enabled` / `function_name_enabled` / `file_line_enabled` — формат вывода
- `file_output` — FILE* файла (NULL если вывод в консоль)
- `console_enabled` — вывод в stdout
- `thread_marker` — числовой маркер потока (0 = нет)
+180
View File
@@ -0,0 +1,180 @@
#include "file_io.h"
#include "platform_compat.h"
#include "debug_config.h"
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>
#ifdef _WIN32
/* Общие пути API — UTF-8; ANSI Win32 API не применяется. */
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) {
if (path && *path && strlen(path) < FILE_IO_PATH_MAX
&& MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, FILE_IO_PATH_MAX)) return 0;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid UTF-8/oversized path winerr=%lu", GetLastError());
return -1;
}
#endif
int file_io_inspect(const char *path, int follow, struct file_io_info *info) {
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX || !info) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect invalid path/output"); return -1;
}
struct file_io_info result = {0};
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(path, wide)) return -1;
DWORD attributes = GetFileAttributesW(wide);
if (attributes == INVALID_FILE_ATTRIBUTES) {
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) return 1;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s winerr=%lu", path, error); return -1;
}
if (!follow && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) result.type = FILE_IO_LINK;
else {
HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
BY_HANDLE_FILE_INFORMATION data;
int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &data);
if (valid) {
result.type = GetFileType(handle) != FILE_TYPE_DISK ? FILE_IO_OTHER
: (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_IO_DIRECTORY : FILE_IO_REGULAR;
uint64_t size = ((uint64_t)data.nFileSizeHigh << 32) | data.nFileSizeLow;
valid = size <= INT64_MAX;
result.size = (int64_t)size;
result.identity[0] = data.dwVolumeSerialNumber;
result.identity[1] = ((uint64_t)data.nFileIndexHigh << 32) | data.nFileIndexLow;
}
DWORD error = GetLastError();
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
if (!valid) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file handle inspect failed path=%s winerr=%lu", path, error); return -1; }
}
#else
struct stat data;
if ((follow ? stat(path, &data) : lstat(path, &data))) {
if (errno == ENOENT) return 1;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s errno=%d", path, errno); return -1;
}
result.type = S_ISREG(data.st_mode) ? FILE_IO_REGULAR : S_ISDIR(data.st_mode) ? FILE_IO_DIRECTORY
: S_ISLNK(data.st_mode) ? FILE_IO_LINK : FILE_IO_OTHER;
if (data.st_size < 0 || (uint64_t)data.st_size > INT64_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file size out of range path=%s", path); return -1;
}
result.size = (int64_t)data.st_size;
result.identity[0] = data.st_dev; result.identity[1] = data.st_ino;
#endif
*info = result;
return 0;
}
FILE *file_io_open_read(const char *path) {
struct file_io_info info;
if (file_io_inspect(path, 1, &info) || info.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file source is not regular/readable path=%s", path ? path : "(null)"); return NULL;
}
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(path, wide)) return NULL;
FILE *file = _wfopen(wide, L"rb");
#else
FILE *file = fopen(path, "rb");
#endif
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file open read failed path=%s errno=%d", path, errno);
return file;
}
int file_io_paths(const char *source, const char *destination) {
struct file_io_info input, output;
if (file_io_inspect(source, 1, &input) || input.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid source path=%s", source ? source : "(null)"); return -1;
}
int status = file_io_inspect(destination, 0, &output);
if (status == 1) return 0;
if (status < 0) return -1;
if (output.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file destination is not regular path=%s type=%d", destination, output.type); return -1;
}
return !memcmp(input.identity, output.identity, sizeof(input.identity));
}
int file_io_temp_open(struct file_io_temp *output, const char *destination) {
if (!output || output->file || output->temporary[0] || !destination || !*destination
|| strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
}
for (unsigned attempt = 0; attempt < 8; attempt++) {
uint8_t random[16]; char suffix[33];
if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; }
for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]);
snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix);
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; }
int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
#else
int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
#endif
if (fd < 0) {
output->temporary[0] = 0;
if (errno == EEXIST) continue;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, errno); return -1;
}
#ifdef _WIN32
output->file = _fdopen(fd, "wb+");
if (!output->file) _close(fd);
#else
output->file = fdopen(fd, "wb+");
if (!output->file) close(fd);
#endif
if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; }
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s", output->temporary);
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1;
}
int file_io_temp_finish(struct file_io_temp *output) {
if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; }
int error = fflush(output->file) ? (errno ? errno : EIO) : 0;
if (fclose(output->file) && !error) error = errno ? errno : EIO;
output->file = NULL;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/close failed path=%s errno=%d", output->temporary, error); return -1; }
return 0;
}
void file_io_temp_discard(struct file_io_temp *output) {
if (!output) return;
if (output->file) {
if (fclose(output->file)) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup close failed errno=%d", errno);
output->file = NULL;
}
if (!output->temporary[0]) return;
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
int error = file_io_wide(output->temporary, wide) ? -1 : _wremove(wide);
#else
int error = remove(output->temporary);
#endif
if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno);
output->temporary[0] = 0;
}
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) {
if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1;
}
#ifdef _WIN32
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
if (file_io_wide(output->temporary, from) || file_io_wide(destination, to)) return -1;
if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1;
}
#else
if (rename(output->temporary, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, errno); return -1;
}
#endif
output->temporary[0] = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination);
return 0;
}
+42
View File
@@ -0,0 +1,42 @@
/* file_io — синхронные файловые операции для workers: UTF-8 пути, идентичность и публикация через соседний temp.
* inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла.
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
* Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */
#ifndef FILE_IO_H
#define FILE_IO_H
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#define FILE_IO_PATH_MAX 4096
enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER };
struct file_io_info {
int type; // обычный файл, каталог, ссылка/reparse point или другой объект
int64_t size; // размер в байтах, без сужения до int
uint64_t identity[3]; // идентификатор файла внутри файловой системы, сравнивать весь массив
};
struct file_io_temp {
char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish
FILE *file; // открытый wb+; NULL после finish
};
// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе.
int file_io_inspect(const char *path, int follow, struct file_io_info *info);
#ifdef _WIN32
// Преобразовать непустой UTF-8 путь в wide[FILE_IO_PATH_MAX]; 0/-1.
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]);
#endif
// Открыть обычный файл для чтения; путь UTF-8. NULL при ошибке, fclose принадлежит вызывающему.
FILE *file_io_open_read(const char *path);
// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
int file_io_paths(const char *source, const char *destination);
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
int file_io_temp_open(struct file_io_temp *output, const char *destination);
// Проверить flush/close, обнулить file при любом результате; temp остаётся собственностью output.
int file_io_temp_finish(struct file_io_temp *output);
// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются.
void file_io_temp_discard(struct file_io_temp *output);
// Повторно проверить paths и заменить destination закрытым temp; одинаковые файлы отклоняются.
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
#endif
+6 -4
View File
@@ -3,6 +3,8 @@
*
* Определяет локальный IP, который ОС выберет для отправки
* на заданный удаленный адрес через заданный локальный адрес.
* Передать local/remote одного семейства AF_INET или AF_INET6 и буфер result.
* Создаёт и закрывает временный UDP-сокет; предназначен для выбора маршрута, не для обработки каждого пакета.
*/
#ifndef GETMYIP_H
@@ -26,13 +28,13 @@ extern "C" {
* Определяет локальный IP-адрес, который ОС выберет для отправки
* на заданный удаленный адрес через заданный локальный адрес.
*
* Создает временный UDP-сокет, выполняет bind на local_addr,
* Создает временный UDP-сокет, выполняет bind на копию local_addr с портом 0,
* connect к remote_addr и получает выбранный локальный IP через getsockname.
*
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения)
* @param remote_addr Удаленный адрес (для определения маршрута)
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения); входной порт игнорируется
* @param remote_addr Удаленный адрес того же семейства (для определения маршрута)
* @param result Заполняемый локальный IP (порт = 0)
* @return 0 при успехе, -1 при ошибке
* @return 0 при успехе, -1 при NULL, неподдерживаемом/разном семействе или ошибке socket/bind/connect/getsockname
*/
int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
const struct sockaddr_storage *remote_addr,
-34
View File
@@ -1,34 +0,0 @@
# Get My IP (getmyip.h)
## 1. Назначение
Определение локального IP-адреса, который ОС выберет для исходящего UDP-соединения на заданный удалённый хост. Используется в `etcp_connections.c` при установке соединения: зная remote-адрес, модуль узнаёт, какой source IP ядро назначит пакетам (нужно для проверки NAT и привязки сокета к правильному интерфейсу).
## 2. Как пользоваться
Передать локальный адрес-подсказку (или `0.0.0.0` для автоопределения) и удалённый адрес. Функция создаст временный UDP-сокет, сделает bind+connect, считает выбранный IP через `getsockname` и вернёт его в `result` (порт = 0, только IP).
```c
struct sockaddr_storage local_hint; // AF_INET6, sin6_addr = in6addr_any
struct sockaddr_storage remote; // AF_INET6, адрес сервера
struct sockaddr_storage chosen_ip;
// ... заполнить local_hint и remote ...
if (get_outgoing_local_ip(&local_hint, &remote, &chosen_ip) == 0) {
// chosen_ip.ss_family == AF_INET6, sin6_addr содержит выбранный source IP
// chosen_ip.sin6_port == 0
}
```
**Нюансы:**
- Функция временно создаёт и закрывает UDP-сокет — нельзя вызывать в hot path
- `local_addr` и `remote_addr` должны быть одного семейства (оба AF_INET или оба AF_INET6)
- Порт в `local_addr` заменяется на 0 (чтобы не конфликтовать с основными сокетами)
- Порт в `result` всегда 0 — только IP
## 3. API
### Функция
- **`get_outgoing_local_ip(local_addr, remote_addr, result)`** — создаёт временный UDP-сокет, привязывает к `local_addr` (с портом 0), делает `connect` к `remote_addr` для вычисления маршрута ядром, получает выбранный source IP через `getsockname`. Возвращает 0 при успехе, -1 при ошибке (NULL-аргументы, несовпадение семейств, ошибки сокета).
### Зависимости
- `socket_compat.h` — кроссплатформенные обёртки: `socket_create_udp`, `SOCKET_INVALID`, `socket_close_wrapper`, `socket_get_error`, `socket_strerror`
+27 -10
View File
@@ -1,4 +1,6 @@
#include "json_flat.h"
#include "mem.h"
#include "debug_config.h"
#include <string.h>
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
@@ -19,6 +21,8 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
case 'n': ch = '\n'; break;
case 'r': ch = '\r'; break;
case 't': ch = '\t'; break;
case 'b': ch = '\b'; break;
case 'f': ch = '\f'; break;
default: return -1;
}
}
@@ -33,20 +37,22 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
return 0;
}
static int json_parse_pair(const char** pp, const char* end,
static int json_parse_pair(const char** pp, const char* end, char* scratch, size_t capacity,
json_flat_cb cb, void* arg) {
const char* p = *pp;
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') { *pp = p; return 0; }
char key[128], val[256];
if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1;
char* key = scratch;
if (json_read_string(&p, end, key, capacity) != 0) return -1;
size_t key_size = strlen(key) + 1;
char* val = scratch + key_size;
SKIP_WS(p, end); CHECK(p, end);
if (*p != ':') return -1; p++;
SKIP_WS(p, end); CHECK(p, end);
if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1;
if (json_read_string(&p, end, val, capacity - key_size) != 0) return -1;
if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; }
if (cb) { int rc = cb(key, val, arg); if (rc != 0) { *pp = p; return rc < 0 ? -1 : 1; } }
SKIP_WS(p, end); CHECK(p, end);
if (*p == ',') p++;
@@ -61,13 +67,23 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
SKIP_WS(p, end); CHECK(p, end);
if (*p != '{') return -1; p++;
/* Decoded key + value + two NULs fit within the encoded object size. */
size_t capacity = strlen(json) + 1;
if (capacity > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: input too large"); return -1; }
char local[384];
char* scratch = capacity <= sizeof(local) ? local : u_malloc(capacity);
if (!scratch) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: allocation failed bytes=%zu", capacity); return -1; }
int result = 0;
for (;;) {
int rc = json_parse_pair(&p, end, cb, arg);
if (rc < 0) return -1;
if (rc > 0) return 0; /* callback stopped */
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') return 0;
int rc = json_parse_pair(&p, end, scratch, capacity, cb, arg);
if (rc < 0) { result = -1; break; }
if (rc > 0) break;
SKIP_WS(p, end);
if (p >= end) { result = -1; break; }
if (*p == '}') break;
}
if (scratch != local) u_free(scratch);
return result;
}
struct get_ctx { const char* key; char* out; size_t sz; int found; };
@@ -81,6 +97,7 @@ static int get_cb(const char* k, const char* v, void* arg) {
}
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) {
if (!key || (value_out && !value_sz)) return -1;
struct get_ctx g = { key, value_out, value_sz, 0 };
if (json_flat_parse(json, get_cb, &g) != 0) return -1;
return g.found ? 0 : -1;
+7 -1
View File
@@ -2,7 +2,12 @@
* @file json_flat.h
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
*
* Только строковые значения. Без malloc, без зависимостей.
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
* Callback получает полные key/value, действительные только до его возврата.
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
* Синтаксис проверяется нестрого: допускаются пропущенные и завершающие запятые;
* хвост после закрывающей } игнорируется даже без остановки callback.
* get ищет первый ключ и копирует значение с NUL, при малом буфере обрезает; 0 найден, -1 ошибка/нет ключа.
*/
#ifndef JSON_FLAT_H
@@ -15,6 +20,7 @@ extern "C" {
#endif
typedef int (*json_flat_cb)(const char* key, const char* value, void* arg);
/* Callback: 0 = continue, >0 = stop successfully, <0 = error. */
int json_flat_parse(const char* json, json_flat_cb cb, void* arg);
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz);
+44
View File
@@ -0,0 +1,44 @@
Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
Jean-Marc Valin, Timothy B. Terriberry,
CSIRO, Gregory Maxwell, Mark Borgerding,
Erik de Castro Lopo, Mozilla, Amazon
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of Internet Society, IETF or IETF Trust, nor the
names of specific contributors, may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Opus is subject to the royalty-free patent licenses which are
specified at:
Xiph.Org Foundation:
https://datatracker.ietf.org/ipr/1524/
Microsoft Corporation:
https://datatracker.ietf.org/ipr/1914/
Broadcom Corporation:
https://datatracker.ietf.org/ipr/1526/
+2
View File
@@ -1,5 +1,7 @@
noinst_LIBRARIES = libopus_internal.a
EXTRA_DIST = COPYING
# Override global CFLAGS to disable ASAN (makes .a incompatible with PIE linking)
CFLAGS = -g -Og -fno-omit-frame-pointer -fno-pie -fPIC
+128 -43
View File
@@ -52,15 +52,23 @@ static void on_get_trampoline(void* arg);
static void empty_trampoline(void* arg) {
struct ll_queue* q = (struct ll_queue*)arg;
q->empty_call_soon_id = NULL;
if (q->count != 0) return;
queue_callback_fn cb = q->empty_callback;
void* cb_arg = q->empty_callback_arg;
q->empty_callback = NULL;
q->empty_callback_arg = NULL;
if (q->count == 0 && cb) cb(q, cb_arg);
if (cb) cb(q, cb_arg);
}
static void on_get_trampoline(void* arg) {
struct ll_queue* q = (struct ll_queue*)arg;
q->on_get_call_soon_id = NULL;
if (!q->on_get_pending || !q->on_get) return;
q->on_get_pending--;
if (q->on_get_pending) {
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
}
if (q->on_get) q->on_get(q, q->on_get_arg);
}
@@ -130,6 +138,7 @@ void queue_free(struct ll_queue* q) {
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
// Это упрощает архитектуру и предотвращает double-u_free
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
struct queue_waiter* w = q->waiter_head;
while (w) {
@@ -159,6 +168,10 @@ void queue_free(struct ll_queue* q) {
q->empty_callback_arg = NULL;
q->on_get = NULL;
q->on_get_arg = NULL;
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
u_free(q);
}
@@ -194,6 +207,19 @@ void queue_resume_callback(struct ll_queue* q) {
}
}
// Общий запуск коллбэка автозабора: immediate (default) или deferred через uasync_call_soon.
// Deferred-режим очищает стек производителя перед обработкой первого элемента.
static void queue_trigger_callback(struct ll_queue* q) {
if (!q->head || q->callback_suspended || !q->callback) return;
if (q->callback_defer) {
if (!q->resume_timeout_id)
q->resume_timeout_id = uasync_call_soon(q->ua, q, queue_resume_timeout_cb);
} else {
q->callback(q, q->callback_arg);
}
}
void queue_set_size_limit(struct ll_queue* q, int lim) {
if (!q) return;
q->size_limit = lim;
@@ -303,22 +329,42 @@ static uint32_t make_hash(const void* data, uint16_t len) {// алгоритм F
return hash;
}
static void waiter_defer_cb(void* arg) {
struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg;
h->call_soon_id = NULL;
if (h->defer_cb) h->defer_cb(h->defer_q, h->defer_arg);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "waiter_defer_cb: NULL callback (stale?) q=%p", (void*)h->defer_q);
static void unlink_deferred_waiter(struct ll_queue* q, struct queue_waiter_handle* h) {
struct queue_waiter_handle** p = &q->deferred_waiters;
while (*p && *p != h) p = &(*p)->defer_next;
if (*p) *p = h->defer_next;
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter not found: handle=%p", q->name, h);
h->defer_next = NULL; h->defer_q = NULL;
}
static inline void queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
static void waiter_defer_cb(void* arg) {
struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg;
struct ll_queue* q = h->defer_q;
queue_threshold_callback_fn cb = h->defer_cb;
void* cb_arg = h->defer_arg;
h->call_soon_id = NULL;
unlink_deferred_waiter(q, h);
h->defer_cb = NULL; h->defer_arg = NULL;
if (cb) cb(q, cb_arg);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter has no callback", q->name);
}
static inline int queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
struct queue_waiter_handle* h) {
if (q->waiter_defer) {
if (h->call_soon_id) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "queue_waiter_call: overwriting existing call_soon_id=%p", h->call_soon_id); uasync_call_soon_cancel(q->ua, h->call_soon_id); }
if (h->call_soon_id) queue_waiter_cancel(h->defer_q, h);
h->defer_q = q; h->defer_cb = cb; h->defer_arg = arg;
h->call_soon_id = uasync_call_soon(q->ua, h, waiter_defer_cb);
if (h->call_soon_id) { h->defer_next = q->deferred_waiters; q->deferred_waiters = h; }
else {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter scheduling failed: handle=%p", q->name, h);
h->defer_q = NULL; h->defer_cb = NULL; h->defer_arg = NULL;
return -1;
}
} else {
cb(q, arg);
}
return 0;
}
// Проверить и запустить первый ожидающий коллбэк (FIFO / round-robin)
@@ -338,13 +384,18 @@ static void check_waiters(struct ll_queue* q) {
queue_threshold_callback_fn cb = waiter->callback;
void* arg = waiter->callback_arg;
u_free(waiter);
if (cb) {
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "check_waiters: waking head waiter, count=%d<=%d, bytes=%zu<=%zu",
q->count, q->threshold_max_packets, q->total_bytes, q->threshold_max_bytes);
queue_waiter_call(q, cb, arg, h);
if (queue_waiter_call(q, cb, arg, h) < 0) {
// Отказ планирования не теряет ожидание: следующий get/remove повторит попытку.
waiter->next = q->waiter_head; q->waiter_head = waiter;
if (!q->waiter_tail) q->waiter_tail = waiter;
h->internal = waiter;
return;
}
}
u_free(waiter);
}
int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
@@ -353,7 +404,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put detected! entry=%p prev=%p next=%p len=%u",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next, entry->len);
abort();
@@ -367,6 +418,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
}
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@@ -378,19 +430,18 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
if (q->tail) q->tail->next = entry; else q->head = entry;
q->tail = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
// НЕ вызываем add_to_hash
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -400,7 +451,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_with_index detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@@ -416,6 +467,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@@ -427,19 +479,18 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
if (q->tail) q->tail->next = entry; else q->head = entry;
q->tail = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
add_to_hash(q, entry);
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -449,7 +500,7 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@@ -462,22 +513,25 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
return -1;
}
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry); queue_entry_free(entry); return -1;
}
entry->next = q->head;
entry->prev = NULL;
if (q->head) q->head->prev = entry; else q->tail = entry;
q->head = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -488,7 +542,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first_with_index detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@@ -504,6 +558,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@@ -514,19 +569,18 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
if (q->head) q->head->prev = entry; else q->tail = entry;
q->head = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
add_to_hash(q, entry);
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -549,6 +603,7 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
entry->next = NULL;
entry->prev = NULL;
entry->owner = NULL;
remove_from_hash(q, entry);
@@ -557,18 +612,21 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
// Приостановить коллбэки для предотвращения рекурсии
q->callback_suspended = 1;
// Проверить ожидающие коллбэки
check_waiters(q);
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
q->empty_call_soon_id = uasync_call_soon(q->ua, q, (timeout_callback_t)empty_trampoline);
if (q->on_get)
uasync_call_soon(q->ua, q, (timeout_callback_t)on_get_trampoline);
if (q->on_get) {
q->on_get_pending++;
if (!q->on_get_call_soon_id) {
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
}
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);// !!!! for debug
#endif
check_waiters(q);
return entry;
}
@@ -647,6 +705,11 @@ void queue_set_waiter_defer(struct ll_queue* q, int enable) {
q->waiter_defer = enable;
}
void queue_set_callback_defer(struct ll_queue* q, int enable) {
if (!q) return;
q->callback_defer = enable;
}
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
if (!q) return;
void* old = q->empty_call_soon_id;
@@ -662,18 +725,24 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
if (!q) return;
q->on_get = cbk_fn;
q->on_get_arg = arg;
if (!cbk_fn) {
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
}
}
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
queue_threshold_callback_fn callback, void* arg) {
if (!q || !h) return -1;
if (!q || !h || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "queue_waiter_wait: invalid arguments q=%p handle=%p callback=%p", q, h, (void*)callback);
return -1;
}
if (h->internal) queue_waiter_cancel(q, h);
if (h->internal || h->call_soon_id) queue_waiter_cancel(h->call_soon_id ? h->defer_q : q, h);
if (q->count <= q->threshold_max_packets
&& (q->threshold_max_bytes == 0 || q->total_bytes <= q->threshold_max_bytes)) {
queue_waiter_call(q, callback, arg, h);
return 1;
return queue_waiter_call(q, callback, arg, h) < 0 ? -1 : 1;
}
struct queue_waiter* waiter = u_malloc(sizeof(struct queue_waiter));
@@ -694,9 +763,18 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
}
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h) {
if (!q || !h || !h->internal) return;
if (h->call_soon_id) { uasync_call_soon_cancel(q->ua, h->call_soon_id); h->call_soon_id = NULL; }
if (!q || !h) return;
/* Отложенный вызов (waiter_defer) может висеть и при internal==NULL:
* queue_waiter_wait по ветке «очередь свободна» планирует call_soon_id,
* не регистрируя waiter. Ранний выход по !internal оставлял бы висячий
* callback с arg=handle, который после teardown разыменовывал бы freed-память. */
if (h->call_soon_id) {
struct ll_queue* deferred_q = h->defer_q;
uasync_call_soon_cancel(deferred_q->ua, h->call_soon_id); h->call_soon_id = NULL;
unlink_deferred_waiter(deferred_q, h); h->defer_cb = NULL; h->defer_arg = NULL;
}
if (!h->internal) return;
struct queue_waiter* waiter = h->internal;
struct queue_waiter* prev = NULL;
struct queue_waiter* curr = q->waiter_head;
@@ -754,6 +832,10 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q, const void* index_
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
if (!q || !entry) return -1;
if (entry->owner != q) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] cannot remove foreign entry=%p owner=%p", q->name, entry, entry->owner);
return -1;
}
#ifdef QUEUE_THREAD_CHECK
queue_check_thread(q);
@@ -777,6 +859,7 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
entry->next = NULL;
entry->prev = NULL;
entry->owner = NULL;
remove_from_hash(q, entry);
@@ -784,6 +867,8 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
#ifdef QUEUE_DEBUG
queue_check_consistency(q);// !!!! for debug
#endif
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
q->empty_call_soon_id = uasync_call_soon(q->ua, q, empty_trampoline);
check_waiters(q);
return 0;
}
+114 -52
View File
@@ -1,6 +1,10 @@
#ifndef LL_QUEUE_H
#define LL_QUEUE_H
/* Очередь одного uasync-потока; наличие pthread-заголовков не делает её межпоточной.
* put может синхронно вызвать consumer/waiter. Держать владельцев живыми до завершения callbacks,
* waiter handle обнулять целиком и отменять до освобождения; entry и dgram освобождаются отдельно. */
#ifdef __cplusplus
extern "C" {
#endif
@@ -37,8 +41,8 @@ extern "C" {
* @note Важные правила использования автозабора (queue_set_callback):
* 1. Коллбэк получает управление только если в очереди есть элементы И !callback_suspended
* 2. Внутри коллбэка ОБЯЗАТЕЛЬНО: queue_data_get() → обработать элемент → queue_resume_callback()
* 3. Без вызова queue_resume_callback очередь навсегда застрянет
* 4. Не извлекайте элементы вручную вне коллбэка — это нарушит логику
* 3. Без queue_resume_callback автозабор остаётся приостановленным
* 4. Ручное извлечение допустимо; для продолжения автозабора после него тоже нужен resume
* 5. queue_data_get() автоматически suspend'ит коллбэки для предотвращения рекурсии
*
* @note Память:
@@ -57,10 +61,11 @@ struct queue_waiter_handle;
* @brief Элемент очереди (переменного размера).
*
* Память выделяется одним блоком: [struct ll_entry + data[size]].
* Поле dgram — отдельный блок (malloc / пул / внешняя память).
* Поле dgram — отдельный блок (u_malloc / пул / внешняя память со своим dgram_free_fn).
*/
struct ll_entry {
char* name;
struct ll_queue* owner; // Владелец элемента; NULL после извлечения
struct ll_entry* next; // Следующий элемент в очереди
struct ll_entry* prev; // Предыдущий элемент в очереди
uint16_t size; // Размер пользовательского буфера data[] (байт)
@@ -70,9 +75,9 @@ struct ll_entry {
uint8_t* dgram; // Указатель на данные пакета
void (*dgram_free_fn)(uint8_t* data); // Кастомная функция освобождения dgram
struct memory_pool* dgram_pool; // Пул для dgram (если используется)
uint32_t index_hash; // Хеш по первым 4 байтам индекса
uint32_t index_hash; // FNV-1a (32 бита) по всем index_size байтам data[] от index_offset
struct ll_entry* hash_next; // Следующий в хеш-цепочке
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = malloc)
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = u_malloc)
uint8_t data[0]; // Гибкий массив пользовательских данных (размер = size)
};
@@ -123,6 +128,7 @@ struct queue_waiter_handle {
queue_threshold_callback_fn defer_cb;
void* defer_arg;
void* call_soon_id; ///< хендл отложенного вызова (для отмены)
struct queue_waiter_handle* defer_next; ///< список отложенных уведомлений очереди
};
/**
@@ -146,9 +152,11 @@ struct ll_queue {
struct queue_waiter* waiter_head; // Голова списка ожидающих
struct queue_waiter* waiter_tail; // Хвост списка ожидающих
struct queue_waiter_handle* deferred_waiters; // Уже запланированные уведомления
int threshold_max_packets; // Общий порог: макс. кол-во элементов (по умолчанию 0)
size_t threshold_max_bytes; // Общий порог: макс. объём данных (0 = не проверять)
int waiter_defer; // 0=immediate callback, 1=defer via uasync_call_soon
int callback_defer; // 0=immediate callback из put (default), 1=defer via uasync_call_soon (очистка стека)
queue_callback_fn empty_callback; // одноразовый коллбэк при опустошении очереди
void* empty_callback_arg;
@@ -156,6 +164,8 @@ struct ll_queue {
queue_callback_fn on_get; // коллбэк после каждого queue_data_get (deferred)
void* on_get_arg;
void* on_get_call_soon_id;
size_t on_get_pending;
struct ll_entry** hash_table; // Хеш-таблица для поиска по id (если hash_size > 0)
size_t hash_size; // Размер хеш-таблицы
@@ -171,7 +181,7 @@ struct ll_queue {
#endif
};
/* ==================== Создание / уничтожение ==================== */
// ==================== Создание / уничтожение ====================
/**
* @brief Создаёт новую пустую очередь.
@@ -179,8 +189,8 @@ struct ll_queue {
* @param hash_size размер хеш-таблицы (0 = поиск по id отключён)
* @param index_offset смещение ключа индекса в data[] каждого entry (0 если hash_size=0)
* @param index_size размер ключа индекса (0 если hash_size=0)
* @param name имя очереди для отладки
* @return указатель на очередь или NULL при ошибке выделения памяти
* @param name заимствованное имя для отладки, должно жить до queue_free
* @return указатель на очередь или NULL при NULL ua / ошибке выделения памяти
*/
struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_offset, uint16_t index_size, char* name);
@@ -190,42 +200,49 @@ struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_o
*
* @warning НЕ ОСВОБОЖДАЕТ элементы в очереди!
* Их нужно извлечь через queue_data_get() и освободить через queue_entry_free().
* Перед дренажем снять callback и отменить ожидания владельцев, затем для каждого entry освободить dgram и entry.
* queue_free отменяет свои отложенные уведомления и waiters; user_arg не освобождает. NULL допустим.
*/
void queue_free(struct ll_queue* q);
/* ==================== Конфигурация ==================== */
// ==================== Конфигурация ====================
/**
* @brief Устанавливает максимальное количество элементов в очереди.
* @param q очередь
* @param lim лимит (-1 = без ограничения)
* @param lim лимит (любое отрицательное значение = без ограничения)
*
* При превышении лимита новые элементы автоматически освобождаются.
*/
void queue_set_size_limit(struct ll_queue* q, int lim);
/* ==================== Автозабор элементов ==================== */
// ==================== Автозабор элементов ====================
/**
* @brief Устанавливает коллбэк для автоматического извлечения элементов.
* @param q очередь
* @param cbk_fn функция-коллбэк
* @param cbk_fn функция-коллбэк (NULL отключает автозабор)
* @param arg пользовательский аргумент
*
* Коллбэк вызывается автоматически при наличии элементов и разрешённом состоянии.
* Только заменяет функцию и аргумент: не запускает обработку, не снимает callback_suspended
* и не отменяет уже запланированный resume. Отложенный resume использует новые функцию и аргумент.
* Для запуска автозабора из уже заполненной очереди вызовите queue_resume_callback().
*/
void queue_set_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Возобновляет работу коллбэка после обработки элемента в следующем event loop.
* @brief Разрешает автозабор и планирует отложенный вызов коллбэка.
* @param q очередь
*
* @warning ОБЯЗАТЕЛЬНО вызывать после завершения обработки элемента в коллбэке.
* Иначе автозабор элементов навсегда остановится.
* Сразу снимает callback_suspended. Если есть элементы и callback, планирует uasync_call_soon,
* если resume ещё не запланирован. При исполнении повторно проверяет наличие элементов,
* callback и отсутствие приостановки. Последующий put в пустую очередь может вызвать callback
* синхронно, если callback_defer == 0.
* @note Вызывать после завершения обработки извлечённого элемента для продолжения автозабора.
*/
void queue_resume_callback(struct ll_queue* q);
/* ==================== Пороговое ожидание (backpressure) ==================== */
// ==================== Пороговое ожидание (backpressure) ====================
/**
* @brief Устанавливает общий порог освобождения очереди.
@@ -234,87 +251,128 @@ void queue_resume_callback(struct ll_queue* q);
* @param max_bytes максимальный объём данных (0 = не проверять)
*
* Порог — общий на всю очередь. Все waiters в связном списке используют этот порог.
* При достижении порога вызывается следующий waiter из списка (FIFO / round-robin).
* После успешного get/remove при выполнении порога вызывается один следующий waiter из FIFO-списка.
* Сам setter старые ожидания не проверяет. Байт-порог использует int_len, зафиксированный при put.
* По умолчанию: max_packets=0, max_bytes=0 (пустая очередь).
*/
void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes);
// enable=0 — синхронный waiter (по умолчанию), !=0 — через uasync_call_soon.
// Deferred-уведомление не резервирует место и повторно порог не проверяет: очередь к исполнению может заполниться.
// Повторную регистрацию producer из его callback при свободной очереди откладывать, чтобы избежать рекурсии.
void queue_set_waiter_defer(struct ll_queue* q, int enable);
/**
* @brief Управляет способом вызова коллбэка автозабора при добавлении элемента.
* @param q очередь
* @param enable 0 — первый запуск коллбэка из queue_data_put* вызывается синхронно (по умолчанию);
* 1 — первый запуск откладывается через uasync_call_soon (очищает стек вызывающего).
*
* @note Влияет только на первый запуск из put. Повторные запуски (queue_resume_callback)
* всегда идут через uasync_call_soon.
*/
void queue_set_callback_defer(struct ll_queue* q, int enable);
/**
* @brief Устанавливает одноразовый коллбэк при опустошении очереди (count==0).
* @param q очередь
* @param cbk_fn коллбэк (NULL для отмены)
* @param arg пользовательский аргумент
*
* Коллбэк вызывается асинхронно (через uasync_call_soon) когда count становится 0.
* После вызова сбрасывается — для повторного использования нужно вызвать снова.
* Заменяет регистрацию и отменяет её прежний отложенный вызов. При count == 0 сразу планирует
* uasync_call_soon; иначе вызов планируется при опустошении через queue_data_get/queue_remove_data.
* При исполнении снова проверяет count: если очередь заполнилась, регистрация сохраняется
* до следующего опустошения. Функция и аргумент сбрасываются ДО вызова callback,
* поэтому внутри него можно зарегистрировать новое уведомление.
*/
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Устанавливает коллбэк вызываемый после каждого queue_data_get (deferred через call_soon).
* @brief Устанавливает коллбэк после каждого успешного queue_data_get (deferred через call_soon).
* @param q очередь
* @param cbk_fn коллбэк (NULL для отмены)
* @param arg пользовательский аргумент
*
* Уведомления подсчитываются, не сливаются в одно; remove их не создаёт.
* NULL отменяет отложенный вызов и обнуляет накопленные уведомления. Замена использует новые функцию/arg.
*/
void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Регистрирует ожидание освобождения очереди до общего порога.
* @param q очередь
* @param h указатель на handle (создаётся как `struct queue_waiter_handle h = {0}` или `h.internal = NULL`)
* @param h handle, полностью обнулённый перед первым использованием: `struct queue_waiter_handle h = {0}`
* @param callback функция, вызываемая при достижении порога
* @param arg аргумент коллбэка
* @return 1 — условие уже выполнено (callback вызван немедленно), 0 — зарегистрирован в списке, -1 — ошибка
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
*
* Если handle уже ожидает (h->internal != NULL) — старый waiter отменяется и создаётся новый.
* При корректных аргументах повторная регистрация в той же очереди отменяет прежнее ожидание.
* Если h->internal != NULL, перед переносом в другую очередь обязательно вызовите
* queue_waiter_cancel(old_q, h): очередь старого ожидания в handle не хранится.
* Уже отложенный callback (h->call_soon_id != NULL) отменяется по сохранённому h->defer_q,
* поэтому в этом состоянии handle можно передать другой очереди без отдельной отмены.
* При queue_free отменяются также уже запланированные уведомления.
*/
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
queue_threshold_callback_fn callback, void* arg);
/**
* @brief Отменяет ожидание (удаляет из списка, если зарегистрирован).
* @param q очередь
* @param q очередь исходного ожидания (для h->internal); не NULL
* @param h указатель на handle
*
* Безопасно вызывать в любом состоянии: если waiter не зарегистрирован — ничего не делает.
* Отложенный callback отменяется через сохранённую h->defer_q. Если ожидания и отложенного
* callback нет — ничего не делает. При h->internal != NULL передавать другую очередь нельзя.
*/
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h);
/* ==================== Работа с данными ==================== */
// ==================== Работа с данными ====================
/**
* Общие правила владения для всех четырёх queue_data_put*:
* - При q == NULL или entry == NULL возвращается -1 без освобождения памяти.
* - Элемент должен быть извлечён из прежней очереди: owner, prev и next должны быть NULL.
* Повторная вставка связанного элемента приводит к abort().
* - При остальных отказах (лимит, неверный вид put или границы индекса) освобождаются
* сначала dgram через queue_dgram_free(), затем entry через queue_entry_free().
* - При успехе владение передаётся очереди/потребителю. Put может синхронно вызвать callback,
* который обработает и освободит entry и даже очередь до возврата из put.
* Использовать эти указатели после put можно только при гарантированном времени жизни.
*/
/**
* @brief Добавляет элемент в конец очереди (FIFO).
* @param q очередь
* @param entry элемент
* @param id идентификатор для поиска
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
* @param q очередь без хеш-таблицы (hash_size == 0)
* @param entry элемент; правила передачи владения см. выше
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
*/
int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
/**
* @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет).
* @param q очередь
* @param entry элемент
* @param id идентификатор
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
* @param q очередь без хеш-таблицы (hash_size == 0)
* @param entry элемент; правила передачи владения см. выше
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
*/
int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry);
/**
* @brief Добавляет элемент в конец очереди с хеш-индексом (использует index_offset/index_size из очереди).
* @param q очередь
* @param entry элемент
* @return 0 — успех, -1 — ошибка
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
*/
int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry);
/**
* @brief Добавляет элемент в начало очереди с хеш-индексом (использует index_offset/index_size из очереди).
* @param q очередь
* @param entry элемент
* @return 0 — успех, -1 — ошибка
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
*/
int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry);
@@ -336,35 +394,35 @@ struct ll_entry* queue_data_get(struct ll_queue* q);
*/
int queue_entry_count(struct ll_queue* q);
/* ==================== Управление памятью элементов ==================== */
// ==================== Управление памятью элементов ====================
// проверить правильность очереди (корректность цепочки, счетчики элементов и байтов)
int queue_check_consistency(struct ll_queue* q);
/**
* @brief Выделяет элемент с отдельным буфером dgram (malloc).
* @brief Выделяет элемент с отдельным буфером dgram через u_malloc (entry->len=0, memlen=len).
* @param len требуемый размер dgram
* @return элемент или NULL при ошибке
*/
struct ll_entry* ll_alloc_lldgram(uint16_t len);
/**
* @brief Создаёт элемент с пользовательским буфером data[] (malloc одним блоком).
* @param data_size размер data[]
* @brief Создаёт обнулённый элемент с пользовательским буфером data[] (u_malloc одним блоком).
* @param data_size размер data[], должен помещаться в uint16_t
* @return элемент или NULL при ошибке
*/
struct ll_entry* queue_entry_new(size_t data_size);
/**
* @brief Создаёт элемент из пула памяти.
* @param pool пул
* @return элемент или NULL, если пул пуст
* @param pool пул с object_size >= sizeof(struct ll_entry); разность размеров должна помещаться в uint16_t
* @return элемент или NULL при ошибке выделения; пустой кеш пула пополняется автоматически
*/
struct ll_entry* queue_entry_new_from_pool(struct memory_pool* pool);
/**
* @brief Освобождает структуру элемента (возвращает в пул или free).
* @param entry элемент
* @brief Освобождает структуру элемента (возвращает в пул или u_free).
* @param entry извлечённый из очереди элемент; NULL допустим
*
* @note НЕ освобождает dgram! Используйте queue_dgram_free() отдельно.
*/
@@ -373,10 +431,14 @@ void queue_entry_free(struct ll_entry* entry);
/**
* @brief Освобождает буфер dgram элемента.
* @param entry элемент
*
* При dgram != NULL вызывает dgram_free_fn, если она задана; иначе возвращает буфер в dgram_pool,
* если задан пул; иначе вызывает u_free. Затем сбрасывает dgram и len. Сам entry не освобождает.
* Внешнему буферу нужен подходящий dgram_free_fn.
*/
void queue_dgram_free(struct ll_entry* entry);
/* ==================== Поиск и удаление ==================== */
// ==================== Поиск и удаление ====================
/**
* @brief Находит элемент по ключу индекса (требуется hash_size > 0 при создании).
@@ -406,12 +468,12 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q,
*/
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry);
/* ==================== Утилиты ==================== */
// ==================== Утилиты ====================
/**
* @brief Возвращает суммарный объём данных в очереди.
* @param q очередь
* @return количество байт
* @return сумма int_len (снимки len при put); изменение len связанного entry счётчик не пересчитывает
*/
static inline size_t queue_total_bytes(struct ll_queue* q) {
return q->total_bytes;
-234
View File
@@ -1,234 +0,0 @@
# ll_queue — Двусвязная FIFO-очередь с автозабором и backpressure
## 1. Назначение
Двусвязная очередь элементов (FIFO) с автоматическим вызовом callback при появлении данных, пороговым ожиданием освобождения (backpressure), хеш-индексом для поиска и поддержкой пулов памяти. Используется как основной механизм передачи данных между компонентами внутри одного потока uasyc: TUN ↔ ETCP ↔ routing ↔ normalizer и т.д.
Работает **строго в одном потоке** (один uasyc). Для многопоточного доступа не предназначена.
## 2. Как пользоваться
### 2.1. Создание очереди
```c
// Без хеша (простая очередь)
struct ll_queue* q = queue_new(ua, 0, 0, 0, "my_queue");
// С хешем — для быстрого поиска по ключу внутри data[]
// hash_size=256, ключ лежит в data[] по смещению 0, размер ключа 4 байта
struct ll_queue* q = queue_new(ua, 256, 0, 4, "my_hash_queue");
```
### 2.2. Создание элемента
Три способа, по убыванию производительности:
```c
// 1. Из memory pool (самый быстрый)
struct ll_entry* e = queue_entry_new_from_pool(instance->pkt_pool);
// 2. Через malloc (data[] фиксированного размера одним блоком)
struct ll_entry* e = queue_entry_new(sizeof(struct my_data));
// 3. Элемент + отдельный буфер dgram (для переменного размера данных)
struct ll_entry* e = ll_alloc_lldgram(dgram_len);
memcpy(e->dgram, src_data, dgram_len);
e->len = dgram_len;
```
### 2.3. Запись в очередь (backpressure)
**Главное правило: не забиваем очередь!** Добавляем следующий элемент только когда очередь опустела до заданного порога. Для этого используется Пороговое ожидание:
```c
// 1. Настроить порог (например: ждать пока count<=0, т.е. очередь совсем пуста)
queue_set_threshold(q, 0, 0);
// 2. В структуре-производителе завести handle:
struct queue_waiter_handle my_waiter = {0};
// 3. При готовности отправить — зарегистрировать ожидание:
static void my_send_cb(struct ll_queue* q, void* arg) {
struct my_producer* p = (struct my_producer*)arg;
struct ll_entry* e = ... // создать элемент
e->len = data_len;
queue_data_put_with_index(q, e);
// Снова зарегистрироваться на следующую порцию
queue_waiter_wait(q, &p->waiter, my_send_cb, p);
}
// Первичная регистрация:
queue_waiter_wait(q, &my_waiter, my_send_cb, producer);
// 4. При деинициализации — отменить ожидание:
queue_waiter_cancel(q, &my_waiter);
```
### 2.4. Чтение из очереди (callback)
```c
// Установить callback — будет вызываться при появлении элементов
queue_set_callback(q, my_callback, my_data);
static void my_callback(struct ll_queue* q, void* arg) {
// 1. ИЗВЛЕЧЬ элемент
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
// 2. ОБРАБОТАТЬ
if (e->dgram && e->len > 0) {
do_something(e->dgram, e->len);
queue_dgram_free(e); // сначала освободить dgram
}
queue_entry_free(e); // потом освободить сам entry
// 3. ОБЯЗАТЕЛЬНО возобновить callback
queue_resume_callback(q);
}
```
**Критически важно:** `queue_data_get()` автоматически приостанавливает callback-и (`callback_suspended = 1`). Без вызова `queue_resume_callback()` очередь **навсегда застрянет** — callback больше никогда не вызовется.
Во **всех ветках** (включая ранние return по ошибке) должен быть `queue_resume_callback(q)`:
```c
// ПРАВИЛЬНО:
if (!e) { queue_resume_callback(q); return; }
if (!e->dgram) { queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; }
// НЕПРАВИЛЬНО — очередь зависнет:
if (!e) return;
```
### 2.5. Поиск по индексу
```c
// Создать очередь с хешем (ключ 4 байта по смещению 0):
struct ll_queue* q = queue_new(ua, 256, 0, 4, "indexed_q");
// Добавлять элементы через _with_index (хеш вычисляется автоматически):
queue_data_put_with_index(q, entry);
// Искать:
uint32_t key = 42;
struct ll_entry* found = queue_find_data_by_index(q, &key);
// Если несколько элементов с одним ключом — продолжать поиск:
struct ll_entry* next = queue_find_next_by_index(q, &key, found);
```
### 2.6. Освобождение ресурсов
```c
// 1. Снять callback
queue_set_callback(q, NULL, NULL);
// 2. Отменить всех waiter-ов
queue_waiter_cancel(q, &my_waiter);
// 3. Извлечь и освободить ВСЕ оставшиеся элементы
struct ll_entry* e;
while ((e = queue_data_get(q)) != NULL) {
queue_dgram_free(e);
queue_entry_free(e);
}
// После извлечения всех элементов resume не нужен — callback уже снят.
// 4. Освободить очередь
queue_free(q);
```
**Важно:** `queue_free()` **не освобождает элементы**. Их нужно извлечь через `queue_data_get()` и освободить вручную.
### 2.7. Приоритетная вставка (LIFO)
```c
// Вставить в начало очереди (высокий приоритет):
queue_data_put_first(q, entry);
queue_data_put_first_with_index(q, entry);
```
### 2.8. Удаление из середины
```c
queue_remove_data(q, entry); // удаляет из очереди, НЕ освобождает память
queue_dgram_free(entry); // освободить dgram отдельно
queue_entry_free(entry); // освободить entry отдельно
```
После `queue_remove_data()` элемент больше не принадлежит очереди — вызывающий сам отвечает за его освобождение.
### 2.9. Callback при опустошении
```c
static void on_empty(struct ll_queue* q, void* arg) {
// Очередь стала пустой (count==0)
// Вызывается однократно, после вызова сбрасывается
}
queue_set_empty_callback(q, on_empty, my_data);
```
## 3. API
### Создание / уничтожение
| Функция | Назначение |
|---------|-----------|
| `queue_new(ua, hash_size, index_offset, index_size, name)` | Создать очередь. `hash_size=0` — без хеша. `ua` обязателен (для uasync_call_soon). |
| `queue_free(q)` | Освободить очередь (НЕ элементы — их надо извлечь и освободить отдельно). |
### Создание / освобождение элементов
| Функция | Назначение |
|---------|-----------|
| `queue_entry_new(data_size)` | Выделить entry с data[] через malloc. |
| `queue_entry_new_from_pool(pool)` | Выделить entry из memory pool. |
| `ll_alloc_lldgram(len)` | Выделить entry + отдельный буфер dgram (malloc). |
| `queue_entry_free(entry)` | Освободить entry (возврат в pool или free). НЕ освобождает dgram! |
| `queue_dgram_free(entry)` | Освободить dgram (через dgram_free_fn, pool или free). |
### Чтение (callback)
| Функция | Назначение |
|---------|-----------|
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
### Запись
| Функция | Назначение |
|---------|-----------|
| `queue_data_put(q, entry)` | Добавить в конец (FIFO). Для очередей БЕЗ хеша. |
| `queue_data_put_with_index(q, entry)` | Добавить в конец с хеш-индексом. |
| `queue_data_put_first(q, entry)` | Добавить в начало (LIFO, высокий приоритет). |
| `queue_data_put_first_with_index(q, entry)` | Добавить в начало с хеш-индексом. |
### Backpressure
| Функция | Назначение |
|---------|-----------|
| `queue_set_threshold(q, max_packets, max_bytes)` | Задать общий порог. Когда `count <= max_packets` И `total_bytes <= max_bytes`, waiter-ы пробуждаются (FIFO). |
| `queue_waiter_wait(q, h, callback, arg)` | Зарегистрировать ожидание. Возвращает 1 (уже выполнено), 0 (в очереди), -1 (ошибка). |
| `queue_waiter_cancel(q, h)` | Отменить ожидание. Безопасно вызывать в любом состоянии. |
| `queue_set_waiter_defer(q, enable)` | Отложенный вызов waiter-ов (через uasync_call_soon). |
| `queue_set_empty_callback(q, cbk_fn, arg)` | Одноразовый callback при count==0. |
| `queue_set_on_get(q, cbk_fn, arg)` | Callback после каждого queue_data_get (deferred). |
### Поиск / удаление
| Функция | Назначение |
|---------|-----------|
| `queue_find_data_by_index(q, index_key)` | Найти по ключу (требуется hash_size>0). Размер ключа = q->index_size. |
| `queue_find_next_by_index(q, index_key, prev_entry)` | Следующий элемент с тем же ключом. |
| `queue_remove_data(q, entry)` | Удалить из очереди (память НЕ освобождает). |
### Утилиты
| Функция | Назначение |
|---------|-----------|
| `queue_entry_count(q)` | Текущее количество элементов. |
| `queue_total_bytes(q)` | Суммарный объём данных (сумма len). |
| `queue_set_size_limit(q, lim)` | Ограничение на количество элементов (при превышении новые удаляются). |
| `queue_check_consistency(q)` | Проверка целостности (счётчики, циклы, prev/next). Только для отладки. |
+32 -10
View File
@@ -13,11 +13,15 @@
#define CANARY 0xDEADBEEF
#define PADDING_FILL 0xAA
#define METADATA_SIZE 128
#define ALLOCATION_OVERHEAD (12 + (size_t)2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE)
#define POINTER_SIZE sizeof(void*)
#define LOCATION_MAX (METADATA_SIZE - POINTER_SIZE * 2)
#define PREV_OFFSET LOCATION_MAX
#define NEXT_OFFSET (LOCATION_MAX + POINTER_SIZE)
// Порог детекта аномально больших аллокаций (гигантские mmap-резервации, напр. VSZ ~4G)
#define LARGE_ALLOC_LOG_THRESHOLD (8u * 1024u * 1024u) // 8 МБ
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
#include <execinfo.h>
#include <pthread.h>
@@ -86,7 +90,7 @@ void u_check(void* ptr, const char* text, const char* location) {
uint8_t* prefix_start = base + METADATA_SIZE;
uint32_t* pre_canary = (uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE);
uint32_t size = *(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4);
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
uint8_t* user = (uint8_t*)ptr;
bool error = false;
for (size_t i = 0; i < BOUNDARY_CHECK_SIZE; i++) {
@@ -106,14 +110,15 @@ void u_check(void* ptr, const char* text, const char* location) {
error = true;
}
}
uint32_t* post_canary = (uint32_t*)(user + size);
if (*post_canary != CANARY) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, *post_canary);
uint32_t post_canary;
memcpy(&post_canary, user + size, sizeof(post_canary));
if (post_canary != CANARY) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, post_canary);
error = true;
}
if (error) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Allocated at: %s", location, text, alloc_loc);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %u bytes", location, text, size, total);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %zu bytes", location, text, size, total);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Full memory dump at base %p:", location, text, base);
hex_dump(base, total);
exit(EXIT_FAILURE);
@@ -121,9 +126,21 @@ void u_check(void* ptr, const char* text, const char* location) {
}
void* u_malloc_impl(uint32_t size, const char* location) {
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
size_t total = size;
if (total > SIZE_MAX - ALLOCATION_OVERHEAD) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC OVERFLOW: %u user bytes + %zu overhead bytes at %s", size, ALLOCATION_OVERHEAD, location);
return NULL;
}
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
size, (double)size / (1024.0 * 1024.0), location);
}
total += ALLOCATION_OVERHEAD;
uint8_t* base = (uint8_t*)malloc(total);
if (!base && size > 0) return NULL;
if (!base) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC FAILED: %u user bytes, %zu total bytes at %s", size, total, location);
return NULL;
}
memset(base, PADDING_FILL, total);
// Set metadata
size_t loc_len = strlen(location);
@@ -134,7 +151,8 @@ void* u_malloc_impl(uint32_t size, const char* location) {
uint8_t* prefix_start = base + METADATA_SIZE;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE) = CANARY;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4) = size;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size) = CANARY;
const uint32_t post_canary = CANARY; /* размер пользователя не обязан быть кратен выравниванию uint32_t */
memcpy(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size, &post_canary, sizeof(post_canary));
// Add to list (thread-safe)
LOCK();
void** prev_ptr = (void**)(base + PREV_OFFSET);
@@ -151,6 +169,10 @@ void* u_malloc_impl(uint32_t size, const char* location) {
}
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location) {
if (nmemb != 0 && size > UINT32_MAX / nmemb) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CALLOC OVERFLOW: %u * %u (wraps uint32) at %s", nmemb, size, location);
return NULL;
}
uint32_t total_size = nmemb * size;
void* ptr = u_malloc_impl(total_size, location);
if (ptr) memset(ptr, 0, total_size);
@@ -196,7 +218,7 @@ void u_free_impl(void* ptr, const char* location) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Base: %p", (void*)base);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Allocated at: %s", alloc_loc);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "User size: %u bytes", size);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %u bytes", size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %zu bytes", (size_t)size + ALLOCATION_OVERHEAD);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Active allocs: %zu", active_count);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "--- Metadata dump at %p (%u bytes) ---", (void*)base, (unsigned)(METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8));
hex_dump(base, METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8);
@@ -218,7 +240,7 @@ void u_free_impl(void* ptr, const char* location) {
allocated_count--;
frees_count++;
UNLOCK();
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
memset(base, 0, total);
free(base);
}
+20 -2
View File
@@ -1,6 +1,12 @@
/**
* Memory management layer
* Provides wrappers for malloc/realloc/calloc/free with error handling
* mem — выделение памяти с защитными полями, местом аллокации и учётом живых блоков.
* Использовать u_malloc/u_calloc/u_realloc/u_strdup и парный u_free; обычный free к этим блокам неприменим.
* Макросы передают LOCATION автоматически. OOM возвращает NULL; обнаруженное повреждение завершает процесс.
* realloc живого блока при ошибке сохраняет его, при size=0 освобождает; NULL обрабатывается как malloc.
* Размеры аргументов _impl — uint32_t; до вызова проверить, что исходные size_t помещаются в этот диапазон.
* Список аллокаций защищён мьютексом; доступ к одному блоку и чтение счётчика согласует вызывающий.
* Защитные поля проверяются для живых блоков; обращение к уже освобождённому/чужому указателю недопустимо.
* Отчёт u_report_unfreed_blocks вызывать после остановки потоков, меняющих список аллокаций.
*/
#ifndef MEM_H
#define MEM_H
@@ -12,18 +18,30 @@ extern "C" {
#define XSTR(s) STR(s)
#define STR(s) #s
#define LOCATION __FILE__ ":" XSTR(__LINE__)
// Проверяет canary/padding живого блока; text/location — строки диагностики. NULL ptr допустим.
// Обнаруженное повреждение логируется в SYS и завершает процесс через exit(EXIT_FAILURE).
void u_check(void* ptr, const char* text, const char* location);
#define u_malloc(size) u_malloc_impl(size, LOCATION)
#define u_calloc(nmemb, size) u_calloc_impl(nmemb, size, LOCATION)
#define u_realloc(ptr, size) u_realloc_impl(ptr, size, LOCATION)
#define u_free(ptr) u_free_impl(ptr, LOCATION)
#define u_strdup(s) u_strdup_impl(s, LOCATION)
// Выделяет size байт с метаданными; NULL при OOM/переполнении полного размера. size=0 тоже создаёт учтённый блок.
// location — обязательная строка места вызова, копируется в метаданные; payload не обнуляется.
void* u_malloc_impl(uint32_t size, const char* location);
// Выделяет и обнуляет nmemb*size байт; NULL при произведении > UINT32_MAX или ошибке malloc.
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location);
// NULL ptr → malloc(size), включая size=0. Для живого ptr: size=0 → free и NULL;
// иначе новый блок с min(старый размер, size) байтами данных. При OOM возвращает NULL, старый блок сохранён.
// Результат сохранять временно: tmp = u_realloc(ptr, size); if (tmp) ptr = tmp; (при size > 0).
void* u_realloc_impl(void* ptr, uint32_t size, const char* location);
// Проверяет и освобождает живой блок из этих обёрток; NULL допустим. Обычный free к нему неприменим.
void u_free_impl(void* ptr, const char* location);
// Копирует ASCIIZ-строку (strlen(s)+1 должно помещаться в uint32_t); NULL при NULL s/OOM. Освобождать u_free.
char* u_strdup_impl(const char* s, const char* location);
// После остановки изменяющих список потоков: статистика и дампы живых блоков; сами блоки не освобождает.
void u_report_unfreed_blocks(void);
// Число живых блоков, чтение без блокировки; параллельные аллокации/освобождения нужно исключить извне.
size_t u_get_allocated_count(void);
#ifdef __cplusplus
}
-48
View File
@@ -1,48 +0,0 @@
# mem (Memory Wrappers с отслеживанием утечек)
## 1. Назначение
Обёртки над стандартными `malloc`/`calloc`/`realloc`/`free`/`strdup` с детекцией:
- **Buffer overflow/underflow** — каждый блок окружён канарейками (`0xDEADBEEF`) и padding-областями, которые проверяются при `u_free` и `u_realloc`. При повреждении — подробный дамп блока и аварийное завершение.
- **Double free** — двусвязный список всех живых аллокаций; повторное освобождение детектируется по отсутствию блока в списке.
- **Утечки памяти** — `u_report_unfreed_blocks()` при завершении выводит все незакрытые блоки с местом аллокации (`файл:строка`), размером и hex-дампом данных.
Функции потокобезопасны (мьютекс/критическая секция).
## 2. Как пользоваться
```c
#include "mem.h"
// Вместо malloc/calloc/realloc/free/strdup используй соответствующие макросы:
void* buf = u_malloc(data_size);
char* str = u_strdup("hello");
buf = u_realloc(buf, new_size);
u_free(buf);
u_free(str);
// В конце main (перед return) — отчёт о незакрытых блоках:
u_report_unfreed_blocks();
// Текущее число живых аллокаций (для диагностики):
size_t count = u_get_allocated_count();
```
**Важно:**
- Макросы (`u_malloc`, `u_calloc`, `u_free`, и т.д.) автоматически подставляют `__FILE__:__LINE__` через `LOCATION` — это нужно для диагностики утечек и повреждений. Напрямую вызывай `_impl`-функции только если нужно явно передать location.
- `u_realloc(NULL, size)` эквивалентен `u_malloc(size)`, `u_realloc(ptr, 0)` эквивалентен `u_free(ptr)`.
- Потокобезопасность гарантирована, но аллокации с метаданными заметно дороже обычного `malloc` — используй `memory_pool` для hot-path объектов.
- При обнаружении повреждения (overflow/underflow/double-free) программа завершается через `exit(EXIT_FAILURE)` после вывода отладочной информации.
## 3. API
| Функция/макрос | Описание |
|---|---|
| `u_malloc(size)` | Аналог `malloc` с boundary check и трекингом |
| `u_calloc(nmemb, size)` | Аналог `calloc` (выделяет + обнуляет) |
| `u_realloc(ptr, size)` | Аналог `realloc`, с проверкой целостности старого блока |
| `u_free(ptr)` | Освобождение с проверкой canary, double-free и удалением из списка |
| `u_strdup(s)` | Аналог `strdup` через u_malloc |
| `u_report_unfreed_blocks()` | Выводит все незакрытые блоки с дампом; вызывается при shutdown |
| `u_get_allocated_count()` | Возвращает количество живых аллокаций |
| `u_check(ptr, text, location)` | Ручная проверка целостности блока (canary + padding) |
+26 -14
View File
@@ -5,47 +5,59 @@
#include "memory_pool.h"
#include "mem.h"
// Layout: [user_data: object_size] [canary: 4 bytes] [alloc_loc: pointer] [counter: 1 byte]
// Layout: [user_data] [canary: 4] [alloc_loc: pointer] [free_loc: pointer] [counter: 1]
#define POOL_CANARY_OFF (pool->object_size)
#define POOL_LOC_OFF (pool->object_size + 4)
#define POOL_COUNTER_OFF (pool->object_size + 4 + sizeof(const char*))
#define POOL_ALLOC_SIZE (pool->object_size + 4 + sizeof(const char*) + 1)
#define POOL_FREE_LOC_OFF (pool->object_size + 4 + sizeof(const char*))
#define POOL_COUNTER_OFF (pool->object_size + 4 + 2 * sizeof(const char*))
#define POOL_ALLOC_SIZE (pool->object_size + 4 + 2 * sizeof(const char*) + 1)
#define POOL_CANARY_VAL 0xDEADBEAF
static void pool_init_tags(struct memory_pool* pool, void* obj, const char* location)
{
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
const uint32_t canary = POOL_CANARY_VAL;
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
*canary = POOL_CANARY_VAL;
*loc = location;
/* Метаданные следуют за payload произвольного размера и могут быть невыровненными. */
memcpy((uint8_t*)obj + POOL_CANARY_OFF, &canary, sizeof(canary));
memcpy((uint8_t*)obj + POOL_LOC_OFF, &location, sizeof(location));
const char* free_loc = NULL;
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &free_loc, sizeof(free_loc));
*counter = 1;
}
static void pool_check_and_clear_tags(struct memory_pool* pool, void* obj, const char* location)
{
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
uint32_t canary;
const char *loc, *free_loc;
memcpy(&canary, (uint8_t*)obj + POOL_CANARY_OFF, sizeof(canary));
memcpy(&loc, (uint8_t*)obj + POOL_LOC_OFF, sizeof(loc));
memcpy(&free_loc, (uint8_t*)obj + POOL_FREE_LOC_OFF, sizeof(free_loc));
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
if (*canary != POOL_CANARY_VAL) {
if (canary != POOL_CANARY_VAL) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free BUFFER OVERFLOW pool=%p name=%s sz=%zu allocs=%zu reuse=%zu free=%d",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, pool->free_count);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc=%s free=%s canary=0x%08x expected=0x%08x",
obj, *loc ? *loc : "(null)", location, *canary, POOL_CANARY_VAL);
/* Повреждённый хвост может содержать битые указатели; их нельзя читать как строки. */
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc_loc_ptr=%p free_loc_ptr=%p free_attempt=%s canary=0x%08x expected=0x%08x",
obj, (void*)loc, (void*)free_loc, location, canary, POOL_CANARY_VAL);
if (pool->object_size)
log_dump(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_SYS, " user_data", obj, pool->object_size);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
volatile int _halt = 1;
while (_halt) {}
}
if (*counter == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu alloc=%s free=%s",
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count,
*loc ? *loc : "(null)", location);
obj);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " alloc=%s first_free=%s repeated_free=%s",
loc ? loc : "(null)", free_loc ? free_loc : "(null)", location);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
volatile int _halt = 1;
while (_halt) {}
}
*counter = 0;
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &location, sizeof(location));
}
static size_t g_total_free = 0;
+20 -6
View File
@@ -1,4 +1,13 @@
// memory_pool.h
/* memory_pool — повторное использование блоков фиксированного размера в одном потоке.
* init создаёт пустой пул; alloc выделяет блок по требованию или берёт свободный и обнуляет payload.
* free проверяет защитный хвост и возвращает не более MEMORY_POOL_MAX_FREE блоков в кеш.
* object_size должен вмещать void*: свободный блок хранит next в начале payload.
* Перед destroy вернуть все выданные блоки: destroy освобождает только кеш и сам пул.
* name заимствован до destroy; блок возвращать только своему пулу, внутренней синхронизации нет. */
/* Хвост блока хранит canary, места alloc/free и метку занятости. Повреждение или повторный возврат
* кешированного блока логируется в SYS и навсегда останавливает текущий поток циклом диагностики.
* Пример для очередей: init(sizeof(struct ll_entry) + inline_size, name), затем queue_entry_new_from_pool.
* Объекты, уже переданные в u_free при переполнении кеша, больше нельзя проверять или возвращать. */
#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H
@@ -20,26 +29,31 @@ struct memory_pool {
void* free_head; // Голова linked list свободных блоков (next хранится в начале каждого блока)
int free_count; // Количество свободных блоков
size_t object_size; // Размер объектов в пуле (пользовательские данные)
size_t allocations; // Общее количество аллокаций (включая новые malloc)
size_t allocations; // Попытки выделить новый блок, включая неудачные; выдачи из кеша не учитываются
size_t reuse_count; // Количество повторных использований из пула
uint8_t alloc_tag_counter; // монотонный счётчик для детекции double-free (1 байт)
uint8_t alloc_tag_counter; // Не используется; init устанавливает 1. Double-free проверяется по метке в хвосте блока
const char* name; // Имя пула для диагностики (напр. "data_pool", "pkt_pool")
};
// сам пул:
// если используем с элементами ll_queue то не забываем что к object_size надо прибавить sizeof(struct ll_entry)
// Создаёт пустой пул: object_size >= sizeof(void*), name живёт до destroy; NULL при OOM.
// Для ll_queue размер включает sizeof(struct ll_entry) и пользовательский data[].
struct memory_pool* memory_pool_init(size_t object_size, const char* name);
// Освобождает кеш и пул, но не выданные объекты; NULL допустим. Все владельцы должны предварительно вернуть блоки.
void memory_pool_destroy(struct memory_pool* pool);
// элементы пула:
// Выдаёт обнулённый payload из кеша или нового блока; NULL при NULL pool/OOM. location — место вызова в хвосте.
void* memory_pool_alloc_impl(struct memory_pool* pool, const char* location);
// Возвращает свой живой obj в кеш или u_free, если кеш заполнен; NULL pool/obj ничего не делает.
void memory_pool_free_impl(struct memory_pool* pool, void* obj, const char* location);
// Копирует счётчики попыток новых аллокаций и выдач из кеша; любой выход может быть NULL, NULL pool ничего не меняет.
void memory_pool_get_stats(struct memory_pool* pool, size_t* allocations, size_t* reuse_count);
// Общее число кешированных блоков всех пулов; глобальный счётчик не синхронизирован.
size_t memory_pool_get_total_free_blocks(void);
#define memory_pool_alloc(pool) memory_pool_alloc_impl(pool, PLOCATION)
#define memory_pool_free(pool, obj) memory_pool_free_impl(pool, obj, PLOCATION)
/* Проверяет присутствие в кеше свободных блоков; не определяет состояние уже освобождённого через u_free блока. */
int memory_pool_is_freed(struct memory_pool* pool, void* obj);
-57
View File
@@ -1,57 +0,0 @@
# memory_pool (Object Pool Allocator)
## 1. Назначение
Быстрый аллокатор объектов фиксированного размера. Вместо постоянных `malloc`/`free` хранит до 64 освобождённых объектов в linked list для повторного использования. Применяется для hot-path объектов, которые часто создаются и уничтожаются: пакеты, фрагменты, ACK-пакеты, структуры маршрутизации, TCP-сегменты lwIP и т.д.
Встроенная защита:
- **Buffer overflow** — канарейка `0xDEADBEAF` после пользовательских данных проверяется при освобождении. При повреждении — бесконечный цикл (hang) для отладки.
- **Double free** — однобайтный счётчик в метаданных; повторное освобождение детектируется и вызывает hang.
- Вся диагностика пишется через `DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, ...)`.
Не потокобезопасен — каждый пул используется из одного потока (обычно в рамках одного u_async event loop).
## 2. Как пользоваться
```c
#include "memory_pool.h"
// 1. Создать пул (обычно в init-функции инстанса):
struct memory_pool* pkt_pool = memory_pool_init(sizeof(struct ETCP_DGRAM), "pkt_pool");
// 2. Выделить объект (если есть свободный — вернёт из пула, иначе u_calloc):
struct ETCP_DGRAM* dgram = memory_pool_alloc(pkt_pool);
// 3. Освободить объект (возвращается в пул, если там меньше 64 блоков):
memory_pool_free(pkt_pool, dgram);
// 4. Получить статистику:
size_t allocs, reuse;
memory_pool_get_stats(pkt_pool, &allocs, &reuse);
// reuse много → эффективно; reuse мало → пул слишком мал для нагрузки
// 5. Проверить, в пуле ли объект (0 — не в пуле, 1 — в пуле):
if (memory_pool_is_freed(pkt_pool, obj)) { /* уже освобождён */ }
// 6. Уничтожить пул (при shutdown):
memory_pool_destroy(pkt_pool);
// реально освобождает все кэшированные блоки через u_free() и сам пул.
```
**Важно:**
- Если объекты пула используются как элементы `ll_queue`, в `object_size` нужно закладывать `sizeof(struct ll_entry)`. Например: `memory_pool_init(sizeof(struct ll_entry) + sizeof(struct dummynet_pkt), "pkt_pool")`.
- `memory_pool_alloc` обнуляет объект перед возвратом (защита от утечки старых данных).
- При заполнении пула (64 свободных блока) лишние `memory_pool_free` реально вызывают `u_free` — блок не кэшируется.
- `memory_pool_destroy` проходит по всем свободным блокам и вызывает `u_free` для каждого, затем освобождает сам `struct memory_pool`.
## 3. API
| Функция/макрос | Описание |
|---|---|
| `memory_pool_init(object_size, name)` | Создаёт пул для объектов заданного размера. `name` — для диагностики. |
| `memory_pool_alloc(pool)` | Выделяет объект: из кэша (если есть) или через `u_calloc`. Обнуляет перед возвратом. |
| `memory_pool_free(pool, obj)` | Возвращает объект в пул (до 64) или вызывает `u_free`. Проверяет canary и double-free. |
| `memory_pool_destroy(pool)` | Уничтожает пул: освобождает все кэшированные блоки и сам пул. |
| `memory_pool_get_stats(pool, &allocs, &reuse)` | Статистика: общее число аллокаций и число повторных использований из кэша. |
| `memory_pool_get_total_free_blocks()` | Глобальный счётчик свободных блоков во всех пулах (для диагностики). |
| `memory_pool_is_freed(pool, obj)` | Проверяет, находится ли объект в свободном списке пула (уже освобождён). |
+206 -48
View File
@@ -6780,7 +6780,9 @@ typedef enum
ma_device_notification_type_rerouted,
ma_device_notification_type_interruption_began,
ma_device_notification_type_interruption_ended,
ma_device_notification_type_unlocked
ma_device_notification_type_unlocked,
ma_device_notification_type_capture_overrun, /* uTun: подтверждённый backend разрыв входного PCM. */
ma_device_notification_type_playback_underrun /* uTun: backend исчерпал буфер воспроизведения. */
} ma_device_notification_type;
typedef struct
@@ -7543,11 +7545,13 @@ struct ma_context
ma_proc pa_context_disconnect;
ma_proc pa_context_set_state_callback;
ma_proc pa_context_get_state;
ma_proc pa_context_errno;
ma_proc pa_context_get_sink_info_list;
ma_proc pa_context_get_source_info_list;
ma_proc pa_context_get_sink_info_by_name;
ma_proc pa_context_get_source_info_by_name;
ma_proc pa_operation_unref;
ma_proc pa_operation_cancel;
ma_proc pa_operation_get_state;
ma_proc pa_channel_map_init_extend;
ma_proc pa_channel_map_valid;
@@ -7563,6 +7567,7 @@ struct ma_context
ma_proc pa_stream_get_buffer_attr;
ma_proc pa_stream_set_buffer_attr;
ma_proc pa_stream_get_device_name;
ma_proc pa_stream_get_latency;
ma_proc pa_stream_set_write_callback;
ma_proc pa_stream_set_read_callback;
ma_proc pa_stream_set_suspended_callback;
@@ -7574,6 +7579,7 @@ struct ma_context
ma_proc pa_stream_cork;
ma_proc pa_stream_trigger;
ma_proc pa_stream_begin_write;
ma_proc pa_stream_cancel_write;
ma_proc pa_stream_write;
ma_proc pa_stream_peek;
ma_proc pa_stream_drop;
@@ -11553,6 +11559,17 @@ IMPLEMENTATION
#ifndef miniaudio_c
#define miniaudio_c
/* uTun: необязательные наблюдатели, без изменения ABI и поведения аудиотракта. */
#ifndef MA_DIAGNOSTIC_EVENT
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) ((void)0)
#endif
#ifndef MA_DIAGNOSTIC_DEVICE_INIT
#define MA_DIAGNOSTIC_DEVICE_INIT(device) ((void)0)
#endif
#ifndef MA_DIAGNOSTIC_LOG
#define MA_DIAGNOSTIC_LOG(log, level, message) ((void)0)
#endif
#include <assert.h>
#include <limits.h> /* For INT_MAX */
#include <math.h> /* sin(), etc. */
@@ -13752,6 +13769,7 @@ MA_API ma_result ma_log_post(ma_log* pLog, ma_uint32 level, const char* pMessage
return MA_INVALID_ARGS;
}
MA_DIAGNOSTIC_LOG(pLog, level, pMessage);
ma_log_lock(pLog);
{
ma_uint32 iLog;
@@ -20200,6 +20218,14 @@ static void ma_device__on_notification(ma_device_notification notification)
{
MA_ASSERT(notification.pDevice != NULL);
if (notification.type == ma_device_notification_type_capture_overrun) {
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_READ, 1, 0);
} else if (notification.type == ma_device_notification_type_playback_underrun) {
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0);
} else {
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_NOTIFY, -1, notification.type, ma_device_get_state(notification.pDevice));
}
if (notification.pDevice->onNotification != NULL) {
notification.pDevice->onNotification(&notification);
}
@@ -20360,6 +20386,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
{
float masterVolumeFactor;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_CALLBACK, frameCount, (pFramesIn != NULL) | ((pFramesOut != NULL) << 1));
ma_device_get_master_volume(pDevice, &masterVolumeFactor); /* Use ma_device_get_master_volume() to ensure the volume is loaded atomically. */
if (pDevice->onData) {
@@ -20407,6 +20434,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
ma_silence_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels);
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_CALLBACK, frameCount, 0);
}
@@ -20590,6 +20618,7 @@ static ma_result ma_device__handle_duplex_callback_capture(ma_device* pDevice, m
if (framesToProcessInClientFormat == 0) {
if (ma_pcm_rb_pointer_distance(pRB) == (ma_int32)ma_pcm_rb_get_subbuffer_size(pRB)) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CAPTURE_DROP, AUDIO_DIAG_READ, framesToProcessInDeviceFormat, ma_pcm_rb_available_read(pRB));
break; /* Overrun. Not enough room in the ring buffer for input frame. Excess frames are dropped. */
}
}
@@ -20625,6 +20654,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
ma_result result;
ma_uint8 silentInputFrames[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
ma_uint32 totalFramesReadOut = 0;
ma_silence_pcm_frames(pFramesInInternalFormat, frameCount, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
MA_ASSERT(pDevice != NULL);
MA_ASSERT(frameCount > 0);
@@ -20636,6 +20666,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
Sitting in the ring buffer should be captured data from the capture callback in external format. If there's not enough data in there for
the whole frameCount frames we just use silence instead for the input data.
*/
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_DUPLEX, frameCount, ma_pcm_rb_available_read(pRB));
MA_ZERO_MEMORY(silentInputFrames, sizeof(silentInputFrames));
while (totalFramesReadOut < frameCount && ma_device_is_started(pDevice)) {
@@ -20646,8 +20677,12 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
if (pDevice->playback.inputCacheRemaining > 0) {
ma_uint64 framesConvertedIn = pDevice->playback.inputCacheRemaining;
ma_uint64 framesConvertedOut = (frameCount - totalFramesReadOut);
ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
result = ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
if (result != MA_SUCCESS || (framesConvertedIn == 0 && framesConvertedOut == 0)) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
pDevice->playback.inputCacheConsumed += framesConvertedIn;
pDevice->playback.inputCacheRemaining -= framesConvertedIn;
@@ -20662,30 +20697,34 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
inputFrameCount = (ma_uint32)pDevice->playback.inputCacheCap;
result = ma_pcm_rb_acquire_read(pRB, &inputFrameCount, &pInputFrames);
if (result == MA_SUCCESS) {
if (inputFrameCount > 0) {
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
} else {
if (ma_pcm_rb_pointer_distance(pRB) == 0) {
break; /* Underrun. */
}
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
if (inputFrameCount > 0) {
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
} else {
/* No capture data available. Feed in silence. */
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap, sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
/* Отсутствие микрофонных кадров не останавливает рендер; синтетический ввод не сдвигает capture-ring. */
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap,
sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, silentInputFrames, inputFrameCount);
}
pDevice->playback.inputCacheConsumed = 0;
pDevice->playback.inputCacheRemaining = inputFrameCount;
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
if (result != MA_SUCCESS) {
return result; /* Should never happen. */
}
}
}
if (totalFramesReadOut < frameCount) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_DUPLEX, frameCount - totalFramesReadOut, ma_pcm_rb_available_read(pRB));
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_DUPLEX, totalFramesReadOut, ma_pcm_rb_available_read(pRB));
return MA_SUCCESS;
}
@@ -24793,6 +24832,7 @@ static ma_result ma_device_read__wasapi(ma_device* pDevice, void* pFrames, ma_ui
/* Overrun detection. */
if ((flags & MA_AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) {
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
/* Glitched. Probably due to an overrun. */
/*
@@ -24923,6 +24963,7 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
{
ma_result result = MA_SUCCESS;
ma_uint32 totalFramesProcessed = 0;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, pDevice->wasapi.mappedBufferPlaybackLen);
/* Keep writing to the device until it's stopped or we've consumed all of our input. */
while (ma_device_get_state(pDevice) == ma_device_state_started && totalFramesProcessed < frameCount) {
@@ -25009,6 +25050,7 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
*pFramesWritten = totalFramesProcessed;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, totalFramesProcessed, result);
return result;
}
@@ -29869,6 +29911,7 @@ static ma_result ma_device_read__alsa(ma_device* pDevice, void* pFramesOut, ma_u
continue; /* Try again. */
} else if (resultALSA == -EPIPE) {
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (read)\n");
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
/* Overrun. Recover and try again. If this fails we need to return an error. */
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMCapture, resultALSA, MA_TRUE);
@@ -29924,6 +29967,7 @@ static ma_result ma_device_write__alsa(ma_device* pDevice, const void* pFrames,
continue; /* Try again. */
} else if (resultALSA == -EPIPE) {
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (write)\n");
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_playback_underrun));
/* Underrun. Recover and try again. If this fails we need to return an error. */
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMPlayback, resultALSA, MA_TRUE); /* MA_TRUE=silent (don't print anything on error). */
@@ -30908,11 +30952,13 @@ typedef int (* ma_pa_context_connect_proc) (
typedef void (* ma_pa_context_disconnect_proc) (ma_pa_context* c);
typedef void (* ma_pa_context_set_state_callback_proc) (ma_pa_context* c, ma_pa_context_notify_cb_t cb, void* userdata);
typedef ma_pa_context_state_t (* ma_pa_context_get_state_proc) (const ma_pa_context* c);
typedef int (* ma_pa_context_errno_proc) (const ma_pa_context* c);
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_list_proc) (ma_pa_context* c, ma_pa_sink_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_source_info_list_proc) (ma_pa_context* c, ma_pa_source_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_by_name_proc) (ma_pa_context* c, const char* name, ma_pa_sink_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_source_info_by_name_proc)(ma_pa_context* c, const char* name, ma_pa_source_info_cb_t cb, void* userdata);
typedef void (* ma_pa_operation_unref_proc) (ma_pa_operation* o);
typedef void (* ma_pa_operation_cancel_proc) (ma_pa_operation* o);
typedef ma_pa_operation_state_t (* ma_pa_operation_get_state_proc) (const ma_pa_operation* o);
typedef ma_pa_channel_map* (* ma_pa_channel_map_init_extend_proc) (ma_pa_channel_map* m, unsigned channels, ma_pa_channel_map_def_t def);
typedef int (* ma_pa_channel_map_valid_proc) (const ma_pa_channel_map* m);
@@ -30927,6 +30973,7 @@ typedef const ma_pa_sample_spec* (* ma_pa_stream_get_sample_spec_proc) (
typedef const ma_pa_channel_map* (* ma_pa_stream_get_channel_map_proc) (ma_pa_stream* s);
typedef const ma_pa_buffer_attr* (* ma_pa_stream_get_buffer_attr_proc) (ma_pa_stream* s);
typedef ma_pa_operation* (* ma_pa_stream_set_buffer_attr_proc) (ma_pa_stream* s, const ma_pa_buffer_attr* attr, ma_pa_stream_success_cb_t cb, void* userdata);
typedef int (* ma_pa_stream_get_latency_proc)(const ma_pa_stream* s, ma_uint64* usec, int* negative);
typedef const char* (* ma_pa_stream_get_device_name_proc) (const ma_pa_stream* s);
typedef void (* ma_pa_stream_set_write_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
typedef void (* ma_pa_stream_set_read_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
@@ -30939,6 +30986,7 @@ typedef int (* ma_pa_stream_is_corked_proc) (
typedef ma_pa_operation* (* ma_pa_stream_cork_proc) (ma_pa_stream* s, int b, ma_pa_stream_success_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_stream_trigger_proc) (ma_pa_stream* s, ma_pa_stream_success_cb_t cb, void* userdata);
typedef int (* ma_pa_stream_begin_write_proc) (ma_pa_stream* s, void** data, size_t* nbytes);
typedef int (* ma_pa_stream_cancel_write_proc) (ma_pa_stream* s);
typedef int (* ma_pa_stream_write_proc) (ma_pa_stream* s, const void* data, size_t nbytes, ma_pa_free_cb_t free_cb, int64_t offset, ma_pa_seek_mode_t seek);
typedef int (* ma_pa_stream_peek_proc) (ma_pa_stream* s, const void** data, size_t* nbytes);
typedef int (* ma_pa_stream_drop_proc) (ma_pa_stream* s);
@@ -31126,23 +31174,49 @@ static ma_pa_channel_position_t ma_channel_position_to_pulse(ma_channel position
}
#endif
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
#ifndef MA_PULSE_WAIT_TIMEOUT_MS
#define MA_PULSE_WAIT_TIMEOUT_MS 1000
#endif
static ma_result ma_pulse_wait_step(ma_context* pContext, ma_ptr pMainLoop, ma_timer* pTimer, const char* operation)
{
int resultPA;
if (ma_timer_get_time_in_seconds(pTimer) * 1000 >= MA_PULSE_WAIT_TIMEOUT_MS) {
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR,
"[PulseAudio] %s timed out after %d ms.\n", operation, MA_PULSE_WAIT_TIMEOUT_MS);
return MA_TIMEOUT;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 0, NULL);
if (resultPA < 0) {
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] %s iterate failed: %d.\n", operation, resultPA);
return ma_result_from_pulse(resultPA);
}
if (resultPA == 0) ma_sleep(1);
return MA_SUCCESS;
}
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
{
ma_result result;
ma_timer timer;
ma_pa_operation_state_t state;
MA_ASSERT(pContext != NULL);
MA_ASSERT(pOP != NULL);
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_operation_get_state_proc)pContext->pulse.pa_operation_get_state)(pOP);
if (state == MA_PA_OPERATION_DONE) return MA_SUCCESS;
if (state != MA_PA_OPERATION_RUNNING) {
break; /* Done. */
ma_log_post(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] Operation cancelled.\n");
return MA_ERROR;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "wait");
if (result != MA_SUCCESS) {
((ma_pa_operation_cancel_proc)pContext->pulse.pa_operation_cancel)(pOP);
return result;
}
}
@@ -31165,9 +31239,11 @@ static ma_result ma_wait_for_operation_and_unref__pulse(ma_context* pContext, ma
static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pPulseContext)
{
int resultPA;
ma_result result;
ma_timer timer;
ma_pa_context_state_t state;
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_context_get_state_proc)pContext->pulse.pa_context_get_state)((ma_pa_context*)pPulseContext);
if (state == MA_PA_CONTEXT_READY) {
@@ -31179,10 +31255,8 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
return MA_ERROR;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
}
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "context connect");
if (result != MA_SUCCESS) return result;
}
/* Should never get here. */
@@ -31191,9 +31265,11 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pStream)
{
int resultPA;
ma_result result;
ma_timer timer;
ma_pa_stream_state_t state;
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_stream_get_state_proc)pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pStream);
if (state == MA_PA_STREAM_READY) {
@@ -31205,10 +31281,8 @@ static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, m
return MA_ERROR;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
}
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "stream connect");
if (result != MA_SUCCESS) return result;
}
return MA_SUCCESS;
@@ -31382,10 +31456,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
if (deviceType == ma_device_type_playback) {
ma_pa_sink_info sinkInfo;
MA_ZERO_OBJECT(&sinkInfo);
sinkInfo.index = (ma_uint32)-1;
result = ma_context_get_sink_info__pulse(pContext, NULL, &sinkInfo);
if (result != MA_SUCCESS) {
return result;
}
if (sinkInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
if (pIndex != NULL) {
*pIndex = sinkInfo.index;
@@ -31394,10 +31471,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
if (deviceType == ma_device_type_capture) {
ma_pa_source_info sourceInfo;
MA_ZERO_OBJECT(&sourceInfo);
sourceInfo.index = (ma_uint32)-1;
result = ma_context_get_source_info__pulse(pContext, NULL, &sourceInfo);
if (result != MA_SUCCESS) {
return result;
}
if (sourceInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
if (pIndex != NULL) {
*pIndex = sourceInfo.index;
@@ -31551,7 +31631,7 @@ static void ma_context_get_device_info_sink_callback__pulse(ma_pa_context* pPuls
{
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
if (endOfList > 0) {
if (endOfList != 0 || pInfo == NULL) {
return;
}
@@ -31588,7 +31668,7 @@ static void ma_context_get_device_info_source_callback__pulse(ma_pa_context* pPu
{
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
if (endOfList > 0) {
if (endOfList != 0 || pInfo == NULL) {
return;
}
@@ -31640,6 +31720,7 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
}
result = ma_context_get_default_device_index__pulse(pContext, deviceType, &callbackData.defaultDeviceIndex);
if (result != MA_SUCCESS) return result;
if (deviceType == ma_device_type_playback) {
pOP = ((ma_pa_context_get_sink_info_by_name_proc)pContext->pulse.pa_context_get_sink_info_by_name)((ma_pa_context*)(pContext->pulse.pPulseContext), pDeviceName, ma_context_get_device_info_sink_callback__pulse, &callbackData);
@@ -31648,7 +31729,8 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
}
if (pOP != NULL) {
ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
result = ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
if (result != MA_SUCCESS) goto done;
} else {
result = MA_ERROR;
goto done;
@@ -31747,6 +31829,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
MA_ASSERT(bpf > 0);
frameCount = byteCount / bpf;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_READ, frameCount, deviceState);
framesProcessed = 0;
while (ma_device_get_state(pDevice) == ma_device_state_started && framesProcessed < frameCount) {
@@ -31756,6 +31839,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
int pulseResult = ((ma_pa_stream_peek_proc)pDevice->pContext->pulse.pa_stream_peek)(pStream, &pMappedPCMFrames, &bytesMapped);
if (pulseResult < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
break; /* Failed to map. Abort. */
}
@@ -31764,12 +31848,25 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
if (pMappedPCMFrames != NULL) {
ma_device_handle_backend_data_callback(pDevice, NULL, pMappedPCMFrames, framesMapped);
} else {
/* It's a hole. */
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[PulseAudio] ma_device_on_read__pulse: Hole.\n");
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, framesMapped, 0);
ma_uint8 silence[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
ma_uint64 remaining = framesMapped;
ma_uint32 capacity = sizeof(silence) / bpf;
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
ma_silence_pcm_frames(silence, capacity, pDevice->capture.internalFormat, pDevice->capture.internalChannels);
while (remaining > 0 && ma_device_get_state(pDevice) == ma_device_state_started) {
ma_uint32 n = (ma_uint32)ma_min(remaining, capacity);
#ifdef MA_AUDIO_CAPTURE_HOLE
MA_AUDIO_CAPTURE_HOLE(framesMapped - remaining);
#endif
ma_device_handle_backend_data_callback(pDevice, NULL, silence, n);
remaining -= n;
}
}
pulseResult = ((ma_pa_stream_drop_proc)pDevice->pContext->pulse.pa_stream_drop)(pStream);
if (pulseResult < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
break; /* Failed to drop the buffer. */
}
@@ -31780,6 +31877,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
break;
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_READ, framesProcessed, 0);
}
static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stream* pStream, ma_uint64* pFramesProcessed)
@@ -31798,6 +31896,7 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
deviceState = ma_device_get_state(pDevice);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_MAP, 0, deviceState);
bytesMapped = ((ma_pa_stream_writable_size_proc)pDevice->pContext->pulse.pa_stream_writable_size)(pStream);
if (bytesMapped != (size_t)-1) {
if (bytesMapped > 0) {
@@ -31810,15 +31909,23 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
}
framesMapped = bytesMapped / bpf;
if (framesMapped == 0 || pMappedPCMFrames == NULL) {
((ma_pa_stream_cancel_write_proc)pDevice->pContext->pulse.pa_stream_cancel_write)(pStream);
result = MA_ERROR;
goto done;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesMapped, 0);
if (deviceState == ma_device_state_started || deviceState == ma_device_state_starting) { /* Check for starting state just in case this is being used to do the initial fill. */
ma_device_handle_backend_data_callback(pDevice, pMappedPCMFrames, NULL, framesMapped);
} else {
/* Device is not started. Write silence. */
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.format, pDevice->playback.channels);
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_SUBMIT, framesMapped, 0);
pulseResult = ((ma_pa_stream_write_proc)pDevice->pContext->pulse.pa_stream_write)(pStream, pMappedPCMFrames, bytesMapped, NULL, 0, MA_PA_SEEK_RELATIVE);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_SUBMIT, framesMapped, pulseResult);
if (pulseResult < 0) {
result = ma_result_from_pulse(pulseResult);
goto done; /* Failed to write data to stream. */
@@ -31835,6 +31942,10 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
}
done:
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesProcessed, result);
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, result);
}
if (pFramesProcessed != NULL) {
*pFramesProcessed = framesProcessed;
}
@@ -31850,6 +31961,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
ma_uint64 framesProcessed;
ma_uint32 deviceState;
ma_result result;
ma_bool32 diagnosticNoProgress = MA_FALSE;
MA_ASSERT(pDevice != NULL);
@@ -31866,6 +31978,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
MA_ASSERT(bpf > 0);
frameCount = byteCount / bpf;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, deviceState);
framesProcessed = 0;
while (framesProcessed < frameCount) {
@@ -31882,8 +31995,14 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
break;
}
if (framesProcessedThisIteration == 0 && !diagnosticNoProgress) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_WRITE, 1, frameCount - framesProcessed);
diagnosticNoProgress = MA_TRUE;
}
if (framesProcessedThisIteration == 0) break;
framesProcessed += framesProcessedThisIteration;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, framesProcessed, 0);
}
static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData)
@@ -31902,10 +32021,10 @@ static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData
if (suspended == 1) {
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Suspended.\n");
ma_device__on_notification_stopped(pDevice);
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_began));
} else {
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Resumed.\n");
ma_device__on_notification_started(pDevice);
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_ended));
}
}
@@ -32044,7 +32163,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
if (pDescriptorCapture->sampleRate != 0) {
ss.rate = pDescriptorCapture->sampleRate;
}
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
if (ma_is_little_endian()) {
@@ -32203,7 +32322,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
ss.rate = pDescriptorPlayback->sampleRate;
}
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
if (ma_is_little_endian()) {
ss.format = MA_PA_SAMPLE_FLOAT32LE;
@@ -32375,6 +32494,10 @@ on_error1:
((ma_pa_stream_unref_proc)pDevice->pContext->pulse.pa_stream_unref)((ma_pa_stream*)pDevice->pulse.pStreamCapture);
}
on_error0:
((ma_pa_context_disconnect_proc)pDevice->pContext->pulse.pa_context_disconnect)((ma_pa_context*)pDevice->pulse.pPulseContext);
((ma_pa_context_unref_proc)pDevice->pContext->pulse.pa_context_unref)((ma_pa_context*)pDevice->pulse.pPulseContext);
((ma_pa_mainloop_free_proc)pDevice->pContext->pulse.pa_mainloop_free)((ma_pa_mainloop*)pDevice->pulse.pMainLoop);
MA_ZERO_OBJECT(&pDevice->pulse);
return result;
}
@@ -32504,8 +32627,24 @@ static ma_result ma_device_data_loop__pulse(ma_device* pDevice)
the callbacks deal with it.
*/
while (ma_device_get_state(pDevice) == ma_device_state_started) {
ma_pa_context_state_t contextState = ((ma_pa_context_get_state_proc)pDevice->pContext->pulse.pa_context_get_state)((ma_pa_context*)pDevice->pulse.pPulseContext);
ma_pa_stream_state_t captureState = pDevice->pulse.pStreamCapture ?
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamCapture) : MA_PA_STREAM_READY;
ma_pa_stream_state_t playbackState = pDevice->pulse.pStreamPlayback ?
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamPlayback) : MA_PA_STREAM_READY;
if (contextState == MA_PA_CONTEXT_FAILED || contextState == MA_PA_CONTEXT_TERMINATED ||
captureState == MA_PA_STREAM_FAILED || captureState == MA_PA_STREAM_TERMINATED ||
playbackState == MA_PA_STREAM_FAILED || playbackState == MA_PA_STREAM_TERMINATED) {
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR,
"[PulseAudio] Device failed context=%d capture=%d playback=%d errno=%d.\n", contextState, captureState,
playbackState, ((ma_pa_context_errno_proc)pDevice->pContext->pulse.pa_context_errno)((ma_pa_context*)pDevice->pulse.pPulseContext));
return MA_ERROR;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_LOOP, 0, 0);
resultPA = ((ma_pa_mainloop_iterate_proc)pDevice->pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pDevice->pulse.pMainLoop, 1, NULL);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_LOOP, 0, resultPA);
if (resultPA < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_LOOP, 1, resultPA);
break;
}
}
@@ -32588,11 +32727,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_context_disconnect = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_disconnect");
pContext->pulse.pa_context_set_state_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_set_state_callback");
pContext->pulse.pa_context_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_state");
pContext->pulse.pa_context_errno = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_errno");
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_list");
pContext->pulse.pa_context_get_source_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_list");
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_by_name");
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_by_name");
pContext->pulse.pa_operation_unref = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_unref");
pContext->pulse.pa_operation_cancel = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_cancel");
pContext->pulse.pa_operation_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_get_state");
pContext->pulse.pa_channel_map_init_extend = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_init_extend");
pContext->pulse.pa_channel_map_valid = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_valid");
@@ -32607,6 +32748,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_get_channel_map = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_channel_map");
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_buffer_attr");
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_buffer_attr");
pContext->pulse.pa_stream_get_latency = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_latency");
pContext->pulse.pa_stream_get_device_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_device_name");
pContext->pulse.pa_stream_set_write_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_write_callback");
pContext->pulse.pa_stream_set_read_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_read_callback");
@@ -32619,6 +32761,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_cork = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cork");
pContext->pulse.pa_stream_trigger = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_trigger");
pContext->pulse.pa_stream_begin_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_begin_write");
pContext->pulse.pa_stream_cancel_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cancel_write");
pContext->pulse.pa_stream_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_write");
pContext->pulse.pa_stream_peek = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_peek");
pContext->pulse.pa_stream_drop = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_drop");
@@ -32651,11 +32794,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_context_disconnect_proc _pa_context_disconnect = pa_context_disconnect;
ma_pa_context_set_state_callback_proc _pa_context_set_state_callback = pa_context_set_state_callback;
ma_pa_context_get_state_proc _pa_context_get_state = pa_context_get_state;
ma_pa_context_errno_proc _pa_context_errno = pa_context_errno;
ma_pa_context_get_sink_info_list_proc _pa_context_get_sink_info_list = pa_context_get_sink_info_list;
ma_pa_context_get_source_info_list_proc _pa_context_get_source_info_list = pa_context_get_source_info_list;
ma_pa_context_get_sink_info_by_name_proc _pa_context_get_sink_info_by_name = pa_context_get_sink_info_by_name;
ma_pa_context_get_source_info_by_name_proc _pa_context_get_source_info_by_name= pa_context_get_source_info_by_name;
ma_pa_operation_unref_proc _pa_operation_unref = pa_operation_unref;
ma_pa_operation_cancel_proc _pa_operation_cancel = pa_operation_cancel;
ma_pa_operation_get_state_proc _pa_operation_get_state = pa_operation_get_state;
ma_pa_channel_map_init_extend_proc _pa_channel_map_init_extend = pa_channel_map_init_extend;
ma_pa_channel_map_valid_proc _pa_channel_map_valid = pa_channel_map_valid;
@@ -32670,6 +32815,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_stream_get_channel_map_proc _pa_stream_get_channel_map = pa_stream_get_channel_map;
ma_pa_stream_get_buffer_attr_proc _pa_stream_get_buffer_attr = pa_stream_get_buffer_attr;
ma_pa_stream_set_buffer_attr_proc _pa_stream_set_buffer_attr = pa_stream_set_buffer_attr;
ma_pa_stream_get_latency_proc _pa_stream_get_latency = pa_stream_get_latency;
ma_pa_stream_get_device_name_proc _pa_stream_get_device_name = pa_stream_get_device_name;
ma_pa_stream_set_write_callback_proc _pa_stream_set_write_callback = pa_stream_set_write_callback;
ma_pa_stream_set_read_callback_proc _pa_stream_set_read_callback = pa_stream_set_read_callback;
@@ -32682,6 +32828,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_stream_cork_proc _pa_stream_cork = pa_stream_cork;
ma_pa_stream_trigger_proc _pa_stream_trigger = pa_stream_trigger;
ma_pa_stream_begin_write_proc _pa_stream_begin_write = pa_stream_begin_write;
ma_pa_stream_cancel_write_proc _pa_stream_cancel_write = pa_stream_cancel_write;
ma_pa_stream_write_proc _pa_stream_write = pa_stream_write;
ma_pa_stream_peek_proc _pa_stream_peek = pa_stream_peek;
ma_pa_stream_drop_proc _pa_stream_drop = pa_stream_drop;
@@ -32713,11 +32860,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_context_disconnect = (ma_proc)_pa_context_disconnect;
pContext->pulse.pa_context_set_state_callback = (ma_proc)_pa_context_set_state_callback;
pContext->pulse.pa_context_get_state = (ma_proc)_pa_context_get_state;
pContext->pulse.pa_context_errno = (ma_proc)_pa_context_errno;
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)_pa_context_get_sink_info_list;
pContext->pulse.pa_context_get_source_info_list = (ma_proc)_pa_context_get_source_info_list;
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)_pa_context_get_sink_info_by_name;
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)_pa_context_get_source_info_by_name;
pContext->pulse.pa_operation_unref = (ma_proc)_pa_operation_unref;
pContext->pulse.pa_operation_cancel = (ma_proc)_pa_operation_cancel;
pContext->pulse.pa_operation_get_state = (ma_proc)_pa_operation_get_state;
pContext->pulse.pa_channel_map_init_extend = (ma_proc)_pa_channel_map_init_extend;
pContext->pulse.pa_channel_map_valid = (ma_proc)_pa_channel_map_valid;
@@ -32732,6 +32881,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_get_channel_map = (ma_proc)_pa_stream_get_channel_map;
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)_pa_stream_get_buffer_attr;
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)_pa_stream_set_buffer_attr;
pContext->pulse.pa_stream_get_latency = (ma_proc)_pa_stream_get_latency;
pContext->pulse.pa_stream_get_device_name = (ma_proc)_pa_stream_get_device_name;
pContext->pulse.pa_stream_set_write_callback = (ma_proc)_pa_stream_set_write_callback;
pContext->pulse.pa_stream_set_read_callback = (ma_proc)_pa_stream_set_read_callback;
@@ -32744,6 +32894,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_cork = (ma_proc)_pa_stream_cork;
pContext->pulse.pa_stream_trigger = (ma_proc)_pa_stream_trigger;
pContext->pulse.pa_stream_begin_write = (ma_proc)_pa_stream_begin_write;
pContext->pulse.pa_stream_cancel_write = (ma_proc)_pa_stream_cancel_write;
pContext->pulse.pa_stream_write = (ma_proc)_pa_stream_write;
pContext->pulse.pa_stream_peek = (ma_proc)_pa_stream_peek;
pContext->pulse.pa_stream_drop = (ma_proc)_pa_stream_drop;
@@ -43767,6 +43918,7 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
result = pContext->callbacks.onDeviceInit(pDevice, pConfig, &descriptorPlayback, &descriptorCapture);
if (result != MA_SUCCESS) {
ma_event_uninit(&pDevice->stopEvent);
ma_event_uninit(&pDevice->startEvent);
ma_event_uninit(&pDevice->wakeupEvent);
ma_mutex_uninit(&pDevice->startStopLock);
@@ -44008,6 +44160,8 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_OPEN, -1, pDevice->type, pDevice->sampleRate);
MA_DIAGNOSTIC_DEVICE_INIT(pDevice);
MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped);
return MA_SUCCESS;
}
@@ -44175,6 +44329,7 @@ MA_API void ma_device_uninit(ma_device* pDevice)
ma_free(pDevice->pContext, &allocationCallbacks);
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CLOSE, -1, 0, 0);
MA_ZERO_OBJECT(pDevice);
}
@@ -44506,6 +44661,14 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
if (frameCount == 0) {
return MA_INVALID_ARGS;
}
if (((pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) && pInput == NULL) ||
(pDevice->type == ma_device_type_playback && pOutput == NULL)) {
return MA_INVALID_ARGS;
}
#ifdef MA_AUDIO_TIMING_BEGIN
MA_AUDIO_TIMING_BEGIN(pDevice, frameCount, pInput != NULL);
#endif
if (pDevice->type == ma_device_type_duplex) {
if (pInput != NULL) {
@@ -44517,22 +44680,17 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
}
} else {
if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) {
if (pInput == NULL) {
return MA_INVALID_ARGS;
}
ma_device__send_frames_to_client(pDevice, frameCount, pInput);
}
if (pDevice->type == ma_device_type_playback) {
if (pOutput == NULL) {
return MA_INVALID_ARGS;
}
ma_device__read_frames_from_client(pDevice, frameCount, pOutput);
}
}
#ifdef MA_AUDIO_TIMING_END
MA_AUDIO_TIMING_END();
#endif
return MA_SUCCESS;
}
+17
View File
@@ -146,6 +146,23 @@ int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type) {
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_SIGNAL(signal_type));
}
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application) {
if (!enc || !enc->opus_enc) return -1;
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_APPLICATION(application));
}
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples) {
if (!enc || !samples) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay invalid arguments"); return -1; }
opus_int32 delay = 0;
int result = opus_encoder_ctl(enc->opus_enc, OPUS_GET_LOOKAHEAD(&delay));
if (result != OPUS_OK || delay < 0 || delay > enc->sample_rate) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay query failed result=%d samples=%d", result, (int)delay); return -1;
}
*samples = delay;
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "Opus capture delay samples=%d rate=%d", (int)delay, enc->sample_rate);
return 0;
}
const char* opus_codec_strerror(int err) {
switch (err) {
case OPUS_OK: return "OK";
+13
View File
@@ -1,3 +1,9 @@
/* opus_codec — владеющие обёртки encoder/decoder libopus для int16 interleaved PCM.
* create принимает частоту и 1/2 канала; неположительная частота → 48000, неверные каналы → моно.
* Encoder/decoder имеют состояние потока; каждому нужен один последовательный владелец.
* frame_samples — отсчёты НА КАНАЛ: PCM вмещает frame_samples*channels элементов.
* encode возвращает байты Opus, decode — отсчёты на канал; отрицательное значение означает ошибку.
* out_cap — байты; decode с data=NULL/len=0 использует PLC libopus. Освобождение — соответствующим destroy. */
#ifndef OPUS_CODEC_H
#define OPUS_CODEC_H
@@ -10,6 +16,10 @@ extern "C" {
typedef struct opus_codec_encoder opus_codec_encoder_t;
typedef struct opus_codec_decoder opus_codec_decoder_t;
/* Режим кодирования Opus: голос (VOIP, default) или музыка (AUDIO). */
#define OPUS_CODEC_APP_VOIP 2048 /* OPUS_APPLICATION_VOIP */
#define OPUS_CODEC_APP_AUDIO 2049 /* OPUS_APPLICATION_AUDIO */
opus_codec_encoder_t* opus_codec_encoder_create(int sample_rate, int channels);
void opus_codec_encoder_destroy(opus_codec_encoder_t* enc);
int opus_codec_encode(opus_codec_encoder_t* enc, const int16_t* pcm, int frame_samples,
@@ -23,6 +33,9 @@ int opus_codec_decode(opus_codec_decoder_t* dec, const uint8_t* data, int len,
int opus_codec_encoder_bitrate_set(opus_codec_encoder_t* enc, int bitrate_bps);
int opus_codec_encoder_complexity_set(opus_codec_encoder_t* enc, int complexity);
int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type);
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application);
/* Задержка codec в сэмплах на канал; запрашивать после настройки encoder, не задавать константой. */
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples);
const char* opus_codec_strerror(int err);
+19
View File
@@ -39,6 +39,25 @@
#endif
#endif
/*
* Классифицирует IPv4-адрес (network byte order):
* 1 = глобальный/публичный (маршрутизируемый в интернете),
* 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
*/
int ip_is_public(uint32_t addr_be) {
uint32_t a = ntohl(addr_be);
if ((a & 0xFF000000u) == 0x0A000000u) return 0; /* 10.0.0.0/8 */
if ((a & 0xFFF00000u) == 0xAC100000u) return 0; /* 172.16.0.0/12 */
if ((a & 0xFFFF0000u) == 0xC0A80000u) return 0; /* 192.168.0.0/16 */
if ((a & 0xFF000000u) == 0x7F000000u) return 0; /* 127.0.0.0/8 */
if ((a & 0xFFFF0000u) == 0xA9FE0000u) return 0; /* 169.254.0.0/16 */
if ((a & 0xFF000000u) == 0x00000000u) return 0; /* 0.0.0.0/8 */
if ((a & 0xF0000000u) == 0xE0000000u) return 0; /* 224.0.0.0/4 */
if ((a & 0xF0000000u) == 0xF0000000u) return 0; /* 240.0.0.0/4 */
if ((a & 0xFFC00000u) == 0x64400000u) return 0; /* 100.64.0.0/10 (CGNAT) */
return 1;
}
/*
* Generate cryptographically secure random bytes
* Returns 0 on success, -1 on error
+25 -10
View File
@@ -1,5 +1,13 @@
/**
* Platform compatibility layer for POSIX functions on Windows
* platform_compat — общие заголовки POSIX/Windows и адаптеры отсутствующих POSIX-функций.
* Также предоставляет криптографический random_bytes, адреса интерфейсов, выбор default route
* и классификацию публичных IPv4. Сокетные типы/ошибки и nonblocking API — в socket_compat.h.
* Подключать этот заголовок вместо прямых POSIX includes для общего кода.
* Windows: htobe16/32/64 и be16/32/64toh через byteswap, strcasecmp/strncasecmp через _stricmp/_strnicmp,
* memmem через inline-поиск (пустой образец возвращает начало), poll через WSAPoll, mkdir через _mkdir.
* utun_gettimeofday — календарное Unix-время (не монотонное); Windows игнорирует tz.
* Windows pipe — _pipe(4096, _O_BINARY). fcntl: F_GETFL возвращает 0, F_SETFL с O_NONBLOCK включает
* неблокирующий режим; остальные флаги игнорируются, обратное переключение этим адаптером не выполняется.
*/
#ifndef PLATFORM_COMPAT_H
@@ -176,31 +184,38 @@ extern "C" {
#include <stdint.h>
// Generate cryptographically secure random bytes
// Returns 0 on success, -1 on error
// Заполняет buffer криптографическими случайными байтами: POSIX /dev/urandom, Windows BCryptGenRandom.
// buffer не NULL, len > 0 (Windows: помещается в ULONG). 0 — всё заполнено, -1 — ошибка/неполное чтение.
int random_bytes(uint8_t *buffer, size_t len);
// Get IPv4 address of interface by its index (ifindex)
// Returns IPv4 address in network byte order, 0 on error
// POSIX: getifaddrs; Windows: IP Helper. Адрес в network byte order, 0 при ошибке/отсутствии адреса.
uint32_t get_interface_ip_by_index(uint32_t netif_index);
// Get IPv6 address of interface by its index (ifindex)
// temporary=1: get temporary (privacy) IPv6, temporary=0: get permanent IPv6
// POSIX фильтрует по IFA_F_TEMPORARY в ifa_flags, но getifaddrs не гарантирует адресные флаги;
// для точного выбора temporary/stable на Linux использовать get_interface_ipv6_addr_nl().
// Windows игнорирует temporary: предпочитает глобальный адрес, затем локальный, исключая link-local/loopback/::.
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error or not found
int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out);
// Get IPv6 address using netlink (Linux-only, accurate IFA_F_* flags)
// prefer_stable=1: prefer permanent/stable (IFA_F_MANAGETEMPADDR) over temporary
// prefer_stable=0: prefer temporary over stable
// Выбрать IPv6: Linux использует netlink с IFA_F_*, прочие POSIX — getifaddrs(), Windows — IP Helper.
// POSIX: prefer_stable=1 предпочитает постоянный адрес, 0 — temporary; допускается fallback на другой тип.
// Windows игнорирует prefer_stable, выбор как у get_interface_ipv6_by_index().
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error
int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out);
// Determine the interface index for the default route (outgoing traffic)
// Creates a temp UDP socket, connects to a well-known address to force
// kernel routing, then matches the chosen source IP to getifaddrs()
// POSIX: UDP connect к 8.8.8.8:53 или [2001:4860:4860::8888]:53, сопоставление локального IP с getifaddrs().
// Windows: GetBestInterfaceEx к тому же адресу. Результат — выбранный маршрут к этому адресу,
// который может отличаться от default route при более специфичных маршрутах.
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
uint32_t get_default_route_netif_index(int family);
// Классифицирует IPv4 (network byte order) по встроенному списку: 0 для 0/8, 10/8, 127/8, 169.254/16,
// 172.16/12, 192.168/16, 100.64/10, 224/4 и 240/4; 1 для остальных. Это не проверка достижимости адреса.
int ip_is_public(uint32_t addr_be);
// Cross-platform mkdir
#ifdef _WIN32
-81
View File
@@ -1,81 +0,0 @@
# platform_compat — Cross-platform compatibility layer
## 1. Назначение
Унифицирует различия между POSIX (Linux/FreeBSD) и Windows (MSYS2 UCRT64) на уровне системных вызовов и типов. Предоставляет единый API для байтового порядка, строковых функций, энтропии, работы с сетевыми интерфейсами и времени. Полностью заголовочный (inline) для макросов и тривиальных обёрток, `.c` только для нетривиальных реализаций.
## 2. Как пользоваться
Подключить `"platform_compat.h"` — всё остальное разрешается автоматически в зависимости от `_WIN32`.
### Байтовый порядок
```c
uint16_t v16 = htobe16(x); // host → big-endian
uint32_t v32 = be32toh(x); // big-endian → host
uint64_t v64 = be64toh(x); // big-endian → host
// На Linux использует <endian.h>, на Windows — _byteswap_*
```
### Криптостойкий random
```c
uint8_t salt[8];
if (random_bytes(salt, sizeof(salt)) != 0) { /* ошибка */ }
// Linux: /dev/urandom, Windows: BCryptGenRandom
```
### Сетевые интерфейсы
```c
// Определить интерфейс маршрута по умолчанию
uint32_t ifidx = get_default_route_netif_index(AF_INET); // или AF_INET6
// Получить IPv4 адрес интерфейса по индексу
uint32_t ipv4 = get_interface_ip_by_index(ifidx); // network byte order
// Получить IPv6 адрес (постоянный или временный)
uint8_t ipv6[16];
if (get_interface_ipv6_by_index(ifidx, 1, ipv6) == 0) { // temporary=1
// ipv6 содержит 16 байт адреса
}
```
**Windows**: `get_interface_ip_by_index`, `get_interface_ipv6_by_index`, `get_default_route_netif_index` — заглушки (возвращают 0/-1).
### Время
```c
struct timeval tv;
utun_gettimeofday(&tv, NULL); // макрос: на Linux → gettimeofday, на Windows → inline-реализация
```
### Прочее
- `strcasecmp`/`strncasecmp` — на Windows `_stricmp`/`_strnicmp`
- `memmem` — на Windows inline-реализация `compat_memmem`
- `pipe` — на Windows `_pipe` с `_O_BINARY`
- `poll` — на Windows `WSAPoll`, флаги `POLLIN`/`POLLOUT` и т.д. определены если отсутствуют
- `ssize_t` — определён если отсутствует
- `fcntl` — на Windows упрощённая реализация только для `F_SETFL`/`O_NONBLOCK`
- `utun_mkdir` — кроссплатформенный `mkdir`
### Ограничения
- Функции сетевых интерфейсов работают только на POSIX, на Windows — заглушки
- `get_default_route_netif_index` создаёт временный UDP-сокет к `8.8.8.8` (IPv4) или `2001:4860:4860::8888` (IPv6) для определения интерфейса
- Только IPv4/IPv6; `family` только `AF_INET` или `AF_INET6`
## 3. API
### Байтовый порядок
| Макрос | Назначение |
|--------|------------|
| `htobe16(x)` / `be16toh(x)` | 16-bit host ↔ big-endian |
| `htobe32(x)` / `be32toh(x)` | 32-bit host ↔ big-endian |
| `htobe64(x)` / `be64toh(x)` | 64-bit host ↔ big-endian |
### Основные функции
| Функция | Назначение |
|---------|------------|
| `random_bytes(buffer, len)` | Криптостойкие случайные байты. 0 — успех, -1 — ошибка |
| `get_interface_ip_by_index(ifindex)` | IPv4 адрес интерфейса в network byte order, 0 при ошибке |
| `get_interface_ipv6_by_index(ifindex, temporary, out)` | IPv6 адрес (temporary=1 — временный privacy-адрес), 0 — успех |
| `get_default_route_netif_index(family)` | ifindex дефолтного маршрута через connect() к внешнему IP |
### Макросы-заменители (условные)
| Макрос | Назначение |
|--------|------------|
| `utun_gettimeofday(tv, tz)` | gettimeofday (inline на Windows) |
| `utun_mkdir(path, mode)` | mkdir |
+249
View File
@@ -0,0 +1,249 @@
#include <string.h>
#include "mem.h"
#include "debug_config.h"
#include "reed_solomon.h"
struct rs_codec {
unsigned n;
unsigned k;
size_t max_block_size;
uint8_t gf_log[256];
uint8_t gf_exp[510];
uint8_t *generator;
uint8_t *work;
uint8_t storage[];
};
/* Умножение в GF(256) с примитивным полиномом x^8+x^4+x^3+x^2+1 (0x11d). */
static uint8_t rs_multiply(const struct rs_codec *ctx, uint8_t a, uint8_t b) {
return a && b ? ctx->gf_exp[ctx->gf_log[a] + ctx->gf_log[b]] : 0;
}
/* Обратить k строк матрицы; результат — правая половина рабочей матрицы [A|I]. */
static int rs_invert(struct rs_codec *ctx, const uint8_t *const rows[]) {
unsigned k = ctx->k;
unsigned stride = 2 * k;
for (unsigned i = 0; i < k; i++) {
uint8_t *row = ctx->work + i * stride;
memcpy(row, rows[i], k);
memset(row + k, 0, k);
row[k + i] = 1;
}
for (unsigned col = 0; col < k; col++) {
unsigned pivot = col;
while (pivot < k && !ctx->work[pivot * stride + col]) pivot++;
if (pivot == k) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: singular matrix n=%u k=%u column=%u", ctx->n, k, col);
return RS_ERR_MATRIX;
}
uint8_t *row = ctx->work + col * stride;
if (pivot != col) {
uint8_t *other = ctx->work + pivot * stride;
for (unsigned j = 0; j < stride; j++) {
uint8_t tmp = row[j];
row[j] = other[j];
other[j] = tmp;
}
}
uint8_t inverse = ctx->gf_exp[255 - ctx->gf_log[row[col]]];
for (unsigned j = col; j < stride; j++) row[j] = rs_multiply(ctx, row[j], inverse);
for (unsigned i = 0; i < k; i++) {
if (i == col) continue;
uint8_t *other = ctx->work + i * stride;
uint8_t factor = other[col];
if (!factor) continue;
for (unsigned j = col; j < stride; j++) other[j] ^= rs_multiply(ctx, factor, row[j]);
}
}
return RS_OK;
}
/* Построить систематическую матрицу G=V*inverse(V[0:k]) в точках 2^i поля GF(256). */
static int rs_build_generator(struct rs_codec *ctx) {
unsigned k = ctx->k;
if (ctx->n != k) {
const uint8_t *rows[RS_MAX_BLOCKS];
for (unsigned i = 0; i < ctx->n; i++) {
uint8_t value = 1;
for (unsigned j = 0; j < k; j++) {
ctx->generator[i * k + j] = value;
value = rs_multiply(ctx, value, ctx->gf_exp[i]);
}
}
for (unsigned i = 0; i < k; i++) rows[i] = ctx->generator + i * k;
int rc = rs_invert(ctx, rows);
if (rc != RS_OK) return rc;
for (unsigned i = k; i < ctx->n; i++) {
uint8_t result[RS_MAX_BLOCKS];
for (unsigned j = 0; j < k; j++) {
uint8_t value = 0;
for (unsigned t = 0; t < k; t++) {
value ^= rs_multiply(ctx, ctx->generator[i * k + t], ctx->work[t * 2 * k + k + j]);
}
result[j] = value;
}
memcpy(ctx->generator + i * k, result, k);
}
}
memset(ctx->generator, 0, k * k);
for (unsigned i = 0; i < k; i++) ctx->generator[i * k + i] = 1;
return RS_OK;
}
/* Инициализация параметров, таблиц поля и матрицы; все рабочие данные в одной аллокации. */
int rs_init(struct rs_codec **out, unsigned n, unsigned r, size_t block_size) {
if (out) *out = NULL;
if (!out || !n || n > RS_MAX_BLOCKS || r >= n || block_size <= RS_HEADER_SIZE || block_size > RS_MAX_BLOCK_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid init ctx=%p n=%u r=%u block_size=%zu", (void *)out, n, r, block_size);
return RS_ERR_INVALID_ARG;
}
unsigned k = n - r;
size_t bytes = sizeof(struct rs_codec) + n * k + 2 * k * k;
struct rs_codec *ctx = u_calloc(1, (uint32_t)bytes);
if (!ctx) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: allocation failed n=%u r=%u block_size=%zu bytes=%zu", n, r, block_size, bytes);
return RS_ERR_MEMORY;
}
ctx->n = n;
ctx->k = k;
ctx->max_block_size = block_size;
ctx->generator = ctx->storage;
ctx->work = ctx->storage + n * k;
unsigned value = 1;
for (unsigned i = 0; i < 255; i++) {
ctx->gf_exp[i] = (uint8_t)value;
ctx->gf_log[value] = (uint8_t)i;
value <<= 1;
if (value & 256) value ^= 0x11d;
}
for (unsigned i = 255; i < 510; i++) ctx->gf_exp[i] = ctx->gf_exp[i - 255];
int rc = rs_build_generator(ctx);
if (rc != RS_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: generator failed n=%u k=%u rc=%d", n, k, rc);
u_free(ctx);
return rc;
}
*out = ctx;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: initialized n=%u r=%u max_block_size=%zu input_capacity=%zu memory=%zu",
n, r, block_size, k * (block_size - RS_HEADER_SIZE), bytes);
return RS_OK;
}
void rs_free(struct rs_codec *ctx) {
if (!ctx) return;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: free n=%u r=%u max_block_size=%zu", ctx->n, ctx->n - ctx->k, ctx->max_block_size);
u_free(ctx);
}
size_t rs_input_capacity(const struct rs_codec *ctx) {
if (!ctx) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: input_capacity requires a context");
return 0;
}
return ctx->k * (ctx->max_block_size - RS_HEADER_SIZE);
}
/* Записать заголовки и исходные субблоки, затем вычислить r проверочных субблоков. */
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
uint8_t *const blocks[], size_t *block_size) {
if (!ctx || !input || !blocks || !block_size || !length || length > rs_input_capacity(ctx)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid encode ctx=%p input=%p blocks=%p length=%zu size_out=%p",
(void *)ctx, (const void *)input, (const void *)blocks, length, (void *)block_size);
return RS_ERR_INVALID_ARG;
}
size_t payload_size = (length + ctx->k - 1) / ctx->k;
for (unsigned i = 0; i < ctx->n; i++) {
if (!blocks[i]) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: missing encode buffer seq=%llu subblock=%u", (unsigned long long)seq, i);
return RS_ERR_INVALID_ARG;
}
}
for (unsigned i = 0; i < ctx->n; i++) {
for (unsigned j = 0; j < 8; j++) blocks[i][j] = (uint8_t)(seq >> (56 - 8 * j));
blocks[i][8] = (uint8_t)i;
uint8_t *payload = blocks[i] + RS_HEADER_SIZE;
memset(payload, 0, payload_size);
if (i < ctx->k) {
size_t offset = i * payload_size;
size_t take = offset < length ? length - offset : 0;
if (take > payload_size) take = payload_size;
if (take) memcpy(payload, input + offset, take);
continue;
}
for (unsigned j = 0; j < ctx->k; j++) {
uint8_t factor = ctx->generator[i * ctx->k + j];
if (!factor) continue;
const uint8_t *source = blocks[j] + RS_HEADER_SIZE;
for (size_t b = 0; b < payload_size; b++) payload[b] ^= rs_multiply(ctx, factor, source[b]);
}
}
*block_size = RS_HEADER_SIZE + payload_size;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: encoded seq=%llu n=%u r=%u length=%zu padding=%zu block_size=%zu max=%zu",
(unsigned long long)seq, ctx->n, ctx->n - ctx->k, length, ctx->k * payload_size - length,
*block_size, ctx->max_block_size);
return RS_OK;
}
/* Проверить всю пачку и восстановить только отсутствующие исходные субблоки. */
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
uint8_t *output, size_t output_capacity) {
if (!ctx || !blocks || !output || block_size <= RS_HEADER_SIZE || block_size > ctx->max_block_size
|| output_capacity < ctx->k * (block_size - RS_HEADER_SIZE)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid decode ctx=%p blocks=%p output=%p block_size=%zu capacity=%zu",
(void *)ctx, (const void *)blocks, (void *)output, block_size, output_capacity);
return RS_ERR_INVALID_ARG;
}
size_t payload_size = block_size - RS_HEADER_SIZE;
const uint8_t *rows[RS_MAX_BLOCKS];
unsigned selected[RS_MAX_BLOCKS];
unsigned count = 0;
unsigned missing = 0;
uint64_t seq = 0;
for (unsigned i = 0; i < ctx->n; i++) {
if (!blocks[i]) {
if (i < ctx->k) missing++;
continue;
}
uint64_t block_seq = 0;
for (unsigned j = 0; j < 8; j++) block_seq = (block_seq << 8) | blocks[i][j];
if (blocks[i][8] != i || (count && block_seq != seq)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid header slot=%u subblock=%u seq=%llu expected_seq=%llu",
i, blocks[i][8], (unsigned long long)block_seq, (unsigned long long)(count ? seq : block_seq));
return RS_ERR_HEADER;
}
seq = block_seq;
if (count < ctx->k) {
selected[count] = i;
rows[count] = ctx->generator + i * ctx->k;
}
count++;
}
if (count < ctx->k) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: insufficient blocks seq=%llu received=%u required=%u n=%u",
(unsigned long long)seq, count, ctx->k, ctx->n);
return RS_ERR_INSUFFICIENT;
}
if (missing) {
int rc = rs_invert(ctx, rows);
if (rc != RS_OK) return rc;
}
for (unsigned i = 0; i < ctx->k; i++) {
uint8_t *target = output + i * payload_size;
if (blocks[i]) {
memcpy(target, blocks[i] + RS_HEADER_SIZE, payload_size);
continue;
}
memset(target, 0, payload_size);
for (unsigned j = 0; j < ctx->k; j++) {
uint8_t factor = ctx->work[i * 2 * ctx->k + ctx->k + j];
if (!factor) continue;
const uint8_t *source = blocks[selected[j]] + RS_HEADER_SIZE;
for (size_t b = 0; b < payload_size; b++) target[b] ^= rs_multiply(ctx, factor, source[b]);
}
DEBUG_TRACE(DEBUG_CATEGORY_GENERAL, "reed_solomon: recovered seq=%llu subblock=%u", (unsigned long long)seq, i);
}
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: decoded seq=%llu received=%u recovered=%u output_size=%zu",
(unsigned long long)seq, count, missing, ctx->k * payload_size);
return RS_OK;
}
+73
View File
@@ -0,0 +1,73 @@
/**
* Reed–Solomon: кодирование и восстановление одной пачки по известным пропускам.
* rs_init задаёт n кодограмм, r проверочных и максимальный размер каждой кодограммы.
* Первые n-r кодограмм содержат исходные данные, остальные — проверочные.
* Формат: seq (8 байт, big-endian), subblock (1 байт), полезные данные.
* blocks[i] всегда соответствует subblock=i; при decode NULL обозначает пропуск.
* Любые n-r присутствующих кодограмм восстанавливают всю исходную пачку.
* Полученные данные и заголовки должны быть проверены транспортом на повреждения.
* Все входные/выходные буферы принадлежат вызывающему коду и не перекрываются.
* Кодек выделяет рабочую память только при init; один контекст используют последовательно.
* encode принимает действительную длину, выбирает минимальный одинаковый размер частей
* и дополняет хвост максимум n-r-1 нулями. Размер и исходную длину хранит вызывающий код.
*/
#ifndef REED_SOLOMON_H
#define REED_SOLOMON_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RS_HEADER_SIZE 9
#define RS_MAX_BLOCK_SIZE 4096
#define RS_MAX_BLOCKS 255
enum rs_result {
RS_OK = 0,
RS_ERR_INVALID_ARG = -1,
RS_ERR_MEMORY = -2,
RS_ERR_INSUFFICIENT = -3,
RS_ERR_HEADER = -4,
RS_ERR_MATRIX = -5
};
struct rs_codec;
/* Создать кодек: 1 <= n <= 255, 0 <= r < n, 10 <= max_block_size <= 4096.
* ctx — выходной указатель; при ошибке *ctx=NULL. Старый контекст сначала освобождают.
* Возвращает RS_OK или отрицательный rs_result.
*/
int rs_init(struct rs_codec **ctx, unsigned n, unsigned r, size_t max_block_size);
/* Освободить контекст и рабочую память; NULL допустим. */
void rs_free(struct rs_codec *ctx);
/* Максимальная длина входа encode: (n-r)*(max_block_size-RS_HEADER_SIZE).
* Для NULL возвращает 0 и логирует ошибку.
*/
size_t rs_input_capacity(const struct rs_codec *ctx);
/* Читать 0 < length <= rs_input_capacity(ctx) байт input, записать n кодограмм.
* blocks — n ненулевых указателей на буферы ёмкостью max_block_size каждый.
* block_size получает RS_HEADER_SIZE+ceil(length/(n-r)); хвост за этим размером не меняется.
* При ошибке аргументов выходные буферы и block_size не изменяются.
*/
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
uint8_t *const blocks[], size_t *block_size);
/* Восстановить (n-r)*(block_size-RS_HEADER_SIZE) байт, включая нулевой хвост.
* Ненулевые буферы содержат block_size байт; NULL обозначает пропуск. Размер общий для пачки.
* output_capacity проверяется до записи; block_size не превышает максимум контекста.
* Проверяет одинаковый seq и subblock=i у всех присутствующих кодограмм.
* Входные буферы не изменяются. При ошибке output не изменяется.
*/
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
uint8_t *output, size_t output_capacity);
#ifdef __cplusplus
}
#endif
#endif
+29 -10
View File
@@ -22,17 +22,24 @@
* • Все динамические данные (строки, массивы, списки) выделяются через u_malloc.
* • При encode длина переменных полей кодируется всегда 2 байтами (uint16_t, big-endian).
* • Для linked list в буфере сохраняется только количество узлов + сырые данные узлов
* (next-указатели НЕ сериализуются, они восстанавливаются при decode).
* (включая байты next/padding; decode заменяет next восстановленными ссылками).
* • serialize_decode принимает буфер БЕЗ заголовка.
* • serialize_free освобождает ВСЮ структуру и все вложенные динамические объекты.
* • Поля в schema.fields должны идти в порядке возрастания offset.
* • max_size (если > 0) — жёсткий лимит размера выходного буфера.
* • serialize_free освобождает структуру, динамические поля схемы и узлы списков; обнуляет указатель на структуру.
* Указатели внутри элементов массивов и узлов списков рекурсивно не освобождаются.
* • Поля кодируются в порядке schema.fields; фиксированные данные копируются без смены endian.
* • max_size (если > 0) — лимит encode; header при header_len > 0 обязателен.
* • При encode counts больше 65535 молча усекаются: сохраняются первые 65535 элементов массива/списка или байтов строки.
* • elem_size=1 добавляет NUL и для byte-массивов.
* • Linked nodes копируются целиком, без рекурсивного кодирования вложенных указателей.
* • Этот формат зависит от ABI сырых полей; сериализация не нормализует размер/выравнивание структур.
* • Схема доверенная: offsets, типы, размеры и выравнивание полей/next проверяет её автор, не decode.
* • Независимые операции не используют общее изменяемое состояние; доступ к одной структуре согласует владелец.
*
* @example
* // 1. Описание структуры
* typedef struct Node {
* uint32_t value;
* struct Node* next; // next по offset = 4
* struct Node* next; // положение задаётся offsetof(Node, next)
* } Node;
*
* typedef struct {
@@ -100,7 +107,7 @@ struct SerializeField {
uint16_t offset; ///< смещение поля в структуре
uint8_t elem_size; ///< размер элемента (для fixed = размер данных, для array/linked = размер одного элемента)
uint8_t type; ///< SERIALIZE_TYPE_*
uint16_t len_offset; ///< смещение поля-счётчика (игнорируется для type 0,1,2)
uint16_t len_offset; ///< type 3–5: смещение счётчика в структуре; type 6: смещение next в узле; type 0–2: игнорируется
};
/**
@@ -111,25 +118,37 @@ struct SerializeSchema {
uint16_t struct_size; ///< размер основной структуры
uint16_t max_size; ///< максимальный размер сериализованных данных (0 = без ограничения)
uint16_t header_len; ///< длина заголовка
const struct SerializeField* fields;
const struct SerializeField* fields; ///< field_count описателей в порядке записи; схема заимствуется на время вызова
};
#define SERIALIZE_ERR_OK 0 ///< успех
#define SERIALIZE_ERR_BUF -1 ///< ошибка выделения памяти
#define SERIALIZE_ERR_BUF -1 ///< нехватка памяти; decode также: обрезанные данные или неизвестный тип поля
#define SERIALIZE_ERR_NULL -2 ///< NULL-указатель
#define SERIALIZE_ERR_SIZE -3 ///< превышен max_size
#define SERIALIZE_ERR_NOTSUP -4 ///< неподдерживаемый тип
#define SERIALIZE_ERR_SIZE -3 ///< encode: превышен max_size
#define SERIALIZE_ERR_NOTSUP -4 ///< encode: неподдерживаемый тип поля
/* Кодирует structure по schema; при header_len > 0 header указывает минимум на header_len байт.
* При успехе *out_buf — новый блок u_malloc (освободить u_free), *out_len — фактически записанная длина с header.
* Входная структура и её вложенные данные остаются у владельца. На ошибке выходные значения не меняются.
* Возврат: OK, NULL (обязательные аргументы), BUF (OOM), SIZE (max_size), NOTSUP (неизвестный тип).
* ASCIIZ требует elem_size=1; порядок fields задаёт формат, сортировка по offset не требуется. */
int serialize_encode(const void* structure,
const struct SerializeSchema* schema,
const uint8_t* header,
uint8_t** out_buf,
size_t* out_len);
/* Декодирует in_len байт БЕЗ header: передавать buf + header_len, len - header_len.
* При успехе *structure — новая структура с выделенными полями; очищать serialize_free с той же схемой.
* При ошибке частично созданные объекты освобождаются, *structure = NULL. Старый *structure не освобождает.
* max_size не ограничивает decode; хвост после последнего поля игнорируется.
* Возврат: OK, NULL (аргументы), BUF (OOM, обрезанные данные или неизвестный тип). */
int serialize_decode(const uint8_t* in_buf, size_t in_len,
const struct SerializeSchema* schema,
void** structure);
// Освобождает выделенную через u_malloc структуру, поля схемы и узлы списков, обнуляет *structure; NULL допустим.
// Вложенные указатели внутри элементов массивов/узлов не обходит; к структуре на стеке применять нельзя.
void serialize_free(const struct SerializeSchema* schema, void** structure);
-117
View File
@@ -1,117 +0,0 @@
# Binary Serialization Library
## 1. Назначение
Библиотека бинарной сериализации C-структур с динамическими полями (строки, массивы, singly-linked списки) в компактный бинарный буфер и обратно. Предназначена для кодирования/декодирования сообщений сетевого протокола (контрольный сервер, синхронизация БД, обмен конфигурацией).
Что сериализуется:
- Фиксированные поля (целые, флаги, структуры) — копируются «как есть».
- ASCIIZ-строки (`char*`) — длина определяется через `strlen`, сохраняется с терминальным нулём.
- Динамические массивы (`uint8_t*`) — счётчик элементов в поле `UINT8`/`UINT16`/`UINT32` или ровно 1 элемент (`ARRAY_FIXED`).
- Singly-linked списки — `next`-указатели не сериализуются, сохраняются только `count+data`.
Формат: header (`header_len` байт, передаётся отдельно) + данные полей. Длина переменных полей кодируется 2 байтами (`uint16_t`, big-endian).
Потокобезопасность: нет, однопоточное использование.
## 2. Как пользоваться
```c
#include "../lib/serialize.h"
// --- 1. Определяем структуру ---
typedef struct Node {
uint32_t id;
struct Node *next;
} Node;
typedef struct {
uint8_t version;
uint8_t name_len;
char *name; // ASCIIZ (elem_size=1)
uint16_t addrs_cnt;
uint8_t *addrs; // массив байт (elem_size=1)
Node *list; // linked list
} Message;
// --- 2. Описываем схему ---
static const struct SerializeField msg_fields[] = {
{offsetof(Message, version), sizeof(uint8_t), SERIALIZE_TYPE_FIXED, 0},
{offsetof(Message, name), 1, SERIALIZE_TYPE_ARRAY_U8, offsetof(Message, name_len)},
{offsetof(Message, addrs), 1, SERIALIZE_TYPE_ARRAY_U16, offsetof(Message, addrs_cnt)},
{offsetof(Message, list), sizeof(Node), SERIALIZE_TYPE_LINKED, offsetof(Node, next)}
};
static const struct SerializeSchema msg_schema = {
.field_count = 4,
.struct_size = sizeof(Message),
.max_size = 4096, // 0 = без лимита
.header_len = 4, // байты заголовка (напр. версия протокола)
.fields = msg_fields
};
// --- 3. Сериализация ---
Message msg = { .version = 1, .name = "test", .name_len = 4, .list = NULL };
msg.addrs_cnt = 2;
msg.addrs = u_malloc(2); msg.addrs[0] = 0xAA; msg.addrs[1] = 0xBB;
uint8_t header[] = {0x01, 0x00, 0x00, 0x00}; // 4-байтный заголовок
uint8_t *buf = NULL;
size_t len;
if (serialize_encode(&msg, &msg_schema, header, &buf, &len) != SERIALIZE_ERR_OK)
goto fail;
// buf содержит: header (4) + version (1) + [len=2]name(4) + '\0' + [len=2]addrs(2)
// --- 4. Десериализация (buf без заголовка!) ---
Message *restored = NULL;
uint8_t *data_ptr = buf + msg_schema.header_len; // пропускаем header
if (serialize_decode(data_ptr, len - msg_schema.header_len, &msg_schema, (void**)&restored) != SERIALIZE_ERR_OK)
goto fail;
// restored->name == "test", restored->name_len == 4
// restored->addrs[0] == 0xAA, restored->addrs_cnt == 2
// restored->list == NULL (список пуст — не падает)
// --- 5. Очистка ---
serialize_free(&msg_schema, (void**)&restored); // освобождает всё: name, addrs, list, структуру
u_free(buf);
```
**Ключевые нюансы:**
- Поля в `schema.fields` должны идти в порядке возрастания `offset` в структуре.
- `serialize_decode` принимает буфер **без заголовка** — передаётся `buf + schema.header_len`.
- `serialize_free` освобождает структуру и все вложенные динамические поля (строки, массивы, списки).
- При ошибке `serialize_decode` **автоматически освобождает** всю частично выделенную память → `*structure = NULL`.
- `max_size > 0` — жёсткий лимит, при превышении `SERIALIZE_ERR_SIZE`.
- Длина переменных полей всегда 2 байта (максимум 65535 элементов).
- ASCIIZ-строки (`elem_size=1`) сохраняются с терминальным `\0`; пустая строка → 1 байт (`\0`).
- Для linked list поле `len_offset` указывает смещение `next`-указателя внутри узла.
- Передавать буфер в `serialize_decode` **без заголовка** — он не знает про `header_len`.
## 3. API
| Функция | Назначение |
|---------|-----------|
| `serialize_encode(structure, schema, header, &buf, &len)` | Кодирует структуру. `header` копируется в начало `buf`. |
| `serialize_decode(in_buf, in_len, schema, &structure)` | Декодирует буфер **без заголовка**. При ошибке всё освобождает. |
| `serialize_free(schema, &structure)` | Рекурсивно освобождает структуру и динамические поля, затем обнуляет указатель. |
**Типы полей (`SERIALIZE_TYPE_*`):**
| Тип | `data` | `elem_size` | `len_offset` |
|-----|--------|-------------|--------------|
| `FIXED` (0) | Встроенные данные | Размер поля | Не исп. |
| `ASCIIZ` (1) | `char*` | 1 | Не исп. (длина = `strlen`) |
| `ARRAY_FIXED` (2) | `uint8_t*` (ровно 1 элемент) | Размер элемента | Не исп. |
| `ARRAY_U8` (3) | `uint8_t*` | Размер элемента | Смещение `uint8_t`-счётчика |
| `ARRAY_U16` (4) | `uint8_t*` | Размер элемента | Смещение `uint16_t`-счётчика |
| `ARRAY_U32` (5) | `uint8_t*` | Размер элемента | Смещение `uint32_t`-счётчика |
| `LINKED` (6) | `void*` (голова списка) | Размер узла | Смещение `next` внутри узла |
**Коды возврата:**
- `SERIALIZE_ERR_OK (0)` — успех.
- `SERIALIZE_ERR_BUF (-1)` — ошибка выделения памяти.
- `SERIALIZE_ERR_NULL (-2)` — NULL-указатель во входных параметрах.
- `SERIALIZE_ERR_SIZE (-3)` — превышен `max_size`.
- `SERIALIZE_ERR_NOTSUP (-4)` — неподдерживаемый тип поля.
+312
View File
@@ -0,0 +1,312 @@
#include "silero_vad.h"
#include "debug_config.h"
#include "mem.h"
#ifdef HAVE_SILERO_VAD
#include <stdio.h>
#include <string.h>
#include <onnxruntime/onnxruntime_c_api.h>
#include "silero_vad_model.inc"
/* Число float-элементов рекуррентного состояния модели: 2 * 1 * 128 */
#define SILERO_VAD_STATE_COUNT 256
/* ONNX-вход: 64 отсчёта предыдущего окна + 512 новых отсчётов @16кГц. */
#define SILERO_VAD_CONTEXT_SAMPLES 64
#define SILERO_VAD_INPUT_SAMPLES (SILERO_VAD_CONTEXT_SAMPLES + SILERO_VAD_WINDOW_SAMPLES)
struct silero_vad {
const OrtApi* ort;
OrtEnv* env;
OrtSession* session;
OrtMemoryInfo* mem_info;
float state[SILERO_VAD_STATE_COUNT]; /* рекуррентное состояние между вызовами */
float context[SILERO_VAD_CONTEXT_SAMPLES]; /* звуковой контекст предыдущего окна */
uint8_t* model_data; /* байты .onnx, загруженные в память (для file-версии) */
size_t model_len;
};
/* Освободить OrtStatus и залогировать его сообщение как ошибку */
static void vad_report_ort_error(const OrtApi* ort, OrtStatus* status, const char* what) {
const char* msg = ort->GetErrorMessage(status);
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "%s: %s", what, msg ? msg : "unknown onnxruntime error");
ort->ReleaseStatus(status);
}
/* Прочитать файл целиком в буфер. 0 при успехе: буфер пишется в out/out_len (u_malloc). */
static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) {
FILE* f;
long size;
uint8_t* buf;
*out = NULL;
*out_len = 0;
f = fopen(path, "rb");
if (!f) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: cannot open model '%s'", path);
return -1;
}
if (fseek(f, 0, SEEK_END) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: fseek failed on '%s'", path);
fclose(f);
return -1;
}
size = ftell(f);
if (size <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: empty or unreadable model '%s'", path);
fclose(f);
return -1;
}
rewind(f);
buf = (uint8_t*)u_malloc((size_t)size);
if (!buf) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: OOM for %ld bytes", size);
fclose(f);
return -1;
}
if (fread(buf, 1, (size_t)size, f) != (size_t)size) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: short read on '%s'", path);
u_free(buf);
fclose(f);
return -1;
}
fclose(f);
*out = buf;
*out_len = (size_t)size;
return 0;
}
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
* owned_buf передаётся детектору только при успехе; при ошибке остаётся у вызывающего.
* Для вшитой модели owned_buf == NULL, её байты не освобождаются. */
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len,
const uint8_t* owned_buf, const char* label) {
silero_vad_t* vad = NULL;
const OrtApi* ort;
OrtStatus* status;
OrtSessionOptions* opts = NULL;
if (!data || len == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): empty model", label);
return NULL;
}
ort = OrtGetApiBase() ? OrtGetApiBase()->GetApi(ORT_API_VERSION) : NULL;
if (!ort) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): onnxruntime C API unavailable", label);
return NULL;
}
vad = (silero_vad_t*)u_calloc(1, sizeof(*vad));
if (!vad) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): OOM", label);
return NULL;
}
vad->ort = ort;
vad->model_len = len;
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env);
if (status) { vad_report_ort_error(ort, status, "CreateEnv"); goto fail; }
status = ort->CreateSessionOptions(&opts);
if (status) { vad_report_ort_error(ort, status, "CreateSessionOptions"); goto fail; }
status = ort->SetIntraOpNumThreads(opts, 1);
if (status) { vad_report_ort_error(ort, status, "SetIntraOpNumThreads"); }
status = ort->SetSessionGraphOptimizationLevel(opts, ORT_ENABLE_ALL);
if (status) { vad_report_ort_error(ort, status, "SetSessionGraphOptimizationLevel"); }
status = ort->CreateSessionFromArray(vad->env, data, len, opts, &vad->session);
if (status) { vad_report_ort_error(ort, status, "CreateSessionFromArray"); goto fail; }
status = ort->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &vad->mem_info);
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; }
ort->ReleaseSessionOptions(opts);
vad->model_data = (uint8_t*)owned_buf;
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu window=%d context=%d input=%d", label, len,
SILERO_VAD_WINDOW_SAMPLES, SILERO_VAD_CONTEXT_SAMPLES, SILERO_VAD_INPUT_SAMPLES);
return vad;
fail:
if (opts) ort->ReleaseSessionOptions(opts);
silero_vad_destroy(vad);
return NULL;
}
silero_vad_t* silero_vad_create(const char* model_path) {
uint8_t* buf = NULL;
size_t len = 0;
silero_vad_t* vad;
if (!model_path || !*model_path) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: empty model path");
return NULL;
}
if (vad_read_file(model_path, &buf, &len) != 0) return NULL;
vad = silero_vad_create_from_bytes(buf, len, buf, model_path);
if (!vad) u_free(buf);
return vad;
}
silero_vad_t* silero_vad_create_default(void) {
return silero_vad_create_from_bytes(silero_vad_model_data, silero_vad_model_len, NULL, "embedded");
}
void silero_vad_destroy(silero_vad_t* vad) {
if (!vad) return;
const OrtApi* ort = vad->ort;
if (ort) {
if (vad->mem_info) ort->ReleaseMemoryInfo(vad->mem_info);
if (vad->session) ort->ReleaseSession(vad->session);
if (vad->env) ort->ReleaseEnv(vad->env);
}
if (vad->model_data) u_free(vad->model_data);
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_destroy");
u_free(vad);
}
void silero_vad_reset(silero_vad_t* vad) {
if (!vad) return;
memset(vad->state, 0, sizeof(vad->state));
memset(vad->context, 0, sizeof(vad->context));
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_reset: recurrent state and audio context cleared");
}
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
const OrtApi* ort;
OrtStatus* status;
OrtValue* in_input = NULL;
OrtValue* in_state = NULL;
OrtValue* in_sr = NULL;
OrtValue* out_prob = NULL;
OrtValue* out_state = NULL;
const char* input_names[3];
const char* output_names[2];
const OrtValue* inputs[3];
OrtValue* outputs[2];
int64_t shape_input[2] = {1, SILERO_VAD_INPUT_SAMPLES};
int64_t shape_state[3] = {2, 1, 128};
int64_t shape_prob[2] = {1, 1};
int64_t sr_value = SILERO_VAD_SAMPLE_RATE;
float prob_local = 0.0f;
float next_state[SILERO_VAD_STATE_COUNT];
float input[SILERO_VAD_INPUT_SAMPLES];
int rc = -1;
if (!vad || !samples || !prob) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process: invalid args");
return -1;
}
ort = vad->ort;
memcpy(input, vad->context, sizeof(vad->context));
memcpy(input + SILERO_VAD_CONTEXT_SAMPLES, samples, SILERO_VAD_WINDOW_SAMPLES * sizeof(float));
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, input,
sizeof(input), shape_input, 2,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_input);
if (status) { vad_report_ort_error(ort, status, "input tensor"); return -1; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, vad->state,
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_state);
if (status) { vad_report_ort_error(ort, status, "state tensor"); goto done; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &sr_value,
sizeof(sr_value), NULL, 0,
ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64, &in_sr);
if (status) { vad_report_ort_error(ort, status, "sr tensor"); goto done; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &prob_local,
sizeof(float), shape_prob, 2,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_prob);
if (status) { vad_report_ort_error(ort, status, "output tensor"); goto done; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, next_state,
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_state);
if (status) { vad_report_ort_error(ort, status, "stateN tensor"); goto done; }
input_names[0] = "input";
input_names[1] = "state";
input_names[2] = "sr";
inputs[0] = in_input;
inputs[1] = in_state;
inputs[2] = in_sr;
output_names[0] = "output";
output_names[1] = "stateN";
outputs[0] = out_prob;
outputs[1] = out_state;
status = ort->Run(vad->session, NULL, input_names, inputs, 3, output_names, 2, outputs);
if (status) {
vad_report_ort_error(ort, status, "Run");
goto done;
}
memcpy(vad->state, next_state, sizeof(next_state));
memcpy(vad->context, input + SILERO_VAD_INPUT_SAMPLES - SILERO_VAD_CONTEXT_SAMPLES, sizeof(vad->context));
*prob = prob_local;
rc = 0;
done:
if (in_input) ort->ReleaseValue(in_input);
if (in_state) ort->ReleaseValue(in_state);
if (in_sr) ort->ReleaseValue(in_sr);
if (out_prob) ort->ReleaseValue(out_prob);
if (out_state) ort->ReleaseValue(out_state);
return rc;
}
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
float samples[SILERO_VAD_WINDOW_SAMPLES];
int i;
if (!pcm) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process_pcm16: pcm == NULL");
return -1;
}
for (i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) {
samples[i] = (float)pcm[i] / 32768.0f;
}
return silero_vad_process(vad, samples, prob);
}
#else /* !HAVE_SILERO_VAD — пустые стабы (сборки без onnxruntime) */
silero_vad_t* silero_vad_create(const char* model_path) {
(void)model_path;
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: built without onnxruntime (HAVE_SILERO_VAD not defined)");
return NULL;
}
silero_vad_t* silero_vad_create_default(void) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create_default: built without onnxruntime (HAVE_SILERO_VAD not defined)");
return NULL;
}
void silero_vad_destroy(silero_vad_t* vad) {
(void)vad;
}
void silero_vad_reset(silero_vad_t* vad) {
(void)vad;
}
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
(void)vad; (void)samples; (void)prob;
return -1;
}
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
(void)vad; (void)pcm; (void)prob;
return -1;
}
#endif /* HAVE_SILERO_VAD */
+77
View File
@@ -0,0 +1,77 @@
/**
* Silero VAD (Voice Activity Detector) — C-обёртка над ONNX Runtime.
*
* Запускает официальную стриминговую модель silero_vad.onnx (v5, ~2.3 МБ),
* которая по окну аудио возвращает вероятность наличия речи [0..1].
* Рекуррентное состояние модели хранится внутри объекта и переносится между
* вызовами. Обёртка также хранит 64 последних отсчёта аудио и добавляет их
* перед новым окном: ONNX получает 576 отсчётов, публичный API принимает 512.
*
* Требования к входному аудио:
* - частота дискретизации 16 кГц (модель поддерживает только 16k);
* - моно;
* - окно ровно SILERO_VAD_WINDOW_SAMPLES = 512 сэмплов (32 мс);
* - float-сэмплы в диапазоне примерно [-1;1] (для int16 есть
* silero_vad_process_pcm16, который сам делит на 32768).
*
* Порог и гистерезис (задержки начала/конца речи) намеренно оставлены
* вызывающему — радио/звонок сами решают, как интерпретировать вероятность.
*
* Зависимость: libonnxruntime (C API). Сборка включается опцией
* `--with-silero-vad` (см. configure.ac) или макросом HAVE_SILERO_VAD в
* CMake-сборках. Без HAVE_SILERO_VAD модуль компилируется в пустые стабы
* (все create-функции возвращают NULL, process — ошибку), чтобы исходники lib/
* можно было GLOB-ить во всех сборках (Android/chatgui) без onnxruntime.
*
* Лицензия модели: MIT (Silero Team, https://github.com/snakers4/silero-vad).
*/
#ifndef SILERO_VAD_H
#define SILERO_VAD_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Фиксированное окно стриминговой модели: 512 сэмплов @ 16 кГц = 32 мс */
#define SILERO_VAD_WINDOW_SAMPLES 512
#define SILERO_VAD_SAMPLE_RATE 16000
typedef struct silero_vad silero_vad_t;
/**
* Создать детектор, загрузив ONNX-модель из файла model_path.
* Возвращает NULL при ошибке (подробности — в лог категории "vad").
*/
silero_vad_t* silero_vad_create(const char* model_path);
/**
* Создать детектор из модели, вшитой в бинарник (silero_vad_model_data в
* silero_vad.c). Не требует файла на диске. NULL при ошибке.
*/
silero_vad_t* silero_vad_create_default(void);
/* Освободить детектор. NULL безопасен. */
void silero_vad_destroy(silero_vad_t* vad);
/* Сбросить рекуррентное состояние и звуковой контекст (начало нового потока / разрыв захвата). */
void silero_vad_reset(silero_vad_t* vad);
/**
* Обработать окно из 512 float-сэмплов @ 16 кГц (примерно [-1;1]).
* При успехе возвращает 0 и заполняет *prob вероятностью речи [0..1];
* при ошибке — отрицательное значение (проб не трогается).
*/
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob);
/**
* То же, но вход — int16 PCM: сэмплы преобразуются к float делением на 32768.
*/
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob);
#ifdef __cplusplus
}
#endif
#endif /* SILERO_VAD_H */
Binary file not shown.
File diff suppressed because it is too large. Load diff
+13
View File
@@ -246,3 +246,16 @@ uint16_t ss_get_port(const struct sockaddr_storage *addr) {
return 0;
}
uint16_t socket_get_bound_port(socket_t sock) {
struct sockaddr_storage address; socklen_t size = sizeof(address);
if (getsockname(sock, (struct sockaddr*)&address, &size) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "getsockname fd=%llu failed: %s",
(unsigned long long)sock, socket_strerror(socket_get_error()));
return 0;
}
uint16_t port = ss_get_port(&address);
if (!port) DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket fd=%llu family=%d has no bound port",
(unsigned long long)sock, address.ss_family);
return port;
}
+21 -8
View File
@@ -1,6 +1,9 @@
/**
* Socket compatibility layer for cross-platform support (POSIX / Windows)
* MSYS2 UCRT64 compatible
* Единые socket_t, коды ошибок и wrappers сокетов POSIX/Windows (включая MSYS2 UCRT64).
* platform_init/cleanup обслуживают Winsock; UDP/nonblocking/options/sendto/recvfrom — операции ОС.
* Созданные сокеты закрывает владелец через socket_close_wrapper; event loop находится в u_async.h.
* Использование: platform_init → create_udp → set_nonblocking/options → sendto/recvfrom → close → cleanup.
* До регистрации в uasync вызывающий должен включить nonblocking. Winsock init/cleanup согласовать между потоками.
*/
#ifndef SOCKET_COMPAT_H
@@ -44,13 +47,16 @@ extern "C" {
#define ERR_INTR EINTR
#endif
// Platform initialization/cleanup
// POSIX: 0 без системной инициализации. Windows: WSAStartup при первом init, счётчик вложенности; 0 / -1.
int socket_platform_init(void);
// Парный cleanup: Windows уменьшает счётчик и вызывает WSACleanup на нуле; POSIX ничего не освобождает.
void socket_platform_cleanup(void);
// Socket operations
// UDP-сокет для family (обычно AF_INET/AF_INET6); SOCKET_INVALID при ошибке. Владение передаётся вызывающему.
socket_t socket_create_udp(int family);
// Включает nonblocking: 0 — успех, -1 — ошибка.
int socket_set_nonblocking(socket_t sock);
/* Закрыть сокет средствами ОС; на POSIX sock=0 оставляет дескриптор открытым и возвращает 0. */
int socket_close_wrapper(socket_t sock);
static inline int socket_get_error(void) {
#ifdef _WIN32
@@ -60,23 +66,30 @@ static inline int socket_get_error(void) {
#endif
}
// Socket options
// Пробует установить оба буфера SO_SNDBUF/SO_RCVBUF: 0 при успехе обоих, -1 при любой ошибке; отката нет.
int socket_set_buffers(socket_t sock, int sndbuf, int rcvbuf);
// Устанавливает SO_REUSEADDR в значение reuse; 0 / -1.
int socket_set_reuseaddr(socket_t sock, int reuse);
// SO_BINDTODEVICE для строки ifname (не NULL); 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
int socket_bind_to_device(socket_t sock, const char* ifname);
// SO_MARK в значение mark; 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
int socket_set_mark(socket_t sock, int mark);
// I/O operations
// Один sendto/recvfrom с flags=0: число байт или SOCKET_ERROR_CODE; errno/WSA-код читать через socket_get_error.
// буферами владеет вызывающий; для Windows len должен помещаться в int. dest_len — размер адреса назначения;
// src_len при приёме — входная ёмкость/выходная длина адреса (может быть NULL по правилам recvfrom ОС).
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
const struct sockaddr* dest, socklen_t dest_len);
ssize_t socket_recvfrom(socket_t sock, void* buf, size_t len,
struct sockaddr* src, socklen_t* src_len);
// Utility
// Заимствованный текст ошибки: Windows — один статический буфер (перезаписывается следующим вызовом), POSIX — strerror.
const char* socket_strerror(int err);
// взять порт из sockaddr_storage
// IPv4/IPv6-порт в host byte order; 0 при NULL или другом семействе.
uint16_t ss_get_port(const struct sockaddr_storage *addr);
// Фактический IPv4/IPv6-порт уже привязанного сокета; 0 при ошибке.
uint16_t socket_get_bound_port(socket_t sock);
#ifdef __cplusplus
-99
View File
@@ -1,99 +0,0 @@
# socket_compat — Cross-platform socket abstraction
## 1. Назначение
Унифицирует работу с UDP-сокетами между POSIX и Windows (MSYS2 UCRT64). Скрывает различия в типах дескрипторов (`int` vs `SOCKET`), инициализации подсистемы (WSAStartup/WSACleanup), кодах ошибок и сигнатурах системных вызовов.
## 2. Как пользоваться
### Инициализация (однократно при старте)
```c
if (socket_platform_init() != 0) { /* фатальная ошибка */ }
// ... работа с сокетами ...
socket_platform_cleanup(); // при завершении
```
На Windows WSAStartup/WSACleanup с refcount — можно вызывать init/cleanup вложенно.
### Создание и настройка сокета
```c
socket_t sock = socket_create_udp(AF_INET); // или AF_INET6
if (sock == SOCKET_INVALID) { /* ошибка */ }
socket_set_nonblocking(sock); // обязательно для u_async
socket_set_reuseaddr(sock, 1);
socket_set_buffers(sock, 256*1024, 256*1024); // SO_SNDBUF/SO_RCVBUF
socket_bind_to_device(sock, "eth0"); // только Linux, SO_BINDTODEVICE
socket_set_mark(sock, 42); // только Linux, SO_MARK
```
### Отправка и приём
```c
ssize_t sent = socket_sendto(sock, buf, len, (struct sockaddr*)&addr, addr_len);
ssize_t recv = socket_recvfrom(sock, buf, sizeof(buf), (struct sockaddr*)&src, &src_len);
uint16_t port = ss_get_port(&src); // порт из sockaddr_storage
```
### Обработка ошибок
```c
if (sent == SOCKET_ERROR_CODE) {
int err = socket_get_error();
if (err == ERR_WOULDBLOCK) { /* нормально для неблокирующего */ }
// на POSIX ERR_WOULDBLOCK == EWOULDBLOCK (может совпадать с EAGAIN)
DEBUG_ERROR(..., "%s", socket_strerror(err));
}
```
### Закрытие
```c
socket_close_wrapper(sock); // на POSIX: close() с защитой от fd 0
```
### Ключевые нюансы
- **Всегда вызывать `socket_platform_init()` перед работой** — иначе на Windows сокеты не будут работать
- **Всегда `socket_set_nonblocking()`** — u_async требует неблокирующих сокетов
- `socket_t` — на POSIX `int`, на Windows `SOCKET` (unsigned)
- `SOCKET_INVALID` — константа для невалидного сокета (`-1` на POSIX, `INVALID_SOCKET` на Windows)
- `SOCKET_ERROR_CODE` — код ошибки сокетных вызовов (`-1` на POSIX, `SOCKET_ERROR` на Windows)
- `socket_strerror()` на Windows использует статический буфер — не thread-safe для параллельного использования
- `socket_bind_to_device` и `socket_set_mark` — только Linux, на остальных платформах возвращают -1 с DEBUG-сообщением
- `socket_close_wrapper` на POSIX не закроет fd 0 (защита)
## 3. API
### Типы и константы
| Имя | Назначение |
|-----|------------|
| `socket_t` | Кроссплатформенный тип дескриптора сокета (`int` / `SOCKET`) |
| `SOCKET_INVALID` | Невалидный сокет (`-1` / `INVALID_SOCKET`) |
| `SOCKET_ERROR_CODE` | Код ошибки сокетного вызова (`-1` / `SOCKET_ERROR`) |
| `ERR_WOULDBLOCK` / `ERR_AGAIN` / `ERR_INTR` | Кроссплатформенные коды ошибок |
### Инициализация
| Функция | Назначение |
|---------|------------|
| `socket_platform_init()` | Инициализация сокетной подсистемы (WSAStartup на Windows). 0 — успех |
| `socket_platform_cleanup()` | Деинициализация (WSACleanup на Windows), refcount |
### Создание и настройка
| Функция | Назначение |
|---------|------------|
| `socket_create_udp(family)` | Создать UDP-сокет (AF_INET или AF_INET6) |
| `socket_set_nonblocking(sock)` | Перевести в неблокирующий режим |
| `socket_set_buffers(sock, snd, rcv)` | Установить SO_SNDBUF и SO_RCVBUF |
| `socket_set_reuseaddr(sock, reuse)` | Установить SO_REUSEADDR |
| `socket_bind_to_device(sock, ifname)` | Привязать к интерфейсу (Linux SO_BINDTODEVICE) |
| `socket_set_mark(sock, mark)` | Установить SO_MARK (Linux) |
### I/O
| Функция | Назначение |
|---------|------------|
| `socket_sendto(sock, buf, len, dest, dest_len)` | Отправить UDP-датаграмму |
| `socket_recvfrom(sock, buf, len, src, src_len)` | Принять UDP-датаграмму |
### Утилиты
| Функция | Назначение |
|---------|------------|
| `socket_get_error()` | Текущий код ошибки (inline: `WSAGetLastError()` / `errno`) |
| `socket_strerror(err)` | Текстовое описание ошибки (Windows: статический буфер, не thread-safe) |
| `socket_close_wrapper(sock)` | Закрыть сокет (POSIX: защита от закрытия fd 0) |
| `ss_get_port(addr)` | Извлечь порт в host byte order из `sockaddr_storage` |
@@ -67,6 +67,7 @@
#include "FIFOSamplePipe.h"
#include "STTypes.h"
#include "SourceSpan.h"
namespace soundtouch
{
@@ -197,6 +198,8 @@ private:
/// 'virtualPitch' parameters.
void calcEffectiveRateAndTempo();
bool sourceProfileSupported() const;
protected :
/// Number of channels
uint channels;
@@ -211,6 +214,13 @@ public:
SoundTouch();
~SoundTouch() override;
/* uTun: подключать после настройки, до первого putSamples либо после clear.
* Поддерживается float PCM, unity pitch/rate, cubic interpolation, AA off, mono/stereo.
* Ошибка сообщает Invalid и возвращает false. Изменение профиля после начала и flush
* инвалидируют карту; clear начинает новую. nullptr отключает события в любой момент.
* События описывают произведённый выход, включая ещё не прочитанные receiveSamples данные. */
bool setSourceCallback(SourceCallback callback, void* context);
/// Get SoundTouch library version string
static const char *getVersionString();
@@ -0,0 +1,25 @@
/* uTun: происхождение PCM без изменения DSP. Индексы — сэмплы НА КАНАЛ от clear().
* Copy: out[i] происходит из input0+i. Mix: источники input0+i и input1+i,
* вес второго i/count, первого 1-i/count. Это описание происхождения, не PCM.
* Reset начинает новую шкалу. Invalid запрещает её использование до clear().
* Callback синхронный: нельзя вызывать SoundTouch повторно, блокировать поток или
* сохранять ссылку на событие. Ошибки передаются через Invalid; логирует потребитель. */
#ifndef SOUNDTOUCH_SOURCE_SPAN_H
#define SOUNDTOUCH_SOURCE_SPAN_H
#include <cstdint>
namespace soundtouch {
enum class SourceKind { Reset, Copy, Mix, Invalid };
enum class SourceError { None, Profile, Reconfigure, Flush, LateAttach };
struct SourceSpan {
SourceKind kind;
SourceError error;
uint64_t output, input0, input1;
uint32_t count;
SourceSpan(SourceKind k, SourceError e = SourceError::None, uint64_t out = 0,
uint64_t first = 0, uint64_t second = 0, uint32_t length = 0)
: kind(k), error(e), output(out), input0(first), input1(second), count(length) {}
};
using SourceCallback = void (*)(void*, const SourceSpan&);
}
#endif
@@ -58,6 +58,9 @@ public:
virtual void resetRegisters() override;
// При fract=0 кубический интерполятор копирует psrc[1], сохраняя четыре входных сэмпла.
int unitySourceOffset() const override { return rate == 1.0 && fract == 0.0 ? 1 : -1; }
virtual int getLatency() const override
{
return 1;
@@ -83,6 +83,9 @@ public:
virtual void setChannels(int channels);
virtual int getLatency() const = 0;
// uTun: смещение точного копирования при rate=1; -1 если реализация не поддерживает карту.
virtual int unitySourceOffset() const { return -1; }
virtual void resetRegisters() = 0;
// static factory function
@@ -133,6 +136,9 @@ public:
AAFilter *getAAFilter();
/// Enables/disables the anti-alias filter. Zero to disable, nonzero to enable
// clear() добавляет getLatency() нулей: они компенсируют смещение интерполятора.
int unitySourceOffset() const { return bUseAAFilter ? -1 : pTransposer->unitySourceOffset() - getLatency(); }
void enableAAFilter(bool newMode);
/// Returns nonzero if anti-alias filter is enabled.
@@ -117,6 +117,28 @@ SoundTouch::~SoundTouch()
delete pTDStretch;
}
bool SoundTouch::sourceProfileSupported() const
{
#if defined(SOUNDTOUCH_FLOAT_SAMPLES) && !defined(SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER)
return rate == 1.0 && virtualPitch == 1.0 && channels >= 1 && channels <= 2
&& bSrateSet && pRateTransposer->unitySourceOffset() >= 0;
#else
return false;
#endif
}
bool SoundTouch::setSourceCallback(SourceCallback callback, void* context)
{
if (!callback) { pTDStretch->setSourceCallback(nullptr, nullptr, 0); return true; }
if (samplesExpectedOut != 0 || !sourceProfileSupported())
{
callback(context, {SourceKind::Invalid, samplesExpectedOut != 0 ? SourceError::LateAttach : SourceError::Profile});
return false;
}
pTDStretch->setSourceCallback(callback, context, (unsigned)pRateTransposer->unitySourceOffset());
return true;
}
/// Get SoundTouch library version string
const char *SoundTouch::getVersionString()
@@ -139,6 +161,8 @@ void SoundTouch::setChannels(uint numChannels)
{
if (!verifyNumberOfChannels(numChannels)) return;
if (samplesExpectedOut != 0) pTDStretch->invalidateSource(SourceError::Reconfigure);
channels = numChannels;
pRateTransposer->setChannels((int)numChannels);
pTDStretch->setChannels((int)numChannels);
@@ -223,6 +247,8 @@ void SoundTouch::calcEffectiveRateAndTempo()
tempo = virtualTempo / virtualPitch;
rate = virtualPitch * virtualRate;
if (rate != 1.0 || virtualPitch != 1.0) pTDStretch->invalidateSource(SourceError::Profile);
if (!TEST_FLOAT_EQUAL(rate,oldRate)) pRateTransposer->setRate(rate);
if (!TEST_FLOAT_EQUAL(tempo, oldTempo)) pTDStretch->setTempo(tempo);
@@ -266,6 +292,7 @@ void SoundTouch::calcEffectiveRateAndTempo()
// Sets sample rate.
void SoundTouch::setSampleRate(uint srate)
{
if (samplesExpectedOut != 0) pTDStretch->invalidateSource(SourceError::Reconfigure);
// set sample rate, leave other tempo changer parameters as they are.
pTDStretch->setParameters((int)srate);
bSrateSet = true;
@@ -285,6 +312,8 @@ void SoundTouch::putSamples(const SAMPLETYPE *samples, uint nSamples)
ST_THROW_RT_ERROR("SoundTouch : Number of channels not defined");
}
if (!sourceProfileSupported()) pTDStretch->invalidateSource(SourceError::Profile);
// accumulate how many samples are expected out from processing, given the current
// processing setting
samplesExpectedOut += (double)nSamples / ((double)rate * (double)tempo);
@@ -317,6 +346,8 @@ void SoundTouch::putSamples(const SAMPLETYPE *samples, uint nSamples)
// in the middle of a sound stream.
void SoundTouch::flush()
{
// Padding и обрезка финального выхода не входят в контракт live-карты.
pTDStretch->invalidateSource(SourceError::Flush);
int i;
int numStillExpected;
SAMPLETYPE *buff = new SAMPLETYPE[128 * channels];
@@ -350,6 +381,8 @@ void SoundTouch::flush()
// 'SETTING_...' defines for available setting ID's.
bool SoundTouch::setSetting(int settingId, int value)
{
if (samplesExpectedOut != 0 && settingId != SETTING_USE_QUICKSEEK)
pTDStretch->invalidateSource(SourceError::Reconfigure);
int sampleRate, sequenceMs, seekWindowMs, overlapMs;
// read current tdstretch routine parameters
@@ -210,6 +210,35 @@ void TDStretch::clear()
{
outputBuffer.clear();
clearInput();
sourceInput = sourceOutput = sourceMid = 0;
sourceValid = sourceCallback != nullptr;
if (sourceCallback) sourceCallback(sourceContext, {SourceKind::Reset});
}
// uTun: метаданные принадлежат потребителю, SoundTouch не выделяет память для карты.
void TDStretch::setSourceCallback(SourceCallback callback, void* context, unsigned offset)
{
sourceCallback = callback;
sourceContext = context;
sourceOffset = offset;
sourceInput = sourceOutput = sourceMid = 0;
sourceValid = callback != nullptr;
if (callback) callback(context, {SourceKind::Reset});
}
void TDStretch::invalidateSource(SourceError reason)
{
if (!sourceValid) return;
sourceValid = false;
sourceCallback(sourceContext, {SourceKind::Invalid, reason});
}
void TDStretch::emitSource(SourceKind kind, uint64_t first, uint64_t second, unsigned count)
{
if (!sourceValid || !count) return;
sourceCallback(sourceContext, {kind, SourceError::None, sourceOutput,
first + sourceOffset, second + sourceOffset, count});
sourceOutput += count;
}
@@ -645,6 +674,7 @@ void TDStretch::processSamples()
// (that's in 'midBuffer')
overlap(outputBuffer.ptrEnd((uint)overlapLength), inputBuffer.ptrBegin(), (uint)offset);
outputBuffer.putSamples((uint)overlapLength);
emitSource(SourceKind::Mix, sourceMid, sourceInput + offset, (unsigned)overlapLength);
offset += overlapLength;
}
else
@@ -683,6 +713,7 @@ void TDStretch::processSamples()
// length of sequence
temp = (seekWindowLength - 2 * overlapLength);
outputBuffer.putSamples(inputBuffer.ptrBegin() + channels * offset, (uint)temp);
emitSource(SourceKind::Copy, sourceInput + offset, 0, (unsigned)temp);
// Copies the end of the current sequence from 'inputBuffer' to
// 'midBuffer' for being mixed with the beginning of the next
@@ -690,6 +721,7 @@ void TDStretch::processSamples()
assert((offset + temp + overlapLength) <= (int)inputBuffer.numSamples());
memcpy(pMidBuffer, inputBuffer.ptrBegin() + channels * (offset + temp),
channels * sizeof(SAMPLETYPE) * overlapLength);
sourceMid = sourceInput + offset + temp;
// Remove the processed samples from the input buffer. Update
// the difference between integer & nominal skip step to 'skipFract'
@@ -697,7 +729,7 @@ void TDStretch::processSamples()
skipFract += nominalSkip; // real skip size
ovlSkip = (int)skipFract; // rounded to integer skip
skipFract -= ovlSkip; // maintain the fraction part, i.e. real vs. integer skip
inputBuffer.receiveSamples((uint)ovlSkip);
sourceInput += inputBuffer.receiveSamples((uint)ovlSkip);
}
}
@@ -41,6 +41,7 @@
#include "STTypes.h"
#include "RateTransposer.h"
#include "FIFOSamplePipe.h"
#include "SourceSpan.h"
namespace soundtouch
{
@@ -135,6 +136,13 @@ protected:
FIFOSampleBuffer outputBuffer;
FIFOSampleBuffer inputBuffer;
SourceCallback sourceCallback = nullptr;
void* sourceContext = nullptr;
bool sourceValid = false;
uint64_t sourceInput = 0, sourceOutput = 0, sourceMid = 0;
unsigned sourceOffset = 0;
void emitSource(SourceKind kind, uint64_t first, uint64_t second, unsigned count);
void acceptNewOverlapLength(int newOverlapLength);
virtual void clearCrossCorrState();
@@ -167,6 +175,10 @@ public:
TDStretch();
virtual ~TDStretch() override;
// Только SoundTouch: регистрация на пустом тракте, затем события из processSamples().
void setSourceCallback(SourceCallback callback, void* context, unsigned offset);
void invalidateSource(SourceError reason);
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
/// depending on if we've a MMX/SSE/etc-capable CPU available or not.
static void *operator new(size_t s);
+178
View File
@@ -0,0 +1,178 @@
// speex_aec.c — C-обёртка над SpeexDSP mdf.c (акустическое эхоподавление).
//
// API см. в speex_aec.h. Ключевые детали реализации:
// - собственная линия задержки рендера на depth кадров (SpeexDSP даёт только
// фиксированные 2 кадра — PLAYBACK_DELAY в mdf.c);
// - синхронный speex_echo_cancellation(rec, play_delayed, out) с выравниванием
// рендер↔захват по линии задержки;
// - дрейф независимых потоков: переполнение линии → отбрасываем старый кадр,
// недозаполнение (старт/underflow) → passthrough без канселлера.
//
// Рендер принимает чанки любого размера; захват — ровно один полный кадр.
// Частые аномалии учитываются счётчиками; владелец публикует сводку вне audio callback.
#include "speex_aec.h"
#include "debug_config.h"
#include "mem.h"
#include <string.h>
#include <limits.h>
#include <speex/speex_echo.h>
#define AEC_ID "speex_aec"
struct speex_aec {
SpeexEchoState* st;
int mic_channels, render_channels;
int frame_samples; /* сэмплов в кадре (960 @48кГц) */
int depth; /* глубина линии задержки в кадрах (>=1) */
int16_t* line; /* кольцо depth*frame_samples */
int head; /* индекс самого старого кадра в line */
int count; /* кадров в линии (0..depth) */
int16_t* play_acc; /* аккумулятор рендера до кадра */
int play_len;
uint32_t overruns; /* отброшено рендер-кадров (дрейф: рендер быстрее) */
uint32_t underruns; /* passthrough-кадров (дрейф: рендер медленнее/старт) */
};
/* Положить полный рендер-кадр в линию задержки (вытесняя старейший при переполнении). */
static void aec_enqueue(speex_aec_t* a, const int16_t* frame) {
if (a->count == a->depth) {
a->head = (a->head + 1) % a->depth;
a->count--;
a->overruns++;
}
memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples * a->render_channels,
frame, (size_t)a->frame_samples * a->render_channels * sizeof(int16_t));
a->count++;
}
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames) {
return speex_aec_create_mc(sample_rate, frame_samples, filter_samples, delay_frames, 1, 1);
}
/* Независимые каналы референса: противофазное стерео не усредняется. */
speex_aec_t* speex_aec_create_mc(int sample_rate, int frame_samples, int filter_samples, int delay_frames,
int mic_channels, int render_channels) {
speex_aec_t* a;
int rate;
if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples ||
mic_channels < 1 || mic_channels > 2 || render_channels < 1 || render_channels > 2 ||
frame_samples > INT_MAX / render_channels || frame_samples > INT_MAX / mic_channels ||
(delay_frames > 0 && delay_frames > INT_MAX / frame_samples / render_channels)) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: bad args rate=%d frame=%d filter=%d delay=%d",
AEC_ID, sample_rate, frame_samples, filter_samples, delay_frames);
return NULL;
}
a = (speex_aec_t*)u_calloc(1, sizeof(*a));
if (!a) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for state", AEC_ID);
return NULL;
}
a->st = speex_echo_state_init_mc(frame_samples, filter_samples, mic_channels, render_channels);
if (!a->st) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: echo state allocation failed", AEC_ID);
u_free(a);
return NULL;
}
rate = sample_rate;
if (speex_echo_ctl(a->st, SPEEX_ECHO_SET_SAMPLING_RATE, &rate) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: SET_SAMPLING_RATE(%d) failed", AEC_ID, sample_rate);
speex_echo_state_destroy(a->st);
u_free(a);
return NULL;
}
a->mic_channels = mic_channels; a->render_channels = render_channels;
a->frame_samples = frame_samples;
a->depth = delay_frames > 0 ? delay_frames : 1;
a->line = (int16_t*)u_calloc((uint32_t)(a->depth * frame_samples * render_channels), sizeof(int16_t));
a->play_acc = (int16_t*)u_calloc((uint32_t)(frame_samples * render_channels), sizeof(int16_t));
if (!a->line || !a->play_acc) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for buffers", AEC_ID);
if (a->line) u_free(a->line);
if (a->play_acc) u_free(a->play_acc);
speex_echo_state_destroy(a->st);
u_free(a);
return NULL;
}
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: created rate=%d frame=%d filter=%d delay=%d frames mic=%d render=%d",
AEC_ID, sample_rate, frame_samples, filter_samples, a->depth, mic_channels, render_channels);
return a;
}
void speex_aec_destroy(speex_aec_t* a) {
if (!a) return;
speex_echo_state_destroy(a->st);
u_free(a->line);
u_free(a->play_acc);
u_free(a);
}
void speex_aec_reset(speex_aec_t* a) {
if (!a) return;
speex_echo_state_reset(a->st);
a->head = 0;
a->count = 0;
a->play_len = 0;
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: reset (filter + delay line)", AEC_ID);
}
void speex_aec_feed_playback(speex_aec_t* a, const int16_t* pcm, int count) {
int fs, take;
if (!a || !pcm || count <= 0) return;
fs = a->frame_samples * a->render_channels;
while (count > 0) {
take = fs - a->play_len;
if (take > count) take = count;
memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t));
a->play_len += take;
pcm += take;
count -= take;
if (a->play_len == fs) {
aec_enqueue(a, a->play_acc);
a->play_len = 0;
}
}
}
int speex_aec_process_capture(speex_aec_t* a, const int16_t* pcm, int count, int16_t* out) {
int fs;
if (!a || !pcm || !out || count <= 0) return 0;
fs = a->frame_samples;
if (count != fs * a->mic_channels) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: capture requires one frame samples=%d expected=%d", AEC_ID, count, fs * a->mic_channels);
return 0;
}
if (a->count == a->depth) {
const int16_t* ref = a->line + (size_t)a->head * fs * a->render_channels;
speex_echo_cancellation(a->st, pcm, ref, out);
a->head = (a->head + 1) % a->depth;
a->count--;
} else {
/* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */
memcpy(out, pcm, (size_t)count * sizeof(int16_t));
a->underruns++;
}
return count;
}
int speex_aec_delay_fill(const speex_aec_t* a) {
return a ? a->count : 0;
}
void speex_aec_get_stats(const speex_aec_t* a, uint32_t* overruns, uint32_t* underruns) {
*overruns = a ? a->overruns : 0;
*underruns = a ? a->underruns : 0;
}
+83
View File
@@ -0,0 +1,83 @@
/*
* speex_aec.h — акустическое эхоподавление (AEC), C-обёртка над SpeexDSP mdf.c.
*
* Подавляет эхо дальнего конца (то, что играем в динамик) в сигнале микрофона
* перед кодированием. Работает на interleaved int16 PCM, 1/2 канала, частота 48000 (можно
* 8000/16000/32000/48000), кадр 20 мс (960 сэмплов @48 кГц).
*
* Объект не имеет внутренних блокировок: feed/process/reset/get_stats выполняет один владелец
* либо вызывающий сериализует их общей блокировкой. Аппаратные callbacks сами AEC не вызывают.
*
* Модель использования (duplex-контур звонка):
* - рендер (far-end, то что пошло в динамик) → speex_aec_feed_playback();
* - захват (near-end, микрофон) → speex_aec_process_capture().
*
* Обёртка держит собственную линию задержки рендера на `delay_frames` кадров и
* зовёт синхронный speex_echo_cancellation(rec, play_delayed, out). Это делает
* выравнивание рендер↔захват предсказуемым и настраиваемым (встроенный буфер
* SpeexDSP фиксирован в 2 кадра и для Android-задержки не годится).
*
* Дрейф двух независимых потоков (capture/play на Android) компенсируется
* ограниченной глубиной линии: переполнение → отбрасываем старый рендер-кадр,
* недозаполнение → passthrough без канселлера; счётчики публикует владелец вне callback.
*
* Зависимости: SpeexDSP (mdf.c/fftwrap.c/kiss_fft*) из lib/speexdsp, флаги
* FLOATING_POINT + USE_KISS_FFT. Собирается всегда (без внешних зависимостей).
*
* Лицензия SpeexDSP: 3-clause BSD (Xiph) — см. lib/speexdsp/COPYING.
*/
#ifndef SPEEX_AEC_H
#define SPEEX_AEC_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct speex_aec speex_aec_t;
/**
* Создать эхоканселлер.
* sample_rate — 48000 (также 8000/16000/32000);
* frame_samples — сэмплов в кадре (960 = 20 мс @48 кГц);
* filter_samples— длина эхо-хвоста в сэмплах (14400 = 300 мс, кратно кадру);
* delay_frames — глубина линии рендер→захват в кадрах (значения <=0 приводятся к 1).
* Возвращает NULL при ошибке (лог категории "aec").
*/
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames);
/* Многоканальный AEC: frame_samples на канал, count в feed/process — все interleaved отсчёты.
* mic_channels и render_channels — 1/2; возвращаемый count захвата равен frame_samples*mic_channels. */
speex_aec_t* speex_aec_create_mc(int sample_rate, int frame_samples, int filter_samples, int delay_frames,
int mic_channels, int render_channels);
/* Освободить канселлер. NULL безопасен. */
void speex_aec_destroy(speex_aec_t* aec);
/* Сбросить адаптивный фильтр и линию задержки (смена устройства/роута). */
void speex_aec_reset(speex_aec_t* aec);
/**
* Подать рендер (дальний конец). Накопление до кадра внутри; полный кадр
* кладётся в линию задержки. count может быть любым (обычно 480 или 960).
*/
void speex_aec_feed_playback(speex_aec_t* aec, const int16_t* pcm, int count);
/**
* Обработать ровно один кадр захвата (count == frame_samples*mic_channels).
* Возвращает count при успехе, 0 при ошибке. out вмещает полный interleaved кадр
* и не должен алиаситься с pcm. Накопитель аппаратных чанков принадлежит I/O-движку.
*/
int speex_aec_process_capture(speex_aec_t* aec, const int16_t* pcm, int count, int16_t* out);
/* Актуальная глубина линии задержки (кадров) — для диагностики дрейфа. */
int speex_aec_delay_fill(const speex_aec_t* aec);
/* Под той же блокировкой, что feed/process; выводить снимок вне callback. */
void speex_aec_get_stats(const speex_aec_t* aec, uint32_t* overruns, uint32_t* underruns);
#ifdef __cplusplus
}
#endif
#endif /* SPEEX_AEC_H */
+35
View File
@@ -0,0 +1,35 @@
Copyright 2002-2008 Xiph.org Foundation
Copyright 2002-2008 Jean-Marc Valin
Copyright 2005-2007 Analog Devices Inc.
Copyright 2005-2008 Commonwealth Scientific and Industrial Research
Organisation (CSIRO)
Copyright 1993, 2002, 2006 David Rowe
Copyright 2003 EpicGames
Copyright 1992-1994 Jutta Degener, Carsten Bormann
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the Xiph.org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+160
View File
@@ -0,0 +1,160 @@
/*
Copyright (c) 2003-2004, Mark Borgerding
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#define MIN(a,b) ((a)<(b) ? (a):(b))
#define MAX(a,b) ((a)>(b) ? (a):(b))
/* kiss_fft.h
defines kiss_fft_scalar as either short or a float type
and defines
typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
#include "kiss_fft.h"
#include "math_approx.h"
#define MAXFACTORS 32
/* e.g. an fft of length 128 has 4 factors
as far as kissfft is concerned
4*4*4*2
*/
struct kiss_fft_state{
int nfft;
int inverse;
int factors[2*MAXFACTORS];
kiss_fft_cpx twiddles[1];
};
/*
Explanation of macros dealing with complex math:
C_MUL(m,a,b) : m = a*b
C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
C_SUB( res, a,b) : res = a - b
C_SUBFROM( res , a) : res -= a
C_ADDTO( res , a) : res += a
* */
#ifdef FIXED_POINT
#include "arch.h"
# define FRACBITS 15
# define SAMPPROD spx_int32_t
#define SAMP_MAX 32767
#define SAMP_MIN -SAMP_MAX
#if defined(CHECK_OVERFLOW)
# define CHECK_OVERFLOW_OP(a,op,b) \
if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \
fprintf(stderr,"WARNING:overflow @ " __FILE__ "(%d): (%d " #op" %d) = %ld\n",__LINE__,(a),(b),(SAMPPROD)(a) op (SAMPPROD)(b) ); }
#endif
# define smul(a,b) ( (SAMPPROD)(a)*(b) )
# define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS )
# define S_MUL(a,b) sround( smul(a,b) )
# define C_MUL(m,a,b) \
do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \
(m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0)
# define C_MUL4(m,a,b) \
do{ (m).r = PSHR32( smul((a).r,(b).r) - smul((a).i,(b).i),17 ); \
(m).i = PSHR32( smul((a).r,(b).i) + smul((a).i,(b).r),17 ); }while(0)
# define DIVSCALAR(x,k) \
(x) = sround( smul( x, SAMP_MAX/k ) )
# define C_FIXDIV(c,div) \
do { DIVSCALAR( (c).r , div); \
DIVSCALAR( (c).i , div); }while (0)
# define C_MULBYSCALAR( c, s ) \
do{ (c).r = sround( smul( (c).r , s ) ) ;\
(c).i = sround( smul( (c).i , s ) ) ; }while(0)
#else /* not FIXED_POINT*/
# define S_MUL(a,b) ( (a)*(b) )
#define C_MUL(m,a,b) \
do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
(m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
#define C_MUL4(m,a,b) C_MUL(m,a,b)
# define C_FIXDIV(c,div) /* NOOP */
# define C_MULBYSCALAR( c, s ) \
do{ (c).r *= (s);\
(c).i *= (s); }while(0)
#endif
#ifndef CHECK_OVERFLOW_OP
# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
#endif
#define C_ADD( res, a,b)\
do { \
CHECK_OVERFLOW_OP((a).r,+,(b).r)\
CHECK_OVERFLOW_OP((a).i,+,(b).i)\
(res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
}while(0)
#define C_SUB( res, a,b)\
do { \
CHECK_OVERFLOW_OP((a).r,-,(b).r)\
CHECK_OVERFLOW_OP((a).i,-,(b).i)\
(res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
}while(0)
#define C_ADDTO( res , a)\
do { \
CHECK_OVERFLOW_OP((res).r,+,(a).r)\
CHECK_OVERFLOW_OP((res).i,+,(a).i)\
(res).r += (a).r; (res).i += (a).i;\
}while(0)
#define C_SUBFROM( res , a)\
do {\
CHECK_OVERFLOW_OP((res).r,-,(a).r)\
CHECK_OVERFLOW_OP((res).i,-,(a).i)\
(res).r -= (a).r; (res).i -= (a).i; \
}while(0)
#ifdef FIXED_POINT
# define KISS_FFT_COS(phase) floor(MIN(32767,MAX(-32767,.5+32768 * cos (phase))))
# define KISS_FFT_SIN(phase) floor(MIN(32767,MAX(-32767,.5+32768 * sin (phase))))
# define HALF_OF(x) ((x)>>1)
#elif defined(USE_SIMD)
# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
# define HALF_OF(x) ((x)*_mm_set1_ps(.5))
#else
# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
# define HALF_OF(x) ((x)*.5)
#endif
#define kf_cexp(x,phase) \
do{ \
(x)->r = KISS_FFT_COS(phase);\
(x)->i = KISS_FFT_SIN(phase);\
}while(0)
#define kf_cexp2(x,phase) \
do{ \
(x)->r = spx_cos_norm((phase));\
(x)->i = spx_cos_norm((phase)-32768);\
}while(0)
/* a debugging function */
#define pcpx(c)\
fprintf(stderr,"%g + %gi\n",(double)((c)->r),(double)((c)->i) )
+232
View File
@@ -0,0 +1,232 @@
/* Copyright (C) 2003 Jean-Marc Valin */
/**
@file arch.h
@brief Various architecture definitions Speex
*/
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the Xiph.org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef ARCH_H
#define ARCH_H
/* A couple test to catch stupid option combinations */
#ifdef FIXED_POINT
#ifdef FLOATING_POINT
#error You cannot compile as floating point and fixed point at the same time
#endif
#ifdef USE_SSE
#error SSE is only for floating-point
#endif
#if defined(ARM4_ASM) + defined(ARM5E_ASM) + defined(BFIN_ASM) > 1
#error Make up your mind. What CPU do you have?
#endif
#ifdef VORBIS_PSYCHO
#error Vorbis-psy model currently not implemented in fixed-point
#endif
#else
#ifndef FLOATING_POINT
#error You now need to define either FIXED_POINT or FLOATING_POINT
#endif
#if defined(ARM4_ASM) || defined(ARM5E_ASM) || defined(BFIN_ASM)
#error I suppose you can have a [ARM4/ARM5E/Blackfin] that has float instructions?
#endif
#ifdef FIXED_DEBUG
#error "Don't you think enabling fixed-point is a good thing to do if you want to debug that?"
#endif
#endif
#ifndef OUTSIDE_SPEEX
#include "speex/speexdsp_types.h"
#endif
#define ABS(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute integer value. */
#define ABS16(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 16-bit value. */
#define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 16-bit value. */
#define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */
#define ABS32(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 32-bit value. */
#define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 32-bit value. */
#define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */
#ifdef FIXED_POINT
typedef spx_int16_t spx_word16_t;
typedef spx_int32_t spx_word32_t;
typedef spx_word32_t spx_mem_t;
typedef spx_word16_t spx_coef_t;
typedef spx_word16_t spx_lsp_t;
typedef spx_word32_t spx_sig_t;
#define Q15ONE 32767
#define LPC_SCALING 8192
#define SIG_SCALING 16384
#define LSP_SCALING 8192.
#define GAMMA_SCALING 32768.
#define GAIN_SCALING 64
#define GAIN_SCALING_1 0.015625
#define LPC_SHIFT 13
#define LSP_SHIFT 13
#define SIG_SHIFT 14
#define GAIN_SHIFT 6
#define WORD2INT(x) ((x) < -32767 ? -32768 : ((x) > 32766 ? 32767 : (x)))
#define VERY_SMALL 0
#define VERY_LARGE32 ((spx_word32_t)2147483647)
#define VERY_LARGE16 ((spx_word16_t)32767)
#define Q15_ONE ((spx_word16_t)32767)
#ifdef FIXED_DEBUG
#include "fixed_debug.h"
#else
#include "fixed_generic.h"
#ifdef ARM5E_ASM
#include "fixed_arm5e.h"
#elif defined(ARM4_ASM)
#include "fixed_arm4.h"
#elif defined(BFIN_ASM)
#include "fixed_bfin.h"
#endif
#endif
#else
typedef float spx_mem_t;
typedef float spx_coef_t;
typedef float spx_lsp_t;
typedef float spx_sig_t;
typedef float spx_word16_t;
typedef float spx_word32_t;
#define Q15ONE 1.0f
#define LPC_SCALING 1.f
#define SIG_SCALING 1.f
#define LSP_SCALING 1.f
#define GAMMA_SCALING 1.f
#define GAIN_SCALING 1.f
#define GAIN_SCALING_1 1.f
#define VERY_SMALL 1e-15f
#define VERY_LARGE32 1e15f
#define VERY_LARGE16 1e15f
#define Q15_ONE ((spx_word16_t)1.f)
#define QCONST16(x,bits) (x)
#define QCONST32(x,bits) (x)
#define NEG16(x) (-(x))
#define NEG32(x) (-(x))
#define EXTRACT16(x) (x)
#define EXTEND32(x) (x)
#define SHR16(a,shift) (a)
#define SHL16(a,shift) (a)
#define SHR32(a,shift) (a)
#define SHL32(a,shift) (a)
#define PSHR16(a,shift) (a)
#define PSHR32(a,shift) (a)
#define VSHR32(a,shift) (a)
#define SATURATE16(x,a) (x)
#define SATURATE32(x,a) (x)
#define SATURATE32PSHR(x,shift,a) (x)
#define PSHR(a,shift) (a)
#define SHR(a,shift) (a)
#define SHL(a,shift) (a)
#define SATURATE(x,a) (x)
#define ADD16(a,b) ((a)+(b))
#define SUB16(a,b) ((a)-(b))
#define ADD32(a,b) ((a)+(b))
#define SUB32(a,b) ((a)-(b))
#define MULT16_16_16(a,b) ((a)*(b))
#define MULT16_32_32(a,b) ((a)*(b))
#define MULT16_16(a,b) ((spx_word32_t)(a)*(spx_word32_t)(b))
#define MAC16_16(c,a,b) ((c)+(spx_word32_t)(a)*(spx_word32_t)(b))
#define MULT16_32_Q15(a,b) ((a)*(b))
#define MULT16_32_P15(a,b) ((a)*(b))
#define MAC16_32_Q15(c,a,b) ((c)+(a)*(b))
#define MAC16_16_Q11(c,a,b) ((c)+(a)*(b))
#define MAC16_16_Q13(c,a,b) ((c)+(a)*(b))
#define MAC16_16_P13(c,a,b) ((c)+(a)*(b))
#define MULT16_16_Q11_32(a,b) ((a)*(b))
#define MULT16_16_Q13(a,b) ((a)*(b))
#define MULT16_16_Q14(a,b) ((a)*(b))
#define MULT16_16_Q15(a,b) ((a)*(b))
#define MULT16_16_P15(a,b) ((a)*(b))
#define MULT16_16_P13(a,b) ((a)*(b))
#define MULT16_16_P14(a,b) ((a)*(b))
#define DIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b))
#define PDIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b))
#define DIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b))
#define PDIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b))
#define WORD2INT(x) ((x) < -32767.5f ? -32768 : \
((x) > 32766.5f ? 32767 : (spx_int16_t)floor(.5 + (x))))
#endif
#if defined(CONFIG_TI_C54X) || defined(CONFIG_TI_C55X)
/* 2 on TI C5x DSP */
#define BYTES_PER_CHAR 2
#define BITS_PER_CHAR 16
#define LOG2_BITS_PER_CHAR 4
#else
#define BYTES_PER_CHAR 1
#define BITS_PER_CHAR 8
#define LOG2_BITS_PER_CHAR 3
#endif
#ifdef FIXED_DEBUG
extern long long spx_mips;
#endif
#endif
+448
View File
@@ -0,0 +1,448 @@
/* Copyright (C) 2005-2006 Jean-Marc Valin
File: fftwrap.c
Wrapper for various FFTs
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the Xiph.org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "arch.h"
#include "os_support.h"
#define MAX_FFT_SIZE 2048
#ifdef FIXED_POINT
static int maximize_range(spx_word16_t *in, spx_word16_t *out, spx_word16_t bound, int len)
{
int i, shift;
spx_word16_t max_val = 0;
for (i=0;i<len;i++)
{
if (in[i]>max_val)
max_val = in[i];
if (-in[i]>max_val)
max_val = -in[i];
}
shift=0;
while (max_val <= (bound>>1) && max_val != 0)
{
max_val <<= 1;
shift++;
}
for (i=0;i<len;i++)
{
out[i] = SHL16(in[i], shift);
}
return shift;
}
static void renorm_range(spx_word16_t *in, spx_word16_t *out, int shift, int len)
{
int i;
for (i=0;i<len;i++)
{
out[i] = PSHR16(in[i], shift);
}
}
#endif
#ifdef USE_SMALLFT
#include "smallft.h"
#include <math.h>
void *spx_fft_init(int size)
{
struct drft_lookup *table;
table = speex_alloc(sizeof(struct drft_lookup));
spx_drft_init((struct drft_lookup *)table, size);
return (void*)table;
}
void spx_fft_destroy(void *table)
{
spx_drft_clear(table);
speex_free(table);
}
void spx_fft(void *table, float *in, float *out)
{
if (in==out)
{
int i;
float scale = 1./((struct drft_lookup *)table)->n;
speex_warning("FFT should not be done in-place");
for (i=0;i<((struct drft_lookup *)table)->n;i++)
out[i] = scale*in[i];
} else {
int i;
float scale = 1./((struct drft_lookup *)table)->n;
for (i=0;i<((struct drft_lookup *)table)->n;i++)
out[i] = scale*in[i];
}
spx_drft_forward((struct drft_lookup *)table, out);
}
void spx_ifft(void *table, float *in, float *out)
{
if (in==out)
{
speex_warning("FFT should not be done in-place");
} else {
int i;
for (i=0;i<((struct drft_lookup *)table)->n;i++)
out[i] = in[i];
}
spx_drft_backward((struct drft_lookup *)table, out);
}
#elif defined(USE_INTEL_MKL)
#include <mkl.h>
struct mkl_config {
DFTI_DESCRIPTOR_HANDLE desc;
int N;
};
void *spx_fft_init(int size)
{
struct mkl_config *table = (struct mkl_config *) speex_alloc(sizeof(struct mkl_config));
table->N = size;
DftiCreateDescriptor(&table->desc, DFTI_SINGLE, DFTI_REAL, 1, size);
DftiSetValue(table->desc, DFTI_PACKED_FORMAT, DFTI_PACK_FORMAT);
DftiSetValue(table->desc, DFTI_PLACEMENT, DFTI_NOT_INPLACE);
DftiSetValue(table->desc, DFTI_FORWARD_SCALE, 1.0f / size);
DftiCommitDescriptor(table->desc);
return table;
}
void spx_fft_destroy(void *table)
{
struct mkl_config *t = (struct mkl_config *) table;
DftiFreeDescriptor(t->desc);
speex_free(table);
}
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct mkl_config *t = (struct mkl_config *) table;
DftiComputeForward(t->desc, in, out);
}
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct mkl_config *t = (struct mkl_config *) table;
DftiComputeBackward(t->desc, in, out);
}
#elif defined(USE_INTEL_IPP)
#include <ipps.h>
struct ipp_fft_config
{
IppsDFTSpec_R_32f *dftSpec;
Ipp8u *buffer;
};
void *spx_fft_init(int size)
{
int bufferSize = 0;
int hint;
struct ipp_fft_config *table;
table = (struct ipp_fft_config *)speex_alloc(sizeof(struct ipp_fft_config));
/* there appears to be no performance difference between ippAlgHintFast and
ippAlgHintAccurate when using the with the floating point version
of the fft. */
hint = ippAlgHintAccurate;
ippsDFTInitAlloc_R_32f(&table->dftSpec, size, IPP_FFT_DIV_FWD_BY_N, hint);
ippsDFTGetBufSize_R_32f(table->dftSpec, &bufferSize);
table->buffer = ippsMalloc_8u(bufferSize);
return table;
}
void spx_fft_destroy(void *table)
{
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
ippsFree(t->buffer);
ippsDFTFree_R_32f(t->dftSpec);
speex_free(t);
}
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
ippsDFTFwd_RToPack_32f(in, out, t->dftSpec, t->buffer);
}
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
ippsDFTInv_PackToR_32f(in, out, t->dftSpec, t->buffer);
}
#elif defined(USE_GPL_FFTW3)
#include <fftw3.h>
struct fftw_config {
float *in;
float *out;
fftwf_plan fft;
fftwf_plan ifft;
int N;
};
void *spx_fft_init(int size)
{
struct fftw_config *table = (struct fftw_config *) speex_alloc(sizeof(struct fftw_config));
table->in = fftwf_malloc(sizeof(float) * (size+2));
table->out = fftwf_malloc(sizeof(float) * (size+2));
table->fft = fftwf_plan_dft_r2c_1d(size, table->in, (fftwf_complex *) table->out, FFTW_PATIENT);
table->ifft = fftwf_plan_dft_c2r_1d(size, (fftwf_complex *) table->in, table->out, FFTW_PATIENT);
table->N = size;
return table;
}
void spx_fft_destroy(void *table)
{
struct fftw_config *t = (struct fftw_config *) table;
fftwf_destroy_plan(t->fft);
fftwf_destroy_plan(t->ifft);
fftwf_free(t->in);
fftwf_free(t->out);
speex_free(table);
}
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int i;
struct fftw_config *t = (struct fftw_config *) table;
const int N = t->N;
float *iptr = t->in;
float *optr = t->out;
const float m = 1.0 / N;
for(i=0;i<N;++i)
iptr[i]=in[i] * m;
fftwf_execute(t->fft);
out[0] = optr[0];
for(i=1;i<N;++i)
out[i] = optr[i+1];
}
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int i;
struct fftw_config *t = (struct fftw_config *) table;
const int N = t->N;
float *iptr = t->in;
float *optr = t->out;
iptr[0] = in[0];
iptr[1] = 0.0f;
for(i=1;i<N;++i)
iptr[i+1] = in[i];
iptr[N+1] = 0.0f;
fftwf_execute(t->ifft);
for(i=0;i<N;++i)
out[i] = optr[i];
}
#elif defined(USE_KISS_FFT)
#include "kiss_fftr.h"
#include "kiss_fft.h"
struct kiss_config {
kiss_fftr_cfg forward;
kiss_fftr_cfg backward;
int N;
};
void *spx_fft_init(int size)
{
struct kiss_config *table;
table = (struct kiss_config*)speex_alloc(sizeof(struct kiss_config));
table->forward = kiss_fftr_alloc(size,0,NULL,NULL);
table->backward = kiss_fftr_alloc(size,1,NULL,NULL);
table->N = size;
return table;
}
void spx_fft_destroy(void *table)
{
struct kiss_config *t = (struct kiss_config *)table;
kiss_fftr_free(t->forward);
kiss_fftr_free(t->backward);
speex_free(table);
}
#ifdef FIXED_POINT
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int shift;
struct kiss_config *t = (struct kiss_config *)table;
shift = maximize_range(in, in, 32000, t->N);
kiss_fftr2(t->forward, in, out);
renorm_range(in, in, shift, t->N);
renorm_range(out, out, shift, t->N);
}
#else
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int i;
float scale;
struct kiss_config *t = (struct kiss_config *)table;
scale = 1./t->N;
kiss_fftr2(t->forward, in, out);
for (i=0;i<t->N;i++)
out[i] *= scale;
}
#endif
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct kiss_config *t = (struct kiss_config *)table;
kiss_fftri2(t->backward, in, out);
}
#else
#error No other FFT implemented
#endif
#ifdef FIXED_POINT
/*#include "smallft.h"*/
void spx_fft_float(void *table, float *in, float *out)
{
int i;
#ifdef USE_SMALLFT
int N = ((struct drft_lookup *)table)->n;
#elif defined(USE_KISS_FFT)
int N = ((struct kiss_config *)table)->N;
#else
#endif
#ifdef VAR_ARRAYS
spx_word16_t _in[N];
spx_word16_t _out[N];
#else
spx_word16_t _in[MAX_FFT_SIZE];
spx_word16_t _out[MAX_FFT_SIZE];
#endif
for (i=0;i<N;i++)
_in[i] = (int)floor(.5+in[i]);
spx_fft(table, _in, _out);
for (i=0;i<N;i++)
out[i] = _out[i];
#if 0
if (!fixed_point)
{
float scale;
struct drft_lookup t;
spx_drft_init(&t, ((struct kiss_config *)table)->N);
scale = 1./((struct kiss_config *)table)->N;
for (i=0;i<((struct kiss_config *)table)->N;i++)
out[i] = scale*in[i];
spx_drft_forward(&t, out);
spx_drft_clear(&t);
}
#endif
}
void spx_ifft_float(void *table, float *in, float *out)
{
int i;
#ifdef USE_SMALLFT
int N = ((struct drft_lookup *)table)->n;
#elif defined(USE_KISS_FFT)
int N = ((struct kiss_config *)table)->N;
#else
#endif
#ifdef VAR_ARRAYS
spx_word16_t _in[N];
spx_word16_t _out[N];
#else
spx_word16_t _in[MAX_FFT_SIZE];
spx_word16_t _out[MAX_FFT_SIZE];
#endif
for (i=0;i<N;i++)
_in[i] = (int)floor(.5+in[i]);
spx_ifft(table, _in, _out);
for (i=0;i<N;i++)
out[i] = _out[i];
#if 0
if (!fixed_point)
{
int i;
struct drft_lookup t;
spx_drft_init(&t, ((struct kiss_config *)table)->N);
for (i=0;i<((struct kiss_config *)table)->N;i++)
out[i] = in[i];
spx_drft_backward(&t, out);
spx_drft_clear(&t);
}
#endif
}
#else
void spx_fft_float(void *table, float *in, float *out)
{
spx_fft(table, in, out);
}
void spx_ifft_float(void *table, float *in, float *out)
{
spx_ifft(table, in, out);
}
#endif
+58
View File
@@ -0,0 +1,58 @@
/* Copyright (C) 2005 Jean-Marc Valin
File: fftwrap.h
Wrapper for various FFTs
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the Xiph.org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef FFTWRAP_H
#define FFTWRAP_H
#include "arch.h"
/** Compute tables for an FFT */
void *spx_fft_init(int size);
/** Destroy tables for an FFT */
void spx_fft_destroy(void *table);
/** Forward (real to half-complex) transform */
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out);
/** Backward (half-complex to real) transform */
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out);
/** Forward (real to half-complex) transform of float data */
void spx_fft_float(void *table, float *in, float *out);
/** Backward (half-complex to real) transform of float data */
void spx_ifft_float(void *table, float *in, float *out);
#endif
+170
View File
@@ -0,0 +1,170 @@
/* Copyright (C) Jean-Marc Valin */
/**
@file speex_echo.h
@brief Echo cancellation
*/
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SPEEX_ECHO_H
#define SPEEX_ECHO_H
/** @defgroup SpeexEchoState SpeexEchoState: Acoustic echo canceller
* This is the acoustic echo canceller module.
* @{
*/
#include "speexdsp_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Obtain frame size used by the AEC */
#define SPEEX_ECHO_GET_FRAME_SIZE 3
/** Set sampling rate */
#define SPEEX_ECHO_SET_SAMPLING_RATE 24
/** Get sampling rate */
#define SPEEX_ECHO_GET_SAMPLING_RATE 25
/* Can't set window sizes */
/** Get size of impulse response (int32) */
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE_SIZE 27
/* Can't set window content */
/** Get impulse response (int32[]) */
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE 29
/** Internal echo canceller state. Should never be accessed directly. */
struct SpeexEchoState_;
/** @class SpeexEchoState
* This holds the state of the echo canceller. You need one per channel.
*/
/** Internal echo canceller state. Should never be accessed directly. */
typedef struct SpeexEchoState_ SpeexEchoState;
/** Creates a new echo canceller state
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms)
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms)
* @return Newly-created echo canceller state
*/
SpeexEchoState *speex_echo_state_init(int frame_size, int filter_length);
/** Creates a new multi-channel echo canceller state
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms)
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms)
* @param nb_mic Number of microphone channels
* @param nb_speakers Number of speaker channels
* @return Newly-created echo canceller state
*/
SpeexEchoState *speex_echo_state_init_mc(int frame_size, int filter_length, int nb_mic, int nb_speakers);
/** Destroys an echo canceller state
* @param st Echo canceller state
*/
void speex_echo_state_destroy(SpeexEchoState *st);
/** Performs echo cancellation a frame, based on the audio sent to the speaker (no delay is added
* to playback in this form)
*
* @param st Echo canceller state
* @param rec Signal from the microphone (near end + far end echo)
* @param play Signal played to the speaker (received from far end)
* @param out Returns near-end signal with echo removed
*/
void speex_echo_cancellation(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out);
/** Performs echo cancellation a frame (deprecated) */
void speex_echo_cancel(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out, spx_int32_t *Yout);
/** Perform echo cancellation using internal playback buffer, which is delayed by two frames
* to account for the delay introduced by most soundcards (but it could be off!)
* @param st Echo canceller state
* @param rec Signal from the microphone (near end + far end echo)
* @param out Returns near-end signal with echo removed
*/
void speex_echo_capture(SpeexEchoState *st, const spx_int16_t *rec, spx_int16_t *out);
/** Let the echo canceller know that a frame was just queued to the soundcard
* @param st Echo canceller state
* @param play Signal played to the speaker (received from far end)
*/
void speex_echo_playback(SpeexEchoState *st, const spx_int16_t *play);
/** Reset the echo canceller to its original state
* @param st Echo canceller state
*/
void speex_echo_state_reset(SpeexEchoState *st);
/** Used like the ioctl function to control the echo canceller parameters
*
* @param st Echo canceller state
* @param request ioctl-type request (one of the SPEEX_ECHO_* macros)
* @param ptr Data exchanged to-from function
* @return 0 if no error, -1 if request in unknown
*/
int speex_echo_ctl(SpeexEchoState *st, int request, void *ptr);
struct SpeexDecorrState_;
typedef struct SpeexDecorrState_ SpeexDecorrState;
/** Create a state for the channel decorrelation algorithm
This is useful for multi-channel echo cancellation only
* @param rate Sampling rate
* @param channels Number of channels (it's a bit pointless if you don't have at least 2)
* @param frame_size Size of the frame to process at ones (counting samples *per* channel)
*/
SpeexDecorrState *speex_decorrelate_new(int rate, int channels, int frame_size);
/** Remove correlation between the channels by modifying the phase and possibly
adding noise in a way that is not (or little) perceptible.
* @param st Decorrelator state
* @param in Input audio in interleaved format
* @param out Result of the decorrelation (out *may* alias in)
* @param strength How much alteration of the audio to apply from 0 to 100.
*/
void speex_decorrelate(SpeexDecorrState *st, const spx_int16_t *in, spx_int16_t *out, int strength);
/** Destroy a Decorrelation state
* @param st State to destroy
*/
void speex_decorrelate_destroy(SpeexDecorrState *st);
#ifdef __cplusplus
}
#endif
/** @}*/
#endif
Loaded 100 of 1558 files, more files were not shown because too many files have changed in this diff. Show more