From 0e1fd699c21a3c4c3bc98b4d7681ddc4d2d60022 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:59:42 -0700 Subject: [PATCH 1/6] Expose pattern and clock sources Signed-off-by: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> --- Cargo.lock | 606 +++++++++++++++++++++++++++++-------- Cargo.toml | 8 +- README.md | 1 + resources/moving-logo.png | 3 - src/media/capture_track.rs | 164 ++++++++++ src/media/logo_track.rs | 217 ------------- src/media/mod.rs | 8 +- src/room/menu_bar.rs | 11 +- src/service.rs | 27 +- 9 files changed, 684 insertions(+), 361 deletions(-) delete mode 100644 resources/moving-logo.png create mode 100644 src/media/capture_track.rs delete mode 100644 src/media/logo_track.rs diff --git a/Cargo.lock b/Cargo.lock index e5f6435..c5ef582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,7 +579,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -620,6 +620,35 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex 1.3.0", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec", +] + [[package]] name = "bit-set" version = "0.10.0" @@ -825,7 +854,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -834,6 +863,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -879,6 +917,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.6.5" @@ -1325,7 +1374,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "pollster", + "pollster 1.0.1", "profiling", "raw-window-handle", "ron", @@ -1334,7 +1383,7 @@ dependencies = [ "wasm-bindgen", "web-sys", "web-time", - "wgpu", + "wgpu 30.0.1", "windows-sys 0.61.2", "winit", ] @@ -1377,7 +1426,7 @@ dependencies = [ "thiserror 2.0.18", "type-map", "web-time", - "wgpu", + "wgpu 30.0.1", "winit", ] @@ -1653,6 +1702,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -2061,6 +2116,26 @@ dependencies = [ "windows", ] +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "guillotiere" version = "0.7.0" @@ -2123,12 +2198,6 @@ dependencies = [ "foldhash 0.2.0", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -2147,6 +2216,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hmac" version = "0.12.1" @@ -2468,15 +2543,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -2737,15 +2803,14 @@ dependencies = [ [[package]] name = "libwebrtc" -version = "0.3.45" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e222d9f53f1fc61dd2656c4835657d987bd387019e8f6026329c918178044eff" +checksum = "14f9145a63a8439508a9b5b51dffa4b1c12f812f49fcc4310ebae3d467f639ab" dependencies = [ "cxx", "jni 0.21.1", "js-sys", "lazy_static", - "livekit-runtime", "log", "parking_lot", "rtrb", @@ -2753,6 +2818,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", + "tokio-stream", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2800,29 +2866,26 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "livekit" -version = "0.8.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c9a22daebb6058be54653f1f6a02eae6a353f9986f73b4c68dc11365134d198" +checksum = "4dc818741891a8179eafd4665156482761d5084fb15238c1de9c211fdb974e3e" dependencies = [ - "base64 0.22.1", "bmrng", "bytes", "chrono", - "flate2", "futures-util", "lazy_static", "libloading", "libwebrtc", - "livekit-api", "livekit-common", "livekit-data-stream", "livekit-datatrack", "livekit-protocol", - "livekit-runtime", + "livekit-rpc", + "livekit-signaling", "log", "parking_lot", "prost", - "semver", "serde", "serde_json", "thiserror 1.0.69", @@ -2832,52 +2895,54 @@ dependencies = [ [[package]] name = "livekit-api" -version = "0.6.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ed580af1187bd8fad22a981fde55cad03d4f9468d9eb9c6ecce6c753371aae" +checksum = "6b9ac53fb386bfdf21d397a28626e342051003c2ed52ff30c6d553b5faf10b78" dependencies = [ - "base64 0.21.7", - "bytes", - "device-info", - "flate2", - "hmac", "http", - "jsonwebtoken", - "livekit-common", - "livekit-net", "livekit-protocol", - "livekit-runtime", + "livekit-token", "log", - "os_info", - "parking_lot", "pbjson-types", "prost", "rand 0.9.4", - "reqwest", - "scopeguard", "serde", - "serde_json", "sha2", - "signature", "thiserror 2.0.18", - "tokio", "url", ] +[[package]] +name = "livekit-capture" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b7c04266141d00b3b1adbf3e6c00cab40d49069a96667622980caebc04bbd1" +dependencies = [ + "bytes", + "chrono", + "livekit", + "log", + "pollster 0.4.0", + "thiserror 2.0.18", + "tokio", + "wgpu 29.0.4", + "yuv-sys", +] + [[package]] name = "livekit-common" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f903bfee33454f21c553dbf14d6f4ffbf062157b23b04fde9821cbb94ff01169" +checksum = "481b851cea3228afcb505a076bccdfd31a9543c6e91e79e433a5bf8c09adc1de" dependencies = [ "livekit-protocol", ] [[package]] name = "livekit-data-stream" -version = "0.1.2" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfdf91af5f2dade508e2f81c7f63c79f77461f3d15cabe6b7059fe5f2839d58" +checksum = "7160843d65baa74d638af85e60520a4926e6cf11e065f5be6ad3098207fe508e" dependencies = [ "async-compression", "bmrng", @@ -2898,18 +2963,16 @@ dependencies = [ [[package]] name = "livekit-datatrack" -version = "0.1.13" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "376a22d6dae592bff4068a551343461b8e683ffe70aab92e037fb91cb9237ad2" +checksum = "4f42d8fb41e9ecd426fda7da1324c4d30f157f6e30c9a801153b098d48164b02" dependencies = [ "anyhow", "bytes", "from_variants", "futures-core", - "futures-util", "indexmap", "livekit-protocol", - "livekit-runtime", "log", "rand 0.9.4", "thiserror 2.0.18", @@ -2919,16 +2982,15 @@ dependencies = [ [[package]] name = "livekit-net" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46155c42ad7acccca96a0d8be2bd0f379565f93e85144fb3aa6fa406371fee22" +checksum = "f9528a26c807f49d3fa6d075c01db5fd77bf32454b1e700c530fb8c670892008" dependencies = [ "async-trait", "base64 0.21.7", "bytes", "futures-util", "http", - "livekit-runtime", "log", "reqwest", "rustls-native-certs", @@ -2940,9 +3002,9 @@ dependencies = [ [[package]] name = "livekit-protocol" -version = "0.7.12" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "526f22bddf409e5f15449d55cf341647d7c16a1f43df9db9b05be106e4208e1c" +checksum = "9f17d2719e29b2bdb5511a4085b2fa62ad5e47a1e14e459f007ddacf285635a3" dependencies = [ "pbjson", "pbjson-types", @@ -2951,20 +3013,75 @@ dependencies = [ ] [[package]] -name = "livekit-runtime" -version = "0.4.0" +name = "livekit-region" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e94621b7d7cbb4544cd568c8e632852cc174d383055b0e77f4638eb91e43d3b" +dependencies = [ + "serde", +] + +[[package]] +name = "livekit-rpc" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" +checksum = "b8d8dcad63cfb870692fb0e14d1373e370bc2e820c71a473f85774bd3f0745c3" dependencies = [ + "livekit-common", + "livekit-data-stream", + "livekit-protocol", + "log", + "parking_lot", + "semver", + "thiserror 2.0.18", "tokio", - "tokio-stream", + "uuid", +] + +[[package]] +name = "livekit-signaling" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fd4fb8ade2b175576955b5927adf2ab4375bbe46f2bfcd3edc658e272f0eef" +dependencies = [ + "base64 0.21.7", + "device-info", + "flate2", + "http", + "livekit-common", + "livekit-net", + "livekit-protocol", + "livekit-region", + "log", + "os_info", + "parking_lot", + "prost", + "serde_json", + "thiserror 2.0.18", + "tokio", + "url", +] + +[[package]] +name = "livekit-token" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d29a80095baaf837f037c313c026af0482d48ccd9ace44b1147d64ac42d1e51" +dependencies = [ + "hmac", + "jsonwebtoken", + "livekit-protocol", + "serde", + "sha2", + "signature", + "thiserror 2.0.18", ] [[package]] name = "livekit-token-source" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f686ce4be5975c8ad522ceaf8d1c2428f83f76d4fdd4ee1d25effaec80ff5de6" +checksum = "31421122bdccd2439327f1a3aec33a24dca365c84127c232de05637d548cef9b" dependencies = [ "async-trait", "base64 0.21.7", @@ -3038,6 +3155,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3075,6 +3198,32 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.18", + "unicode-ident", +] + [[package]] name = "naga" version = "30.0.1" @@ -3082,7 +3231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.10.0", "bitflags 2.13.0", "cfg-if", "cfg_aliases", @@ -3176,6 +3325,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -3753,7 +3912,7 @@ dependencies = [ "backtrace", "cfg-if", "libc", - "petgraph", + "petgraph 0.6.5", "redox_syscall 0.5.18", "smallvec", "windows-link", @@ -3773,31 +3932,31 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pbjson" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" +checksum = "e8edd1efdd8ab23ba9cb9ace3d9987a72663d5d7c9f74fa00b51d6213645cf6c" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "serde", ] [[package]] name = "pbjson-build" -version = "0.6.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" +checksum = "2ed4d5c6ae95e08ac768883c8401cf0e8deb4e6e1d6a4e1fd3d2ec4f0ec63200" dependencies = [ - "heck 0.4.1", - "itertools 0.11.0", + "heck", + "itertools 0.14.0", "prost", "prost-types", ] [[package]] name = "pbjson-types" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" +checksum = "a14e2757d877c0f607a82ce1b8560e224370f159d66c5d52eb55ea187ef0350e" dependencies = [ "bytes", "chrono", @@ -3833,7 +3992,18 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", + "indexmap", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset 0.5.7", + "hashbrown 0.15.5", "indexmap", ] @@ -3956,6 +4126,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "pollster" version = "1.0.1" @@ -4059,9 +4235,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.6" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -4069,17 +4245,15 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.6" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "bytes", - "heck 0.5.0", - "itertools 0.12.1", + "heck", + "itertools 0.14.0", "log", "multimap", - "once_cell", - "petgraph", + "petgraph 0.8.3", "prettyplease", "prost", "prost-types", @@ -4090,12 +4264,12 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -4103,9 +4277,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.6" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -4541,12 +4715,13 @@ dependencies = [ "image", "livekit", "livekit-api", + "livekit-capture", "livekit-token-source", "log", "parking_lot", "serde", "tokio", - "wgpu", + "wgpu 30.0.1", "winit", ] @@ -4835,6 +5010,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" @@ -5907,9 +6088,9 @@ dependencies = [ [[package]] name = "webrtc-sys" -version = "0.3.42" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd328126bc3653b10eab635579f74a562a472ff032ff09b5d1681d35e6201fd" +checksum = "9d87fa13acfe63505a0e8c1311b5dc60323a429196c4a7ba18b63de34e5cba67" dependencies = [ "cc", "cxx", @@ -5922,9 +6103,9 @@ dependencies = [ [[package]] name = "webrtc-sys-build" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d46da6b5a5cbd091fae0400f77189f4ca4807c0d9442b85838a584f28720570" +checksum = "0cd4f418e16f4b7ba028e6b5c70e4f532c7d2b5ca39726d42b699f3e4591ed5c" dependencies = [ "anyhow", "fs2", @@ -5941,6 +6122,36 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.0", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga 29.0.4", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core 29.0.4", + "wgpu-hal 29.0.4", + "wgpu-types 29.0.4", +] + [[package]] name = "wgpu" version = "30.0.1" @@ -5956,7 +6167,7 @@ dependencies = [ "hashbrown 0.17.1", "js-sys", "log", - "naga", + "naga 30.0.1", "parking_lot", "portable-atomic", "profiling", @@ -5966,9 +6177,42 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "wgpu-core 30.0.1", + "wgpu-hal 30.0.1", + "wgpu-types 30.0.1", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec", + "bitflags 2.13.0", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga 29.0.4", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-apple 29.0.4", + "wgpu-core-deps-emscripten 29.0.4", + "wgpu-core-deps-windows-linux-android 29.0.4", + "wgpu-hal 29.0.4", + "wgpu-naga-bridge 29.0.4", + "wgpu-types 29.0.4", ] [[package]] @@ -5978,7 +6222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.10.0", "bit-vec", "bitflags 2.13.0", "bytemuck", @@ -5987,7 +6231,7 @@ dependencies = [ "hashbrown 0.17.1", "indexmap", "log", - "naga", + "naga 30.0.1", "naga-types", "once_cell", "parking_lot", @@ -5997,13 +6241,22 @@ dependencies = [ "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.18", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", + "wgpu-core-deps-apple 30.0.1", + "wgpu-core-deps-emscripten 30.0.1", "wgpu-core-deps-wasm", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-naga-bridge", - "wgpu-types", + "wgpu-core-deps-windows-linux-android 30.0.1", + "wgpu-hal 30.0.1", + "wgpu-naga-bridge 30.0.1", + "wgpu-types 30.0.1", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +dependencies = [ + "wgpu-hal 29.0.4", ] [[package]] @@ -6012,7 +6265,16 @@ version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" dependencies = [ - "wgpu-hal", + "wgpu-hal 30.0.1", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" +dependencies = [ + "wgpu-hal 29.0.4", ] [[package]] @@ -6021,7 +6283,7 @@ version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" dependencies = [ - "wgpu-hal", + "wgpu-hal 30.0.1", ] [[package]] @@ -6030,7 +6292,16 @@ version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae" dependencies = [ - "wgpu-hal", + "wgpu-hal 30.0.1", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal 29.0.4", ] [[package]] @@ -6039,7 +6310,61 @@ version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" dependencies = [ - "wgpu-hal", + "wgpu-hal 30.0.1", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.9.1", + "bitflags 2.13.0", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga 29.0.4", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge 29.0.4", + "wgpu-types 29.0.4", + "windows", + "windows-core", + "windows-result", ] [[package]] @@ -6051,7 +6376,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.10.0", "bitflags 2.13.0", "block2 0.6.2", "bytemuck", @@ -6066,7 +6391,7 @@ dependencies = [ "libc", "libloading", "log", - "naga", + "naga 30.0.1", "naga-types", "ndk-sys", "objc2 0.6.4", @@ -6091,21 +6416,45 @@ dependencies = [ "wasm-bindgen", "wayland-sys", "web-sys", - "wgpu-naga-bridge", - "wgpu-types", + "wgpu-naga-bridge 30.0.1", + "wgpu-types 30.0.1", "windows", "windows-core", "windows-result", ] +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga 29.0.4", + "wgpu-types 29.0.4", +] + [[package]] name = "wgpu-naga-bridge" version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" dependencies = [ - "naga", - "wgpu-types", + "naga 30.0.1", + "wgpu-types 30.0.1", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", ] [[package]] @@ -6505,7 +6854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -6516,7 +6865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap", "prettyplease", "syn 2.0.117", @@ -6675,6 +7024,19 @@ dependencies = [ "synstructure", ] +[[package]] +name = "yuv-sys" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769d9a5cace19cbafd5cde04054c0bbaaf948dea1fc317121919bac791b1b53d" +dependencies = [ + "bindgen", + "cc", + "pkg-config", + "rayon", + "regex", +] + [[package]] name = "zbus" version = "5.16.0" diff --git a/Cargo.toml b/Cargo.toml index 237f499..1762bd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,10 @@ tokio = { version = "1", features = ["full", "parking_lot"] } # egui-wgpu 0.36 requires wgpu ^30.0; "30.0" resolves to a compatible 30.0.x. wgpu = "30.0" winit = { version = "0.30.13", features = [ "android-native-activity" ] } -livekit = { version = "0.8.3", features = ["rustls-tls-native-roots"] } -livekit-api = { version = "0.6.3", default-features = false, features = ["access-token"] } -livekit-token-source = { version = "0.1.1", features = ["rustls-tls-native-roots"] } +livekit = { version = "0.9.4", features = ["rustls-tls-native-roots"] } +livekit-api = { version = "0.8.2", default-features = false, features = ["access-token"] } +livekit-token-source = { version = "0.1.3", features = ["rustls-tls-native-roots"] } +livekit-capture = { version = "0.1.4", default-features = false, features = ["source-pattern", "source-clock"] } # Building against a local rust-sdks checkout (see "Building against a local # rust-sdks" in README.md). To build against the released crates instead, @@ -28,6 +29,7 @@ livekit-token-source = { version = "0.1.1", features = ["rustls-tls-native-roots # livekit = { path = "../rust-sdks/livekit", features = ["rustls-tls-native-roots"] } # livekit-api = { path = "../rust-sdks/livekit-api", default-features = false, features = ["access-token"] } # livekit-token-source = { path = "../rust-sdks/livekit-token-source", features = ["rustls-tls-native-roots"] } +# livekit-capture = { path = "../rust-sdks/livekit-capture", default-features = false, features = ["source-pattern", "source-clock"] } [package.metadata.bundle] name = "LiveKit Client" diff --git a/README.md b/README.md index 0c41bee..f1f3ecd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ An example of building a cross-platform, GUI application using the [LiveKit Rust - [x] Connect to multiple LiveKit rooms - [x] Use either pre-generated token or project API key/secret - [x] Publish test tracks +- [x] Publish generated video ([livekit-capture](https://github.com/livekit/rust-sdks/tree/main/livekit-capture) gradient, logo, and clock sources) - [x] Publish local microphone audio (platform audio) - [x] Subscribe to tracks - [x] Simulate fault scenarios (e.g., reconnect, migration, etc.) diff --git a/resources/moving-logo.png b/resources/moving-logo.png deleted file mode 100644 index ba54fa1..0000000 --- a/resources/moving-logo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e3ba3c18d7a6627a7b6fab53d8523bdc504d06ae9a26f8ae042732b4199a8383 -size 7170 diff --git a/src/media/capture_track.rs b/src/media/capture_track.rs new file mode 100644 index 0000000..4989456 --- /dev/null +++ b/src/media/capture_track.rs @@ -0,0 +1,164 @@ +use livekit::options::{TrackPublishOptions, VideoCodec}; +use livekit::prelude::*; +use livekit_capture::{ + error::SourceError, + pixel::{PixelVideoPump, PixelVideoSource}, + primitive::VideoResolution, + pump::PumpStop, + sources::{ + clock::{ClockVideoSource, ClockVideoSourceConfig}, + pattern::{Pattern, PatternVideoSource, PatternVideoSourceConfig}, + }, +}; +use std::error::Error; +use std::sync::Arc; +use tokio::task::JoinHandle; + +const RESOLUTION: VideoResolution = VideoResolution::new(1280, 720); +const FRAMERATE_FPS: u32 = 30; + +type BoxError = Box; + +/// A user-publishable video source backed by `livekit-capture`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CaptureSource { + Gradient, + Logo, + Clock, +} + +impl CaptureSource { + pub const ALL: [Self; 3] = [Self::Gradient, Self::Logo, Self::Clock]; + + pub fn label(self) -> &'static str { + match self { + Self::Gradient => "Gradient", + Self::Logo => "Logo", + Self::Clock => "Clock", + } + } + + pub fn description(self) -> &'static str { + match self { + Self::Gradient => "Animated color gradient test pattern", + Self::Logo => "Bouncing LiveKit logo test pattern", + Self::Clock => "Local wall clock with millisecond precision, for measuring latency", + } + } + + fn track_name(self) -> &'static str { + match self { + Self::Gradient => "gradient", + Self::Logo => "logo", + Self::Clock => "clock", + } + } + + /// Builds the source; GPU setup runs on the blocking pool. + async fn open(self) -> Result, SourceError> { + let pattern = match self { + Self::Gradient => Pattern::Gradient, + Self::Logo => Pattern::Logo, + Self::Clock => { + let config = ClockVideoSourceConfig { + resolution: RESOLUTION, + framerate_fps: FRAMERATE_FPS, + }; + return Ok(Box::new(ClockVideoSource::new(config).await?)); + } + }; + let config = PatternVideoSourceConfig { + resolution: RESOLUTION, + framerate_fps: FRAMERATE_FPS, + pattern, + }; + Ok(Box::new(PatternVideoSource::new(config).await?)) + } +} + +/// Publishes a [`CaptureSource`] as a video track. +/// +/// The track's publication is tied to its pump: a task owns the running pump +/// and unpublishes the track whenever the pump exits, whether stopped here or +/// on its own (end of stream, error, or panic). Dropping stops the pump without +/// waiting for the unpublish. +pub struct CaptureTrack { + room: Arc, + source: CaptureSource, + handle: Option, +} + +struct TrackHandle { + stop: PumpStop, + /// Finishes once the pump has exited and the track is unpublished. + task: JoinHandle<()>, +} + +impl CaptureTrack { + pub fn new(room: Arc, source: CaptureSource) -> Self { + Self { + room, + source, + handle: None, + } + } + + pub fn is_published(&self) -> bool { + self.handle + .as_ref() + .is_some_and(|handle| !handle.task.is_finished()) + } + + pub async fn publish(&mut self) -> Result<(), BoxError> { + self.unpublish().await; + + let pump = PixelVideoPump::new(self.source.open().await?); + let track = + LocalVideoTrack::create_video_track(self.source.track_name(), pump.rtc_source()); + let options = TrackPublishOptions { + source: TrackSource::Camera, + simulcast: true, + video_codec: VideoCodec::H265, + ..pump.publish_options() + }; + let participant = self.room.local_participant(); + participant + .publish_track(LocalTrack::Video(track.clone()), options) + .await?; + + let pump = match pump.spawn() { + Ok(pump) => pump, + Err(err) => { + let _ = participant.unpublish_track(&track.sid()).await; + return Err(err.into()); + } + }; + let stop = pump.stop_handle(); + let source = self.source; + let task = tokio::spawn(async move { + let result = pump.join().await; + log::info!("{source:?} capture pump exited: {result:?}"); + if let Err(err) = participant.unpublish_track(&track.sid()).await { + log::warn!("failed to unpublish {source:?} capture track: {err}"); + } + }); + self.handle = Some(TrackHandle { stop, task }); + Ok(()) + } + + /// Stops the pump and waits for its task to unpublish the track. + pub async fn unpublish(&mut self) { + if let Some(handle) = self.handle.take() { + handle.stop.stop(); + let _ = handle.task.await; + } + } +} + +impl Drop for CaptureTrack { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + handle.stop.stop(); + } + } +} diff --git a/src/media/logo_track.rs b/src/media/logo_track.rs deleted file mode 100644 index 3e5dce8..0000000 --- a/src/media/logo_track.rs +++ /dev/null @@ -1,217 +0,0 @@ -use image::ImageFormat; -use image::RgbaImage; -use livekit::options::TrackPublishOptions; -use livekit::options::VideoCodec; -use livekit::prelude::*; -use livekit::webrtc::video_source::RtcVideoSource; -use livekit::webrtc::video_source::VideoResolution; -use livekit::webrtc::{ - native::yuv_helper, - video_frame::{I420Buffer, VideoFrame, VideoRotation}, - video_source::native::NativeVideoSource, -}; -use parking_lot::Mutex; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::oneshot; -use tokio::task::JoinHandle; - -// The logo must not be bigger than the framebuffer -const PIXEL_SIZE: usize = 4; -const FRAME_RATE: u64 = 30; -const MOVE_SPEED: i32 = 16; -const FB_WIDTH: usize = 1280; -const FB_HEIGHT: usize = 720; - -#[derive(Clone)] -struct FrameData { - image: Arc, - framebuffer: Arc>>, - video_frame: Arc>>, - pos: (i32, i32), - direction: (i32, i32), -} - -struct TrackHandle { - close_tx: oneshot::Sender<()>, - track: LocalVideoTrack, - task: JoinHandle<()>, -} - -pub struct LogoTrack { - rtc_source: NativeVideoSource, - room: Arc, - handle: Option, -} - -impl LogoTrack { - pub fn new(room: Arc) -> Self { - Self { - rtc_source: NativeVideoSource::new( - VideoResolution { - width: FB_WIDTH as u32, - height: FB_HEIGHT as u32, - }, - false, - ), - room, - handle: None, - } - } - - pub fn is_published(&self) -> bool { - self.handle.is_some() - } - - pub async fn publish(&mut self) -> Result<(), RoomError> { - self.unpublish().await?; - - let (close_tx, close_rx) = oneshot::channel(); - let track = LocalVideoTrack::create_video_track( - "livekit_logo", - RtcVideoSource::Native(self.rtc_source.clone()), - ); - - let task = tokio::spawn(Self::track_task(close_rx, self.rtc_source.clone())); - - self.room - .local_participant() - .publish_track( - LocalTrack::Video(track.clone()), - TrackPublishOptions { - source: TrackSource::Camera, - simulcast: true, - video_codec: VideoCodec::H265, - ..Default::default() - }, - ) - .await?; - - let handle = TrackHandle { - close_tx, - task, - track, - }; - - self.handle = Some(handle); - Ok(()) - } - - pub async fn unpublish(&mut self) -> Result<(), RoomError> { - if let Some(handle) = self.handle.take() { - let _ = handle.close_tx.send(()); - let _ = handle.task.await; - - self.room - .local_participant() - .unpublish_track(&handle.track.sid()) - .await?; - } - Ok(()) - } - - async fn track_task(mut close_rx: oneshot::Receiver<()>, rtc_source: NativeVideoSource) { - let mut interval = tokio::time::interval(Duration::from_millis(1000 / FRAME_RATE)); - - let image = tokio::task::spawn_blocking(|| { - image::load_from_memory_with_format( - include_bytes!("../../resources/moving-logo.png"), - ImageFormat::Png, - ) - .unwrap() - .to_rgba8() - }) - .await - .unwrap(); - - let mut data = FrameData { - image: Arc::new(image), - framebuffer: Arc::new(Mutex::new(vec![0u8; FB_WIDTH * FB_HEIGHT * 4])), - video_frame: Arc::new(Mutex::new(VideoFrame { - rotation: VideoRotation::VideoRotation0, - timestamp_us: 0, - frame_metadata: None, - buffer: I420Buffer::new(FB_WIDTH as u32, FB_HEIGHT as u32), - })), - pos: (0, 0), - direction: (1, 1), - }; - - loop { - tokio::select! { - _ = &mut close_rx => { - break; - } - _ = interval.tick() => {} - } - - data.pos.0 += data.direction.0 * MOVE_SPEED; - data.pos.1 += data.direction.1 * MOVE_SPEED; - - if data.pos.0 >= (FB_WIDTH - data.image.width() as usize) as i32 { - data.direction.0 = -1; - } else if data.pos.0 <= 0 { - data.direction.0 = 1; - } - - if data.pos.1 >= (FB_HEIGHT - data.image.height() as usize) as i32 { - data.direction.1 = -1; - } else if data.pos.1 <= 0 { - data.direction.1 = 1; - } - - tokio::task::spawn_blocking({ - let data = data.clone(); - let source = rtc_source.clone(); - move || { - let image = data.image.as_raw(); - let mut framebuffer = data.framebuffer.lock(); - let mut video_frame = data.video_frame.lock(); - let i420_buffer = &mut video_frame.buffer; - - let (stride_y, stride_u, stride_v) = i420_buffer.strides(); - let (data_y, data_u, data_v) = i420_buffer.data_mut(); - - framebuffer.fill(0); - for i in 0..data.image.height() as usize { - let x = data.pos.0 as usize; - let y = data.pos.1 as usize; - let frame_width = data.image.width() as usize; - let logo_stride = frame_width * PIXEL_SIZE; - let row_start = (x + ((i + y) * FB_WIDTH)) * PIXEL_SIZE; - let row_end = row_start + logo_stride; - - framebuffer[row_start..row_end].copy_from_slice( - &image[i * logo_stride..i * logo_stride + logo_stride], - ); - } - - yuv_helper::abgr_to_i420( - &framebuffer, - (FB_WIDTH * PIXEL_SIZE) as u32, - data_y, - stride_y, - data_u, - stride_u, - data_v, - stride_v, - FB_WIDTH as i32, - FB_HEIGHT as i32, - ); - - source.capture_frame(&*video_frame); - } - }) - .await - .unwrap(); - } - } -} - -impl Drop for LogoTrack { - fn drop(&mut self) { - if let Some(handle) = self.handle.take() { - let _ = handle.close_tx.send(()); - } - } -} diff --git a/src/media/mod.rs b/src/media/mod.rs index bdf0bda..a44409f 100644 --- a/src/media/mod.rs +++ b/src/media/mod.rs @@ -1,15 +1,15 @@ //! Media plumbing over WebRTC, independent of the rest of the app (no `crate::` -//! deps): the local sources we publish — a generated logo video -//! ([`LogoTrack`]), a synthetic sine-wave audio track ([`SineTrack`]), and a +//! deps): the local sources we publish — generated video from `livekit-capture` +//! ([`CaptureTrack`]), a synthetic sine-wave audio track ([`SineTrack`]), and a //! real microphone audio track ([`MicTrack`]) — plus the [`VideoRenderer`] that //! turns incoming video frames into egui textures. -pub mod logo_track; +pub mod capture_track; pub mod mic_track; pub mod sine_track; pub mod video_renderer; -pub use logo_track::LogoTrack; +pub use capture_track::{CaptureSource, CaptureTrack}; pub use mic_track::MicTrack; pub use sine_track::{SineParameters, SineTrack}; pub use video_renderer::VideoRenderer; diff --git a/src/room/menu_bar.rs b/src/room/menu_bar.rs index 7aa394b..7dfdcc0 100644 --- a/src/room/menu_bar.rs +++ b/src/room/menu_bar.rs @@ -1,3 +1,4 @@ +use crate::media::CaptureSource; use crate::room::RoomContext; use crate::service::{AsyncCmd, LkService}; use livekit::SimulateScenario; @@ -23,8 +24,14 @@ impl egui::Widget for TopMenuBar<'_> { fn publish_menu(ui: &mut egui::Ui, service: &LkService) { ui.menu_button("Publish", |ui| { - if ui.button("Logo").clicked() { - let _ = service.send(AsyncCmd::ToggleLogo); + for source in CaptureSource::ALL { + if ui + .button(source.label()) + .on_hover_text(source.description()) + .clicked() + { + let _ = service.send(AsyncCmd::ToggleCapture { source }); + } } if ui.button("Sine Wave").clicked() { let _ = service.send(AsyncCmd::ToggleSine); diff --git a/src/service.rs b/src/service.rs index 847879c..09723f9 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,5 +1,5 @@ use crate::connect::Auth; -use crate::media::{LogoTrack, MicTrack, SineParameters, SineTrack}; +use crate::media::{CaptureSource, CaptureTrack, MicTrack, SineParameters, SineTrack}; use livekit::{ SimulateScenario, StreamByteOptions, StreamTextOptions, e2ee::{E2eeOptions, EncryptionType, key_provider::*}, @@ -7,6 +7,7 @@ use livekit::{ track::VideoQuality, }; use parking_lot::Mutex; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::mpsc::{self, error::SendError}; @@ -25,7 +26,9 @@ pub enum AsyncCmd { SimulateScenario { scenario: SimulateScenario, }, - ToggleLogo, + ToggleCapture { + source: CaptureSource, + }, ToggleSine, ToggleMic, ToggleDataTrack, @@ -150,7 +153,7 @@ impl LkService { async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedReceiver) { struct RunningState { room: Arc, - logo_track: LogoTrack, + capture_tracks: HashMap, sine_track: SineTrack, mic_track: MicTrack, data_track: Option, @@ -219,7 +222,9 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei running_state = Some(RunningState { room: new_room.clone(), - logo_track: LogoTrack::new(new_room.clone()), + capture_tracks: CaptureSource::ALL + .map(|source| (source, CaptureTrack::new(new_room.clone(), source))) + .into(), sine_track: SineTrack::new(new_room.clone(), SineParameters::default()), mic_track: MicTrack::new(new_room.clone()), data_track: None, @@ -252,12 +257,14 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei log::error!("failed to simulate scenario: {:?}", err); } } - AsyncCmd::ToggleLogo => { - if let Some(state) = running_state.as_mut() { - if state.logo_track.is_published() { - state.logo_track.unpublish().await.unwrap(); - } else { - state.logo_track.publish().await.unwrap(); + AsyncCmd::ToggleCapture { source } => { + if let Some(state) = running_state.as_mut() + && let Some(track) = state.capture_tracks.get_mut(&source) + { + if track.is_published() { + track.unpublish().await; + } else if let Err(err) = track.publish().await { + log::error!("failed to publish {} capture track: {err}", source.label()); } } } From f5110f5c4e4d1ed1db76fda4feb64b0595d0c7da Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:53:22 +0200 Subject: [PATCH 2/6] Adding clang 21 to support the new version of libwebrtc --- .github/actions/platform-deps/action.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/actions/platform-deps/action.yml b/.github/actions/platform-deps/action.yml index 2a71cae..5ee3124 100644 --- a/.github/actions/platform-deps/action.yml +++ b/.github/actions/platform-deps/action.yml @@ -37,6 +37,15 @@ runs: libdrm-dev \ libgbm-dev \ libgl1-mesa-dev + - name: Install clang 21 (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + # webrtc-sys links Chromium's hermetic libc++, which requires clang >= 21; + # ubuntu-latest ships clang 18. build.rs honors CXX. + curl -fsSL https://apt.llvm.org/llvm.sh | sudo bash -s -- 21 + echo "CC=clang-21" >> "$GITHUB_ENV" + echo "CXX=clang++-21" >> "$GITHUB_ENV" - name: Install hardware acceleration headers (Linux x86_64) if: runner.arch == 'X64' && runner.os == 'Linux' shell: bash From 4c5d3b44517d4d889b5390449901fe7036b9b5b1 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:57:28 +0200 Subject: [PATCH 3/6] Rename publish menu items by media kind Co-Authored-By: Claude Opus 5.5 --- src/media/capture_track.rs | 6 +++--- src/room/menu_bar.rs | 4 ++-- src/service.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/media/capture_track.rs b/src/media/capture_track.rs index 4989456..d82db83 100644 --- a/src/media/capture_track.rs +++ b/src/media/capture_track.rs @@ -32,9 +32,9 @@ impl CaptureSource { pub fn label(self) -> &'static str { match self { - Self::Gradient => "Gradient", - Self::Logo => "Logo", - Self::Clock => "Clock", + Self::Gradient => "Video: Gradient", + Self::Logo => "Video: Logo", + Self::Clock => "Video: Clock", } } diff --git a/src/room/menu_bar.rs b/src/room/menu_bar.rs index 7dfdcc0..3fdd26e 100644 --- a/src/room/menu_bar.rs +++ b/src/room/menu_bar.rs @@ -33,10 +33,10 @@ fn publish_menu(ui: &mut egui::Ui, service: &LkService) { let _ = service.send(AsyncCmd::ToggleCapture { source }); } } - if ui.button("Sine Wave").clicked() { + if ui.button("Audio: Sine Wave").clicked() { let _ = service.send(AsyncCmd::ToggleSine); } - if ui.button("Data Track").clicked() { + if ui.button("Data Track: Float").clicked() { let _ = service.send(AsyncCmd::ToggleDataTrack); } }); diff --git a/src/service.rs b/src/service.rs index 09723f9..16f0dd0 100644 --- a/src/service.rs +++ b/src/service.rs @@ -264,7 +264,7 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei if track.is_published() { track.unpublish().await; } else if let Err(err) = track.publish().await { - log::error!("failed to publish {} capture track: {err}", source.label()); + log::error!("failed to publish {source:?} capture track: {err}"); } } } From 6f8ffee9a6c0de67fb50969de7fd908a8d1b0126 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:59:35 +0200 Subject: [PATCH 4/6] Show publish state as checkboxes in the Publish menu Unify capture, sine, and data track toggles behind TogglePublish and report each source's PublishState back to the UI, including capture pumps that exit on their own. Checkboxes are disabled while a toggle is in flight. Co-Authored-By: Claude Opus 5.5 --- src/media/capture_track.rs | 13 ++- src/media/mod.rs | 2 +- src/room/menu_bar.rs | 29 +++--- src/room/window.rs | 13 ++- src/service.rs | 199 +++++++++++++++++++++++++++---------- 5 files changed, 187 insertions(+), 69 deletions(-) diff --git a/src/media/capture_track.rs b/src/media/capture_track.rs index d82db83..8268b2c 100644 --- a/src/media/capture_track.rs +++ b/src/media/capture_track.rs @@ -19,6 +19,9 @@ const FRAMERATE_FPS: u32 = 30; type BoxError = Box; +/// Called once the track is unpublished after its pump exits. +pub type OnUnpublished = Arc; + /// A user-publishable video source backed by `livekit-capture`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CaptureSource { @@ -80,11 +83,12 @@ impl CaptureSource { /// /// The track's publication is tied to its pump: a task owns the running pump /// and unpublishes the track whenever the pump exits, whether stopped here or -/// on its own (end of stream, error, or panic). Dropping stops the pump without -/// waiting for the unpublish. +/// on its own (end of stream, error, or panic), then calls `on_unpublished`. +/// Dropping stops the pump without waiting for the unpublish. pub struct CaptureTrack { room: Arc, source: CaptureSource, + on_unpublished: OnUnpublished, handle: Option, } @@ -95,10 +99,11 @@ struct TrackHandle { } impl CaptureTrack { - pub fn new(room: Arc, source: CaptureSource) -> Self { + pub fn new(room: Arc, source: CaptureSource, on_unpublished: OnUnpublished) -> Self { Self { room, source, + on_unpublished, handle: None, } } @@ -135,12 +140,14 @@ impl CaptureTrack { }; let stop = pump.stop_handle(); let source = self.source; + let on_unpublished = self.on_unpublished.clone(); let task = tokio::spawn(async move { let result = pump.join().await; log::info!("{source:?} capture pump exited: {result:?}"); if let Err(err) = participant.unpublish_track(&track.sid()).await { log::warn!("failed to unpublish {source:?} capture track: {err}"); } + on_unpublished(); }); self.handle = Some(TrackHandle { stop, task }); Ok(()) diff --git a/src/media/mod.rs b/src/media/mod.rs index a44409f..c03b0e6 100644 --- a/src/media/mod.rs +++ b/src/media/mod.rs @@ -9,7 +9,7 @@ pub mod mic_track; pub mod sine_track; pub mod video_renderer; -pub use capture_track::{CaptureSource, CaptureTrack}; +pub use capture_track::{CaptureSource, CaptureTrack, OnUnpublished}; pub use mic_track::MicTrack; pub use sine_track::{SineParameters, SineTrack}; pub use video_renderer::VideoRenderer; diff --git a/src/room/menu_bar.rs b/src/room/menu_bar.rs index 3fdd26e..426f9f4 100644 --- a/src/room/menu_bar.rs +++ b/src/room/menu_bar.rs @@ -1,11 +1,12 @@ -use crate::media::CaptureSource; use crate::room::RoomContext; -use crate::service::{AsyncCmd, LkService}; +use crate::service::{AsyncCmd, LkService, LocalSource, PublishState}; use livekit::SimulateScenario; +use std::collections::HashMap; /// Top menu bar: Simulate / Publish / Debug actions, all sent to the service. pub struct TopMenuBar<'a> { pub ctx: &'a RoomContext<'a>, + pub publish_states: &'a HashMap, } impl egui::Widget for TopMenuBar<'_> { @@ -13,7 +14,7 @@ impl egui::Widget for TopMenuBar<'_> { let service = self.ctx.service; egui::MenuBar::new() .ui(ui, |ui| { - publish_menu(ui, service); + publish_menu(ui, service, self.publish_states); simulate_menu(ui, service); debug_menu(ui, service); help_menu(ui); @@ -22,23 +23,25 @@ impl egui::Widget for TopMenuBar<'_> { } } -fn publish_menu(ui: &mut egui::Ui, service: &LkService) { +fn publish_menu( + ui: &mut egui::Ui, + service: &LkService, + states: &HashMap, +) { ui.menu_button("Publish", |ui| { - for source in CaptureSource::ALL { + for source in LocalSource::ALL { + let state = states.get(&source).copied().unwrap_or_default(); + // Throwaway copy: the service owns the real state and reports it back. + let mut published = state == PublishState::Published; + let checkbox = egui::Checkbox::new(&mut published, source.label()); if ui - .button(source.label()) + .add_enabled(state != PublishState::Pending, checkbox) .on_hover_text(source.description()) .clicked() { - let _ = service.send(AsyncCmd::ToggleCapture { source }); + let _ = service.send(AsyncCmd::TogglePublish { source }); } } - if ui.button("Audio: Sine Wave").clicked() { - let _ = service.send(AsyncCmd::ToggleSine); - } - if ui.button("Data Track: Float").clicked() { - let _ = service.send(AsyncCmd::ToggleDataTrack); - } }); } diff --git a/src/room/window.rs b/src/room/window.rs index 3d29219..69b63b6 100644 --- a/src/room/window.rs +++ b/src/room/window.rs @@ -7,7 +7,7 @@ use crate::{ room::right_panel::{RightPanel, RightPanelState}, room::status_bar::{StatusBar, StatusBarActions}, room::track_grid_view::TrackGridView, - service::{AsyncCmd, LkService, UiCmd}, + service::{AsyncCmd, LkService, LocalSource, PublishState, UiCmd}, }; use livekit::prelude::*; use std::collections::HashMap; @@ -22,6 +22,7 @@ pub struct RoomWindow { video_renderers: HashMap<(ParticipantIdentity, TrackSid), VideoRenderer>, local_data_tracks: Vec, remote_data_tracks: Vec, + publish_states: HashMap, connecting: bool, connection_failure: Option, render_state: egui_wgpu::RenderState, @@ -44,6 +45,7 @@ impl RoomWindow { video_renderers: HashMap::new(), local_data_tracks: Vec::new(), remote_data_tracks: Vec::new(), + publish_states: HashMap::new(), connecting: false, connection_failure: None, render_state, @@ -85,6 +87,9 @@ impl RoomWindow { UiCmd::DataTrackUnpublished => { self.local_data_tracks.clear(); } + UiCmd::PublishState { source, state } => { + self.publish_states.insert(source, state); + } UiCmd::RpcSendResult { request_id, result } => { self.right_panel.rpc.handle_send_result(request_id, result); } @@ -173,6 +178,7 @@ impl RoomWindow { self.video_renderers.clear(); self.local_data_tracks.clear(); self.remote_data_tracks.clear(); + self.publish_states.clear(); self.right_panel.rpc.on_disconnect(); self.right_panel.data_streams.on_disconnect(); } @@ -208,7 +214,10 @@ impl RoomWindow { .inner_margin(egui::Margin::symmetric(10, 6)), ) .show(ui, |ui| { - ui.add(TopMenuBar { ctx: &ctx }); + ui.add(TopMenuBar { + ctx: &ctx, + publish_states: &self.publish_states, + }); }); egui::Panel::bottom(ctx.id.with("status_bar")) diff --git a/src/service.rs b/src/service.rs index 16f0dd0..490c360 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,5 +1,7 @@ use crate::connect::Auth; -use crate::media::{CaptureSource, CaptureTrack, MicTrack, SineParameters, SineTrack}; +use crate::media::{ + CaptureSource, CaptureTrack, MicTrack, OnUnpublished, SineParameters, SineTrack, +}; use livekit::{ SimulateScenario, StreamByteOptions, StreamTextOptions, e2ee::{E2eeOptions, EncryptionType, key_provider::*}, @@ -26,12 +28,10 @@ pub enum AsyncCmd { SimulateScenario { scenario: SimulateScenario, }, - ToggleCapture { - source: CaptureSource, + TogglePublish { + source: LocalSource, }, - ToggleSine, ToggleMic, - ToggleDataTrack, SubscribeTrack { publication: RemoteTrackPublication, }, @@ -66,6 +66,49 @@ pub enum DataStreamPayload { Bytes(Vec), } +/// A local source the user can publish from the Publish menu. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LocalSource { + Capture(CaptureSource), + Sine, + DataTrack, +} + +impl LocalSource { + pub const ALL: [Self; 5] = [ + Self::Capture(CaptureSource::Gradient), + Self::Capture(CaptureSource::Logo), + Self::Capture(CaptureSource::Clock), + Self::Sine, + Self::DataTrack, + ]; + + pub fn label(self) -> &'static str { + match self { + Self::Capture(source) => source.label(), + Self::Sine => "Audio: Sine Wave", + Self::DataTrack => "Data Track: Float", + } + } + + pub fn description(self) -> &'static str { + match self { + Self::Capture(source) => source.description(), + Self::Sine => "Synthetic 440 Hz sine wave tone", + Self::DataTrack => "Data track carrying values set with a slider", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum PublishState { + #[default] + Unpublished, + /// A publish or unpublish is in flight. + Pending, + Published, +} + #[derive(Debug)] pub enum UiCmd { ConnectResult { @@ -80,6 +123,10 @@ pub enum UiCmd { track: LocalDataTrack, }, DataTrackUnpublished, + PublishState { + source: LocalSource, + state: PublishState, + }, RpcSendResult { request_id: u64, result: Result, @@ -163,6 +210,65 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei platform_audio: Option, } + impl RunningState { + /// Toggles `source` and returns whether it ends up published. + async fn toggle_publish( + &mut self, + source: LocalSource, + ui_tx: &mpsc::UnboundedSender, + ) -> bool { + match source { + LocalSource::Capture(capture) => { + let Some(track) = self.capture_tracks.get_mut(&capture) else { + return false; + }; + if track.is_published() { + track.unpublish().await; + } else if let Err(err) = track.publish().await { + log::error!("failed to publish {capture:?} capture track: {err}"); + } + track.is_published() + } + LocalSource::Sine => { + let result = if self.sine_track.is_published() { + self.sine_track.unpublish().await + } else { + self.sine_track.publish().await + }; + if let Err(err) = result { + log::error!("failed to toggle sine track: {err}"); + } + self.sine_track.is_published() + } + LocalSource::DataTrack => { + if let Some(track) = self.data_track.take() { + track.unpublish(); + let _ = ui_tx.send(UiCmd::DataTrackUnpublished); + return false; + } + match self + .room + .local_participant() + .publish_data_track("slider") + .await + { + Ok(track) => { + let _ = ui_tx.send(UiCmd::DataTrackPublished { + track: track.clone(), + }); + self.data_track = Some(track); + true + } + Err(err) => { + log::error!("failed to publish data track: {err}"); + false + } + } + } + } + } + } + let mut running_state = None; while let Some(event) = cmd_rx.recv().await { @@ -222,9 +328,7 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei running_state = Some(RunningState { room: new_room.clone(), - capture_tracks: CaptureSource::ALL - .map(|source| (source, CaptureTrack::new(new_room.clone(), source))) - .into(), + capture_tracks: capture_tracks(&new_room, &inner.ui_tx), sine_track: SineTrack::new(new_room.clone(), SineParameters::default()), mic_track: MicTrack::new(new_room.clone()), data_track: None, @@ -257,25 +361,20 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei log::error!("failed to simulate scenario: {:?}", err); } } - AsyncCmd::ToggleCapture { source } => { - if let Some(state) = running_state.as_mut() - && let Some(track) = state.capture_tracks.get_mut(&source) - { - if track.is_published() { - track.unpublish().await; - } else if let Err(err) = track.publish().await { - log::error!("failed to publish {source:?} capture track: {err}"); - } - } - } - AsyncCmd::ToggleSine => { - if let Some(state) = running_state.as_mut() { - if state.sine_track.is_published() { - state.sine_track.unpublish().await.unwrap(); - } else { - state.sine_track.publish().await.unwrap(); - } - } + AsyncCmd::TogglePublish { source } => { + let Some(state) = running_state.as_mut() else { + continue; + }; + let report = |state| { + let _ = inner.ui_tx.send(UiCmd::PublishState { source, state }); + }; + report(PublishState::Pending); + let published = state.toggle_publish(source, &inner.ui_tx).await; + report(if published { + PublishState::Published + } else { + PublishState::Unpublished + }); } AsyncCmd::ToggleMic => { if let Some(state) = running_state.as_mut() { @@ -295,29 +394,6 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei } } } - AsyncCmd::ToggleDataTrack => { - if let Some(state) = running_state.as_mut() { - if let Some(track) = state.data_track.take() { - track.unpublish(); - let _ = inner.ui_tx.send(UiCmd::DataTrackUnpublished); - } else { - match state - .room - .local_participant() - .publish_data_track("slider") - .await - { - Ok(track) => { - let _ = inner.ui_tx.send(UiCmd::DataTrackPublished { - track: track.clone(), - }); - state.data_track = Some(track); - } - Err(err) => log::error!("failed to publish data track: {err}"), - } - } - } - } AsyncCmd::SubscribeTrack { publication } => { publication.set_subscribed(true); } @@ -431,6 +507,29 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei } } +/// One [`CaptureTrack`] per source, each reporting to the UI when its pump +/// exits so the menu never shows a dead track as published. +fn capture_tracks( + room: &Arc, + ui_tx: &mpsc::UnboundedSender, +) -> HashMap { + CaptureSource::ALL + .map(|source| { + let ui_tx = ui_tx.clone(); + let on_unpublished: OnUnpublished = Arc::new(move || { + let _ = ui_tx.send(UiCmd::PublishState { + source: LocalSource::Capture(source), + state: PublishState::Unpublished, + }); + }); + ( + source, + CaptureTrack::new(room.clone(), source, on_unpublished), + ) + }) + .into() +} + /// Task basically used to forward room events to the UI. /// It will automatically close when the room is disconnected. async fn room_task(inner: Arc, mut events: mpsc::UnboundedReceiver) { From 4f3d43037bad155f847c85b52a718c2bae9c5195 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:07:37 +0200 Subject: [PATCH 5/6] Unpublish all local tracks on disconnect Tear down every local track and report each source unpublished, both on user-initiated disconnect and when the room disconnects on its own. Previously the sine generator task kept running after disconnect. Co-Authored-By: Claude Opus 5.5 --- src/service.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/src/service.rs b/src/service.rs index 490c360..2775f18 100644 --- a/src/service.rs +++ b/src/service.rs @@ -211,6 +211,30 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei } impl RunningState { + /// Unpublishes every local track and reports each source unpublished. + async fn unpublish_all(&mut self, ui_tx: &mpsc::UnboundedSender) { + for track in self.capture_tracks.values_mut() { + track.unpublish().await; + } + if let Err(err) = self.sine_track.unpublish().await { + log::warn!("failed to unpublish sine track: {err}"); + } + if let Some(track) = self.data_track.take() { + track.unpublish(); + let _ = ui_tx.send(UiCmd::DataTrackUnpublished); + } + if self.mic_track.is_published() + && let Some(platform_audio) = self.platform_audio.clone() + && let Err(err) = self.mic_track.unpublish(&platform_audio).await + { + log::warn!("failed to unpublish microphone: {err}"); + } + for source in LocalSource::ALL { + let state = PublishState::Unpublished; + let _ = ui_tx.send(UiCmd::PublishState { source, state }); + } + } + /// Toggles `source` and returns whether it ends up published. async fn toggle_publish( &mut self, @@ -269,9 +293,25 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei } } - let mut running_state = None; + let mut running_state: Option = None; + let (disconnected_tx, mut disconnected_rx) = mpsc::unbounded_channel(); - while let Some(event) = cmd_rx.recv().await { + loop { + let event = tokio::select! { + cmd = cmd_rx.recv() => { + let Some(cmd) = cmd else { break }; + cmd + } + Some(()) = disconnected_rx.recv() => { + // Ignore a stale notice from a room we already replaced. + if let Some(mut state) = running_state.take_if(|state| { + state.room.connection_state() == ConnectionState::Disconnected + }) { + state.unpublish_all(&inner.ui_tx).await; + } + continue; + } + }; match event { AsyncCmd::RoomConnect { auth, @@ -310,7 +350,7 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei if let Ok((new_room, events)) = res { log::info!("connected to room: {}", new_room.name()); - tokio::spawn(room_task(inner.clone(), events)); + tokio::spawn(room_task(inner.clone(), events, disconnected_tx.clone())); let new_room = Arc::new(new_room); @@ -347,8 +387,9 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei } } AsyncCmd::RoomDisconnect => { - if let Some(state) = running_state.take() { + if let Some(mut state) = running_state.take() { *inner.room.lock() = None; + state.unpublish_all(&inner.ui_tx).await; if let Err(err) = state.room.close().await { log::error!("failed to disconnect from room: {:?}", err); } @@ -532,8 +573,15 @@ fn capture_tracks( /// Task basically used to forward room events to the UI. /// It will automatically close when the room is disconnected. -async fn room_task(inner: Arc, mut events: mpsc::UnboundedReceiver) { +async fn room_task( + inner: Arc, + mut events: mpsc::UnboundedReceiver, + disconnected_tx: mpsc::UnboundedSender<()>, +) { while let Some(event) = events.recv().await { + if matches!(event, RoomEvent::Disconnected { .. }) { + let _ = disconnected_tx.send(()); + } let _ = inner.ui_tx.send(UiCmd::RoomEvent { event }); } } From c17f7b319cbefcea0f62e7860caae8513688ecef Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:12:46 +0200 Subject: [PATCH 6/6] Rename data track publish item to Slider Co-Authored-By: Claude Opus 5.5 --- src/service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/service.rs b/src/service.rs index 2775f18..95f63fc 100644 --- a/src/service.rs +++ b/src/service.rs @@ -87,7 +87,7 @@ impl LocalSource { match self { Self::Capture(source) => source.label(), Self::Sine => "Audio: Sine Wave", - Self::DataTrack => "Data Track: Float", + Self::DataTrack => "Data Track: Slider", } } @@ -95,7 +95,7 @@ impl LocalSource { match self { Self::Capture(source) => source.description(), Self::Sine => "Synthetic 440 Hz sine wave tone", - Self::DataTrack => "Data track carrying values set with a slider", + Self::DataTrack => "Integer from 0 to 512 set with a slider, sent as text", } } }