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 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..8268b2c --- /dev/null +++ b/src/media/capture_track.rs @@ -0,0 +1,171 @@ +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; + +/// 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 { + 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 => "Video: Gradient", + Self::Logo => "Video: Logo", + Self::Clock => "Video: 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), 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, +} + +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, on_unpublished: OnUnpublished) -> Self { + Self { + room, + source, + on_unpublished, + 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 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(()) + } + + /// 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..c03b0e6 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, 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 7aa394b..426f9f4 100644 --- a/src/room/menu_bar.rs +++ b/src/room/menu_bar.rs @@ -1,10 +1,12 @@ 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<'_> { @@ -12,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); @@ -21,16 +23,24 @@ 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| { - if ui.button("Logo").clicked() { - let _ = service.send(AsyncCmd::ToggleLogo); - } - if ui.button("Sine Wave").clicked() { - let _ = service.send(AsyncCmd::ToggleSine); - } - if ui.button("Data Track").clicked() { - let _ = service.send(AsyncCmd::ToggleDataTrack); + 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 + .add_enabled(state != PublishState::Pending, checkbox) + .on_hover_text(source.description()) + .clicked() + { + let _ = service.send(AsyncCmd::TogglePublish { source }); + } } }); } 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 847879c..95f63fc 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,5 +1,7 @@ use crate::connect::Auth; -use crate::media::{LogoTrack, MicTrack, SineParameters, SineTrack}; +use crate::media::{ + CaptureSource, CaptureTrack, MicTrack, OnUnpublished, SineParameters, SineTrack, +}; use livekit::{ SimulateScenario, StreamByteOptions, StreamTextOptions, e2ee::{E2eeOptions, EncryptionType, key_provider::*}, @@ -7,6 +9,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,10 +28,10 @@ pub enum AsyncCmd { SimulateScenario { scenario: SimulateScenario, }, - ToggleLogo, - ToggleSine, + TogglePublish { + source: LocalSource, + }, ToggleMic, - ToggleDataTrack, SubscribeTrack { publication: RemoteTrackPublication, }, @@ -63,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: Slider", + } + } + + pub fn description(self) -> &'static str { + match self { + Self::Capture(source) => source.description(), + Self::Sine => "Synthetic 440 Hz sine wave tone", + Self::DataTrack => "Integer from 0 to 512 set with a slider, sent as text", + } + } +} + +#[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 { @@ -77,6 +123,10 @@ pub enum UiCmd { track: LocalDataTrack, }, DataTrackUnpublished, + PublishState { + source: LocalSource, + state: PublishState, + }, RpcSendResult { request_id: u64, result: Result, @@ -150,7 +200,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, @@ -160,9 +210,108 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei platform_audio: Option, } - let mut running_state = None; + 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 }); + } + } - while let Some(event) = cmd_rx.recv().await { + /// 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: Option = None; + let (disconnected_tx, mut disconnected_rx) = mpsc::unbounded_channel(); + + 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, @@ -201,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); @@ -219,7 +368,7 @@ 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: 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, @@ -238,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); } @@ -252,23 +402,20 @@ 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::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() { @@ -288,29 +435,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); } @@ -424,10 +548,40 @@ 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) { +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 }); } }