From 9c3d87124efa260a9f5454fc81f68ab7d024bcb6 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:18:31 +0200 Subject: [PATCH 1/4] Adds token source tab Sandbox id works, but no other fields possible yet Cargo changes for local dev Also have fields for all fetch options Some cleanup after CI complained Adapt to fetch options builder Using factory pattern Make cargo toml use real deps Needed for rebase Adapt to stream options builder API StreamTextOptions and StreamByteOptions no longer implement Default in the local rust-sdks checkout; construct them via new_with_topic and with_destination_identities instead. Co-Authored-By: Claude Fable 5 Adapted to newest version Adjusted to basic factory constructor Working version matches Format check --- AGENTS.md | 3 + Cargo.lock | 196 ++++++++++++++++---------------------------- Cargo.toml | 8 +- src/connect.rs | 198 ++++++++++++++++++++++++++++++++++++++++----- src/lib.rs | 2 +- src/room/window.rs | 13 +-- src/service.rs | 29 +++++-- 7 files changed, 278 insertions(+), 171 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1c82cba..7cc1b71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,10 +61,13 @@ - Adhere to requirements in [_CONTRIBUTING.md_](./CONTRIBUTING.md) - Always format using `cargo fmt` + - CI enforces this via `cargo fmt --check` - Always address all issues, both clippy and compiler warnings + - Verify with the same invocation CI uses: `cargo clippy --all-targets -- -D warnings --no-deps` - Do not reach for `#[allow(...)]` to bypass warnings unless it is unavoidable in the context - Be explicit when you are bypassing warnings - Always run cspell and fix spelling issues + - `npx cspell --no-progress "**"` checks all files, matching CI - If a flagged word is valid project terminology, add it to _cspell.yml_ and sort the list alphabetically ## Release process diff --git a/Cargo.lock b/Cargo.lock index e6cd049..49636e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,7 +307,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "x11rb", ] @@ -881,18 +881,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstyle", "clap_lex", @@ -1091,9 +1091,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1107,9 +1107,9 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] name = "cxx" -version = "1.0.194" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" dependencies = [ "cc", "cxx-build", @@ -1122,9 +1122,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.194" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" dependencies = [ "cc", "codespan-reporting", @@ -1132,39 +1132,39 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.194" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" dependencies = [ "clap", "codespan-reporting", "indexmap", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "cxxbridge-flags" -version = "1.0.194" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" [[package]] name = "cxxbridge-macro" -version = "1.0.194" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" dependencies = [ "indexmap", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -1204,9 +1204,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "device-info" @@ -1863,9 +1863,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1967,9 +1967,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "glow" @@ -2195,9 +2195,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2205,9 +2205,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -2215,9 +2215,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -2234,9 +2234,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2495,9 +2495,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -2988,6 +2988,19 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "livekit-token-source" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1a446c9335adaa5ccbf242fd485c7a568ff2c73c7b57fdfba0d16141f04d710" +dependencies = [ + "async-trait", + "livekit-net", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -4144,9 +4157,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -4186,16 +4199,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4541,6 +4554,7 @@ dependencies = [ "image", "livekit", "livekit-api", + "livekit-token-source", "log", "parking_lot", "serde", @@ -4604,9 +4618,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -4631,9 +4645,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -4814,9 +4828,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -5223,9 +5237,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -5276,9 +5290,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -5304,9 +5318,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -6278,15 +6292,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -6320,30 +6325,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -6365,12 +6353,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6383,12 +6365,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6401,24 +6377,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6431,12 +6395,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6449,12 +6407,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6467,12 +6419,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -6485,12 +6431,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winit" version = "0.30.13" diff --git a/Cargo.toml b/Cargo.toml index d0a6793..ffb8053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,14 @@ wgpu = "29.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"] } -# For local SDK development, comment out the two lines above and uncomment these -# (clone https://github.com/livekit/rust-sdks to ../rust-sdks first; see -# "Building against a local rust-sdks" in README.md): +# Building against a local rust-sdks checkout (see "Building against a local +# rust-sdks" in README.md). To build against the released crates instead, +# comment out the three lines below and uncomment the ones above: # 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"] } [package.metadata.bundle] name = "LiveKit Client" diff --git a/src/connect.rs b/src/connect.rs index 6a52589..aa7aa8a 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -1,26 +1,42 @@ use crate::ui::{labeled_field::LabeledTextEdit, prominent_button::ProminentButton}; +use livekit_token_source::{ + TokenSourceConfigurable, TokenSourceFetchOptions, development_token_server, +}; -/// How a room connection is authenticated. -#[derive(Clone)] +/// How a room connection is authenticated. The methods that target a known +/// server carry its URL; the token-source method learns it from the development token server. +#[derive(Clone, Debug)] pub enum Auth { /// A pre-generated access token (the room is encoded in it). - Token(String), + Token { url: String, token: String }, /// API credentials from which a join token is generated on demand. ApiKey { + url: String, api_key: String, api_secret: String, identity: String, room: String, }, + /// A LiveKit Cloud development token server, which provides both the server + /// URL and the join token. `options` customizes the request; unset + /// fields are left to server defaults. + TokenSource { + token_server_id: String, + options: TokenSourceFetchOptions, + }, } impl Auth { - /// Resolve to a room-connection JWT, generating one from the API credentials - /// when this is the API-key method. - pub fn access_token(&self) -> Result { + /// Resolve to `(server_url, token)`: the JWT is generated locally for the + /// API-key method, fetched over HTTP for the token-source method. + /// + /// Async because of that fetch — call it from the service task, not the UI + /// thread. + pub async fn connection_details(&self) -> Result<(String, String), String> { match self { - Auth::Token(token) => Ok(token.clone()), + Auth::Token { url, token } => Ok((url.clone(), token.clone())), Auth::ApiKey { + url, api_key, api_secret, identity, @@ -34,7 +50,30 @@ impl Auth { ..Default::default() }) .to_jwt() + .map(|token| (url.clone(), token)) .map_err(|e| e.to_string()), + Auth::TokenSource { + token_server_id, + options, + } => { + let token_source = development_token_server(token_server_id); + let response = token_source + .fetch(options) + .await + .map_err(|e| e.to_string())?; + Ok((response.server_url, response.participant_token)) + } + } + } + + /// Short label of the connection target for window titles: the server URL + /// when known up front, otherwise the token server id. + pub fn target_label(&self) -> &str { + match self { + Auth::Token { url, .. } | Auth::ApiKey { url, .. } => url, + Auth::TokenSource { + token_server_id, .. + } => token_server_id, } } } @@ -43,7 +82,6 @@ impl Auth { /// per application, passed to each room window as it is opened. #[derive(Clone)] pub struct ConnectSettings { - pub url: String, pub auth: Auth, pub key: String, pub auto_subscribe: bool, @@ -59,6 +97,7 @@ enum AuthMethod { #[default] ApiKey, Token, + TokenSource, } /// The root window: a welcome screen holding the only connect form in the app. @@ -74,6 +113,16 @@ pub struct ConnectView { method: AuthMethod, url: String, token: String, + token_server_id: String, + // Token-source fetch options (`ts_` to keep them apart from the API-key + // tab's identity/room). Empty means "omit, let the server default". + ts_room_name: String, + ts_participant_name: String, + ts_participant_identity: String, + ts_participant_metadata: String, + ts_agent_name: String, + ts_agent_metadata: String, + ts_agent_deployment: String, api_key: String, api_secret: String, identity: String, @@ -101,6 +150,14 @@ impl Default for ConnectView { method: AuthMethod::default(), url: env_or("LIVEKIT_URL", "ws://localhost:7880"), token: env_or("LIVEKIT_TOKEN", ""), + token_server_id: "token-server-id".to_string(), + ts_room_name: String::new(), + ts_participant_name: String::new(), + ts_participant_identity: String::new(), + ts_participant_metadata: String::new(), + ts_agent_name: String::new(), + ts_agent_metadata: String::new(), + ts_agent_deployment: String::new(), api_key: env_or("LIVEKIT_API_KEY", "devkey"), api_secret: env_or("LIVEKIT_API_SECRET", "secret"), identity: "participant-0".to_string(), @@ -116,32 +173,70 @@ impl Default for ConnectView { impl ConnectView { fn is_connect_enabled(&self) -> bool { - if self.url.trim().is_empty() { - return false; - } + // The URL only matters for the methods that use it; the token-source + // method gets its server URL from the development token server response. match self.method { AuthMethod::ApiKey => { - !self.api_key.trim().is_empty() + !self.url.trim().is_empty() + && !self.api_key.trim().is_empty() && !self.api_secret.trim().is_empty() && !self.identity.trim().is_empty() && !self.room.trim().is_empty() } - AuthMethod::Token => !self.token.trim().is_empty(), + AuthMethod::Token => !self.url.trim().is_empty() && !self.token.trim().is_empty(), + AuthMethod::TokenSource => !self.token_server_id.trim().is_empty(), } } fn current_settings(&self) -> ConnectSettings { let auth = match self.method { AuthMethod::ApiKey => Auth::ApiKey { + url: self.url.clone(), api_key: self.api_key.clone(), api_secret: self.api_secret.clone(), identity: self.identity.clone(), room: self.room.clone(), }, - AuthMethod::Token => Auth::Token(self.token.clone()), + AuthMethod::Token => Auth::Token { + url: self.url.clone(), + token: self.token.clone(), + }, + AuthMethod::TokenSource => { + // Empty (or whitespace-only) fields are left unset so the + // token server applies its defaults. + let opt = |s: &str| { + let s = s.trim(); + (!s.is_empty()).then(|| s.to_string()) + }; + let mut options = TokenSourceFetchOptions::new(); + if let Some(v) = opt(&self.ts_room_name) { + options = options.with_room_name(v); + } + if let Some(v) = opt(&self.ts_participant_name) { + options = options.with_participant_name(v); + } + if let Some(v) = opt(&self.ts_participant_identity) { + options = options.with_participant_identity(v); + } + if let Some(v) = opt(&self.ts_participant_metadata) { + options = options.with_participant_metadata(v); + } + if let Some(v) = opt(&self.ts_agent_name) { + options = options.with_agent_name(v); + } + if let Some(v) = opt(&self.ts_agent_metadata) { + options = options.with_agent_metadata(v); + } + if let Some(v) = opt(&self.ts_agent_deployment) { + options = options.with_deployment(v); + } + Auth::TokenSource { + token_server_id: self.token_server_id.clone(), + options, + } + } }; ConnectSettings { - url: self.url.clone(), auth, key: self.key.clone(), auto_subscribe: self.auto_subscribe, @@ -218,12 +313,10 @@ impl egui::Widget for ConnectForm<'_> { ui.label(egui::RichText::new("Connect to a Room").text_style(egui::TextStyle::Heading)); ui.add_space(8.0); - ui.add(LabeledTextEdit::singleline("URL", &mut view.url)); - ui.add_space(8.0); - ui.horizontal(|ui| { ui.selectable_value(&mut view.method, AuthMethod::ApiKey, "API Key"); ui.selectable_value(&mut view.method, AuthMethod::Token, "Token"); + ui.selectable_value(&mut view.method, AuthMethod::TokenSource, "TokenSource"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let toggle = ui .add(egui::Button::selectable(view.show_secrets, "👁")) @@ -239,15 +332,23 @@ impl egui::Widget for ConnectForm<'_> { // the eye toggle above is on. let mask = !view.show_secrets; - // Scope each method's fields under a distinct id so switching tabs - // is seen as a layout change, not an unstable widget id (egui warns - // when a rect's id changes between passes under the same parent). - ui.push_id(view.method, |ui| match view.method { + // Scope the method's fields under a *constant* id. The single-field + // tabs (Token / TokenSource) render the same full-width widget at the + // same rect, so a per-method salt would give that rect a different id + // each switch — which is exactly what egui's "rect changed id between + // passes" warning flags. A stable salt keeps the id constant. + // The URL lives inside the Token / API Key tabs (shared between + // them): the token-source method gets its URL from the development token server. + ui.push_id("auth_method_fields", |ui| match view.method { AuthMethod::Token => { + ui.add(LabeledTextEdit::singleline("URL", &mut view.url)); + ui.add_space(8.0); ui.add(LabeledTextEdit::singleline("Token", &mut view.token).password(mask)); ui.add_space(8.0); } AuthMethod::ApiKey => { + ui.add(LabeledTextEdit::singleline("URL", &mut view.url)); + ui.add_space(8.0); ui.columns(2, |columns| { columns[0].add( LabeledTextEdit::singleline("API Key", &mut view.api_key) @@ -265,6 +366,59 @@ impl egui::Widget for ConnectForm<'_> { }); ui.add_space(8.0); } + AuthMethod::TokenSource => { + ui.add(LabeledTextEdit::singleline( + "Token Server Id", + &mut view.token_server_id, + )); + ui.add_space(8.0); + + ui.label( + egui::RichText::new( + "Optional overrides — empty fields use server defaults", + ) + .text_style(egui::TextStyle::Small), + ); + ui.add_space(8.0); + ui.columns(2, |columns| { + columns[0].add(LabeledTextEdit::singleline( + "Room Name", + &mut view.ts_room_name, + )); + columns[1].add(LabeledTextEdit::singleline( + "Participant Name", + &mut view.ts_participant_name, + )); + }); + ui.add_space(8.0); + ui.columns(2, |columns| { + columns[0].add(LabeledTextEdit::singleline( + "Participant Identity", + &mut view.ts_participant_identity, + )); + columns[1].add(LabeledTextEdit::singleline( + "Participant Metadata", + &mut view.ts_participant_metadata, + )); + }); + ui.add_space(8.0); + ui.columns(2, |columns| { + columns[0].add(LabeledTextEdit::singleline( + "Agent Name", + &mut view.ts_agent_name, + )); + columns[1].add(LabeledTextEdit::singleline( + "Agent Deployment", + &mut view.ts_agent_deployment, + )); + }); + ui.add_space(8.0); + ui.add(LabeledTextEdit::singleline( + "Agent Metadata", + &mut view.ts_agent_metadata, + )); + ui.add_space(8.0); + } }); ui.add( diff --git a/src/lib.rs b/src/lib.rs index 9c027d7..1c33b69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,7 @@ impl AppRoot { let id = self.next_window_id; self.next_window_id += 1; - let title = format!("{} - {}", APP_NAME, request.url); + let title = format!("{} - {}", APP_NAME, request.auth.target_label()); let window = RoomWindow::new( id, self.async_runtime.handle().clone(), diff --git a/src/room/window.rs b/src/room/window.rs index 075be27..3d29219 100644 --- a/src/room/window.rs +++ b/src/room/window.rs @@ -54,19 +54,10 @@ impl RoomWindow { } fn connect(&mut self) { - let token = match self.request.auth.access_token() { - Ok(token) => token, - Err(err) => { - self.connecting = false; - self.connection_failure = Some(err); - return; - } - }; self.connecting = true; self.connection_failure = None; let _ = self.service.send(AsyncCmd::RoomConnect { - url: self.request.url.clone(), - token, + auth: Box::new(self.request.auth.clone()), auto_subscribe: self.request.auto_subscribe, dynacast: self.request.dynacast, enable_e2ee: self.request.enable_e2ee, @@ -85,7 +76,7 @@ impl RoomWindow { UiCmd::ConnectResult { result } => { self.connecting = false; if let Err(err) = result { - self.connection_failure = Some(err.to_string()); + self.connection_failure = Some(err); } } UiCmd::DataTrackPublished { track } => { diff --git a/src/service.rs b/src/service.rs index 4187361..847879c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,3 +1,4 @@ +use crate::connect::Auth; use crate::media::{LogoTrack, MicTrack, SineParameters, SineTrack}; use livekit::{ SimulateScenario, StreamByteOptions, StreamTextOptions, @@ -12,8 +13,9 @@ use tokio::sync::mpsc::{self, error::SendError}; #[derive(Debug)] pub enum AsyncCmd { RoomConnect { - url: String, - token: String, + /// Boxed to keep `AsyncCmd` small (clippy: `result_large_err` on + /// [`LkService::send`]); the token-source options make `Auth` large. + auth: Box, auto_subscribe: bool, dynacast: bool, enable_e2ee: bool, @@ -64,7 +66,9 @@ pub enum DataStreamPayload { #[derive(Debug)] pub enum UiCmd { ConnectResult { - result: RoomResult<()>, + /// `Err` is a human-readable message: token resolution and room + /// connection can each fail, with different error types. + result: Result<(), String>, }, RoomEvent { event: RoomEvent, @@ -161,13 +165,24 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei while let Some(event) = cmd_rx.recv().await { match event { AsyncCmd::RoomConnect { - url, - token, + auth, auto_subscribe, dynacast, enable_e2ee, key, } => { + // Resolved here rather than UI-side: the token-source method + // fetches the connection details over HTTP, which must not + // block the UI. + let (url, token) = match auth.connection_details().await { + Ok(details) => details, + Err(err) => { + log::error!("failed to resolve connection details: {err}"); + let _ = inner.ui_tx.send(UiCmd::ConnectResult { result: Err(err) }); + continue; + } + }; + log::info!("connecting to room: {}", url); let key_provider = @@ -217,7 +232,9 @@ async fn service_task(inner: Arc, mut cmd_rx: mpsc::UnboundedRecei let _ = inner.ui_tx.send(UiCmd::ConnectResult { result: Ok(()) }); } else if let Err(err) = res { log::error!("failed to connect to room: {:?}", err); - let _ = inner.ui_tx.send(UiCmd::ConnectResult { result: Err(err) }); + let _ = inner.ui_tx.send(UiCmd::ConnectResult { + result: Err(err.to_string()), + }); } } AsyncCmd::RoomDisconnect => { From 044110591934bd491fc301074ab0e172d8ca13b0 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:38:20 +0200 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Ryan Gaus --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ffb8053..feb0841 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ livekit-token-source = { version = "0.1.1", features = ["rustls-tls-native-roots # Building against a local rust-sdks checkout (see "Building against a local # rust-sdks" in README.md). To build against the released crates instead, -# comment out the three lines below and uncomment the ones above: +# uncomment out the three lines below and comment the ones above: # 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"] } From f22c065c6b9cd9eed56166d6c3125a1ff53745cd Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:13:57 +0200 Subject: [PATCH 3/4] Addressed review comments --- src/connect.rs | 127 +++++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/src/connect.rs b/src/connect.rs index aa7aa8a..eed8bc8 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -20,7 +20,7 @@ pub enum Auth { /// A LiveKit Cloud development token server, which provides both the server /// URL and the join token. `options` customizes the request; unset /// fields are left to server defaults. - TokenSource { + DevelopmentTokenServer { token_server_id: String, options: TokenSourceFetchOptions, }, @@ -52,7 +52,7 @@ impl Auth { .to_jwt() .map(|token| (url.clone(), token)) .map_err(|e| e.to_string()), - Auth::TokenSource { + Auth::DevelopmentTokenServer { token_server_id, options, } => { @@ -71,7 +71,7 @@ impl Auth { pub fn target_label(&self) -> &str { match self { Auth::Token { url, .. } | Auth::ApiKey { url, .. } => url, - Auth::TokenSource { + Auth::DevelopmentTokenServer { token_server_id, .. } => token_server_id, } @@ -97,7 +97,53 @@ enum AuthMethod { #[default] ApiKey, Token, - TokenSource, + DevelopmentTokenServer, +} + +#[derive(Default, serde::Serialize, serde::Deserialize)] +#[serde(default)] +struct TokenSourceOptions { + room_name: String, + participant_name: String, + participant_identity: String, + participant_metadata: String, + agent_name: String, + agent_metadata: String, + agent_deployment: String, +} + +impl From<&TokenSourceOptions> for TokenSourceFetchOptions { + fn from(value: &TokenSourceOptions) -> Self { + // Empty (or whitespace-only) fields are left unset so the + // token server applies its defaults. + let opt = |s: &str| { + let s = s.trim(); + (!s.is_empty()).then(|| s.to_string()) + }; + let mut options = TokenSourceFetchOptions::new(); + if let Some(v) = opt(&value.room_name) { + options = options.with_room_name(v); + } + if let Some(v) = opt(&value.participant_name) { + options = options.with_participant_name(v); + } + if let Some(v) = opt(&value.participant_identity) { + options = options.with_participant_identity(v); + } + if let Some(v) = opt(&value.participant_metadata) { + options = options.with_participant_metadata(v); + } + if let Some(v) = opt(&value.agent_name) { + options = options.with_agent_name(v); + } + if let Some(v) = opt(&value.agent_metadata) { + options = options.with_agent_metadata(v); + } + if let Some(v) = opt(&value.agent_deployment) { + options = options.with_deployment(v); + } + options + } } /// The root window: a welcome screen holding the only connect form in the app. @@ -114,15 +160,7 @@ pub struct ConnectView { url: String, token: String, token_server_id: String, - // Token-source fetch options (`ts_` to keep them apart from the API-key - // tab's identity/room). Empty means "omit, let the server default". - ts_room_name: String, - ts_participant_name: String, - ts_participant_identity: String, - ts_participant_metadata: String, - ts_agent_name: String, - ts_agent_metadata: String, - ts_agent_deployment: String, + token_source_options: TokenSourceOptions, api_key: String, api_secret: String, identity: String, @@ -150,14 +188,8 @@ impl Default for ConnectView { method: AuthMethod::default(), url: env_or("LIVEKIT_URL", "ws://localhost:7880"), token: env_or("LIVEKIT_TOKEN", ""), - token_server_id: "token-server-id".to_string(), - ts_room_name: String::new(), - ts_participant_name: String::new(), - ts_participant_identity: String::new(), - ts_participant_metadata: String::new(), - ts_agent_name: String::new(), - ts_agent_metadata: String::new(), - ts_agent_deployment: String::new(), + token_server_id: String::new(), + token_source_options: TokenSourceOptions::default(), api_key: env_or("LIVEKIT_API_KEY", "devkey"), api_secret: env_or("LIVEKIT_API_SECRET", "secret"), identity: "participant-0".to_string(), @@ -184,7 +216,7 @@ impl ConnectView { && !self.room.trim().is_empty() } AuthMethod::Token => !self.url.trim().is_empty() && !self.token.trim().is_empty(), - AuthMethod::TokenSource => !self.token_server_id.trim().is_empty(), + AuthMethod::DevelopmentTokenServer => !self.token_server_id.trim().is_empty(), } } @@ -201,36 +233,9 @@ impl ConnectView { url: self.url.clone(), token: self.token.clone(), }, - AuthMethod::TokenSource => { - // Empty (or whitespace-only) fields are left unset so the - // token server applies its defaults. - let opt = |s: &str| { - let s = s.trim(); - (!s.is_empty()).then(|| s.to_string()) - }; - let mut options = TokenSourceFetchOptions::new(); - if let Some(v) = opt(&self.ts_room_name) { - options = options.with_room_name(v); - } - if let Some(v) = opt(&self.ts_participant_name) { - options = options.with_participant_name(v); - } - if let Some(v) = opt(&self.ts_participant_identity) { - options = options.with_participant_identity(v); - } - if let Some(v) = opt(&self.ts_participant_metadata) { - options = options.with_participant_metadata(v); - } - if let Some(v) = opt(&self.ts_agent_name) { - options = options.with_agent_name(v); - } - if let Some(v) = opt(&self.ts_agent_metadata) { - options = options.with_agent_metadata(v); - } - if let Some(v) = opt(&self.ts_agent_deployment) { - options = options.with_deployment(v); - } - Auth::TokenSource { + AuthMethod::DevelopmentTokenServer => { + let options = TokenSourceFetchOptions::from(&self.token_source_options); + Auth::DevelopmentTokenServer { token_server_id: self.token_server_id.clone(), options, } @@ -316,7 +321,7 @@ impl egui::Widget for ConnectForm<'_> { ui.horizontal(|ui| { ui.selectable_value(&mut view.method, AuthMethod::ApiKey, "API Key"); ui.selectable_value(&mut view.method, AuthMethod::Token, "Token"); - ui.selectable_value(&mut view.method, AuthMethod::TokenSource, "TokenSource"); + ui.selectable_value(&mut view.method, AuthMethod::DevelopmentTokenServer, "TokenSource"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let toggle = ui .add(egui::Button::selectable(view.show_secrets, "👁")) @@ -366,7 +371,7 @@ impl egui::Widget for ConnectForm<'_> { }); ui.add_space(8.0); } - AuthMethod::TokenSource => { + AuthMethod::DevelopmentTokenServer => { ui.add(LabeledTextEdit::singleline( "Token Server Id", &mut view.token_server_id, @@ -383,39 +388,39 @@ impl egui::Widget for ConnectForm<'_> { ui.columns(2, |columns| { columns[0].add(LabeledTextEdit::singleline( "Room Name", - &mut view.ts_room_name, + &mut view.token_source_options.room_name, )); columns[1].add(LabeledTextEdit::singleline( "Participant Name", - &mut view.ts_participant_name, + &mut view.token_source_options.participant_name, )); }); ui.add_space(8.0); ui.columns(2, |columns| { columns[0].add(LabeledTextEdit::singleline( "Participant Identity", - &mut view.ts_participant_identity, + &mut view.token_source_options.participant_identity, )); columns[1].add(LabeledTextEdit::singleline( "Participant Metadata", - &mut view.ts_participant_metadata, + &mut view.token_source_options.participant_metadata, )); }); ui.add_space(8.0); ui.columns(2, |columns| { columns[0].add(LabeledTextEdit::singleline( "Agent Name", - &mut view.ts_agent_name, + &mut view.token_source_options.agent_name, )); columns[1].add(LabeledTextEdit::singleline( "Agent Deployment", - &mut view.ts_agent_deployment, + &mut view.token_source_options.agent_deployment, )); }); ui.add_space(8.0); ui.add(LabeledTextEdit::singleline( "Agent Metadata", - &mut view.ts_agent_metadata, + &mut view.token_source_options.agent_metadata, )); ui.add_space(8.0); } From a8d2ed5fb5e42ccfd599de021269d0837ade2d3f Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:28:01 +0200 Subject: [PATCH 4/4] Format --- src/connect.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/connect.rs b/src/connect.rs index eed8bc8..444b908 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -321,7 +321,11 @@ impl egui::Widget for ConnectForm<'_> { ui.horizontal(|ui| { ui.selectable_value(&mut view.method, AuthMethod::ApiKey, "API Key"); ui.selectable_value(&mut view.method, AuthMethod::Token, "Token"); - ui.selectable_value(&mut view.method, AuthMethod::DevelopmentTokenServer, "TokenSource"); + ui.selectable_value( + &mut view.method, + AuthMethod::DevelopmentTokenServer, + "TokenSource", + ); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let toggle = ui .add(egui::Button::selectable(view.show_secrets, "👁"))