From c7812e3527b5caa30ccc7c4226499aba995904c9 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/5] Adds token source tab --- src/connect.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/connect.rs b/src/connect.rs index 6a52589..496920a 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -12,6 +12,7 @@ pub enum Auth { identity: String, room: String, }, + TokenSource{sandbox_id: String} } impl Auth { @@ -35,6 +36,7 @@ impl Auth { }) .to_jwt() .map_err(|e| e.to_string()), + Auth::TokenSource{sandbox_id} => Ok("".to_string()), } } } @@ -59,6 +61,7 @@ enum AuthMethod { #[default] ApiKey, Token, + TokenSource } /// The root window: a welcome screen holding the only connect form in the app. @@ -74,6 +77,7 @@ pub struct ConnectView { method: AuthMethod, url: String, token: String, + sandbox_id: String, api_key: String, api_secret: String, identity: String, @@ -101,6 +105,7 @@ impl Default for ConnectView { method: AuthMethod::default(), url: env_or("LIVEKIT_URL", "ws://localhost:7880"), token: env_or("LIVEKIT_TOKEN", ""), + sandbox_id: "sandbox-id".to_string(), api_key: env_or("LIVEKIT_API_KEY", "devkey"), api_secret: env_or("LIVEKIT_API_SECRET", "secret"), identity: "participant-0".to_string(), @@ -127,6 +132,7 @@ impl ConnectView { && !self.room.trim().is_empty() } AuthMethod::Token => !self.token.trim().is_empty(), + AuthMethod::TokenSource => !self.sandbox_id.trim().is_empty() } } @@ -139,6 +145,7 @@ impl ConnectView { room: self.room.clone(), }, AuthMethod::Token => Auth::Token(self.token.clone()), + AuthMethod::TokenSource => Auth::TokenSource{sandbox_id: self.sandbox_id.clone()}, }; ConnectSettings { url: self.url.clone(), @@ -224,6 +231,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.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let toggle = ui .add(egui::Button::selectable(view.show_secrets, "👁")) @@ -239,10 +247,12 @@ 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. + ui.push_id("auth_method_fields", |ui| match view.method { AuthMethod::Token => { ui.add(LabeledTextEdit::singleline("Token", &mut view.token).password(mask)); ui.add_space(8.0); @@ -264,6 +274,10 @@ impl egui::Widget for ConnectForm<'_> { columns[1].add(LabeledTextEdit::singleline("Room", &mut view.room)); }); ui.add_space(8.0); + }, + AuthMethod::TokenSource => { + ui.add(LabeledTextEdit::singleline("Sandbox Id", &mut view.sandbox_id)); + ui.add_space(8.0); } }); From 57ae2de8641e739f6d19f04a0e2f17d06a9ab875 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:02:18 +0200 Subject: [PATCH 2/5] Sandbox id works, but no other fields possible yet --- src/connect.rs | 84 ++++++++++++++++++++++++++++++++++------------ src/lib.rs | 2 +- src/room/window.rs | 13 ++----- src/service.rs | 27 +++++++++++---- 4 files changed, 86 insertions(+), 40 deletions(-) diff --git a/src/connect.rs b/src/connect.rs index 496920a..818ed24 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -1,27 +1,36 @@ use crate::ui::{labeled_field::LabeledTextEdit, prominent_button::ProminentButton}; +use livekit_token_source::{TokenSourceSandbox, TokenSourceFetchOptions}; -/// 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 sandbox. +#[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, }, - TokenSource{sandbox_id: String} + /// A LiveKit Cloud sandbox token server, which provides both the server + /// URL and the join token. + TokenSource { sandbox_id: String }, } 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, @@ -35,8 +44,30 @@ impl Auth { ..Default::default() }) .to_jwt() + .map(|token| (url.clone(), token)) .map_err(|e| e.to_string()), - Auth::TokenSource{sandbox_id} => Ok("".to_string()), + Auth::TokenSource { sandbox_id } => { + let options = TokenSourceFetchOptions { + agent_name: Some("Church".to_string()), + ..Default::default() + }; + + let token_source = TokenSourceSandbox::new(sandbox_id.to_owned()); + 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 sandbox id. + pub fn target_label(&self) -> &str { + match self { + Auth::Token { url, .. } | Auth::ApiKey { url, .. } => url, + Auth::TokenSource { sandbox_id } => sandbox_id, } } } @@ -45,7 +76,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, @@ -121,34 +151,41 @@ 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 sandbox 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::TokenSource => !self.sandbox_id.trim().is_empty() + AuthMethod::Token => { + !self.url.trim().is_empty() && !self.token.trim().is_empty() + } + AuthMethod::TokenSource => !self.sandbox_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::TokenSource => Auth::TokenSource{sandbox_id: self.sandbox_id.clone()}, + AuthMethod::Token => Auth::Token { + url: self.url.clone(), + token: self.token.clone(), + }, + AuthMethod::TokenSource => Auth::TokenSource { + sandbox_id: self.sandbox_id.clone(), + }, }; ConnectSettings { - url: self.url.clone(), auth, key: self.key.clone(), auto_subscribe: self.auto_subscribe, @@ -225,9 +262,6 @@ 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"); @@ -252,12 +286,18 @@ impl egui::Widget for ConnectForm<'_> { // 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 sandbox. 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) 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..a223e7b 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: 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 addf4f9..1c38177 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,7 @@ use tokio::sync::mpsc::{self, error::SendError}; #[derive(Debug)] pub enum AsyncCmd { RoomConnect { - url: String, - token: String, + auth: Auth, auto_subscribe: bool, dynacast: bool, enable_e2ee: bool, @@ -64,7 +64,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 +163,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 +230,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 52493e510ef9226c3b93b5128600f54550934149 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:02:32 +0200 Subject: [PATCH 3/5] Cargo changes for local dev --- Cargo.lock | 41 +++++++++++++++-------------------------- Cargo.toml | 9 +++++---- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f035a6d..8d09738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1163,8 +1163,6 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "device-info" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ca8e71544c1b67dcdbc2699ab258828aff985e5bc8d5f6b486d90d7df2f848" dependencies = [ "core-foundation 0.10.1", "jni 0.21.1", @@ -2716,9 +2714,7 @@ dependencies = [ [[package]] name = "libwebrtc" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2da2ccaceaf34cf580c2b6819c57f80c85e53f7da7e8cf070b1d0a7ba12f3b3" +version = "0.3.42" dependencies = [ "cxx", "jni 0.21.1", @@ -2779,9 +2775,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "livekit" -version = "0.7.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63eeb45aa7d13d3e5c0009ea39c9ce8b7a3587f795eb7c3a13aa4880637295f" +version = "0.7.53" dependencies = [ "base64 0.22.1", "bmrng", @@ -2810,9 +2804,7 @@ dependencies = [ [[package]] name = "livekit-api" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1006f7c009369fe5f16f2eb6e93d6d0dcb2b427306a2f61b26ff5d2e440ec282" +version = "0.5.6" dependencies = [ "base64 0.21.7", "bytes", @@ -2822,6 +2814,7 @@ dependencies = [ "hmac", "http", "jsonwebtoken", + "livekit-common", "livekit-protocol", "livekit-runtime", "log", @@ -2847,8 +2840,6 @@ dependencies = [ [[package]] name = "livekit-common" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a76cd816c062654d105b697ebab136da87d14126ee77b2f13280cb895bcb58" dependencies = [ "livekit-protocol", ] @@ -2856,8 +2847,6 @@ dependencies = [ [[package]] name = "livekit-data-stream" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ad1cca8f05fbe29026d99931884641199df544c33777d1f1b321df6f1031dd" dependencies = [ "bmrng", "bytes", @@ -2877,8 +2866,6 @@ dependencies = [ [[package]] name = "livekit-datatrack" version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201bec4becf4db2b0af616a73ff3b2421f6af3693c287b82f5182028ee36e5f5" dependencies = [ "anyhow", "bytes", @@ -2898,8 +2885,6 @@ dependencies = [ [[package]] name = "livekit-protocol" version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d26880e94e2f9bab298445e7d86a3794453d211a12ddbd051bd9991a343f9ff" dependencies = [ "pbjson", "pbjson-types", @@ -2910,13 +2895,20 @@ dependencies = [ [[package]] name = "livekit-runtime" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" dependencies = [ "tokio", "tokio-stream", ] +[[package]] +name = "livekit-token-source" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -4443,6 +4435,7 @@ dependencies = [ "image", "livekit", "livekit-api", + "livekit-token-source", "log", "parking_lot", "serde", @@ -5798,9 +5791,7 @@ dependencies = [ [[package]] name = "webrtc-sys" -version = "0.3.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca21867ef47ef33e43564a6d1e6947658728579216da9c97c2415ca555f2c96" +version = "0.3.39" dependencies = [ "cc", "cxx", @@ -5814,8 +5805,6 @@ dependencies = [ [[package]] name = "webrtc-sys-build" version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d46da6b5a5cbd091fae0400f77189f4ca4807c0d9442b85838a584f28720570" dependencies = [ "anyhow", "fs2", diff --git a/Cargo.toml b/Cargo.toml index 117bc0f..bd5d921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,15 @@ tokio = { version = "1", features = ["full", "parking_lot"] } # egui-wgpu 0.35 requires wgpu ^29.0; "29.0" resolves to a compatible 29.0.x. wgpu = "29.0" winit = { version = "0.30.13", features = [ "android-native-activity" ] } -livekit = { version = "0.7.49", features = ["rustls-tls-native-roots"] } -livekit-api = { version = "0.5.4", default-features = false, features = ["access-token"] } +# livekit = { version = "0.7.49", features = ["rustls-tls-native-roots"] } +# livekit-api = { version = "0.5.4", default-features = false, features = ["access-token"] } # 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): -# livekit = { path = "../rust-sdks/livekit", features = ["rustls-tls-native-roots"] } -# livekit-api = { path = "../rust-sdks/livekit-api", default-features = false, features = ["access-token"] } +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" } [package.metadata.bundle] name = "LiveKit Client" From 86cc4b7b0b2b0f92bf33a463191ad8b8ec728882 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:11:25 +0200 Subject: [PATCH 4/5] Also have fields for all fetch options --- src/connect.rs | 102 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/src/connect.rs b/src/connect.rs index 818ed24..466ce6f 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -16,8 +16,12 @@ pub enum Auth { room: String, }, /// A LiveKit Cloud sandbox token server, which provides both the server - /// URL and the join token. - TokenSource { sandbox_id: String }, + /// URL and the join token. `options` parameterizes the request; unset + /// fields are left to server defaults. + TokenSource { + sandbox_id: String, + options: TokenSourceFetchOptions, + }, } impl Auth { @@ -46,15 +50,13 @@ impl Auth { .to_jwt() .map(|token| (url.clone(), token)) .map_err(|e| e.to_string()), - Auth::TokenSource { sandbox_id } => { - let options = TokenSourceFetchOptions { - agent_name: Some("Church".to_string()), - ..Default::default() - }; - + Auth::TokenSource { + sandbox_id, + options, + } => { let token_source = TokenSourceSandbox::new(sandbox_id.to_owned()); let response = token_source - .fetch(&options) + .fetch(options) .await .map_err(|e| e.to_string())?; Ok((response.server_url, response.participant_token)) @@ -67,7 +69,7 @@ impl Auth { pub fn target_label(&self) -> &str { match self { Auth::Token { url, .. } | Auth::ApiKey { url, .. } => url, - Auth::TokenSource { sandbox_id } => sandbox_id, + Auth::TokenSource { sandbox_id, .. } => sandbox_id, } } } @@ -108,6 +110,15 @@ pub struct ConnectView { url: String, token: String, sandbox_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, @@ -136,6 +147,13 @@ impl Default for ConnectView { url: env_or("LIVEKIT_URL", "ws://localhost:7880"), token: env_or("LIVEKIT_TOKEN", ""), sandbox_id: "sandbox-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(), @@ -181,9 +199,27 @@ impl ConnectView { url: self.url.clone(), token: self.token.clone(), }, - AuthMethod::TokenSource => Auth::TokenSource { - sandbox_id: self.sandbox_id.clone(), - }, + AuthMethod::TokenSource => { + // Empty (or whitespace-only) fields are omitted from the + // request so the token server applies its defaults. + let opt = |s: &str| { + let s = s.trim(); + (!s.is_empty()).then(|| s.to_string()) + }; + Auth::TokenSource { + sandbox_id: self.sandbox_id.clone(), + options: TokenSourceFetchOptions { + room_name: opt(&self.ts_room_name), + participant_name: opt(&self.ts_participant_name), + participant_identity: opt(&self.ts_participant_identity), + participant_metadata: opt(&self.ts_participant_metadata), + agent_name: opt(&self.ts_agent_name), + agent_metadata: opt(&self.ts_agent_metadata), + agent_deployment: opt(&self.ts_agent_deployment), + ..Default::default() + }, + } + } }; ConnectSettings { auth, @@ -318,6 +354,46 @@ impl egui::Widget for ConnectForm<'_> { AuthMethod::TokenSource => { ui.add(LabeledTextEdit::singleline("Sandbox Id", &mut view.sandbox_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); } }); From 82dc6df54c39bbe84b4c06c6a43c8c459f95c679 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:50:33 +0200 Subject: [PATCH 5/5] Some cleanup after CI complained --- AGENTS.md | 3 +++ Cargo.lock | 32 ++++++++++++++++++++++---------- Cargo.toml | 13 +++++++------ src/connect.rs | 35 +++++++++++++++++++++-------------- src/room/window.rs | 2 +- src/service.rs | 4 +++- 6 files changed, 57 insertions(+), 32 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 8d09738..cfa9f7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1163,6 +1163,8 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "device-info" version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ca8e71544c1b67dcdbc2699ab258828aff985e5bc8d5f6b486d90d7df2f848" dependencies = [ "core-foundation 0.10.1", "jni 0.21.1", @@ -2715,6 +2717,8 @@ dependencies = [ [[package]] name = "libwebrtc" version = "0.3.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5497ff0694ddcee8f88129c78139defa5ec3fc434c3e182906ed0a72372bd9" dependencies = [ "cxx", "jni 0.21.1", @@ -2776,6 +2780,8 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "livekit" version = "0.7.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8ed793e154d63b588397ea7a84cfb3606b6d049f4d24137d9d9e0384617059" dependencies = [ "base64 0.22.1", "bmrng", @@ -2805,6 +2811,8 @@ dependencies = [ [[package]] name = "livekit-api" version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e827d3444235ddccf360fc1d4737034e86ee2ea2c0a696f148ecc08e7249bfd3" dependencies = [ "base64 0.21.7", "bytes", @@ -2840,6 +2848,8 @@ dependencies = [ [[package]] name = "livekit-common" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a76cd816c062654d105b697ebab136da87d14126ee77b2f13280cb895bcb58" dependencies = [ "livekit-protocol", ] @@ -2847,6 +2857,8 @@ dependencies = [ [[package]] name = "livekit-data-stream" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ad1cca8f05fbe29026d99931884641199df544c33777d1f1b321df6f1031dd" dependencies = [ "bmrng", "bytes", @@ -2866,6 +2878,8 @@ dependencies = [ [[package]] name = "livekit-datatrack" version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201bec4becf4db2b0af616a73ff3b2421f6af3693c287b82f5182028ee36e5f5" dependencies = [ "anyhow", "bytes", @@ -2885,6 +2899,8 @@ dependencies = [ [[package]] name = "livekit-protocol" version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d26880e94e2f9bab298445e7d86a3794453d211a12ddbd051bd9991a343f9ff" dependencies = [ "pbjson", "pbjson-types", @@ -2895,20 +2911,13 @@ dependencies = [ [[package]] name = "livekit-runtime" version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" dependencies = [ "tokio", "tokio-stream", ] -[[package]] -name = "livekit-token-source" -version = "0.1.0" -dependencies = [ - "reqwest", - "serde", - "thiserror 2.0.18", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -4435,7 +4444,6 @@ dependencies = [ "image", "livekit", "livekit-api", - "livekit-token-source", "log", "parking_lot", "serde", @@ -5792,6 +5800,8 @@ dependencies = [ [[package]] name = "webrtc-sys" version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a555de886b8d4961e6c2878ac3ca1f716b75c77f60311c5cf6ed136d0df14e1" dependencies = [ "cc", "cxx", @@ -5805,6 +5815,8 @@ dependencies = [ [[package]] name = "webrtc-sys-build" version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d46da6b5a5cbd091fae0400f77189f4ca4807c0d9442b85838a584f28720570" dependencies = [ "anyhow", "fs2", diff --git a/Cargo.toml b/Cargo.toml index bd5d921..5df2831 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,15 +18,16 @@ tokio = { version = "1", features = ["full", "parking_lot"] } # egui-wgpu 0.35 requires wgpu ^29.0; "29.0" resolves to a compatible 29.0.x. wgpu = "29.0" winit = { version = "0.30.13", features = [ "android-native-activity" ] } -# livekit = { version = "0.7.49", features = ["rustls-tls-native-roots"] } -# livekit-api = { version = "0.5.4", default-features = false, features = ["access-token"] } +livekit = { version = "0.7.49", features = ["rustls-tls-native-roots"] } +livekit-api = { version = "0.5.4", default-features = false, features = ["access-token"] } +livekit-token-source = { version = "0.1.0" } -# For local SDK development, comment out the two lines above and uncomment these +# For local SDK development, comment out the three 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): -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" } +# 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" } [package.metadata.bundle] name = "LiveKit Client" diff --git a/src/connect.rs b/src/connect.rs index 466ce6f..7b54966 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -1,5 +1,5 @@ use crate::ui::{labeled_field::LabeledTextEdit, prominent_button::ProminentButton}; -use livekit_token_source::{TokenSourceSandbox, TokenSourceFetchOptions}; +use livekit_token_source::{TokenSourceFetchOptions, TokenSourceSandbox}; /// How a room connection is authenticated. The methods that target a known /// server carry its URL; the token-source method learns it from the sandbox. @@ -16,7 +16,7 @@ pub enum Auth { room: String, }, /// A LiveKit Cloud sandbox token server, which provides both the server - /// URL and the join token. `options` parameterizes the request; unset + /// URL and the join token. `options` customizes the request; unset /// fields are left to server defaults. TokenSource { sandbox_id: String, @@ -93,7 +93,7 @@ enum AuthMethod { #[default] ApiKey, Token, - TokenSource + TokenSource, } /// The root window: a welcome screen holding the only connect form in the app. @@ -179,9 +179,7 @@ impl ConnectView { && !self.identity.trim().is_empty() && !self.room.trim().is_empty() } - AuthMethod::Token => { - !self.url.trim().is_empty() && !self.token.trim().is_empty() - } + AuthMethod::Token => !self.url.trim().is_empty() && !self.token.trim().is_empty(), AuthMethod::TokenSource => !self.sandbox_id.trim().is_empty(), } } @@ -350,19 +348,26 @@ impl egui::Widget for ConnectForm<'_> { columns[1].add(LabeledTextEdit::singleline("Room", &mut view.room)); }); ui.add_space(8.0); - }, + } AuthMethod::TokenSource => { - ui.add(LabeledTextEdit::singleline("Sandbox Id", &mut view.sandbox_id)); + ui.add(LabeledTextEdit::singleline( + "Sandbox Id", + &mut view.sandbox_id, + )); ui.add_space(8.0); ui.label( - egui::RichText::new("Optional overrides — empty fields use server defaults") - .text_style(egui::TextStyle::Small), + 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[0].add(LabeledTextEdit::singleline( + "Room Name", + &mut view.ts_room_name, + )); columns[1].add(LabeledTextEdit::singleline( "Participant Name", &mut view.ts_participant_name, @@ -381,8 +386,10 @@ impl egui::Widget for ConnectForm<'_> { }); ui.add_space(8.0); ui.columns(2, |columns| { - columns[0] - .add(LabeledTextEdit::singleline("Agent Name", &mut view.ts_agent_name)); + columns[0].add(LabeledTextEdit::singleline( + "Agent Name", + &mut view.ts_agent_name, + )); columns[1].add(LabeledTextEdit::singleline( "Agent Deployment", &mut view.ts_agent_deployment, diff --git a/src/room/window.rs b/src/room/window.rs index a223e7b..3d29219 100644 --- a/src/room/window.rs +++ b/src/room/window.rs @@ -57,7 +57,7 @@ impl RoomWindow { self.connecting = true; self.connection_failure = None; let _ = self.service.send(AsyncCmd::RoomConnect { - auth: self.request.auth.clone(), + auth: Box::new(self.request.auth.clone()), auto_subscribe: self.request.auto_subscribe, dynacast: self.request.dynacast, enable_e2ee: self.request.enable_e2ee, diff --git a/src/service.rs b/src/service.rs index 1c38177..7c7595c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -13,7 +13,9 @@ use tokio::sync::mpsc::{self, error::SendError}; #[derive(Debug)] pub enum AsyncCmd { RoomConnect { - auth: Auth, + /// 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,