From a3cc7491f3cc024d52e4e231b85efaef97c539bb Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 10:33:52 -0400 Subject: [PATCH 01/29] feat: add initial token source sketch --- Cargo.lock | 106 +++++++- livekit-protocol/protocol | 2 +- livekit/Cargo.toml | 1 + livekit/src/room/mod.rs | 1 + livekit/src/room/token_source.rs | 445 +++++++++++++++++++++++++++++++ 5 files changed, 542 insertions(+), 13 deletions(-) create mode 100644 livekit/src/room/token_source.rs diff --git a/Cargo.lock b/Cargo.lock index bb87c26f3..d21956314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,7 +497,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -799,7 +799,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1958,7 +1958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.2", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -2124,6 +2124,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.11.4", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -2308,7 +2327,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2332,6 +2351,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2408,9 +2428,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2879,7 +2901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -2985,6 +3007,7 @@ dependencies = [ "log", "parking_lot", "prost 0.12.6", + "reqwest", "semver", "serde", "serde_json", @@ -3867,7 +3890,7 @@ dependencies = [ "petgraph 0.6.5", "redox_syscall 0.5.18", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4555,9 +4578,11 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -4567,6 +4592,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "native-tls", "percent-encoding", "pin-project-lite", @@ -5228,6 +5254,27 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -5568,7 +5615,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -6548,7 +6595,7 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement 0.60.2", "windows-interface 0.59.3", - "windows-link", + "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", ] @@ -6597,12 +6644,29 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -6621,13 +6685,22 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -6640,13 +6713,22 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -6700,7 +6782,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -6755,7 +6837,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 2bc93ddc2..357f7686d 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 2bc93ddc27ccfa66ee8d270a1bcd115586fb601d +Subproject commit 357f7686de5bab17e1d3e3f1b8b805dba5a0ff56 diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index e41c726f7..b36bc6ec1 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -45,6 +45,7 @@ libloading = { version = "0.8.6" } bytes = "1.10.1" bmrng = "0.5.2" test-log = "0.2.18" +reqwest = { version = "0.12", features = ["json"] } [dev-dependencies] anyhow = "1.0.99" diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 3ee94088d..ca0a02990 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -62,6 +62,7 @@ pub mod participant; pub mod publication; pub mod track; pub(crate) mod utils; +pub mod token_source; pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/livekit/src/room/token_source.rs b/livekit/src/room/token_source.rs new file mode 100644 index 000000000..31a1e0e9b --- /dev/null +++ b/livekit/src/room/token_source.rs @@ -0,0 +1,445 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use livekit_api::access_token::{self, AccessTokenError}; +use livekit_protocol::{RoomAgentDispatch, RoomConfiguration}; +use serde::{Serialize, Deserialize}; +use std::{collections::HashMap, error::Error}; + +#[derive(Clone, Default)] +pub struct TokenSourceFetchOptions { + pub room_name: Option, + pub participant_name: Option, + pub participant_identity: Option, + pub participant_metadata: Option, + pub participant_attributes: Option>, + + pub agent_name: Option, + pub agent_metadata: Option, +} + +impl TokenSourceFetchOptions { + pub fn with_room_name(&mut self, room_name: &str) -> &mut Self { + self.room_name = Some(room_name.into()); + self + } + pub fn with_participant_name(&mut self, participant_name: &str) -> &mut Self { + self.participant_name = Some(participant_name.into()); + self + } + pub fn with_participant_identity(&mut self, participant_identity: &str) -> &mut Self { + self.participant_identity = Some(participant_identity.into()); + self + } + pub fn with_participant_metadata(&mut self, participant_metadata: &str) -> &mut Self { + self.participant_metadata = Some(participant_metadata.into()); + self + } + + fn ensure_participant_attributes_defined(&mut self) -> &mut HashMap { + if self.participant_attributes.is_none() { + self.participant_attributes = Some(HashMap::new()); + }; + + let Some(participant_attribute_mut) = self.participant_attributes.as_mut() else { unreachable!(); }; + participant_attribute_mut + } + + pub fn with_participant_attribute(&mut self, attribute_key: &str, attribute_value: &str) -> &mut Self { + self.ensure_participant_attributes_defined().insert(attribute_key.into(), attribute_value.into()); + self + } + + pub fn with_participant_attributes(&mut self, participant_attributes: HashMap) -> &mut Self { + self.ensure_participant_attributes_defined().extend(participant_attributes); + self + } + + pub fn with_agent_name(&mut self, agent_name: &str) -> &mut Self { + self.agent_name = Some(agent_name.into()); + self + } + pub fn with_agent_metadata(&mut self, agent_metadata: &str) -> &mut Self { + self.agent_metadata = Some(agent_metadata.into()); + self + } +} + +impl Into for TokenSourceFetchOptions { + fn into(self) -> TokenSourceRequest { + let mut agent_dispatch = RoomAgentDispatch::default(); + if let Some(agent_name) = self.agent_name { + agent_dispatch.agent_name = agent_name; + } + if let Some(agent_metadata) = self.agent_metadata { + agent_dispatch.metadata = agent_metadata; + } + + let room_config = if agent_dispatch != RoomAgentDispatch::default() { + let mut room_config = RoomConfiguration::default(); + room_config.agents.push(agent_dispatch); + Some(room_config) + } else { + None + }; + + TokenSourceRequest { + room_name: self.room_name, + participant_name: self.participant_name, + participant_identity: self.participant_identity, + participant_metadata: self.participant_metadata, + participant_attributes: self.participant_attributes, + room_config, + } + } +} + +// FIXME: use the protobuf version of this struct instead! +#[derive(Debug, Clone, Serialize)] +pub struct TokenSourceRequest { + /// The name of the room being requested when generating credentials + pub room_name: Option, + + /// The name of the participant being requested for this client when generating credentials + pub participant_name: Option, + + /// The identity of the participant being requested for this client when generating credentials + pub participant_identity: Option, + + /// Any participant metadata being included along with the credentials generation operation + pub participant_metadata: Option, + + /// Any participant attributes being included along with the credentials generation operation + pub participant_attributes: Option>, + + /// A RoomConfiguration object can be passed to request extra parameters should be included when + /// generating connection credentials - dispatching agents, defining egress settings, etc + /// More info: https://docs.livekit.io/home/get-started/authentication/#room-configuration + pub room_config: Option, +} + +// FIXME: use the protobuf version of this struct instead! +#[derive(Debug, Clone, Deserialize)] +pub struct TokenSourceResponse { + pub server_url: String, + pub participant_token: String, +} + +impl TokenSourceResponse { + pub fn new(server_url: &str, participant_token: &str) -> Self { + Self { server_url: server_url.into(), participant_token: participant_token.into() } + } +} + +pub trait TokenSourceFixed { + // FIXME: what should the error type of the result be? + async fn fetch(&self) -> Result>; +} + +pub trait TokenSourceConfigurable { + // FIXME: what should the error type of the result be? + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result>; +} + + + + +pub trait TokenLiteralGenerator { + fn apply(&self) -> TokenSourceResponse; +} + +impl TokenLiteralGenerator for TokenSourceResponse { + fn apply(&self) -> TokenSourceResponse { + self.clone() + } +} + +impl TokenSourceResponse> TokenLiteralGenerator for F { + fn apply(&self) -> TokenSourceResponse { + self() + } +} + +pub struct TokenSourceLiteral { + generator: Generator, +} +impl TokenSourceLiteral { + pub fn new(generator: G) -> Self { + Self { generator } + } +} + +impl TokenSourceFixed for TokenSourceLiteral { + async fn fetch(&self) -> Result> { + Ok(self.generator.apply()) + } +} + + + + +trait MinterCredentials { + fn get(&self) -> (String, String, String); +} + +struct MinterCredentialsLiteral(String, String, String); + +impl MinterCredentialsLiteral { + pub fn new(server_url: &str, api_key: &str, api_secret: &str) -> Self { + Self(server_url.into(), api_key.into(), api_secret.into()) + } +} + +impl MinterCredentials for MinterCredentialsLiteral { + fn get(&self) -> (String, String, String) { + (self.0.clone(), self.1.clone(), self.2.clone()) + } +} + +// FIXME: maybe add dotenv source too? Or have this look there too? +struct MinterCredentialsEnvironment(String, String, String); + +impl MinterCredentialsEnvironment { + pub fn new(url_variable: &str, api_key_variable: &str, api_secret_variable: &str) -> Self { + Self(url_variable.into(), api_key_variable.into(), api_secret_variable.into()) + } +} + +impl MinterCredentials for MinterCredentialsEnvironment { + fn get(&self) -> (String, String, String) { + let (url_variable, api_key_variable, api_secret_variable) = (&self.0, &self.1, &self.2); + let url = std::env::var(url_variable).expect(format!("{url_variable} is not set").as_str()); + let api_key = std::env::var(api_key_variable).expect(format!("{api_key_variable} is not set").as_str()); + let api_secret = std::env::var(api_secret_variable).expect(format!("{api_secret_variable} is not set").as_str()); + (url, api_key, api_secret) + } +} + +impl Default for MinterCredentialsEnvironment { + fn default() -> Self { + Self("LIVEKIT_URL".into(), "LIVEKIT_API_KEY".into(), "LIVEKIT_API_SECRET".into()) + } +} + +impl (String, String, String)> MinterCredentials for F { + fn get(&self) -> (String, String, String) { + self() + } +} + + +struct TokenSourceMinter { + credentials: Credentials, +} + +impl TokenSourceMinter { + pub fn new(credentials: C) -> Self { + Self { credentials } + } +} + +impl TokenSourceConfigurable for TokenSourceMinter { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + let (server_url, api_key, api_secret) = self.credentials.get(); + + // FIXME: apply options in the below code! + let participant_token = access_token::AccessToken::with_api_key(&api_key, &api_secret) + .with_identity("rust-bot") + .with_name("Rust Bot") + .with_grants(access_token::VideoGrants { + room_join: true, + room: "my-room".to_string(), + ..Default::default() + }) + .to_jwt()?; + + Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) + } +} + +impl Default for TokenSourceMinter { + fn default() -> Self { + Self::new(MinterCredentialsEnvironment::default()) + } +} + + + + + + +struct TokenSourceCustomMinter< + MintFn: Fn(access_token::AccessToken) -> Result, + Credentials: MinterCredentials, +> { + mint_fn: MintFn, + credentials: Credentials, +} + +impl< + MF: Fn(access_token::AccessToken) -> Result, + C: MinterCredentials +> TokenSourceCustomMinter { + pub fn new_with_credentials(mint_fn: MF, credentials: C) -> Self { + Self { mint_fn, credentials } + } + pub fn new(mint_fn: MF) -> TokenSourceCustomMinter { + TokenSourceCustomMinter::new_with_credentials(mint_fn, MinterCredentialsEnvironment::default()) + } +} + + +impl< + MF: Fn(access_token::AccessToken) -> Result, + C: MinterCredentials, +> TokenSourceFixed for TokenSourceCustomMinter { + async fn fetch(&self) -> Result> { + let (server_url, api_key, api_secret) = self.credentials.get(); + + let participant_token = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret))?; + + Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) + } +} + + + + + +use reqwest::{header::HeaderMap, Method}; + +struct TokenSourceEndpoint { + url: String, + method: Method, + headers: HeaderMap, +} + +impl TokenSourceEndpoint { + pub fn new(url: &str) -> Self { + Self { + url: url.into(), + method: Method::POST, + headers: HeaderMap::new(), + } + } +} + +impl TokenSourceConfigurable for TokenSourceEndpoint { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + let client = reqwest::Client::new(); + + // FIXME: What are the best practices around implementing Into on a reference to avoid the + // clone? + let request_body: TokenSourceRequest = options.clone().into(); + + let response = client + .request(self.method.clone(), &self.url) + .json(&request_body) + .headers(self.headers.clone()) + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Error generating token from endpoint {}: received {:?} / {}", self.url, response.status(), response.text().await?).into()); + } + + let response_json = response.json::().await?; + + Ok(TokenSourceResponse::new(&response_json.server_url, &response_json.participant_token)) + } +} + + + + +struct TokenSourceSandboxTokenServer(TokenSourceEndpoint); + +impl TokenSourceSandboxTokenServer { + pub fn new(sandbox_id: &str) -> Self { + Self::new_with_base_url(sandbox_id, "https://cloud-api.livekit.io") + } + pub fn new_with_base_url(sandbox_id: &str, base_url: &str) -> Self { + let mut endpoint = TokenSourceEndpoint::new( + &format!("{base_url}/api/v2/sandbox/connection-details") + ); + endpoint.headers.insert("X-Sandbox-ID", sandbox_id.parse().unwrap()); + Self(endpoint) + } +} + +impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + self.0.fetch(options).await + } +} + + + + +struct TokenSourceCustom(CustomFn); + +impl< + CustomFn: Fn(&TokenSourceFetchOptions) -> TokenSourceResponse +> TokenSourceCustom { + pub fn new(custom_fn: CustomFn) -> Self { + Self(custom_fn) + } +} + +impl< + CustomFn: Fn(&TokenSourceFetchOptions) -> TokenSourceResponse +> TokenSourceConfigurable for TokenSourceCustom { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + let response = (self.0)(options); + Ok(response) + } +} + + + + +async fn test() { + let literal = TokenSourceLiteral::new(TokenSourceResponse::new("...", "...")); + let _ = literal.fetch().await; + + let minter = TokenSourceMinter::default(); + let _ = minter.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + + let minter_literal_credentials = TokenSourceMinter::new(MinterCredentialsLiteral::new("server url", "api key", "api secret")); + let _ = minter.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + + let minter_literal_custom = TokenSourceMinter::new(|| ("server url".into(), "api key".into(), "api secret".into())); + let _ = minter.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + + let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { + access_token + .with_identity("rust-bot") + .with_name("Rust Bot") + .with_grants(access_token::VideoGrants { + room_join: true, + room: "my-room".to_string(), + ..Default::default() + }) + .to_jwt() + }); + let _ = custom_minter.fetch().await; + + let endpoint = TokenSourceEndpoint::new("https://example.com/my/example/auth/endpoint"); + let _ = endpoint.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + + let endpoint = TokenSourceSandboxTokenServer::new("SANDBOX ID HERE"); + let _ = endpoint.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + + // TODO: custom +} From 74b92c4f91db7d3b993c2fde5b744d0030bdae9e Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 10:58:33 -0400 Subject: [PATCH 02/29] feat: get TokenSourceCustom working --- livekit/src/room/token_source.rs | 43 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/livekit/src/room/token_source.rs b/livekit/src/room/token_source.rs index 31a1e0e9b..db842b9c1 100644 --- a/livekit/src/room/token_source.rs +++ b/livekit/src/room/token_source.rs @@ -30,19 +30,19 @@ pub struct TokenSourceFetchOptions { } impl TokenSourceFetchOptions { - pub fn with_room_name(&mut self, room_name: &str) -> &mut Self { + pub fn with_room_name(mut self, room_name: &str) -> Self { self.room_name = Some(room_name.into()); self } - pub fn with_participant_name(&mut self, participant_name: &str) -> &mut Self { + pub fn with_participant_name(mut self, participant_name: &str) -> Self { self.participant_name = Some(participant_name.into()); self } - pub fn with_participant_identity(&mut self, participant_identity: &str) -> &mut Self { + pub fn with_participant_identity(mut self, participant_identity: &str) -> Self { self.participant_identity = Some(participant_identity.into()); self } - pub fn with_participant_metadata(&mut self, participant_metadata: &str) -> &mut Self { + pub fn with_participant_metadata(mut self, participant_metadata: &str) -> Self { self.participant_metadata = Some(participant_metadata.into()); self } @@ -56,21 +56,21 @@ impl TokenSourceFetchOptions { participant_attribute_mut } - pub fn with_participant_attribute(&mut self, attribute_key: &str, attribute_value: &str) -> &mut Self { + pub fn with_participant_attribute(mut self, attribute_key: &str, attribute_value: &str) -> Self { self.ensure_participant_attributes_defined().insert(attribute_key.into(), attribute_value.into()); self } - pub fn with_participant_attributes(&mut self, participant_attributes: HashMap) -> &mut Self { + pub fn with_participant_attributes(mut self, participant_attributes: HashMap) -> Self { self.ensure_participant_attributes_defined().extend(participant_attributes); self } - pub fn with_agent_name(&mut self, agent_name: &str) -> &mut Self { + pub fn with_agent_name(mut self, agent_name: &str) -> Self { self.agent_name = Some(agent_name.into()); self } - pub fn with_agent_metadata(&mut self, agent_metadata: &str) -> &mut Self { + pub fn with_agent_metadata(mut self, agent_metadata: &str) -> Self { self.agent_metadata = Some(agent_metadata.into()); self } @@ -387,10 +387,12 @@ impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { -struct TokenSourceCustom(CustomFn); +struct TokenSourceCustom< + CustomFn: AsyncFn(&TokenSourceFetchOptions) -> Result>, +>(CustomFn); impl< - CustomFn: Fn(&TokenSourceFetchOptions) -> TokenSourceResponse + CustomFn: AsyncFn(&TokenSourceFetchOptions) -> Result>, > TokenSourceCustom { pub fn new(custom_fn: CustomFn) -> Self { Self(custom_fn) @@ -398,11 +400,10 @@ impl< } impl< - CustomFn: Fn(&TokenSourceFetchOptions) -> TokenSourceResponse + CustomFn: AsyncFn(&TokenSourceFetchOptions) -> Result>, > TokenSourceConfigurable for TokenSourceCustom { async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { - let response = (self.0)(options); - Ok(response) + (self.0)(options).await } } @@ -410,17 +411,19 @@ impl< async fn test() { + let fetch_options = TokenSourceFetchOptions::default().with_agent_name("voice ai quickstart"); + let literal = TokenSourceLiteral::new(TokenSourceResponse::new("...", "...")); let _ = literal.fetch().await; let minter = TokenSourceMinter::default(); - let _ = minter.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + let _ = minter.fetch(&fetch_options).await; let minter_literal_credentials = TokenSourceMinter::new(MinterCredentialsLiteral::new("server url", "api key", "api secret")); - let _ = minter.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + let _ = minter.fetch(&fetch_options).await; let minter_literal_custom = TokenSourceMinter::new(|| ("server url".into(), "api key".into(), "api secret".into())); - let _ = minter.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + let _ = minter.fetch(&fetch_options).await; let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { access_token @@ -436,10 +439,14 @@ async fn test() { let _ = custom_minter.fetch().await; let endpoint = TokenSourceEndpoint::new("https://example.com/my/example/auth/endpoint"); - let _ = endpoint.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + let _ = endpoint.fetch(&fetch_options).await; let endpoint = TokenSourceSandboxTokenServer::new("SANDBOX ID HERE"); - let _ = endpoint.fetch(&TokenSourceFetchOptions { /* agentName: "my agent", ... etc ... */ }).await; + let _ = endpoint.fetch(&fetch_options).await; // TODO: custom + let custom = TokenSourceCustom::new(async |_options| { + Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) + }); + let _ = custom.fetch(&fetch_options).await; } From e06e01051d17519dd92289c346c7d31288be7b49 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 11:51:22 -0400 Subject: [PATCH 03/29] feat: use dyn Future in TokenSourceFixed trait --- livekit/src/room/token_source.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/livekit/src/room/token_source.rs b/livekit/src/room/token_source.rs index db842b9c1..923bfb8eb 100644 --- a/livekit/src/room/token_source.rs +++ b/livekit/src/room/token_source.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use futures_util::future; use livekit_api::access_token::{self, AccessTokenError}; use livekit_protocol::{RoomAgentDispatch, RoomConfiguration}; use serde::{Serialize, Deserialize}; -use std::{collections::HashMap, error::Error}; +use std::{collections::HashMap, error::Error, future::Future, pin::Pin}; #[derive(Clone, Default)] pub struct TokenSourceFetchOptions { @@ -144,7 +145,7 @@ impl TokenSourceResponse { pub trait TokenSourceFixed { // FIXME: what should the error type of the result be? - async fn fetch(&self) -> Result>; + fn fetch(&self) -> Pin>> + '_>>; } pub trait TokenSourceConfigurable { @@ -181,8 +182,8 @@ impl TokenSourceLiteral { } impl TokenSourceFixed for TokenSourceLiteral { - async fn fetch(&self) -> Result> { - Ok(self.generator.apply()) + fn fetch(&self) -> Pin>> + '_>> { + Box::pin(future::ready(Ok(self.generator.apply()))) } } @@ -304,12 +305,21 @@ impl< MF: Fn(access_token::AccessToken) -> Result, C: MinterCredentials, > TokenSourceFixed for TokenSourceCustomMinter { - async fn fetch(&self) -> Result> { + fn fetch(&self) -> Pin>> + '_>> { let (server_url, api_key, api_secret) = self.credentials.get(); - let participant_token = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret))?; + let result = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret)).and_then(|participant_token| { + Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) + }); - Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) + // FIXME: think more broadly about how error handling should be done and get rid of stuff + // like this + let coerced_result: Result> = match result { + Ok(value) => Ok(value), + Err(err) => Err(Box::new(err)), + }; + + Box::pin(future::ready(coerced_result)) } } From 4f712a39c1252cb81a844e01610c916029f7e95a Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 12:43:32 -0400 Subject: [PATCH 04/29] feat: add TokenSourceFixedSynchronous / TokenSourceConfigurableSynchronous traits These allow for ? to be used within their fn bodies --- livekit/src/room/token_source.rs | 58 +++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/livekit/src/room/token_source.rs b/livekit/src/room/token_source.rs index 923bfb8eb..60f2b2fcf 100644 --- a/livekit/src/room/token_source.rs +++ b/livekit/src/room/token_source.rs @@ -145,14 +145,41 @@ impl TokenSourceResponse { pub trait TokenSourceFixed { // FIXME: what should the error type of the result be? - fn fetch(&self) -> Pin>> + '_>>; + fn fetch(&self) -> impl Future>>; } pub trait TokenSourceConfigurable { // FIXME: what should the error type of the result be? - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result>; + fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>>; } +pub trait TokenSourceConfigurableSynchronous { + // FIXME: what should the error type of the result be? + fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result>; +} + +impl TokenSourceConfigurable for T { + // FIXME: what should the error type of the result be? + fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>> { + future::ready(self.fetch_synchronous(options)) + } +} + + + + + +pub trait TokenSourceFixedSynchronous { + // FIXME: what should the error type of the result be? + fn fetch_synchronous(&self) -> Result>; +} + +impl TokenSourceFixed for T { + // FIXME: what should the error type of the result be? + fn fetch(&self) -> impl Future>> { + future::ready(self.fetch_synchronous()) + } +} @@ -181,9 +208,9 @@ impl TokenSourceLiteral { } } -impl TokenSourceFixed for TokenSourceLiteral { - fn fetch(&self) -> Pin>> + '_>> { - Box::pin(future::ready(Ok(self.generator.apply()))) +impl TokenSourceFixedSynchronous for TokenSourceLiteral { + fn fetch_synchronous(&self) -> Result> { + Ok(self.generator.apply()) } } @@ -250,8 +277,8 @@ impl TokenSourceMinter { } } -impl TokenSourceConfigurable for TokenSourceMinter { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { +impl TokenSourceConfigurableSynchronous for TokenSourceMinter { + fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result> { let (server_url, api_key, api_secret) = self.credentials.get(); // FIXME: apply options in the below code! @@ -304,22 +331,13 @@ impl< impl< MF: Fn(access_token::AccessToken) -> Result, C: MinterCredentials, -> TokenSourceFixed for TokenSourceCustomMinter { - fn fetch(&self) -> Pin>> + '_>> { +> TokenSourceFixedSynchronous for TokenSourceCustomMinter { + fn fetch_synchronous(&self) -> Result> { let (server_url, api_key, api_secret) = self.credentials.get(); - let result = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret)).and_then(|participant_token| { - Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) - }); - - // FIXME: think more broadly about how error handling should be done and get rid of stuff - // like this - let coerced_result: Result> = match result { - Ok(value) => Ok(value), - Err(err) => Err(Box::new(err)), - }; + let participant_token = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret))?; - Box::pin(future::ready(coerced_result)) + Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) } } From fd8d50aeaae09e1dc8a317f15eb75ce457ad6b44 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 12:46:21 -0400 Subject: [PATCH 05/29] fix: update incorrect variable references --- livekit/src/room/token_source.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit/src/room/token_source.rs b/livekit/src/room/token_source.rs index 60f2b2fcf..03c821431 100644 --- a/livekit/src/room/token_source.rs +++ b/livekit/src/room/token_source.rs @@ -448,10 +448,10 @@ async fn test() { let _ = minter.fetch(&fetch_options).await; let minter_literal_credentials = TokenSourceMinter::new(MinterCredentialsLiteral::new("server url", "api key", "api secret")); - let _ = minter.fetch(&fetch_options).await; + let _ = minter_literal_credentials.fetch(&fetch_options).await; let minter_literal_custom = TokenSourceMinter::new(|| ("server url".into(), "api key".into(), "api secret".into())); - let _ = minter.fetch(&fetch_options).await; + let _ = minter_literal_custom.fetch(&fetch_options).await; let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { access_token From 92ac520ff6d57d57fae122c095b74c45a4ec480b Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 13:14:26 -0400 Subject: [PATCH 06/29] feat: get custom working but it is not really correct --- livekit/src/room/token_source.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/livekit/src/room/token_source.rs b/livekit/src/room/token_source.rs index 03c821431..77d5a8f0a 100644 --- a/livekit/src/room/token_source.rs +++ b/livekit/src/room/token_source.rs @@ -416,11 +416,11 @@ impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { struct TokenSourceCustom< - CustomFn: AsyncFn(&TokenSourceFetchOptions) -> Result>, + CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, >(CustomFn); impl< - CustomFn: AsyncFn(&TokenSourceFetchOptions) -> Result>, + CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, > TokenSourceCustom { pub fn new(custom_fn: CustomFn) -> Self { Self(custom_fn) @@ -428,7 +428,7 @@ impl< } impl< - CustomFn: AsyncFn(&TokenSourceFetchOptions) -> Result>, + CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, > TokenSourceConfigurable for TokenSourceCustom { async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { (self.0)(options).await @@ -472,9 +472,19 @@ async fn test() { let endpoint = TokenSourceSandboxTokenServer::new("SANDBOX ID HERE"); let _ = endpoint.fetch(&fetch_options).await; + // let foo = Box::pin(async |options: &TokenSourceFetchOptions| { + // Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) + // }); + + // // TODO: custom + // let custom = TokenSourceCustom::new(foo); + // let _ = custom.fetch(&fetch_options).await; + // TODO: custom - let custom = TokenSourceCustom::new(async |_options| { - Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) + let custom = TokenSourceCustom::new(|_options| { + Box::pin(future::ready( + Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) + )) }); let _ = custom.fetch(&fetch_options).await; } From e57f5b59cb36da4322e64fd9cd4761eb884a74b8 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 13:44:06 -0400 Subject: [PATCH 07/29] feat: update livekit-proto to be built from protocol repo version v1.42.1 --- livekit-protocol/generate_proto.sh | 3 +- livekit-protocol/src/livekit.rs | 213 +++- livekit-protocol/src/livekit.serde.rs | 1635 ++++++++++++++++++++++++- 3 files changed, 1818 insertions(+), 33 deletions(-) diff --git a/livekit-protocol/generate_proto.sh b/livekit-protocol/generate_proto.sh index d8b5c19c7..68d942d27 100755 --- a/livekit-protocol/generate_proto.sh +++ b/livekit-protocol/generate_proto.sh @@ -32,4 +32,5 @@ protoc \ $PROTOCOL/livekit_room.proto \ $PROTOCOL/livekit_webhook.proto \ $PROTOCOL/livekit_sip.proto \ - $PROTOCOL/livekit_models.proto + $PROTOCOL/livekit_models.proto \ + $PROTOCOL/livekit_token_source.proto diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 676becb62..73219bbe4 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -12,7 +12,7 @@ pub struct MetricsBatch { /// This is useful for storing participant identities, track names, etc. /// There is also a predefined list of labels that can be used to reference common metrics. /// They have reserved indices from 0 to (METRIC_LABEL_PREDEFINED_MAX_VALUE - 1). - /// Indexes pointing at str_data should start from METRIC_LABEL_PREDEFINED_MAX_VALUE, + /// Indexes pointing at str_data should start from METRIC_LABEL_PREDEFINED_MAX_VALUE, /// such that str_data\[0\] == index of METRIC_LABEL_PREDEFINED_MAX_VALUE. #[prost(string, repeated, tag="3")] pub str_data: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, @@ -78,6 +78,14 @@ pub struct EventMetric { #[prost(uint32, tag="9")] pub rid: u32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MetricsRecordingHeader { + #[prost(string, tag="1")] + pub room_id: ::prost::alloc::string::String, + #[prost(bool, optional, tag="2")] + pub enable_user_data_training: ::core::option::Option, +} // // Protocol used to record metrics for a specific session. // @@ -217,7 +225,7 @@ pub struct ListUpdate { pub add: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// delete items from a list #[prost(string, repeated, tag="3")] - pub del: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + pub remove: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// sets the list to an empty list #[prost(bool, tag="4")] pub clear: bool, @@ -398,6 +406,10 @@ pub mod participant_info { Sip = 3, /// LiveKit agents Agent = 4, + /// Connectors participants + /// + /// NEXT_ID: 8 + Connector = 7, } impl Kind { /// String value of the enum field names used in the ProtoBuf definition. @@ -411,6 +423,7 @@ pub mod participant_info { Kind::Egress => "EGRESS", Kind::Sip => "SIP", Kind::Agent => "AGENT", + Kind::Connector => "CONNECTOR", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -421,6 +434,7 @@ pub mod participant_info { "EGRESS" => Some(Self::Egress), "SIP" => Some(Self::Sip), "AGENT" => Some(Self::Agent), + "CONNECTOR" => Some(Self::Connector), _ => None, } } @@ -521,11 +535,9 @@ pub struct TrackInfo { pub muted: bool, /// original width of video (unset for audio) /// clients may receive a lower resolution version with simulcast - #[deprecated] #[prost(uint32, tag="5")] pub width: u32, /// original height of video (unset for audio) - #[deprecated] #[prost(uint32, tag="6")] pub height: u32, /// true if track is simulcasted @@ -601,6 +613,7 @@ pub mod video_layer { Unused = 0, OneSpatialLayerPerStream = 1, MultipleSpatialLayersPerStream = 2, + OneSpatialLayerPerStreamIncompleteRtcpSr = 3, } impl Mode { /// String value of the enum field names used in the ProtoBuf definition. @@ -612,6 +625,7 @@ pub mod video_layer { Mode::Unused => "MODE_UNUSED", Mode::OneSpatialLayerPerStream => "ONE_SPATIAL_LAYER_PER_STREAM", Mode::MultipleSpatialLayersPerStream => "MULTIPLE_SPATIAL_LAYERS_PER_STREAM", + Mode::OneSpatialLayerPerStreamIncompleteRtcpSr => "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -620,6 +634,7 @@ pub mod video_layer { "MODE_UNUSED" => Some(Self::Unused), "ONE_SPATIAL_LAYER_PER_STREAM" => Some(Self::OneSpatialLayerPerStream), "MULTIPLE_SPATIAL_LAYERS_PER_STREAM" => Some(Self::MultipleSpatialLayersPerStream), + "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR" => Some(Self::OneSpatialLayerPerStreamIncompleteRtcpSr), _ => None, } } @@ -715,7 +730,7 @@ pub struct EncryptedPacket { pub iv: ::prost::alloc::vec::Vec, #[prost(uint32, tag="3")] pub key_index: u32, - /// This is an encrypted EncryptedPacketPayload message representation + /// This is an encrypted EncryptedPacketPayload message representation #[prost(bytes="vec", tag="4")] pub encrypted_value: ::prost::alloc::vec::Vec, } @@ -1440,11 +1455,29 @@ pub mod data_stream { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct FilterParams { + #[prost(string, repeated, tag="1")] + pub include_events: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, repeated, tag="2")] + pub exclude_events: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct WebhookConfig { #[prost(string, tag="1")] pub url: ::prost::alloc::string::String, #[prost(string, tag="2")] pub signing_key: ::prost::alloc::string::String, + #[prost(message, optional, tag="3")] + pub filter_params: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribedAudioCodec { + #[prost(string, tag="1")] + pub codec: ::prost::alloc::string::String, + #[prost(bool, tag="2")] + pub enabled: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] @@ -1452,6 +1485,7 @@ pub enum AudioCodec { DefaultAc = 0, Opus = 1, Aac = 2, + AcMp3 = 3, } impl AudioCodec { /// String value of the enum field names used in the ProtoBuf definition. @@ -1463,6 +1497,7 @@ impl AudioCodec { AudioCodec::DefaultAc => "DEFAULT_AC", AudioCodec::Opus => "OPUS", AudioCodec::Aac => "AAC", + AudioCodec::AcMp3 => "AC_MP3", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1471,6 +1506,7 @@ impl AudioCodec { "DEFAULT_AC" => Some(Self::DefaultAc), "OPUS" => Some(Self::Opus), "AAC" => Some(Self::Aac), + "AC_MP3" => Some(Self::AcMp3), _ => None, } } @@ -2703,6 +2739,7 @@ pub enum EncodedFileType { DefaultFiletype = 0, Mp4 = 1, Ogg = 2, + Mp3 = 3, } impl EncodedFileType { /// String value of the enum field names used in the ProtoBuf definition. @@ -2714,6 +2751,7 @@ impl EncodedFileType { EncodedFileType::DefaultFiletype => "DEFAULT_FILETYPE", EncodedFileType::Mp4 => "MP4", EncodedFileType::Ogg => "OGG", + EncodedFileType::Mp3 => "MP3", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -2722,6 +2760,7 @@ impl EncodedFileType { "DEFAULT_FILETYPE" => Some(Self::DefaultFiletype), "MP4" => Some(Self::Mp4), "OGG" => Some(Self::Ogg), + "MP3" => Some(Self::Mp3), _ => None, } } @@ -3056,7 +3095,7 @@ pub mod signal_request { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalResponse { - #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25")] + #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3139,6 +3178,12 @@ pub mod signal_response { /// notify number of required media sections to satisfy subscribed tracks #[prost(message, tag="25")] MediaSectionsRequirement(super::MediaSectionsRequirement), + /// when audio subscription changes, used to enable simulcasting of audio codecs based on subscriptions + #[prost(message, tag="26")] + SubscribedAudioCodecUpdate(super::SubscribedAudioCodecUpdate), + /// sent when server answers publisher, includes the mid -> trackID mapping + #[prost(message, tag="27")] + MappedAnswer(super::MappedSessionDescription), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3302,6 +3347,14 @@ pub struct SessionDescription { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct MappedSessionDescription { + #[prost(message, optional, tag="1")] + pub session_description: ::core::option::Option, + #[prost(map="string, string", tag="2")] + pub mid_to_track_id: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ParticipantUpdate { #[prost(message, repeated, tag="1")] pub participants: ::prost::alloc::vec::Vec, @@ -3520,6 +3573,14 @@ pub struct SubscribedQualityUpdate { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribedAudioCodecUpdate { + #[prost(string, tag="1")] + pub track_sid: ::prost::alloc::string::String, + #[prost(message, repeated, tag="2")] + pub subscribed_audio_codecs: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TrackPermission { /// permission could be granted either by participant sid or identity #[prost(string, tag="1")] @@ -3697,6 +3758,8 @@ pub struct RequestResponse { pub reason: i32, #[prost(string, tag="3")] pub message: ::prost::alloc::string::String, + #[prost(oneof="request_response::Request", tags="4, 5, 6, 7, 8, 9")] + pub request: ::core::option::Option, } /// Nested message and enum types in `RequestResponse`. pub mod request_response { @@ -3707,6 +3770,9 @@ pub mod request_response { NotFound = 1, NotAllowed = 2, LimitExceeded = 3, + Queued = 4, + UnsupportedType = 5, + UnclassifiedError = 6, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -3719,6 +3785,9 @@ pub mod request_response { Reason::NotFound => "NOT_FOUND", Reason::NotAllowed => "NOT_ALLOWED", Reason::LimitExceeded => "LIMIT_EXCEEDED", + Reason::Queued => "QUEUED", + Reason::UnsupportedType => "UNSUPPORTED_TYPE", + Reason::UnclassifiedError => "UNCLASSIFIED_ERROR", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -3728,10 +3797,29 @@ pub mod request_response { "NOT_FOUND" => Some(Self::NotFound), "NOT_ALLOWED" => Some(Self::NotAllowed), "LIMIT_EXCEEDED" => Some(Self::LimitExceeded), + "QUEUED" => Some(Self::Queued), + "UNSUPPORTED_TYPE" => Some(Self::UnsupportedType), + "UNCLASSIFIED_ERROR" => Some(Self::UnclassifiedError), _ => None, } } } + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Request { + #[prost(message, tag="4")] + Trickle(super::TrickleRequest), + #[prost(message, tag="5")] + AddTrack(super::AddTrackRequest), + #[prost(message, tag="6")] + Mute(super::MuteTrackRequest), + #[prost(message, tag="7")] + UpdateMetadata(super::UpdateParticipantMetadata), + #[prost(message, tag="8")] + UpdateAudioTrack(super::UpdateLocalAudioTrack), + #[prost(message, tag="9")] + UpdateVideoTrack(super::UpdateLocalVideoTrack), + } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3928,6 +4016,8 @@ pub struct Job { pub agent_name: ::prost::alloc::string::String, #[prost(message, optional, tag="8")] pub state: ::core::option::Option, + #[prost(bool, tag="10")] + pub enable_recording: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -4571,6 +4661,26 @@ pub struct MoveParticipantResponse { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PerformRpcRequest { + #[prost(string, tag="1")] + pub room: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub destination_identity: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub method: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub payload: ::prost::alloc::string::String, + #[prost(uint32, tag="5")] + pub response_timeout_ms: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PerformRpcResponse { + #[prost(string, tag="1")] + pub payload: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct CreateIngressRequest { #[prost(enumeration="IngressInput", tag="1")] pub input_type: i32, @@ -5058,6 +5168,16 @@ pub struct CreateSipTrunkRequest { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProviderInfo { + #[prost(string, tag="1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub name: ::prost::alloc::string::String, + #[prost(enumeration="ProviderType", tag="3")] + pub r#type: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct SipTrunkInfo { #[prost(string, tag="1")] pub sip_trunk_id: ::prost::alloc::string::String, @@ -5715,10 +5835,18 @@ pub struct CreateSipParticipantRequest { #[prost(enumeration="SipMediaEncryption", tag="18")] pub media_encryption: i32, /// Wait for the answer for the call before returning. - /// - /// NEXT ID: 21 #[prost(bool, tag="19")] pub wait_until_answered: bool, + /// Optional display name for the 'From' SIP header. + /// + /// Cases: + /// 1) Unspecified: Use legacy behavior - display name will be set to be the caller's number. + /// 2) Empty string: Do not send a display name, which will result in a CNAM lookup downstream. + /// 3) Non-empty: Use the specified value as the display name. + /// + /// NEXT ID: 22 + #[prost(string, optional, tag="21")] + pub display_name: ::core::option::Option<::prost::alloc::string::String>, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -5806,6 +5934,12 @@ pub struct SipCallInfo { pub audio_codec: ::prost::alloc::string::String, #[prost(string, tag="21")] pub media_encryption: ::prost::alloc::string::String, + #[prost(string, tag="25")] + pub pcap_file_link: ::prost::alloc::string::String, + #[prost(message, repeated, tag="26")] + pub call_context: ::prost::alloc::vec::Vec<::pbjson_types::Any>, + #[prost(message, optional, tag="27")] + pub provider_info: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6106,6 +6240,37 @@ impl SipMediaEncryption { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum ProviderType { + Unknown = 0, + /// Internally implemented + Internal = 1, + /// Vendor provided + External = 2, +} +impl ProviderType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + ProviderType::Unknown => "PROVIDER_TYPE_UNKNOWN", + ProviderType::Internal => "PROVIDER_TYPE_INTERNAL", + ProviderType::External => "PROVIDER_TYPE_EXTERNAL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "PROVIDER_TYPE_UNKNOWN" => Some(Self::Unknown), + "PROVIDER_TYPE_INTERNAL" => Some(Self::Internal), + "PROVIDER_TYPE_EXTERNAL" => Some(Self::External), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum SipCallStatus { /// Incoming call is being handled by the SIP service. The SIP participant hasn't joined a LiveKit room yet ScsCallIncoming = 0, @@ -6228,5 +6393,37 @@ impl SipCallDirection { } } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TokenSourceRequest { + /// The name of the room being requested when generating credentials + #[prost(string, optional, tag="1")] + pub room_name: ::core::option::Option<::prost::alloc::string::String>, + /// The name of the participant being requested for this client when generating credentials + #[prost(string, optional, tag="2")] + pub participant_name: ::core::option::Option<::prost::alloc::string::String>, + /// The identity of the participant being requested for this client when generating credentials + #[prost(string, optional, tag="3")] + pub participant_identity: ::core::option::Option<::prost::alloc::string::String>, + /// Any participant metadata being included along with the credentials generation operation + #[prost(string, optional, tag="4")] + pub participant_metadata: ::core::option::Option<::prost::alloc::string::String>, + /// Any participant attributes being included along with the credentials generation operation + #[prost(map="string, string", tag="5")] + pub participant_attributes: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, + /// A RoomConfiguration object can be passed to request extra parameters should be included when + /// generating connection credentials - dispatching agents, defining egress settings, etc + /// More info: + #[prost(message, optional, tag="6")] + pub room_config: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TokenSourceResponse { + #[prost(string, tag="1")] + pub server_url: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub participant_token: ::prost::alloc::string::String, +} include!("livekit.serde.rs"); // @@protoc_insertion_point(module) diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index 3c79f6c7a..2decf9d14 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -959,6 +959,7 @@ impl serde::Serialize for AudioCodec { Self::DefaultAc => "DEFAULT_AC", Self::Opus => "OPUS", Self::Aac => "AAC", + Self::AcMp3 => "AC_MP3", }; serializer.serialize_str(variant) } @@ -973,6 +974,7 @@ impl<'de> serde::Deserialize<'de> for AudioCodec { "DEFAULT_AC", "OPUS", "AAC", + "AC_MP3", ]; struct GeneratedVisitor; @@ -1016,6 +1018,7 @@ impl<'de> serde::Deserialize<'de> for AudioCodec { "DEFAULT_AC" => Ok(AudioCodec::DefaultAc), "OPUS" => Ok(AudioCodec::Opus), "AAC" => Ok(AudioCodec::Aac), + "AC_MP3" => Ok(AudioCodec::AcMp3), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -4759,6 +4762,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if self.wait_until_answered { len += 1; } + if self.display_name.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.CreateSIPParticipantRequest", len)?; if !self.sip_trunk_id.is_empty() { struct_ser.serialize_field("sipTrunkId", &self.sip_trunk_id)?; @@ -4824,6 +4830,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if self.wait_until_answered { struct_ser.serialize_field("waitUntilAnswered", &self.wait_until_answered)?; } + if let Some(v) = self.display_name.as_ref() { + struct_ser.serialize_field("displayName", v)?; + } struct_ser.end() } } @@ -4871,6 +4880,8 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "mediaEncryption", "wait_until_answered", "waitUntilAnswered", + "display_name", + "displayName", ]; #[allow(clippy::enum_variant_names)] @@ -4895,6 +4906,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { KrispEnabled, MediaEncryption, WaitUntilAnswered, + DisplayName, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -4937,6 +4949,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "krispEnabled" | "krisp_enabled" => Ok(GeneratedField::KrispEnabled), "mediaEncryption" | "media_encryption" => Ok(GeneratedField::MediaEncryption), "waitUntilAnswered" | "wait_until_answered" => Ok(GeneratedField::WaitUntilAnswered), + "displayName" | "display_name" => Ok(GeneratedField::DisplayName), _ => Ok(GeneratedField::__SkipField__), } } @@ -4976,6 +4989,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { let mut krisp_enabled__ = None; let mut media_encryption__ = None; let mut wait_until_answered__ = None; + let mut display_name__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::SipTrunkId => { @@ -5102,6 +5116,12 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { } wait_until_answered__ = Some(map_.next_value()?); } + GeneratedField::DisplayName => { + if display_name__.is_some() { + return Err(serde::de::Error::duplicate_field("displayName")); + } + display_name__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -5128,6 +5148,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { krisp_enabled: krisp_enabled__.unwrap_or_default(), media_encryption: media_encryption__.unwrap_or_default(), wait_until_answered: wait_until_answered__.unwrap_or_default(), + display_name: display_name__, }) } } @@ -8911,6 +8932,7 @@ impl serde::Serialize for EncodedFileType { Self::DefaultFiletype => "DEFAULT_FILETYPE", Self::Mp4 => "MP4", Self::Ogg => "OGG", + Self::Mp3 => "MP3", }; serializer.serialize_str(variant) } @@ -8925,6 +8947,7 @@ impl<'de> serde::Deserialize<'de> for EncodedFileType { "DEFAULT_FILETYPE", "MP4", "OGG", + "MP3", ]; struct GeneratedVisitor; @@ -8968,6 +8991,7 @@ impl<'de> serde::Deserialize<'de> for EncodedFileType { "DEFAULT_FILETYPE" => Ok(EncodedFileType::DefaultFiletype), "MP4" => Ok(EncodedFileType::Mp4), "OGG" => Ok(EncodedFileType::Ogg), + "MP3" => Ok(EncodedFileType::Mp3), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -10334,6 +10358,120 @@ impl<'de> serde::Deserialize<'de> for FileInfo { deserializer.deserialize_struct("livekit.FileInfo", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for FilterParams { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.include_events.is_empty() { + len += 1; + } + if !self.exclude_events.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.FilterParams", len)?; + if !self.include_events.is_empty() { + struct_ser.serialize_field("includeEvents", &self.include_events)?; + } + if !self.exclude_events.is_empty() { + struct_ser.serialize_field("excludeEvents", &self.exclude_events)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for FilterParams { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "include_events", + "includeEvents", + "exclude_events", + "excludeEvents", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + IncludeEvents, + ExcludeEvents, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "includeEvents" | "include_events" => Ok(GeneratedField::IncludeEvents), + "excludeEvents" | "exclude_events" => Ok(GeneratedField::ExcludeEvents), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = FilterParams; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.FilterParams") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut include_events__ = None; + let mut exclude_events__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::IncludeEvents => { + if include_events__.is_some() { + return Err(serde::de::Error::duplicate_field("includeEvents")); + } + include_events__ = Some(map_.next_value()?); + } + GeneratedField::ExcludeEvents => { + if exclude_events__.is_some() { + return Err(serde::de::Error::duplicate_field("excludeEvents")); + } + exclude_events__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(FilterParams { + include_events: include_events__.unwrap_or_default(), + exclude_events: exclude_events__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.FilterParams", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for ForwardParticipantRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -13656,6 +13794,9 @@ impl serde::Serialize for Job { if self.state.is_some() { len += 1; } + if self.enable_recording { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.Job", len)?; if !self.id.is_empty() { struct_ser.serialize_field("id", &self.id)?; @@ -13686,6 +13827,9 @@ impl serde::Serialize for Job { if let Some(v) = self.state.as_ref() { struct_ser.serialize_field("state", v)?; } + if self.enable_recording { + struct_ser.serialize_field("enableRecording", &self.enable_recording)?; + } struct_ser.end() } } @@ -13707,6 +13851,8 @@ impl<'de> serde::Deserialize<'de> for Job { "agent_name", "agentName", "state", + "enable_recording", + "enableRecording", ]; #[allow(clippy::enum_variant_names)] @@ -13720,6 +13866,7 @@ impl<'de> serde::Deserialize<'de> for Job { Metadata, AgentName, State, + EnableRecording, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -13751,6 +13898,7 @@ impl<'de> serde::Deserialize<'de> for Job { "metadata" => Ok(GeneratedField::Metadata), "agentName" | "agent_name" => Ok(GeneratedField::AgentName), "state" => Ok(GeneratedField::State), + "enableRecording" | "enable_recording" => Ok(GeneratedField::EnableRecording), _ => Ok(GeneratedField::__SkipField__), } } @@ -13779,6 +13927,7 @@ impl<'de> serde::Deserialize<'de> for Job { let mut metadata__ = None; let mut agent_name__ = None; let mut state__ = None; + let mut enable_recording__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Id => { @@ -13835,6 +13984,12 @@ impl<'de> serde::Deserialize<'de> for Job { } state__ = map_.next_value()?; } + GeneratedField::EnableRecording => { + if enable_recording__.is_some() { + return Err(serde::de::Error::duplicate_field("enableRecording")); + } + enable_recording__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -13850,6 +14005,7 @@ impl<'de> serde::Deserialize<'de> for Job { metadata: metadata__.unwrap_or_default(), agent_name: agent_name__.unwrap_or_default(), state: state__, + enable_recording: enable_recording__.unwrap_or_default(), }) } } @@ -17209,7 +17365,7 @@ impl serde::Serialize for ListUpdate { if !self.add.is_empty() { len += 1; } - if !self.del.is_empty() { + if !self.remove.is_empty() { len += 1; } if self.clear { @@ -17222,8 +17378,8 @@ impl serde::Serialize for ListUpdate { if !self.add.is_empty() { struct_ser.serialize_field("add", &self.add)?; } - if !self.del.is_empty() { - struct_ser.serialize_field("del", &self.del)?; + if !self.remove.is_empty() { + struct_ser.serialize_field("remove", &self.remove)?; } if self.clear { struct_ser.serialize_field("clear", &self.clear)?; @@ -17240,7 +17396,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { const FIELDS: &[&str] = &[ "set", "add", - "del", + "remove", "clear", ]; @@ -17248,7 +17404,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { enum GeneratedField { Set, Add, - Del, + Remove, Clear, __SkipField__, } @@ -17274,7 +17430,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { match value { "set" => Ok(GeneratedField::Set), "add" => Ok(GeneratedField::Add), - "del" => Ok(GeneratedField::Del), + "remove" => Ok(GeneratedField::Remove), "clear" => Ok(GeneratedField::Clear), _ => Ok(GeneratedField::__SkipField__), } @@ -17297,7 +17453,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { { let mut set__ = None; let mut add__ = None; - let mut del__ = None; + let mut remove__ = None; let mut clear__ = None; while let Some(k) = map_.next_key()? { match k { @@ -17313,11 +17469,11 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { } add__ = Some(map_.next_value()?); } - GeneratedField::Del => { - if del__.is_some() { - return Err(serde::de::Error::duplicate_field("del")); + GeneratedField::Remove => { + if remove__.is_some() { + return Err(serde::de::Error::duplicate_field("remove")); } - del__ = Some(map_.next_value()?); + remove__ = Some(map_.next_value()?); } GeneratedField::Clear => { if clear__.is_some() { @@ -17333,7 +17489,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { Ok(ListUpdate { set: set__.unwrap_or_default(), add: add__.unwrap_or_default(), - del: del__.unwrap_or_default(), + remove: remove__.unwrap_or_default(), clear: clear__.unwrap_or_default(), }) } @@ -17341,6 +17497,122 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { deserializer.deserialize_struct("livekit.ListUpdate", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for MappedSessionDescription { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.session_description.is_some() { + len += 1; + } + if !self.mid_to_track_id.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.MappedSessionDescription", len)?; + if let Some(v) = self.session_description.as_ref() { + struct_ser.serialize_field("sessionDescription", v)?; + } + if !self.mid_to_track_id.is_empty() { + struct_ser.serialize_field("midToTrackId", &self.mid_to_track_id)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MappedSessionDescription { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "session_description", + "sessionDescription", + "mid_to_track_id", + "midToTrackId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + SessionDescription, + MidToTrackId, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "sessionDescription" | "session_description" => Ok(GeneratedField::SessionDescription), + "midToTrackId" | "mid_to_track_id" => Ok(GeneratedField::MidToTrackId), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MappedSessionDescription; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.MappedSessionDescription") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut session_description__ = None; + let mut mid_to_track_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::SessionDescription => { + if session_description__.is_some() { + return Err(serde::de::Error::duplicate_field("sessionDescription")); + } + session_description__ = map_.next_value()?; + } + GeneratedField::MidToTrackId => { + if mid_to_track_id__.is_some() { + return Err(serde::de::Error::duplicate_field("midToTrackId")); + } + mid_to_track_id__ = Some( + map_.next_value::>()? + ); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(MappedSessionDescription { + session_description: session_description__, + mid_to_track_id: mid_to_track_id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.MappedSessionDescription", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for MediaSectionsRequirement { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -17895,6 +18167,120 @@ impl<'de> serde::Deserialize<'de> for MetricsBatch { deserializer.deserialize_struct("livekit.MetricsBatch", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for MetricsRecordingHeader { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.room_id.is_empty() { + len += 1; + } + if self.enable_user_data_training.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.MetricsRecordingHeader", len)?; + if !self.room_id.is_empty() { + struct_ser.serialize_field("roomId", &self.room_id)?; + } + if let Some(v) = self.enable_user_data_training.as_ref() { + struct_ser.serialize_field("enableUserDataTraining", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "room_id", + "roomId", + "enable_user_data_training", + "enableUserDataTraining", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RoomId, + EnableUserDataTraining, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "roomId" | "room_id" => Ok(GeneratedField::RoomId), + "enableUserDataTraining" | "enable_user_data_training" => Ok(GeneratedField::EnableUserDataTraining), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MetricsRecordingHeader; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.MetricsRecordingHeader") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut room_id__ = None; + let mut enable_user_data_training__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RoomId => { + if room_id__.is_some() { + return Err(serde::de::Error::duplicate_field("roomId")); + } + room_id__ = Some(map_.next_value()?); + } + GeneratedField::EnableUserDataTraining => { + if enable_user_data_training__.is_some() { + return Err(serde::de::Error::duplicate_field("enableUserDataTraining")); + } + enable_user_data_training__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(MetricsRecordingHeader { + room_id: room_id__.unwrap_or_default(), + enable_user_data_training: enable_user_data_training__, + }) + } + } + deserializer.deserialize_struct("livekit.MetricsRecordingHeader", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for MigrateJobRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -19307,6 +19693,7 @@ impl serde::Serialize for participant_info::Kind { Self::Egress => "EGRESS", Self::Sip => "SIP", Self::Agent => "AGENT", + Self::Connector => "CONNECTOR", }; serializer.serialize_str(variant) } @@ -19323,6 +19710,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "EGRESS", "SIP", "AGENT", + "CONNECTOR", ]; struct GeneratedVisitor; @@ -19368,6 +19756,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "EGRESS" => Ok(participant_info::Kind::Egress), "SIP" => Ok(participant_info::Kind::Sip), "AGENT" => Ok(participant_info::Kind::Agent), + "CONNECTOR" => Ok(participant_info::Kind::Connector), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -19973,6 +20362,268 @@ impl<'de> serde::Deserialize<'de> for ParticipantUpdate { deserializer.deserialize_struct("livekit.ParticipantUpdate", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for PerformRpcRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.room.is_empty() { + len += 1; + } + if !self.destination_identity.is_empty() { + len += 1; + } + if !self.method.is_empty() { + len += 1; + } + if !self.payload.is_empty() { + len += 1; + } + if self.response_timeout_ms != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.PerformRpcRequest", len)?; + if !self.room.is_empty() { + struct_ser.serialize_field("room", &self.room)?; + } + if !self.destination_identity.is_empty() { + struct_ser.serialize_field("destinationIdentity", &self.destination_identity)?; + } + if !self.method.is_empty() { + struct_ser.serialize_field("method", &self.method)?; + } + if !self.payload.is_empty() { + struct_ser.serialize_field("payload", &self.payload)?; + } + if self.response_timeout_ms != 0 { + struct_ser.serialize_field("responseTimeoutMs", &self.response_timeout_ms)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for PerformRpcRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "room", + "destination_identity", + "destinationIdentity", + "method", + "payload", + "response_timeout_ms", + "responseTimeoutMs", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Room, + DestinationIdentity, + Method, + Payload, + ResponseTimeoutMs, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "room" => Ok(GeneratedField::Room), + "destinationIdentity" | "destination_identity" => Ok(GeneratedField::DestinationIdentity), + "method" => Ok(GeneratedField::Method), + "payload" => Ok(GeneratedField::Payload), + "responseTimeoutMs" | "response_timeout_ms" => Ok(GeneratedField::ResponseTimeoutMs), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = PerformRpcRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.PerformRpcRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut room__ = None; + let mut destination_identity__ = None; + let mut method__ = None; + let mut payload__ = None; + let mut response_timeout_ms__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Room => { + if room__.is_some() { + return Err(serde::de::Error::duplicate_field("room")); + } + room__ = Some(map_.next_value()?); + } + GeneratedField::DestinationIdentity => { + if destination_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("destinationIdentity")); + } + destination_identity__ = Some(map_.next_value()?); + } + GeneratedField::Method => { + if method__.is_some() { + return Err(serde::de::Error::duplicate_field("method")); + } + method__ = Some(map_.next_value()?); + } + GeneratedField::Payload => { + if payload__.is_some() { + return Err(serde::de::Error::duplicate_field("payload")); + } + payload__ = Some(map_.next_value()?); + } + GeneratedField::ResponseTimeoutMs => { + if response_timeout_ms__.is_some() { + return Err(serde::de::Error::duplicate_field("responseTimeoutMs")); + } + response_timeout_ms__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(PerformRpcRequest { + room: room__.unwrap_or_default(), + destination_identity: destination_identity__.unwrap_or_default(), + method: method__.unwrap_or_default(), + payload: payload__.unwrap_or_default(), + response_timeout_ms: response_timeout_ms__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.PerformRpcRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for PerformRpcResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.payload.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.PerformRpcResponse", len)?; + if !self.payload.is_empty() { + struct_ser.serialize_field("payload", &self.payload)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for PerformRpcResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "payload", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Payload, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "payload" => Ok(GeneratedField::Payload), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = PerformRpcResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.PerformRpcResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut payload__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Payload => { + if payload__.is_some() { + return Err(serde::de::Error::duplicate_field("payload")); + } + payload__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(PerformRpcResponse { + payload: payload__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.PerformRpcResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for Ping { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -20347,6 +20998,211 @@ impl<'de> serde::Deserialize<'de> for Pong { deserializer.deserialize_struct("livekit.Pong", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for ProviderInfo { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.id.is_empty() { + len += 1; + } + if !self.name.is_empty() { + len += 1; + } + if self.r#type != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.ProviderInfo", len)?; + if !self.id.is_empty() { + struct_ser.serialize_field("id", &self.id)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if self.r#type != 0 { + let v = ProviderType::try_from(self.r#type) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.r#type)))?; + struct_ser.serialize_field("type", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ProviderInfo { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + "name", + "type", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Id, + Name, + Type, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "id" => Ok(GeneratedField::Id), + "name" => Ok(GeneratedField::Name), + "type" => Ok(GeneratedField::Type), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ProviderInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut name__ = None; + let mut r#type__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = Some(map_.next_value()?); + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Type => { + if r#type__.is_some() { + return Err(serde::de::Error::duplicate_field("type")); + } + r#type__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ProviderInfo { + id: id__.unwrap_or_default(), + name: name__.unwrap_or_default(), + r#type: r#type__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ProviderInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ProviderType { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unknown => "PROVIDER_TYPE_UNKNOWN", + Self::Internal => "PROVIDER_TYPE_INTERNAL", + Self::External => "PROVIDER_TYPE_EXTERNAL", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for ProviderType { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "PROVIDER_TYPE_UNKNOWN", + "PROVIDER_TYPE_INTERNAL", + "PROVIDER_TYPE_EXTERNAL", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderType; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "PROVIDER_TYPE_UNKNOWN" => Ok(ProviderType::Unknown), + "PROVIDER_TYPE_INTERNAL" => Ok(ProviderType::Internal), + "PROVIDER_TYPE_EXTERNAL" => Ok(ProviderType::External), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} impl serde::Serialize for ProxyConfig { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -23217,6 +24073,9 @@ impl serde::Serialize for RequestResponse { if !self.message.is_empty() { len += 1; } + if self.request.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.RequestResponse", len)?; if self.request_id != 0 { struct_ser.serialize_field("requestId", &self.request_id)?; @@ -23229,6 +24088,28 @@ impl serde::Serialize for RequestResponse { if !self.message.is_empty() { struct_ser.serialize_field("message", &self.message)?; } + if let Some(v) = self.request.as_ref() { + match v { + request_response::Request::Trickle(v) => { + struct_ser.serialize_field("trickle", v)?; + } + request_response::Request::AddTrack(v) => { + struct_ser.serialize_field("addTrack", v)?; + } + request_response::Request::Mute(v) => { + struct_ser.serialize_field("mute", v)?; + } + request_response::Request::UpdateMetadata(v) => { + struct_ser.serialize_field("updateMetadata", v)?; + } + request_response::Request::UpdateAudioTrack(v) => { + struct_ser.serialize_field("updateAudioTrack", v)?; + } + request_response::Request::UpdateVideoTrack(v) => { + struct_ser.serialize_field("updateVideoTrack", v)?; + } + } + } struct_ser.end() } } @@ -23243,6 +24124,16 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "requestId", "reason", "message", + "trickle", + "add_track", + "addTrack", + "mute", + "update_metadata", + "updateMetadata", + "update_audio_track", + "updateAudioTrack", + "update_video_track", + "updateVideoTrack", ]; #[allow(clippy::enum_variant_names)] @@ -23250,6 +24141,12 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { RequestId, Reason, Message, + Trickle, + AddTrack, + Mute, + UpdateMetadata, + UpdateAudioTrack, + UpdateVideoTrack, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -23275,6 +24172,12 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "requestId" | "request_id" => Ok(GeneratedField::RequestId), "reason" => Ok(GeneratedField::Reason), "message" => Ok(GeneratedField::Message), + "trickle" => Ok(GeneratedField::Trickle), + "addTrack" | "add_track" => Ok(GeneratedField::AddTrack), + "mute" => Ok(GeneratedField::Mute), + "updateMetadata" | "update_metadata" => Ok(GeneratedField::UpdateMetadata), + "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), + "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), _ => Ok(GeneratedField::__SkipField__), } } @@ -23297,6 +24200,7 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { let mut request_id__ = None; let mut reason__ = None; let mut message__ = None; + let mut request__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::RequestId => { @@ -23319,6 +24223,48 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { } message__ = Some(map_.next_value()?); } + GeneratedField::Trickle => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("trickle")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::Trickle) +; + } + GeneratedField::AddTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("addTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::AddTrack) +; + } + GeneratedField::Mute => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("mute")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::Mute) +; + } + GeneratedField::UpdateMetadata => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("updateMetadata")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateMetadata) +; + } + GeneratedField::UpdateAudioTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("updateAudioTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateAudioTrack) +; + } + GeneratedField::UpdateVideoTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("updateVideoTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateVideoTrack) +; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -23328,6 +24274,7 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { request_id: request_id__.unwrap_or_default(), reason: reason__.unwrap_or_default(), message: message__.unwrap_or_default(), + request: request__, }) } } @@ -23345,6 +24292,9 @@ impl serde::Serialize for request_response::Reason { Self::NotFound => "NOT_FOUND", Self::NotAllowed => "NOT_ALLOWED", Self::LimitExceeded => "LIMIT_EXCEEDED", + Self::Queued => "QUEUED", + Self::UnsupportedType => "UNSUPPORTED_TYPE", + Self::UnclassifiedError => "UNCLASSIFIED_ERROR", }; serializer.serialize_str(variant) } @@ -23360,6 +24310,9 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "NOT_FOUND", "NOT_ALLOWED", "LIMIT_EXCEEDED", + "QUEUED", + "UNSUPPORTED_TYPE", + "UNCLASSIFIED_ERROR", ]; struct GeneratedVisitor; @@ -23404,6 +24357,9 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "NOT_FOUND" => Ok(request_response::Reason::NotFound), "NOT_ALLOWED" => Ok(request_response::Reason::NotAllowed), "LIMIT_EXCEEDED" => Ok(request_response::Reason::LimitExceeded), + "QUEUED" => Ok(request_response::Reason::Queued), + "UNSUPPORTED_TYPE" => Ok(request_response::Reason::UnsupportedType), + "UNCLASSIFIED_ERROR" => Ok(request_response::Reason::UnclassifiedError), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -26056,6 +27012,15 @@ impl serde::Serialize for SipCallInfo { if !self.media_encryption.is_empty() { len += 1; } + if !self.pcap_file_link.is_empty() { + len += 1; + } + if !self.call_context.is_empty() { + len += 1; + } + if self.provider_info.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SIPCallInfo", len)?; if !self.call_id.is_empty() { struct_ser.serialize_field("callId", &self.call_id)?; @@ -26151,6 +27116,15 @@ impl serde::Serialize for SipCallInfo { if !self.media_encryption.is_empty() { struct_ser.serialize_field("mediaEncryption", &self.media_encryption)?; } + if !self.pcap_file_link.is_empty() { + struct_ser.serialize_field("pcapFileLink", &self.pcap_file_link)?; + } + if !self.call_context.is_empty() { + struct_ser.serialize_field("callContext", &self.call_context)?; + } + if let Some(v) = self.provider_info.as_ref() { + struct_ser.serialize_field("providerInfo", v)?; + } struct_ser.end() } } @@ -26207,6 +27181,12 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "audioCodec", "media_encryption", "mediaEncryption", + "pcap_file_link", + "pcapFileLink", + "call_context", + "callContext", + "provider_info", + "providerInfo", ]; #[allow(clippy::enum_variant_names)] @@ -26235,6 +27215,9 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { CallStatusCode, AudioCodec, MediaEncryption, + PcapFileLink, + CallContext, + ProviderInfo, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -26281,6 +27264,9 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "callStatusCode" | "call_status_code" => Ok(GeneratedField::CallStatusCode), "audioCodec" | "audio_codec" => Ok(GeneratedField::AudioCodec), "mediaEncryption" | "media_encryption" => Ok(GeneratedField::MediaEncryption), + "pcapFileLink" | "pcap_file_link" => Ok(GeneratedField::PcapFileLink), + "callContext" | "call_context" => Ok(GeneratedField::CallContext), + "providerInfo" | "provider_info" => Ok(GeneratedField::ProviderInfo), _ => Ok(GeneratedField::__SkipField__), } } @@ -26324,6 +27310,9 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { let mut call_status_code__ = None; let mut audio_codec__ = None; let mut media_encryption__ = None; + let mut pcap_file_link__ = None; + let mut call_context__ = None; + let mut provider_info__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::CallId => { @@ -26484,6 +27473,24 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { } media_encryption__ = Some(map_.next_value()?); } + GeneratedField::PcapFileLink => { + if pcap_file_link__.is_some() { + return Err(serde::de::Error::duplicate_field("pcapFileLink")); + } + pcap_file_link__ = Some(map_.next_value()?); + } + GeneratedField::CallContext => { + if call_context__.is_some() { + return Err(serde::de::Error::duplicate_field("callContext")); + } + call_context__ = Some(map_.next_value()?); + } + GeneratedField::ProviderInfo => { + if provider_info__.is_some() { + return Err(serde::de::Error::duplicate_field("providerInfo")); + } + provider_info__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -26514,6 +27521,9 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { call_status_code: call_status_code__, audio_codec: audio_codec__.unwrap_or_default(), media_encryption: media_encryption__.unwrap_or_default(), + pcap_file_link: pcap_file_link__.unwrap_or_default(), + call_context: call_context__.unwrap_or_default(), + provider_info: provider_info__, }) } } @@ -32506,6 +33516,12 @@ impl serde::Serialize for SignalResponse { signal_response::Message::MediaSectionsRequirement(v) => { struct_ser.serialize_field("mediaSectionsRequirement", v)?; } + signal_response::Message::SubscribedAudioCodecUpdate(v) => { + struct_ser.serialize_field("subscribedAudioCodecUpdate", v)?; + } + signal_response::Message::MappedAnswer(v) => { + struct_ser.serialize_field("mappedAnswer", v)?; + } } } struct_ser.end() @@ -32557,6 +33573,10 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "roomMoved", "media_sections_requirement", "mediaSectionsRequirement", + "subscribed_audio_codec_update", + "subscribedAudioCodecUpdate", + "mapped_answer", + "mappedAnswer", ]; #[allow(clippy::enum_variant_names)] @@ -32585,6 +33605,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { TrackSubscribed, RoomMoved, MediaSectionsRequirement, + SubscribedAudioCodecUpdate, + MappedAnswer, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -32631,6 +33653,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "trackSubscribed" | "track_subscribed" => Ok(GeneratedField::TrackSubscribed), "roomMoved" | "room_moved" => Ok(GeneratedField::RoomMoved), "mediaSectionsRequirement" | "media_sections_requirement" => Ok(GeneratedField::MediaSectionsRequirement), + "subscribedAudioCodecUpdate" | "subscribed_audio_codec_update" => Ok(GeneratedField::SubscribedAudioCodecUpdate), + "mappedAnswer" | "mapped_answer" => Ok(GeneratedField::MappedAnswer), _ => Ok(GeneratedField::__SkipField__), } } @@ -32817,6 +33841,20 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { return Err(serde::de::Error::duplicate_field("mediaSectionsRequirement")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::MediaSectionsRequirement) +; + } + GeneratedField::SubscribedAudioCodecUpdate => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("subscribedAudioCodecUpdate")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::SubscribedAudioCodecUpdate) +; + } + GeneratedField::MappedAnswer => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("mappedAnswer")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::MappedAnswer) ; } GeneratedField::__SkipField__ => { @@ -34822,7 +35860,225 @@ impl<'de> serde::Deserialize<'de> for StreamStateUpdate { E: serde::de::Error, { match value { - "streamStates" | "stream_states" => Ok(GeneratedField::StreamStates), + "streamStates" | "stream_states" => Ok(GeneratedField::StreamStates), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = StreamStateUpdate; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.StreamStateUpdate") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut stream_states__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::StreamStates => { + if stream_states__.is_some() { + return Err(serde::de::Error::duplicate_field("streamStates")); + } + stream_states__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(StreamStateUpdate { + stream_states: stream_states__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.StreamStateUpdate", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for SubscribedAudioCodec { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.codec.is_empty() { + len += 1; + } + if self.enabled { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.SubscribedAudioCodec", len)?; + if !self.codec.is_empty() { + struct_ser.serialize_field("codec", &self.codec)?; + } + if self.enabled { + struct_ser.serialize_field("enabled", &self.enabled)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for SubscribedAudioCodec { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "codec", + "enabled", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Codec, + Enabled, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "codec" => Ok(GeneratedField::Codec), + "enabled" => Ok(GeneratedField::Enabled), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = SubscribedAudioCodec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.SubscribedAudioCodec") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut codec__ = None; + let mut enabled__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Codec => { + if codec__.is_some() { + return Err(serde::de::Error::duplicate_field("codec")); + } + codec__ = Some(map_.next_value()?); + } + GeneratedField::Enabled => { + if enabled__.is_some() { + return Err(serde::de::Error::duplicate_field("enabled")); + } + enabled__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(SubscribedAudioCodec { + codec: codec__.unwrap_or_default(), + enabled: enabled__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.SubscribedAudioCodec", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for SubscribedAudioCodecUpdate { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.track_sid.is_empty() { + len += 1; + } + if !self.subscribed_audio_codecs.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.SubscribedAudioCodecUpdate", len)?; + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; + } + if !self.subscribed_audio_codecs.is_empty() { + struct_ser.serialize_field("subscribedAudioCodecs", &self.subscribed_audio_codecs)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for SubscribedAudioCodecUpdate { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "track_sid", + "trackSid", + "subscribed_audio_codecs", + "subscribedAudioCodecs", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TrackSid, + SubscribedAudioCodecs, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + "subscribedAudioCodecs" | "subscribed_audio_codecs" => Ok(GeneratedField::SubscribedAudioCodecs), _ => Ok(GeneratedField::__SkipField__), } } @@ -34832,36 +36088,44 @@ impl<'de> serde::Deserialize<'de> for StreamStateUpdate { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = StreamStateUpdate; + type Value = SubscribedAudioCodecUpdate; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.StreamStateUpdate") + formatter.write_str("struct livekit.SubscribedAudioCodecUpdate") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { - let mut stream_states__ = None; + let mut track_sid__ = None; + let mut subscribed_audio_codecs__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::StreamStates => { - if stream_states__.is_some() { - return Err(serde::de::Error::duplicate_field("streamStates")); + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); } - stream_states__ = Some(map_.next_value()?); + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::SubscribedAudioCodecs => { + if subscribed_audio_codecs__.is_some() { + return Err(serde::de::Error::duplicate_field("subscribedAudioCodecs")); + } + subscribed_audio_codecs__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(StreamStateUpdate { - stream_states: stream_states__.unwrap_or_default(), + Ok(SubscribedAudioCodecUpdate { + track_sid: track_sid__.unwrap_or_default(), + subscribed_audio_codecs: subscribed_audio_codecs__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.StreamStateUpdate", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.SubscribedAudioCodecUpdate", FIELDS, GeneratedVisitor) } } impl serde::Serialize for SubscribedCodec { @@ -36244,6 +37508,308 @@ impl<'de> serde::Deserialize<'de> for TokenPagination { deserializer.deserialize_struct("livekit.TokenPagination", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for TokenSourceRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.room_name.is_some() { + len += 1; + } + if self.participant_name.is_some() { + len += 1; + } + if self.participant_identity.is_some() { + len += 1; + } + if self.participant_metadata.is_some() { + len += 1; + } + if !self.participant_attributes.is_empty() { + len += 1; + } + if self.room_config.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TokenSourceRequest", len)?; + if let Some(v) = self.room_name.as_ref() { + struct_ser.serialize_field("roomName", v)?; + } + if let Some(v) = self.participant_name.as_ref() { + struct_ser.serialize_field("participantName", v)?; + } + if let Some(v) = self.participant_identity.as_ref() { + struct_ser.serialize_field("participantIdentity", v)?; + } + if let Some(v) = self.participant_metadata.as_ref() { + struct_ser.serialize_field("participantMetadata", v)?; + } + if !self.participant_attributes.is_empty() { + struct_ser.serialize_field("participantAttributes", &self.participant_attributes)?; + } + if let Some(v) = self.room_config.as_ref() { + struct_ser.serialize_field("roomConfig", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TokenSourceRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "room_name", + "roomName", + "participant_name", + "participantName", + "participant_identity", + "participantIdentity", + "participant_metadata", + "participantMetadata", + "participant_attributes", + "participantAttributes", + "room_config", + "roomConfig", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RoomName, + ParticipantName, + ParticipantIdentity, + ParticipantMetadata, + ParticipantAttributes, + RoomConfig, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "roomName" | "room_name" => Ok(GeneratedField::RoomName), + "participantName" | "participant_name" => Ok(GeneratedField::ParticipantName), + "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), + "participantMetadata" | "participant_metadata" => Ok(GeneratedField::ParticipantMetadata), + "participantAttributes" | "participant_attributes" => Ok(GeneratedField::ParticipantAttributes), + "roomConfig" | "room_config" => Ok(GeneratedField::RoomConfig), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TokenSourceRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.TokenSourceRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut room_name__ = None; + let mut participant_name__ = None; + let mut participant_identity__ = None; + let mut participant_metadata__ = None; + let mut participant_attributes__ = None; + let mut room_config__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RoomName => { + if room_name__.is_some() { + return Err(serde::de::Error::duplicate_field("roomName")); + } + room_name__ = map_.next_value()?; + } + GeneratedField::ParticipantName => { + if participant_name__.is_some() { + return Err(serde::de::Error::duplicate_field("participantName")); + } + participant_name__ = map_.next_value()?; + } + GeneratedField::ParticipantIdentity => { + if participant_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("participantIdentity")); + } + participant_identity__ = map_.next_value()?; + } + GeneratedField::ParticipantMetadata => { + if participant_metadata__.is_some() { + return Err(serde::de::Error::duplicate_field("participantMetadata")); + } + participant_metadata__ = map_.next_value()?; + } + GeneratedField::ParticipantAttributes => { + if participant_attributes__.is_some() { + return Err(serde::de::Error::duplicate_field("participantAttributes")); + } + participant_attributes__ = Some( + map_.next_value::>()? + ); + } + GeneratedField::RoomConfig => { + if room_config__.is_some() { + return Err(serde::de::Error::duplicate_field("roomConfig")); + } + room_config__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(TokenSourceRequest { + room_name: room_name__, + participant_name: participant_name__, + participant_identity: participant_identity__, + participant_metadata: participant_metadata__, + participant_attributes: participant_attributes__.unwrap_or_default(), + room_config: room_config__, + }) + } + } + deserializer.deserialize_struct("livekit.TokenSourceRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TokenSourceResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.server_url.is_empty() { + len += 1; + } + if !self.participant_token.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TokenSourceResponse", len)?; + if !self.server_url.is_empty() { + struct_ser.serialize_field("serverUrl", &self.server_url)?; + } + if !self.participant_token.is_empty() { + struct_ser.serialize_field("participantToken", &self.participant_token)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TokenSourceResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "server_url", + "serverUrl", + "participant_token", + "participantToken", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + ServerUrl, + ParticipantToken, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "serverUrl" | "server_url" => Ok(GeneratedField::ServerUrl), + "participantToken" | "participant_token" => Ok(GeneratedField::ParticipantToken), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TokenSourceResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.TokenSourceResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut server_url__ = None; + let mut participant_token__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ServerUrl => { + if server_url__.is_some() { + return Err(serde::de::Error::duplicate_field("serverUrl")); + } + server_url__ = Some(map_.next_value()?); + } + GeneratedField::ParticipantToken => { + if participant_token__.is_some() { + return Err(serde::de::Error::duplicate_field("participantToken")); + } + participant_token__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(TokenSourceResponse { + server_url: server_url__.unwrap_or_default(), + participant_token: participant_token__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.TokenSourceResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for TrackCompositeEgressRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -41842,6 +43408,7 @@ impl serde::Serialize for video_layer::Mode { Self::Unused => "MODE_UNUSED", Self::OneSpatialLayerPerStream => "ONE_SPATIAL_LAYER_PER_STREAM", Self::MultipleSpatialLayersPerStream => "MULTIPLE_SPATIAL_LAYERS_PER_STREAM", + Self::OneSpatialLayerPerStreamIncompleteRtcpSr => "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR", }; serializer.serialize_str(variant) } @@ -41856,6 +43423,7 @@ impl<'de> serde::Deserialize<'de> for video_layer::Mode { "MODE_UNUSED", "ONE_SPATIAL_LAYER_PER_STREAM", "MULTIPLE_SPATIAL_LAYERS_PER_STREAM", + "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR", ]; struct GeneratedVisitor; @@ -41899,6 +43467,7 @@ impl<'de> serde::Deserialize<'de> for video_layer::Mode { "MODE_UNUSED" => Ok(video_layer::Mode::Unused), "ONE_SPATIAL_LAYER_PER_STREAM" => Ok(video_layer::Mode::OneSpatialLayerPerStream), "MULTIPLE_SPATIAL_LAYERS_PER_STREAM" => Ok(video_layer::Mode::MultipleSpatialLayersPerStream), + "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR" => Ok(video_layer::Mode::OneSpatialLayerPerStreamIncompleteRtcpSr), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -42319,6 +43888,9 @@ impl serde::Serialize for WebhookConfig { if !self.signing_key.is_empty() { len += 1; } + if self.filter_params.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.WebhookConfig", len)?; if !self.url.is_empty() { struct_ser.serialize_field("url", &self.url)?; @@ -42326,6 +43898,9 @@ impl serde::Serialize for WebhookConfig { if !self.signing_key.is_empty() { struct_ser.serialize_field("signingKey", &self.signing_key)?; } + if let Some(v) = self.filter_params.as_ref() { + struct_ser.serialize_field("filterParams", v)?; + } struct_ser.end() } } @@ -42339,12 +43914,15 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { "url", "signing_key", "signingKey", + "filter_params", + "filterParams", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Url, SigningKey, + FilterParams, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -42369,6 +43947,7 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { match value { "url" => Ok(GeneratedField::Url), "signingKey" | "signing_key" => Ok(GeneratedField::SigningKey), + "filterParams" | "filter_params" => Ok(GeneratedField::FilterParams), _ => Ok(GeneratedField::__SkipField__), } } @@ -42390,6 +43969,7 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { { let mut url__ = None; let mut signing_key__ = None; + let mut filter_params__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Url => { @@ -42404,6 +43984,12 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { } signing_key__ = Some(map_.next_value()?); } + GeneratedField::FilterParams => { + if filter_params__.is_some() { + return Err(serde::de::Error::duplicate_field("filterParams")); + } + filter_params__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -42412,6 +43998,7 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { Ok(WebhookConfig { url: url__.unwrap_or_default(), signing_key: signing_key__.unwrap_or_default(), + filter_params: filter_params__, }) } } From 593d08a0d0d0b2dba7ec03b6f1c06eeb71b07635 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 13:55:14 -0400 Subject: [PATCH 08/29] fix: add missing participant_info::Kind case as part of livekit-protocol upgrade --- livekit/src/proto.rs | 1 + livekit/src/room/participant/mod.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/livekit/src/proto.rs b/livekit/src/proto.rs index cd274f1ab..5a97f0629 100644 --- a/livekit/src/proto.rs +++ b/livekit/src/proto.rs @@ -154,6 +154,7 @@ impl From for participant::ParticipantKind { participant_info::Kind::Egress => participant::ParticipantKind::Egress, participant_info::Kind::Sip => participant::ParticipantKind::Sip, participant_info::Kind::Agent => participant::ParticipantKind::Agent, + participant_info::Kind::Connector => participant::ParticipantKind::Connector, } } } diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index d3105d5e8..751d7f2fc 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -44,6 +44,7 @@ pub enum ParticipantKind { Egress, Sip, Agent, + Connector, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] From f6f90d1e2c765ceb3c0e841598158c8eb74831c5 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 14:21:48 -0400 Subject: [PATCH 09/29] refactor: split up token source into many files --- livekit/src/room/token_source.rs | 490 ------------------ .../src/room/token_source/fetch_options.rs | 110 ++++ .../room/token_source/minter_credentials.rs | 77 +++ livekit/src/room/token_source/mod.rs | 303 +++++++++++ livekit/src/room/token_source/traits.rs | 70 +++ 5 files changed, 560 insertions(+), 490 deletions(-) delete mode 100644 livekit/src/room/token_source.rs create mode 100644 livekit/src/room/token_source/fetch_options.rs create mode 100644 livekit/src/room/token_source/minter_credentials.rs create mode 100644 livekit/src/room/token_source/mod.rs create mode 100644 livekit/src/room/token_source/traits.rs diff --git a/livekit/src/room/token_source.rs b/livekit/src/room/token_source.rs deleted file mode 100644 index 77d5a8f0a..000000000 --- a/livekit/src/room/token_source.rs +++ /dev/null @@ -1,490 +0,0 @@ -// Copyright 2025 LiveKit, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use futures_util::future; -use livekit_api::access_token::{self, AccessTokenError}; -use livekit_protocol::{RoomAgentDispatch, RoomConfiguration}; -use serde::{Serialize, Deserialize}; -use std::{collections::HashMap, error::Error, future::Future, pin::Pin}; - -#[derive(Clone, Default)] -pub struct TokenSourceFetchOptions { - pub room_name: Option, - pub participant_name: Option, - pub participant_identity: Option, - pub participant_metadata: Option, - pub participant_attributes: Option>, - - pub agent_name: Option, - pub agent_metadata: Option, -} - -impl TokenSourceFetchOptions { - pub fn with_room_name(mut self, room_name: &str) -> Self { - self.room_name = Some(room_name.into()); - self - } - pub fn with_participant_name(mut self, participant_name: &str) -> Self { - self.participant_name = Some(participant_name.into()); - self - } - pub fn with_participant_identity(mut self, participant_identity: &str) -> Self { - self.participant_identity = Some(participant_identity.into()); - self - } - pub fn with_participant_metadata(mut self, participant_metadata: &str) -> Self { - self.participant_metadata = Some(participant_metadata.into()); - self - } - - fn ensure_participant_attributes_defined(&mut self) -> &mut HashMap { - if self.participant_attributes.is_none() { - self.participant_attributes = Some(HashMap::new()); - }; - - let Some(participant_attribute_mut) = self.participant_attributes.as_mut() else { unreachable!(); }; - participant_attribute_mut - } - - pub fn with_participant_attribute(mut self, attribute_key: &str, attribute_value: &str) -> Self { - self.ensure_participant_attributes_defined().insert(attribute_key.into(), attribute_value.into()); - self - } - - pub fn with_participant_attributes(mut self, participant_attributes: HashMap) -> Self { - self.ensure_participant_attributes_defined().extend(participant_attributes); - self - } - - pub fn with_agent_name(mut self, agent_name: &str) -> Self { - self.agent_name = Some(agent_name.into()); - self - } - pub fn with_agent_metadata(mut self, agent_metadata: &str) -> Self { - self.agent_metadata = Some(agent_metadata.into()); - self - } -} - -impl Into for TokenSourceFetchOptions { - fn into(self) -> TokenSourceRequest { - let mut agent_dispatch = RoomAgentDispatch::default(); - if let Some(agent_name) = self.agent_name { - agent_dispatch.agent_name = agent_name; - } - if let Some(agent_metadata) = self.agent_metadata { - agent_dispatch.metadata = agent_metadata; - } - - let room_config = if agent_dispatch != RoomAgentDispatch::default() { - let mut room_config = RoomConfiguration::default(); - room_config.agents.push(agent_dispatch); - Some(room_config) - } else { - None - }; - - TokenSourceRequest { - room_name: self.room_name, - participant_name: self.participant_name, - participant_identity: self.participant_identity, - participant_metadata: self.participant_metadata, - participant_attributes: self.participant_attributes, - room_config, - } - } -} - -// FIXME: use the protobuf version of this struct instead! -#[derive(Debug, Clone, Serialize)] -pub struct TokenSourceRequest { - /// The name of the room being requested when generating credentials - pub room_name: Option, - - /// The name of the participant being requested for this client when generating credentials - pub participant_name: Option, - - /// The identity of the participant being requested for this client when generating credentials - pub participant_identity: Option, - - /// Any participant metadata being included along with the credentials generation operation - pub participant_metadata: Option, - - /// Any participant attributes being included along with the credentials generation operation - pub participant_attributes: Option>, - - /// A RoomConfiguration object can be passed to request extra parameters should be included when - /// generating connection credentials - dispatching agents, defining egress settings, etc - /// More info: https://docs.livekit.io/home/get-started/authentication/#room-configuration - pub room_config: Option, -} - -// FIXME: use the protobuf version of this struct instead! -#[derive(Debug, Clone, Deserialize)] -pub struct TokenSourceResponse { - pub server_url: String, - pub participant_token: String, -} - -impl TokenSourceResponse { - pub fn new(server_url: &str, participant_token: &str) -> Self { - Self { server_url: server_url.into(), participant_token: participant_token.into() } - } -} - -pub trait TokenSourceFixed { - // FIXME: what should the error type of the result be? - fn fetch(&self) -> impl Future>>; -} - -pub trait TokenSourceConfigurable { - // FIXME: what should the error type of the result be? - fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>>; -} - -pub trait TokenSourceConfigurableSynchronous { - // FIXME: what should the error type of the result be? - fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result>; -} - -impl TokenSourceConfigurable for T { - // FIXME: what should the error type of the result be? - fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>> { - future::ready(self.fetch_synchronous(options)) - } -} - - - - - -pub trait TokenSourceFixedSynchronous { - // FIXME: what should the error type of the result be? - fn fetch_synchronous(&self) -> Result>; -} - -impl TokenSourceFixed for T { - // FIXME: what should the error type of the result be? - fn fetch(&self) -> impl Future>> { - future::ready(self.fetch_synchronous()) - } -} - - - -pub trait TokenLiteralGenerator { - fn apply(&self) -> TokenSourceResponse; -} - -impl TokenLiteralGenerator for TokenSourceResponse { - fn apply(&self) -> TokenSourceResponse { - self.clone() - } -} - -impl TokenSourceResponse> TokenLiteralGenerator for F { - fn apply(&self) -> TokenSourceResponse { - self() - } -} - -pub struct TokenSourceLiteral { - generator: Generator, -} -impl TokenSourceLiteral { - pub fn new(generator: G) -> Self { - Self { generator } - } -} - -impl TokenSourceFixedSynchronous for TokenSourceLiteral { - fn fetch_synchronous(&self) -> Result> { - Ok(self.generator.apply()) - } -} - - - - -trait MinterCredentials { - fn get(&self) -> (String, String, String); -} - -struct MinterCredentialsLiteral(String, String, String); - -impl MinterCredentialsLiteral { - pub fn new(server_url: &str, api_key: &str, api_secret: &str) -> Self { - Self(server_url.into(), api_key.into(), api_secret.into()) - } -} - -impl MinterCredentials for MinterCredentialsLiteral { - fn get(&self) -> (String, String, String) { - (self.0.clone(), self.1.clone(), self.2.clone()) - } -} - -// FIXME: maybe add dotenv source too? Or have this look there too? -struct MinterCredentialsEnvironment(String, String, String); - -impl MinterCredentialsEnvironment { - pub fn new(url_variable: &str, api_key_variable: &str, api_secret_variable: &str) -> Self { - Self(url_variable.into(), api_key_variable.into(), api_secret_variable.into()) - } -} - -impl MinterCredentials for MinterCredentialsEnvironment { - fn get(&self) -> (String, String, String) { - let (url_variable, api_key_variable, api_secret_variable) = (&self.0, &self.1, &self.2); - let url = std::env::var(url_variable).expect(format!("{url_variable} is not set").as_str()); - let api_key = std::env::var(api_key_variable).expect(format!("{api_key_variable} is not set").as_str()); - let api_secret = std::env::var(api_secret_variable).expect(format!("{api_secret_variable} is not set").as_str()); - (url, api_key, api_secret) - } -} - -impl Default for MinterCredentialsEnvironment { - fn default() -> Self { - Self("LIVEKIT_URL".into(), "LIVEKIT_API_KEY".into(), "LIVEKIT_API_SECRET".into()) - } -} - -impl (String, String, String)> MinterCredentials for F { - fn get(&self) -> (String, String, String) { - self() - } -} - - -struct TokenSourceMinter { - credentials: Credentials, -} - -impl TokenSourceMinter { - pub fn new(credentials: C) -> Self { - Self { credentials } - } -} - -impl TokenSourceConfigurableSynchronous for TokenSourceMinter { - fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result> { - let (server_url, api_key, api_secret) = self.credentials.get(); - - // FIXME: apply options in the below code! - let participant_token = access_token::AccessToken::with_api_key(&api_key, &api_secret) - .with_identity("rust-bot") - .with_name("Rust Bot") - .with_grants(access_token::VideoGrants { - room_join: true, - room: "my-room".to_string(), - ..Default::default() - }) - .to_jwt()?; - - Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) - } -} - -impl Default for TokenSourceMinter { - fn default() -> Self { - Self::new(MinterCredentialsEnvironment::default()) - } -} - - - - - - -struct TokenSourceCustomMinter< - MintFn: Fn(access_token::AccessToken) -> Result, - Credentials: MinterCredentials, -> { - mint_fn: MintFn, - credentials: Credentials, -} - -impl< - MF: Fn(access_token::AccessToken) -> Result, - C: MinterCredentials -> TokenSourceCustomMinter { - pub fn new_with_credentials(mint_fn: MF, credentials: C) -> Self { - Self { mint_fn, credentials } - } - pub fn new(mint_fn: MF) -> TokenSourceCustomMinter { - TokenSourceCustomMinter::new_with_credentials(mint_fn, MinterCredentialsEnvironment::default()) - } -} - - -impl< - MF: Fn(access_token::AccessToken) -> Result, - C: MinterCredentials, -> TokenSourceFixedSynchronous for TokenSourceCustomMinter { - fn fetch_synchronous(&self) -> Result> { - let (server_url, api_key, api_secret) = self.credentials.get(); - - let participant_token = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret))?; - - Ok(TokenSourceResponse::new(server_url.as_str(), participant_token.as_str())) - } -} - - - - - -use reqwest::{header::HeaderMap, Method}; - -struct TokenSourceEndpoint { - url: String, - method: Method, - headers: HeaderMap, -} - -impl TokenSourceEndpoint { - pub fn new(url: &str) -> Self { - Self { - url: url.into(), - method: Method::POST, - headers: HeaderMap::new(), - } - } -} - -impl TokenSourceConfigurable for TokenSourceEndpoint { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { - let client = reqwest::Client::new(); - - // FIXME: What are the best practices around implementing Into on a reference to avoid the - // clone? - let request_body: TokenSourceRequest = options.clone().into(); - - let response = client - .request(self.method.clone(), &self.url) - .json(&request_body) - .headers(self.headers.clone()) - .send() - .await?; - - if !response.status().is_success() { - return Err(format!("Error generating token from endpoint {}: received {:?} / {}", self.url, response.status(), response.text().await?).into()); - } - - let response_json = response.json::().await?; - - Ok(TokenSourceResponse::new(&response_json.server_url, &response_json.participant_token)) - } -} - - - - -struct TokenSourceSandboxTokenServer(TokenSourceEndpoint); - -impl TokenSourceSandboxTokenServer { - pub fn new(sandbox_id: &str) -> Self { - Self::new_with_base_url(sandbox_id, "https://cloud-api.livekit.io") - } - pub fn new_with_base_url(sandbox_id: &str, base_url: &str) -> Self { - let mut endpoint = TokenSourceEndpoint::new( - &format!("{base_url}/api/v2/sandbox/connection-details") - ); - endpoint.headers.insert("X-Sandbox-ID", sandbox_id.parse().unwrap()); - Self(endpoint) - } -} - -impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { - self.0.fetch(options).await - } -} - - - - -struct TokenSourceCustom< - CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, ->(CustomFn); - -impl< - CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, -> TokenSourceCustom { - pub fn new(custom_fn: CustomFn) -> Self { - Self(custom_fn) - } -} - -impl< - CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, -> TokenSourceConfigurable for TokenSourceCustom { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { - (self.0)(options).await - } -} - - - - -async fn test() { - let fetch_options = TokenSourceFetchOptions::default().with_agent_name("voice ai quickstart"); - - let literal = TokenSourceLiteral::new(TokenSourceResponse::new("...", "...")); - let _ = literal.fetch().await; - - let minter = TokenSourceMinter::default(); - let _ = minter.fetch(&fetch_options).await; - - let minter_literal_credentials = TokenSourceMinter::new(MinterCredentialsLiteral::new("server url", "api key", "api secret")); - let _ = minter_literal_credentials.fetch(&fetch_options).await; - - let minter_literal_custom = TokenSourceMinter::new(|| ("server url".into(), "api key".into(), "api secret".into())); - let _ = minter_literal_custom.fetch(&fetch_options).await; - - let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { - access_token - .with_identity("rust-bot") - .with_name("Rust Bot") - .with_grants(access_token::VideoGrants { - room_join: true, - room: "my-room".to_string(), - ..Default::default() - }) - .to_jwt() - }); - let _ = custom_minter.fetch().await; - - let endpoint = TokenSourceEndpoint::new("https://example.com/my/example/auth/endpoint"); - let _ = endpoint.fetch(&fetch_options).await; - - let endpoint = TokenSourceSandboxTokenServer::new("SANDBOX ID HERE"); - let _ = endpoint.fetch(&fetch_options).await; - - // let foo = Box::pin(async |options: &TokenSourceFetchOptions| { - // Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) - // }); - - // // TODO: custom - // let custom = TokenSourceCustom::new(foo); - // let _ = custom.fetch(&fetch_options).await; - - // TODO: custom - let custom = TokenSourceCustom::new(|_options| { - Box::pin(future::ready( - Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) - )) - }); - let _ = custom.fetch(&fetch_options).await; -} diff --git a/livekit/src/room/token_source/fetch_options.rs b/livekit/src/room/token_source/fetch_options.rs new file mode 100644 index 000000000..531f9e2c3 --- /dev/null +++ b/livekit/src/room/token_source/fetch_options.rs @@ -0,0 +1,110 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use livekit_protocol::{RoomAgentDispatch, RoomConfiguration, TokenSourceRequest}; + +/// Options that can be used when fetching new credentials from a TokenSourceConfigurable +/// +/// Example: +/// ```rust +/// let _ = TokenSourceFetchOptions::default().with_agent_name("my agent name"); +/// ``` +#[derive(Clone, Default)] +pub struct TokenSourceFetchOptions { + pub room_name: Option, + pub participant_name: Option, + pub participant_identity: Option, + pub participant_metadata: Option, + pub participant_attributes: Option>, + + pub agent_name: Option, + pub agent_metadata: Option, +} + +impl TokenSourceFetchOptions { + pub fn with_room_name(mut self, room_name: &str) -> Self { + self.room_name = Some(room_name.into()); + self + } + pub fn with_participant_name(mut self, participant_name: &str) -> Self { + self.participant_name = Some(participant_name.into()); + self + } + pub fn with_participant_identity(mut self, participant_identity: &str) -> Self { + self.participant_identity = Some(participant_identity.into()); + self + } + pub fn with_participant_metadata(mut self, participant_metadata: &str) -> Self { + self.participant_metadata = Some(participant_metadata.into()); + self + } + + fn ensure_participant_attributes_defined(&mut self) -> &mut HashMap { + if self.participant_attributes.is_none() { + self.participant_attributes = Some(HashMap::new()); + }; + + let Some(participant_attribute_mut) = self.participant_attributes.as_mut() else { unreachable!(); }; + participant_attribute_mut + } + + pub fn with_participant_attribute(mut self, attribute_key: &str, attribute_value: &str) -> Self { + self.ensure_participant_attributes_defined().insert(attribute_key.into(), attribute_value.into()); + self + } + + pub fn with_participant_attributes(mut self, participant_attributes: HashMap) -> Self { + self.ensure_participant_attributes_defined().extend(participant_attributes); + self + } + + pub fn with_agent_name(mut self, agent_name: &str) -> Self { + self.agent_name = Some(agent_name.into()); + self + } + pub fn with_agent_metadata(mut self, agent_metadata: &str) -> Self { + self.agent_metadata = Some(agent_metadata.into()); + self + } +} + +impl Into for TokenSourceFetchOptions { + fn into(self) -> TokenSourceRequest { + let mut agent_dispatch = RoomAgentDispatch::default(); + if let Some(agent_name) = self.agent_name { + agent_dispatch.agent_name = agent_name; + } + if let Some(agent_metadata) = self.agent_metadata { + agent_dispatch.metadata = agent_metadata; + } + + let room_config = if agent_dispatch != RoomAgentDispatch::default() { + let mut room_config = RoomConfiguration::default(); + room_config.agents.push(agent_dispatch); + Some(room_config) + } else { + None + }; + + TokenSourceRequest { + room_name: self.room_name, + participant_name: self.participant_name, + participant_identity: self.participant_identity, + participant_metadata: self.participant_metadata, + participant_attributes: self.participant_attributes.unwrap_or_default(), + room_config, + } + } +} diff --git a/livekit/src/room/token_source/minter_credentials.rs b/livekit/src/room/token_source/minter_credentials.rs new file mode 100644 index 000000000..0f7230bd1 --- /dev/null +++ b/livekit/src/room/token_source/minter_credentials.rs @@ -0,0 +1,77 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const LIVEKIT_URL_ENV_NAME: &'static str = "LIVEKIT_URL"; +const LIVEKIT_API_KEY_ENV_NAME: &'static str = "LIVEKIT_API_KEY"; +const LIVEKIT_API_SECRET_ENV_NAME: &'static str = "LIVEKIT_API_SECRET"; + + +/// MinterCredentials provides a way to configure a TokenSourceMinter with a `LIVEKIT_URL`, +/// `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET` value. +pub trait MinterCredentialsSource { + fn get(&self) -> MinterCredentials; +} + +#[derive(Debug, Clone)] +pub struct MinterCredentials { + pub url: String, + pub api_key: String, + pub api_secret: String, +} +impl MinterCredentials { + pub fn new(server_url: &str, api_key: &str, api_secret: &str) -> Self { + Self { + url: server_url.into(), + api_key: api_key.into(), + api_secret: api_secret.into(), + } + } +} + +impl MinterCredentialsSource for MinterCredentials { + fn get(&self) -> MinterCredentials { + self.clone() + } +} + +// FIXME: maybe add dotenv source too? Or have this look there too? +pub struct MinterCredentialsEnvironment(String, String, String); + +impl MinterCredentialsEnvironment { + pub fn new(url_variable: &str, api_key_variable: &str, api_secret_variable: &str) -> Self { + Self(url_variable.into(), api_key_variable.into(), api_secret_variable.into()) + } +} + +impl MinterCredentialsSource for MinterCredentialsEnvironment { + fn get(&self) -> MinterCredentials { + let (url_variable, api_key_variable, api_secret_variable) = (&self.0, &self.1, &self.2); + let url = std::env::var(url_variable).expect(format!("{url_variable} is not set").as_str()); + let api_key = std::env::var(api_key_variable).expect(format!("{api_key_variable} is not set").as_str()); + let api_secret = std::env::var(api_secret_variable).expect(format!("{api_secret_variable} is not set").as_str()); + MinterCredentials { url, api_key, api_secret } + } +} + +impl Default for MinterCredentialsEnvironment { + fn default() -> Self { + Self(LIVEKIT_URL_ENV_NAME.into(), LIVEKIT_API_KEY_ENV_NAME.into(), LIVEKIT_API_SECRET_ENV_NAME.into()) + } +} + +impl MinterCredentials> MinterCredentialsSource for F { + fn get(&self) -> MinterCredentials { + self() + } +} diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs new file mode 100644 index 000000000..4569127b1 --- /dev/null +++ b/livekit/src/room/token_source/mod.rs @@ -0,0 +1,303 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{error::Error, future::Future, pin::Pin}; +use futures_util::future; +use livekit_api::access_token::{self, AccessTokenError}; +use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; + +mod fetch_options; +mod traits; +mod minter_credentials; + +pub use fetch_options::TokenSourceFetchOptions; +pub use traits::{ + TokenSourceFixed, + TokenSourceConfigurable, + TokenSourceFixedSynchronous, + TokenSourceConfigurableSynchronous, +}; +pub use minter_credentials::{ + MinterCredentialsSource, + MinterCredentials, + MinterCredentialsEnvironment, +}; + + + +pub trait TokenLiteralGenerator { + fn apply(&self) -> TokenSourceResponse; +} + +impl TokenLiteralGenerator for TokenSourceResponse { + fn apply(&self) -> TokenSourceResponse { + self.clone() + } +} + +impl TokenSourceResponse> TokenLiteralGenerator for F { + // FIXME: allow this to be an async fn! + fn apply(&self) -> TokenSourceResponse { + self() + } +} + +pub struct TokenSourceLiteral { + generator: Generator, +} +impl TokenSourceLiteral { + pub fn new(generator: G) -> Self { + Self { generator } + } +} + +impl TokenSourceFixedSynchronous for TokenSourceLiteral { + fn fetch_synchronous(&self) -> Result> { + Ok(self.generator.apply()) + } +} + + + + +struct TokenSourceMinter { + credentials_source: CredentialsSource, +} + +impl TokenSourceMinter { + pub fn new(credentials_source: CS) -> Self { + Self { credentials_source } + } +} + +impl TokenSourceConfigurableSynchronous for TokenSourceMinter { + fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result> { + let MinterCredentials { url: server_url, api_key, api_secret } = self.credentials_source.get(); + + // FIXME: apply options in the below code! + let participant_token = access_token::AccessToken::with_api_key(&api_key, &api_secret) + .with_identity("rust-bot") + .with_name("Rust Bot") + .with_grants(access_token::VideoGrants { + room_join: true, + room: "my-room".to_string(), + ..Default::default() + }) + .to_jwt()?; + + Ok(TokenSourceResponse { server_url, participant_token }) + } +} + +impl Default for TokenSourceMinter { + fn default() -> Self { + Self::new(MinterCredentialsEnvironment::default()) + } +} + + + + + + +struct TokenSourceCustomMinter< + MintFn: Fn(access_token::AccessToken) -> Result, + Credentials: MinterCredentialsSource, +> { + mint_fn: MintFn, + credentials_source: Credentials, +} + +impl< + MF: Fn(access_token::AccessToken) -> Result, + CS: MinterCredentialsSource +> TokenSourceCustomMinter { + pub fn new_with_credentials(mint_fn: MF, credentials_source: CS) -> Self { + Self { mint_fn, credentials_source } + } + pub fn new(mint_fn: MF) -> TokenSourceCustomMinter { + TokenSourceCustomMinter::new_with_credentials(mint_fn, MinterCredentialsEnvironment::default()) + } +} + + +impl< + MF: Fn(access_token::AccessToken) -> Result, + C: MinterCredentialsSource, +> TokenSourceFixedSynchronous for TokenSourceCustomMinter { + fn fetch_synchronous(&self) -> Result> { + let MinterCredentials { url: server_url, api_key, api_secret } = self.credentials_source.get(); + + let participant_token = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret))?; + + Ok(TokenSourceResponse { server_url, participant_token }) + } +} + + + + + +use reqwest::{header::HeaderMap, Method}; + +struct TokenSourceEndpoint { + url: String, + method: Method, + headers: HeaderMap, +} + +impl TokenSourceEndpoint { + pub fn new(url: &str) -> Self { + Self { + url: url.into(), + method: Method::POST, + headers: HeaderMap::new(), + } + } +} + +impl TokenSourceConfigurable for TokenSourceEndpoint { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + let client = reqwest::Client::new(); + + // FIXME: What are the best practices around implementing Into on a reference to avoid the + // clone? + let request_body: TokenSourceRequest = options.clone().into(); + + let response = client + .request(self.method.clone(), &self.url) + .json(&request_body) + .headers(self.headers.clone()) + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Error generating token from endpoint {}: received {:?} / {}", self.url, response.status(), response.text().await?).into()); + } + + let response_json = response.json::().await?; + + Ok(response_json) + } +} + + + + +struct TokenSourceSandboxTokenServer(TokenSourceEndpoint); + +impl TokenSourceSandboxTokenServer { + pub fn new(sandbox_id: &str) -> Self { + Self::new_with_base_url(sandbox_id, "https://cloud-api.livekit.io") + } + pub fn new_with_base_url(sandbox_id: &str, base_url: &str) -> Self { + let mut endpoint = TokenSourceEndpoint::new( + &format!("{base_url}/api/v2/sandbox/connection-details") + ); + endpoint.headers.insert("X-Sandbox-ID", sandbox_id.parse().unwrap()); + Self(endpoint) + } +} + +impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + self.0.fetch(options).await + } +} + + + + +struct TokenSourceCustom< + CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, +>(CustomFn); + +impl< + CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, +> TokenSourceCustom { + pub fn new(custom_fn: CustomFn) -> Self { + Self(custom_fn) + } +} + +impl< + CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, +> TokenSourceConfigurable for TokenSourceCustom { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + (self.0)(options).await + } +} + + + + +async fn test() { + let fetch_options = TokenSourceFetchOptions::default().with_agent_name("voice ai quickstart"); + + let literal = TokenSourceLiteral::new(TokenSourceResponse { + server_url: "...".into(), + participant_token: "...".into(), + }); + let _ = literal.fetch().await; + + let minter = TokenSourceMinter::default(); + let _ = minter.fetch(&fetch_options).await; + + let minter_literal_credentials = TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret")); + let _ = minter_literal_credentials.fetch(&fetch_options).await; + + let minter_env = TokenSourceMinter::new(MinterCredentialsEnvironment::new("SERVER_URL", "API_KEY", "API_SECRET")); + let _ = minter_env.fetch(&fetch_options).await; + + let minter_literal_custom = TokenSourceMinter::new(|| MinterCredentials::new("server url", "api key", "api secret")); + let _ = minter_literal_custom.fetch(&fetch_options).await; + + let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { + access_token + .with_identity("rust-bot") + .with_name("Rust Bot") + .with_grants(access_token::VideoGrants { + room_join: true, + room: "my-room".to_string(), + ..Default::default() + }) + .to_jwt() + }); + let _ = custom_minter.fetch().await; + + let endpoint = TokenSourceEndpoint::new("https://example.com/my/example/auth/endpoint"); + let _ = endpoint.fetch(&fetch_options).await; + + let endpoint = TokenSourceSandboxTokenServer::new("SANDBOX ID HERE"); + let _ = endpoint.fetch(&fetch_options).await; + + // let foo = Box::pin(async |options: &TokenSourceFetchOptions| { + // Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) + // }); + + // // TODO: custom + // let custom = TokenSourceCustom::new(foo); + // let _ = custom.fetch(&fetch_options).await; + + // TODO: custom + let custom = TokenSourceCustom::new(|_options| { + Box::pin(future::ready( + Ok(TokenSourceResponse { + server_url: "...".into(), + participant_token: "... _options should be encoded in here ...".into(), + }) + )) + }); + let _ = custom.fetch(&fetch_options).await; +} diff --git a/livekit/src/room/token_source/traits.rs b/livekit/src/room/token_source/traits.rs new file mode 100644 index 000000000..029ae4dfe --- /dev/null +++ b/livekit/src/room/token_source/traits.rs @@ -0,0 +1,70 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{error::Error, future::Future}; +use futures_util::future; +use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; + +use crate::token_source::TokenSourceFetchOptions; + + +/// A Fixed TokenSource is a token source that takes no parameters and returns a completely +/// independently derived value on each fetch() call. +/// +/// The most common downstream implementer is TokenSourceLiteral. +pub trait TokenSourceFixed { + // FIXME: what should the error type of the result be? + fn fetch(&self) -> impl Future>>; +} + +/// A helper trait to more easily implement a TokenSourceFixed which not async. +pub trait TokenSourceFixedSynchronous { + // FIXME: what should the error type of the result be? + fn fetch_synchronous(&self) -> Result>; +} + +impl TokenSourceFixed for T { + // FIXME: what should the error type of the result be? + fn fetch(&self) -> impl Future>> { + future::ready(self.fetch_synchronous()) + } +} + + +/// A Configurable TokenSource is a token source that takes a +/// TokenSourceFetchOptions object as input and returns a deterministic +/// TokenSourceResponseObject output based on the options specified. +/// +/// For example, if options.participantName is set, it should be expected that +/// all tokens that are generated will have participant name field set to the +/// provided value. +/// +/// A few common downstream implementers are TokenSourceEndpoint and TokenSourceCustom. +pub trait TokenSourceConfigurable { + // FIXME: what should the error type of the result be? + fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>>; +} + +/// A helper trait to more easily implement a TokenSourceConfigurable which not async. +pub trait TokenSourceConfigurableSynchronous { + // FIXME: what should the error type of the result be? + fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result>; +} + +impl TokenSourceConfigurable for T { + // FIXME: what should the error type of the result be? + fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>> { + future::ready(self.fetch_synchronous(options)) + } +} From 94a8972eea37c27a8e7a848a8b45250125b0dc9a Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 16:19:07 -0400 Subject: [PATCH 10/29] feat: add kitchen sink type token source example Probably this won't be what we want longer term but I wanted to start somewhere so folks can try it out. --- Cargo.lock | 11 ++++ Cargo.toml | 1 + examples/token_source/Cargo.toml | 11 ++++ examples/token_source/src/main.rs | 87 +++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 examples/token_source/Cargo.toml create mode 100644 examples/token_source/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index d21956314..c1d20b902 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5462,6 +5462,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "token_source" +version = "0.1.0" +dependencies = [ + "env_logger 0.10.2", + "livekit", + "livekit-api", + "log", + "tokio", +] + [[package]] name = "tokio" version = "1.48.0" diff --git a/Cargo.toml b/Cargo.toml index c1808ff0a..f270243b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "examples/send_bytes", "examples/webhooks", "examples/wgpu_room", + "examples/token_source", ] [workspace.dependencies] diff --git a/examples/token_source/Cargo.toml b/examples/token_source/Cargo.toml new file mode 100644 index 000000000..46f80d768 --- /dev/null +++ b/examples/token_source/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "token_source" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } +env_logger = "0.10" +livekit = { path = "../../livekit", features = ["rustls-tls-native-roots"]} +livekit-api = { path = "../../livekit-api", features = ["rustls-tls-native-roots"]} +log = "0.4" diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs new file mode 100644 index 000000000..ab2bee5b0 --- /dev/null +++ b/examples/token_source/src/main.rs @@ -0,0 +1,87 @@ +use std::future; + +use livekit::prelude::*; +use livekit_api::access_token; +use livekit::token_source::{ + MinterCredentials, + MinterCredentialsEnvironment, + TokenSourceCustom, + TokenSourceCustomMinter, + TokenSourceEndpoint, + TokenSourceFetchOptions, + TokenSourceLiteral, + TokenSourceMinter, + TokenSourceSandboxTokenServer, + TokenSourceResponse, +}; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let fetch_options = TokenSourceFetchOptions::default().with_agent_name("voice ai quickstart"); + + let literal = TokenSourceLiteral::new(TokenSourceResponse { + server_url: "...".into(), + participant_token: "...".into(), + }); + let literal_response = literal.fetch().await; + log::info!("Example TokenSourceLiteral response: {:?}", literal_response); + + let minter = TokenSourceMinter::default(); + let minter_response = minter.fetch(&fetch_options).await; + log::info!("Example TokenSourceMinter::default() result: {:?}", minter_response); + + let minter_literal_credentials = TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret")); + let minter_literal_credentials_response = minter_literal_credentials.fetch(&fetch_options).await; + log::info!("Example TokenSourceMinter / literal MinterCredentials result: {:?}", minter_literal_credentials_response); + + let minter_env = TokenSourceMinter::new(MinterCredentialsEnvironment::new("SERVER_URL", "API_KEY", "API_SECRET")); + let minter_env_response = minter_env.fetch(&fetch_options).await; + log::info!("Example TokenSourceMinter / MinterCredentialsEnvironment result: {:?}", minter_env_response); + + let minter_literal_custom = TokenSourceMinter::new(|| MinterCredentials::new("server url", "api key", "api secret")); + let minter_literal_custom_response = minter_literal_custom.fetch(&fetch_options).await; + log::info!("Example TokenSourceMinter / custom credentials result: {:?}", minter_literal_custom_response); + + let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { + access_token + .with_identity("rust-bot") + .with_name("Rust Bot") + .with_grants(access_token::VideoGrants { + room_join: true, + room: "my-room".to_string(), + ..Default::default() + }) + .to_jwt() + }); + let custom_minter_response = custom_minter.fetch().await; + log::info!("Example TokenSourceCustomMinter response: {:?}", custom_minter_response); + + let endpoint = TokenSourceEndpoint::new("https://example.com/my/example/auth/endpoint"); + let endpoint_response = endpoint.fetch(&fetch_options).await; + log::info!("Example TokenSourceMinter response: {:?}", endpoint_response); + + let sandbox_token_server = TokenSourceSandboxTokenServer::new("SANDBOX ID HERE"); + let sandbox_token_server_response = sandbox_token_server.fetch(&fetch_options).await; + log::info!("Example TokenSourceSandboxTokenServer: {:?}", sandbox_token_server_response); + + // let foo = Box::pin(async |options: &TokenSourceFetchOptions| { + // Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) + // }); + + // // TODO: custom + // let custom = TokenSourceCustom::new(foo); + // let _ = custom.fetch(&fetch_options).await; + + let custom = TokenSourceCustom::new(|_options| { + Box::pin(future::ready( + Ok(TokenSourceResponse { + server_url: "...".into(), + participant_token: "... _options should be encoded in here ...".into(), + }) + )) + }); + let custom_response = custom.fetch(&fetch_options).await; + log::info!("Example TokenSourceCustomResponse: {:?}", endpoint_response); +} From cbe2c1c116eaea42d28253ece8a0848f9d6669e2 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 16:20:19 -0400 Subject: [PATCH 11/29] feat: add TokenSourceFixed / TokenSourceConfigurable to prelude --- livekit/src/prelude.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 505cf7e2a..3316413d5 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -27,4 +27,5 @@ pub use crate::{ }, ConnectionState, DataPacket, DataPacketKind, Room, RoomError, RoomEvent, RoomOptions, RoomResult, RoomSdkOptions, SipDTMF, Transcription, TranscriptionSegment, + token_source::{TokenSourceFixed, TokenSourceConfigurable}, }; From 8793b8139e58d423459622fcf98a60eb2e846580 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 16:20:50 -0400 Subject: [PATCH 12/29] feat: add reexport of tokensource livekit protocol structs --- livekit/src/room/token_source/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index 4569127b1..79720e4f3 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{error::Error, future::Future, pin::Pin}; -use futures_util::future; +use std::{error::Error, future::{ready, Future}, pin::Pin}; use livekit_api::access_token::{self, AccessTokenError}; -use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; + +// FIXME: is reexporting these from here a good idea? +pub use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; mod fetch_options; mod traits; From 42cf5abe3829c46af5d5b059bdae031c028b1056 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 16:21:25 -0400 Subject: [PATCH 13/29] feat: make a bunch of structs public --- livekit/src/room/token_source/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index 79720e4f3..d971beffe 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -72,7 +72,7 @@ impl TokenSourceFixedSynchronous for TokenSourceLitera -struct TokenSourceMinter { +pub struct TokenSourceMinter { credentials_source: CredentialsSource, } @@ -112,7 +112,7 @@ impl Default for TokenSourceMinter { -struct TokenSourceCustomMinter< +pub struct TokenSourceCustomMinter< MintFn: Fn(access_token::AccessToken) -> Result, Credentials: MinterCredentialsSource, > { @@ -152,7 +152,7 @@ impl< use reqwest::{header::HeaderMap, Method}; -struct TokenSourceEndpoint { +pub struct TokenSourceEndpoint { url: String, method: Method, headers: HeaderMap, @@ -196,7 +196,7 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { -struct TokenSourceSandboxTokenServer(TokenSourceEndpoint); +pub struct TokenSourceSandboxTokenServer(TokenSourceEndpoint); impl TokenSourceSandboxTokenServer { pub fn new(sandbox_id: &str) -> Self { @@ -220,7 +220,7 @@ impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { -struct TokenSourceCustom< +pub struct TokenSourceCustom< CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, >(CustomFn); From aa8f3e2836c3923289664b84aaa3d153e39071f5 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 16:21:44 -0400 Subject: [PATCH 14/29] fix: ensure the proper future::ready implementation is being used --- livekit/src/room/token_source/mod.rs | 2 +- livekit/src/room/token_source/traits.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index d971beffe..51f423819 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -293,7 +293,7 @@ async fn test() { // TODO: custom let custom = TokenSourceCustom::new(|_options| { - Box::pin(future::ready( + Box::pin(ready( Ok(TokenSourceResponse { server_url: "...".into(), participant_token: "... _options should be encoded in here ...".into(), diff --git a/livekit/src/room/token_source/traits.rs b/livekit/src/room/token_source/traits.rs index 029ae4dfe..06208fa65 100644 --- a/livekit/src/room/token_source/traits.rs +++ b/livekit/src/room/token_source/traits.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{error::Error, future::Future}; -use futures_util::future; +use std::{error::Error, future::{ready, Future}}; use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; use crate::token_source::TokenSourceFetchOptions; @@ -37,7 +36,7 @@ pub trait TokenSourceFixedSynchronous { impl TokenSourceFixed for T { // FIXME: what should the error type of the result be? fn fetch(&self) -> impl Future>> { - future::ready(self.fetch_synchronous()) + ready(self.fetch_synchronous()) } } @@ -65,6 +64,6 @@ pub trait TokenSourceConfigurableSynchronous { impl TokenSourceConfigurable for T { // FIXME: what should the error type of the result be? fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>> { - future::ready(self.fetch_synchronous(options)) + ready(self.fetch_synchronous(options)) } } From 1484c236a33093dd814828af299aae88709faa59 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 16:51:45 -0400 Subject: [PATCH 15/29] refactor: run cargo fmt --- examples/token_source/src/main.rs | 78 ++++---- livekit/src/prelude.rs | 2 +- livekit/src/room/mod.rs | 2 +- .../src/room/token_source/fetch_options.rs | 20 +- .../room/token_source/minter_credentials.rs | 19 +- livekit/src/room/token_source/mod.rs | 174 +++++++++--------- livekit/src/room/token_source/traits.rs | 26 ++- 7 files changed, 180 insertions(+), 141 deletions(-) diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index ab2bee5b0..7a95fe9fc 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -1,19 +1,12 @@ use std::future; use livekit::prelude::*; -use livekit_api::access_token; use livekit::token_source::{ - MinterCredentials, - MinterCredentialsEnvironment, - TokenSourceCustom, - TokenSourceCustomMinter, - TokenSourceEndpoint, - TokenSourceFetchOptions, - TokenSourceLiteral, - TokenSourceMinter, - TokenSourceSandboxTokenServer, - TokenSourceResponse, + MinterCredentials, MinterCredentialsEnvironment, TokenSourceCustom, TokenSourceCustomMinter, + TokenSourceEndpoint, TokenSourceFetchOptions, TokenSourceLiteral, TokenSourceMinter, + TokenSourceResponse, TokenSourceSandboxTokenServer, }; +use livekit_api::access_token; #[tokio::main] async fn main() { @@ -32,29 +25,46 @@ async fn main() { let minter_response = minter.fetch(&fetch_options).await; log::info!("Example TokenSourceMinter::default() result: {:?}", minter_response); - let minter_literal_credentials = TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret")); - let minter_literal_credentials_response = minter_literal_credentials.fetch(&fetch_options).await; - log::info!("Example TokenSourceMinter / literal MinterCredentials result: {:?}", minter_literal_credentials_response); + let minter_literal_credentials = + TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret")); + let minter_literal_credentials_response = + minter_literal_credentials.fetch(&fetch_options).await; + log::info!( + "Example TokenSourceMinter / literal MinterCredentials result: {:?}", + minter_literal_credentials_response + ); - let minter_env = TokenSourceMinter::new(MinterCredentialsEnvironment::new("SERVER_URL", "API_KEY", "API_SECRET")); + let minter_env = TokenSourceMinter::new(MinterCredentialsEnvironment::new( + "SERVER_URL", + "API_KEY", + "API_SECRET", + )); let minter_env_response = minter_env.fetch(&fetch_options).await; - log::info!("Example TokenSourceMinter / MinterCredentialsEnvironment result: {:?}", minter_env_response); + log::info!( + "Example TokenSourceMinter / MinterCredentialsEnvironment result: {:?}", + minter_env_response + ); - let minter_literal_custom = TokenSourceMinter::new(|| MinterCredentials::new("server url", "api key", "api secret")); + let minter_literal_custom = + TokenSourceMinter::new(|| MinterCredentials::new("server url", "api key", "api secret")); let minter_literal_custom_response = minter_literal_custom.fetch(&fetch_options).await; - log::info!("Example TokenSourceMinter / custom credentials result: {:?}", minter_literal_custom_response); + log::info!( + "Example TokenSourceMinter / custom credentials result: {:?}", + minter_literal_custom_response + ); - let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { - access_token - .with_identity("rust-bot") - .with_name("Rust Bot") - .with_grants(access_token::VideoGrants { - room_join: true, - room: "my-room".to_string(), - ..Default::default() - }) - .to_jwt() - }); + let custom_minter = + TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { + access_token + .with_identity("rust-bot") + .with_name("Rust Bot") + .with_grants(access_token::VideoGrants { + room_join: true, + room: "my-room".to_string(), + ..Default::default() + }) + .to_jwt() + }); let custom_minter_response = custom_minter.fetch().await; log::info!("Example TokenSourceCustomMinter response: {:?}", custom_minter_response); @@ -75,12 +85,10 @@ async fn main() { // let _ = custom.fetch(&fetch_options).await; let custom = TokenSourceCustom::new(|_options| { - Box::pin(future::ready( - Ok(TokenSourceResponse { - server_url: "...".into(), - participant_token: "... _options should be encoded in here ...".into(), - }) - )) + Box::pin(future::ready(Ok(TokenSourceResponse { + server_url: "...".into(), + participant_token: "... _options should be encoded in here ...".into(), + }))) }); let custom_response = custom.fetch(&fetch_options).await; log::info!("Example TokenSourceCustomResponse: {:?}", endpoint_response); diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 3316413d5..ed375b44e 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -21,11 +21,11 @@ pub use crate::{ RemoteParticipant, RpcError, RpcErrorCode, RpcInvocationData, }, publication::{LocalTrackPublication, RemoteTrackPublication, TrackPublication}, + token_source::{TokenSourceConfigurable, TokenSourceFixed}, track::{ AudioTrack, LocalAudioTrack, LocalTrack, LocalVideoTrack, RemoteAudioTrack, RemoteTrack, RemoteVideoTrack, StreamState, Track, TrackDimension, TrackKind, TrackSource, VideoTrack, }, ConnectionState, DataPacket, DataPacketKind, Room, RoomError, RoomEvent, RoomOptions, RoomResult, RoomSdkOptions, SipDTMF, Transcription, TranscriptionSegment, - token_source::{TokenSourceFixed, TokenSourceConfigurable}, }; diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index ca0a02990..48a969d53 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -60,9 +60,9 @@ pub mod id; pub mod options; pub mod participant; pub mod publication; +pub mod token_source; pub mod track; pub(crate) mod utils; -pub mod token_source; pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/livekit/src/room/token_source/fetch_options.rs b/livekit/src/room/token_source/fetch_options.rs index 531f9e2c3..7ad8bafcb 100644 --- a/livekit/src/room/token_source/fetch_options.rs +++ b/livekit/src/room/token_source/fetch_options.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::HashMap; use livekit_protocol::{RoomAgentDispatch, RoomConfiguration, TokenSourceRequest}; +use std::collections::HashMap; /// Options that can be used when fetching new credentials from a TokenSourceConfigurable /// @@ -56,16 +56,26 @@ impl TokenSourceFetchOptions { self.participant_attributes = Some(HashMap::new()); }; - let Some(participant_attribute_mut) = self.participant_attributes.as_mut() else { unreachable!(); }; + let Some(participant_attribute_mut) = self.participant_attributes.as_mut() else { + unreachable!(); + }; participant_attribute_mut } - pub fn with_participant_attribute(mut self, attribute_key: &str, attribute_value: &str) -> Self { - self.ensure_participant_attributes_defined().insert(attribute_key.into(), attribute_value.into()); + pub fn with_participant_attribute( + mut self, + attribute_key: &str, + attribute_value: &str, + ) -> Self { + self.ensure_participant_attributes_defined() + .insert(attribute_key.into(), attribute_value.into()); self } - pub fn with_participant_attributes(mut self, participant_attributes: HashMap) -> Self { + pub fn with_participant_attributes( + mut self, + participant_attributes: HashMap, + ) -> Self { self.ensure_participant_attributes_defined().extend(participant_attributes); self } diff --git a/livekit/src/room/token_source/minter_credentials.rs b/livekit/src/room/token_source/minter_credentials.rs index 0f7230bd1..b75acf6e6 100644 --- a/livekit/src/room/token_source/minter_credentials.rs +++ b/livekit/src/room/token_source/minter_credentials.rs @@ -16,7 +16,6 @@ const LIVEKIT_URL_ENV_NAME: &'static str = "LIVEKIT_URL"; const LIVEKIT_API_KEY_ENV_NAME: &'static str = "LIVEKIT_API_KEY"; const LIVEKIT_API_SECRET_ENV_NAME: &'static str = "LIVEKIT_API_SECRET"; - /// MinterCredentials provides a way to configure a TokenSourceMinter with a `LIVEKIT_URL`, /// `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET` value. pub trait MinterCredentialsSource { @@ -31,11 +30,7 @@ pub struct MinterCredentials { } impl MinterCredentials { pub fn new(server_url: &str, api_key: &str, api_secret: &str) -> Self { - Self { - url: server_url.into(), - api_key: api_key.into(), - api_secret: api_secret.into(), - } + Self { url: server_url.into(), api_key: api_key.into(), api_secret: api_secret.into() } } } @@ -58,15 +53,21 @@ impl MinterCredentialsSource for MinterCredentialsEnvironment { fn get(&self) -> MinterCredentials { let (url_variable, api_key_variable, api_secret_variable) = (&self.0, &self.1, &self.2); let url = std::env::var(url_variable).expect(format!("{url_variable} is not set").as_str()); - let api_key = std::env::var(api_key_variable).expect(format!("{api_key_variable} is not set").as_str()); - let api_secret = std::env::var(api_secret_variable).expect(format!("{api_secret_variable} is not set").as_str()); + let api_key = std::env::var(api_key_variable) + .expect(format!("{api_key_variable} is not set").as_str()); + let api_secret = std::env::var(api_secret_variable) + .expect(format!("{api_secret_variable} is not set").as_str()); MinterCredentials { url, api_key, api_secret } } } impl Default for MinterCredentialsEnvironment { fn default() -> Self { - Self(LIVEKIT_URL_ENV_NAME.into(), LIVEKIT_API_KEY_ENV_NAME.into(), LIVEKIT_API_SECRET_ENV_NAME.into()) + Self( + LIVEKIT_URL_ENV_NAME.into(), + LIVEKIT_API_KEY_ENV_NAME.into(), + LIVEKIT_API_SECRET_ENV_NAME.into(), + ) } } diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index 51f423819..fe0abd13a 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -12,30 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{error::Error, future::{ready, Future}, pin::Pin}; use livekit_api::access_token::{self, AccessTokenError}; +use std::{ + error::Error, + future::{ready, Future}, + pin::Pin, +}; // FIXME: is reexporting these from here a good idea? pub use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; mod fetch_options; -mod traits; mod minter_credentials; +mod traits; pub use fetch_options::TokenSourceFetchOptions; +pub use minter_credentials::{ + MinterCredentials, MinterCredentialsEnvironment, MinterCredentialsSource, +}; pub use traits::{ - TokenSourceFixed, - TokenSourceConfigurable, + TokenSourceConfigurable, TokenSourceConfigurableSynchronous, TokenSourceFixed, TokenSourceFixedSynchronous, - TokenSourceConfigurableSynchronous, }; -pub use minter_credentials::{ - MinterCredentialsSource, - MinterCredentials, - MinterCredentialsEnvironment, -}; - - pub trait TokenLiteralGenerator { fn apply(&self) -> TokenSourceResponse; @@ -69,9 +67,6 @@ impl TokenSourceFixedSynchronous for TokenSourceLitera } } - - - pub struct TokenSourceMinter { credentials_source: CredentialsSource, } @@ -83,8 +78,12 @@ impl TokenSourceMinter { } impl TokenSourceConfigurableSynchronous for TokenSourceMinter { - fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result> { - let MinterCredentials { url: server_url, api_key, api_secret } = self.credentials_source.get(); + fn fetch_synchronous( + &self, + options: &TokenSourceFetchOptions, + ) -> Result> { + let MinterCredentials { url: server_url, api_key, api_secret } = + self.credentials_source.get(); // FIXME: apply options in the below code! let participant_token = access_token::AccessToken::with_api_key(&api_key, &api_secret) @@ -107,11 +106,6 @@ impl Default for TokenSourceMinter { } } - - - - - pub struct TokenSourceCustomMinter< MintFn: Fn(access_token::AccessToken) -> Result, Credentials: MinterCredentialsSource, @@ -121,35 +115,37 @@ pub struct TokenSourceCustomMinter< } impl< - MF: Fn(access_token::AccessToken) -> Result, - CS: MinterCredentialsSource -> TokenSourceCustomMinter { + MF: Fn(access_token::AccessToken) -> Result, + CS: MinterCredentialsSource, + > TokenSourceCustomMinter +{ pub fn new_with_credentials(mint_fn: MF, credentials_source: CS) -> Self { Self { mint_fn, credentials_source } } pub fn new(mint_fn: MF) -> TokenSourceCustomMinter { - TokenSourceCustomMinter::new_with_credentials(mint_fn, MinterCredentialsEnvironment::default()) + TokenSourceCustomMinter::new_with_credentials( + mint_fn, + MinterCredentialsEnvironment::default(), + ) } } - impl< - MF: Fn(access_token::AccessToken) -> Result, - C: MinterCredentialsSource, -> TokenSourceFixedSynchronous for TokenSourceCustomMinter { + MF: Fn(access_token::AccessToken) -> Result, + C: MinterCredentialsSource, + > TokenSourceFixedSynchronous for TokenSourceCustomMinter +{ fn fetch_synchronous(&self) -> Result> { - let MinterCredentials { url: server_url, api_key, api_secret } = self.credentials_source.get(); + let MinterCredentials { url: server_url, api_key, api_secret } = + self.credentials_source.get(); - let participant_token = (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret))?; + let participant_token = + (self.mint_fn)(access_token::AccessToken::with_api_key(&api_key, &api_secret))?; Ok(TokenSourceResponse { server_url, participant_token }) } } - - - - use reqwest::{header::HeaderMap, Method}; pub struct TokenSourceEndpoint { @@ -160,16 +156,15 @@ pub struct TokenSourceEndpoint { impl TokenSourceEndpoint { pub fn new(url: &str) -> Self { - Self { - url: url.into(), - method: Method::POST, - headers: HeaderMap::new(), - } + Self { url: url.into(), method: Method::POST, headers: HeaderMap::new() } } } impl TokenSourceConfigurable for TokenSourceEndpoint { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + async fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> Result> { let client = reqwest::Client::new(); // FIXME: What are the best practices around implementing Into on a reference to avoid the @@ -184,7 +179,13 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { .await?; if !response.status().is_success() { - return Err(format!("Error generating token from endpoint {}: received {:?} / {}", self.url, response.status(), response.text().await?).into()); + return Err(format!( + "Error generating token from endpoint {}: received {:?} / {}", + self.url, + response.status(), + response.text().await? + ) + .into()); } let response_json = response.json::().await?; @@ -193,9 +194,6 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { } } - - - pub struct TokenSourceSandboxTokenServer(TokenSourceEndpoint); impl TokenSourceSandboxTokenServer { @@ -203,46 +201,53 @@ impl TokenSourceSandboxTokenServer { Self::new_with_base_url(sandbox_id, "https://cloud-api.livekit.io") } pub fn new_with_base_url(sandbox_id: &str, base_url: &str) -> Self { - let mut endpoint = TokenSourceEndpoint::new( - &format!("{base_url}/api/v2/sandbox/connection-details") - ); + let mut endpoint = + TokenSourceEndpoint::new(&format!("{base_url}/api/v2/sandbox/connection-details")); endpoint.headers.insert("X-Sandbox-ID", sandbox_id.parse().unwrap()); Self(endpoint) } } impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + async fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> Result> { self.0.fetch(options).await } } - - - pub struct TokenSourceCustom< - CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, + CustomFn: Fn( + &TokenSourceFetchOptions, + ) -> Pin>>>>, >(CustomFn); impl< - CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, -> TokenSourceCustom { + CustomFn: Fn( + &TokenSourceFetchOptions, + ) -> Pin>>>>, + > TokenSourceCustom +{ pub fn new(custom_fn: CustomFn) -> Self { Self(custom_fn) } } impl< - CustomFn: Fn(&TokenSourceFetchOptions) -> Pin>>>>, -> TokenSourceConfigurable for TokenSourceCustom { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> Result> { + CustomFn: Fn( + &TokenSourceFetchOptions, + ) -> Pin>>>>, + > TokenSourceConfigurable for TokenSourceCustom +{ + async fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> Result> { (self.0)(options).await } } - - - async fn test() { let fetch_options = TokenSourceFetchOptions::default().with_agent_name("voice ai quickstart"); @@ -255,26 +260,33 @@ async fn test() { let minter = TokenSourceMinter::default(); let _ = minter.fetch(&fetch_options).await; - let minter_literal_credentials = TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret")); + let minter_literal_credentials = + TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret")); let _ = minter_literal_credentials.fetch(&fetch_options).await; - let minter_env = TokenSourceMinter::new(MinterCredentialsEnvironment::new("SERVER_URL", "API_KEY", "API_SECRET")); + let minter_env = TokenSourceMinter::new(MinterCredentialsEnvironment::new( + "SERVER_URL", + "API_KEY", + "API_SECRET", + )); let _ = minter_env.fetch(&fetch_options).await; - let minter_literal_custom = TokenSourceMinter::new(|| MinterCredentials::new("server url", "api key", "api secret")); + let minter_literal_custom = + TokenSourceMinter::new(|| MinterCredentials::new("server url", "api key", "api secret")); let _ = minter_literal_custom.fetch(&fetch_options).await; - let custom_minter = TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { - access_token - .with_identity("rust-bot") - .with_name("Rust Bot") - .with_grants(access_token::VideoGrants { - room_join: true, - room: "my-room".to_string(), - ..Default::default() - }) - .to_jwt() - }); + let custom_minter = + TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { + access_token + .with_identity("rust-bot") + .with_name("Rust Bot") + .with_grants(access_token::VideoGrants { + room_join: true, + room: "my-room".to_string(), + ..Default::default() + }) + .to_jwt() + }); let _ = custom_minter.fetch().await; let endpoint = TokenSourceEndpoint::new("https://example.com/my/example/auth/endpoint"); @@ -293,12 +305,10 @@ async fn test() { // TODO: custom let custom = TokenSourceCustom::new(|_options| { - Box::pin(ready( - Ok(TokenSourceResponse { - server_url: "...".into(), - participant_token: "... _options should be encoded in here ...".into(), - }) - )) + Box::pin(ready(Ok(TokenSourceResponse { + server_url: "...".into(), + participant_token: "... _options should be encoded in here ...".into(), + }))) }); let _ = custom.fetch(&fetch_options).await; } diff --git a/livekit/src/room/token_source/traits.rs b/livekit/src/room/token_source/traits.rs index 06208fa65..b0d1e41d8 100644 --- a/livekit/src/room/token_source/traits.rs +++ b/livekit/src/room/token_source/traits.rs @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{error::Error, future::{ready, Future}}; use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; +use std::{ + error::Error, + future::{ready, Future}, +}; use crate::token_source::TokenSourceFetchOptions; - /// A Fixed TokenSource is a token source that takes no parameters and returns a completely /// independently derived value on each fetch() call. /// @@ -32,7 +34,7 @@ pub trait TokenSourceFixedSynchronous { // FIXME: what should the error type of the result be? fn fetch_synchronous(&self) -> Result>; } - + impl TokenSourceFixed for T { // FIXME: what should the error type of the result be? fn fetch(&self) -> impl Future>> { @@ -40,7 +42,6 @@ impl TokenSourceFixed for T { } } - /// A Configurable TokenSource is a token source that takes a /// TokenSourceFetchOptions object as input and returns a deterministic /// TokenSourceResponseObject output based on the options specified. @@ -52,18 +53,27 @@ impl TokenSourceFixed for T { /// A few common downstream implementers are TokenSourceEndpoint and TokenSourceCustom. pub trait TokenSourceConfigurable { // FIXME: what should the error type of the result be? - fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>>; + fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> impl Future>>; } /// A helper trait to more easily implement a TokenSourceConfigurable which not async. pub trait TokenSourceConfigurableSynchronous { // FIXME: what should the error type of the result be? - fn fetch_synchronous(&self, options: &TokenSourceFetchOptions) -> Result>; + fn fetch_synchronous( + &self, + options: &TokenSourceFetchOptions, + ) -> Result>; } - + impl TokenSourceConfigurable for T { // FIXME: what should the error type of the result be? - fn fetch(&self, options: &TokenSourceFetchOptions) -> impl Future>> { + fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> impl Future>> { ready(self.fetch_synchronous(options)) } } From be374fe07df3d8660b9d8d22b1211b7f9cc21d8f Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 16:53:03 -0400 Subject: [PATCH 16/29] fix: remove token source test fn --- livekit/src/room/token_source/mod.rs | 65 ---------------------------- 1 file changed, 65 deletions(-) diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index fe0abd13a..c18ffb371 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -247,68 +247,3 @@ impl< (self.0)(options).await } } - -async fn test() { - let fetch_options = TokenSourceFetchOptions::default().with_agent_name("voice ai quickstart"); - - let literal = TokenSourceLiteral::new(TokenSourceResponse { - server_url: "...".into(), - participant_token: "...".into(), - }); - let _ = literal.fetch().await; - - let minter = TokenSourceMinter::default(); - let _ = minter.fetch(&fetch_options).await; - - let minter_literal_credentials = - TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret")); - let _ = minter_literal_credentials.fetch(&fetch_options).await; - - let minter_env = TokenSourceMinter::new(MinterCredentialsEnvironment::new( - "SERVER_URL", - "API_KEY", - "API_SECRET", - )); - let _ = minter_env.fetch(&fetch_options).await; - - let minter_literal_custom = - TokenSourceMinter::new(|| MinterCredentials::new("server url", "api key", "api secret")); - let _ = minter_literal_custom.fetch(&fetch_options).await; - - let custom_minter = - TokenSourceCustomMinter::<_, MinterCredentialsEnvironment>::new(|access_token| { - access_token - .with_identity("rust-bot") - .with_name("Rust Bot") - .with_grants(access_token::VideoGrants { - room_join: true, - room: "my-room".to_string(), - ..Default::default() - }) - .to_jwt() - }); - let _ = custom_minter.fetch().await; - - let endpoint = TokenSourceEndpoint::new("https://example.com/my/example/auth/endpoint"); - let _ = endpoint.fetch(&fetch_options).await; - - let endpoint = TokenSourceSandboxTokenServer::new("SANDBOX ID HERE"); - let _ = endpoint.fetch(&fetch_options).await; - - // let foo = Box::pin(async |options: &TokenSourceFetchOptions| { - // Ok(TokenSourceResponse::new("...", "... _options should be encoded in here ...")) - // }); - - // // TODO: custom - // let custom = TokenSourceCustom::new(foo); - // let _ = custom.fetch(&fetch_options).await; - - // TODO: custom - let custom = TokenSourceCustom::new(|_options| { - Box::pin(ready(Ok(TokenSourceResponse { - server_url: "...".into(), - participant_token: "... _options should be encoded in here ...".into(), - }))) - }); - let _ = custom.fetch(&fetch_options).await; -} From 16b8b26012970988cf6e1dfeb75dd4b1f4680f1a Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 15 Oct 2025 17:12:47 -0400 Subject: [PATCH 17/29] fix: add ANOTHER missing participant_info::Kind case as part of livekit-protocol upgrade --- livekit-ffi/src/conversion/participant.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit-ffi/src/conversion/participant.rs b/livekit-ffi/src/conversion/participant.rs index c7cbc9b97..dacf17a1e 100644 --- a/livekit-ffi/src/conversion/participant.rs +++ b/livekit-ffi/src/conversion/participant.rs @@ -46,6 +46,7 @@ impl From for proto::ParticipantKind { ParticipantKind::Ingress => proto::ParticipantKind::Ingress, ParticipantKind::Egress => proto::ParticipantKind::Egress, ParticipantKind::Agent => proto::ParticipantKind::Agent, + ParticipantKind::Connector => proto::ParticipantKind::Connector, } } } From f60e5393dcda8b5db068b02abca2a307bb7435fb Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 16 Oct 2025 11:20:45 -0400 Subject: [PATCH 18/29] fix: add PARTICIPANT_KIND_CONNECTOR to livekit-ffi proto definition --- livekit-ffi/protocol/participant.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit-ffi/protocol/participant.proto b/livekit-ffi/protocol/participant.proto index 5e7ab34ba..23a0903e4 100644 --- a/livekit-ffi/protocol/participant.proto +++ b/livekit-ffi/protocol/participant.proto @@ -40,6 +40,7 @@ enum ParticipantKind { PARTICIPANT_KIND_EGRESS = 2; PARTICIPANT_KIND_SIP = 3; PARTICIPANT_KIND_AGENT = 4; + PARTICIPANT_KIND_CONNECTOR = 5; } enum DisconnectReason { From ae2e6f4679a67cf1007415494c895ad80ca40ffc Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 16 Oct 2025 11:53:17 -0400 Subject: [PATCH 19/29] fix: get rid of pubs in TokenSourceFetchOptions --- livekit/src/room/token_source/fetch_options.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/livekit/src/room/token_source/fetch_options.rs b/livekit/src/room/token_source/fetch_options.rs index 7ad8bafcb..04965501c 100644 --- a/livekit/src/room/token_source/fetch_options.rs +++ b/livekit/src/room/token_source/fetch_options.rs @@ -23,14 +23,14 @@ use std::collections::HashMap; /// ``` #[derive(Clone, Default)] pub struct TokenSourceFetchOptions { - pub room_name: Option, - pub participant_name: Option, - pub participant_identity: Option, - pub participant_metadata: Option, - pub participant_attributes: Option>, + room_name: Option, + participant_name: Option, + participant_identity: Option, + participant_metadata: Option, + participant_attributes: Option>, - pub agent_name: Option, - pub agent_metadata: Option, + agent_name: Option, + agent_metadata: Option, } impl TokenSourceFetchOptions { From 275634f364fd162f6009a6d797f4b893c32889ca Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 16 Oct 2025 12:19:04 -0400 Subject: [PATCH 20/29] fix: address docs example missing import --- livekit/src/room/token_source/fetch_options.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit/src/room/token_source/fetch_options.rs b/livekit/src/room/token_source/fetch_options.rs index 04965501c..f343d5404 100644 --- a/livekit/src/room/token_source/fetch_options.rs +++ b/livekit/src/room/token_source/fetch_options.rs @@ -19,6 +19,7 @@ use std::collections::HashMap; /// /// Example: /// ```rust +/// use livekit::token_source::TokenSourceFetchOptions; /// let _ = TokenSourceFetchOptions::default().with_agent_name("my agent name"); /// ``` #[derive(Clone, Default)] From 5289e98c005ff12f803e51bc31cf9d9ad73db280 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 16 Oct 2025 12:27:10 -0400 Subject: [PATCH 21/29] feat: add separate proto / locally defined TokenSourceRequest + TokenSourceResponse --- .../src/room/token_source/fetch_options.rs | 10 +-- livekit/src/room/token_source/mod.rs | 24 +++---- .../src/room/token_source/request_response.rs | 67 +++++++++++++++++++ livekit/src/room/token_source/traits.rs | 3 +- 4 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 livekit/src/room/token_source/request_response.rs diff --git a/livekit/src/room/token_source/fetch_options.rs b/livekit/src/room/token_source/fetch_options.rs index f343d5404..3b92745fb 100644 --- a/livekit/src/room/token_source/fetch_options.rs +++ b/livekit/src/room/token_source/fetch_options.rs @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use livekit_protocol::{RoomAgentDispatch, RoomConfiguration, TokenSourceRequest}; +use livekit_protocol as proto; use std::collections::HashMap; +use crate::token_source::TokenSourceRequest; + /// Options that can be used when fetching new credentials from a TokenSourceConfigurable /// /// Example: @@ -93,7 +95,7 @@ impl TokenSourceFetchOptions { impl Into for TokenSourceFetchOptions { fn into(self) -> TokenSourceRequest { - let mut agent_dispatch = RoomAgentDispatch::default(); + let mut agent_dispatch = proto::RoomAgentDispatch::default(); if let Some(agent_name) = self.agent_name { agent_dispatch.agent_name = agent_name; } @@ -101,8 +103,8 @@ impl Into for TokenSourceFetchOptions { agent_dispatch.metadata = agent_metadata; } - let room_config = if agent_dispatch != RoomAgentDispatch::default() { - let mut room_config = RoomConfiguration::default(); + let room_config = if agent_dispatch != proto::RoomAgentDispatch::default() { + let mut room_config = proto::RoomConfiguration::default(); room_config.agents.push(agent_dispatch); Some(room_config) } else { diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index c18ffb371..09e9f68b5 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -13,23 +13,19 @@ // limitations under the License. use livekit_api::access_token::{self, AccessTokenError}; -use std::{ - error::Error, - future::{ready, Future}, - pin::Pin, -}; - -// FIXME: is reexporting these from here a good idea? -pub use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; +use livekit_protocol as proto; +use std::{error::Error, future::Future, pin::Pin}; mod fetch_options; mod minter_credentials; +mod request_response; mod traits; pub use fetch_options::TokenSourceFetchOptions; pub use minter_credentials::{ MinterCredentials, MinterCredentialsEnvironment, MinterCredentialsSource, }; +pub use request_response::{TokenSourceRequest, TokenSourceResponse}; pub use traits::{ TokenSourceConfigurable, TokenSourceConfigurableSynchronous, TokenSourceFixed, TokenSourceFixedSynchronous, @@ -167,13 +163,12 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { ) -> Result> { let client = reqwest::Client::new(); - // FIXME: What are the best practices around implementing Into on a reference to avoid the - // clone? - let request_body: TokenSourceRequest = options.clone().into(); + let request: TokenSourceRequest = options.clone().into(); + let request_proto: proto::TokenSourceRequest = request.into(); let response = client .request(self.method.clone(), &self.url) - .json(&request_body) + .json(&request_proto) .headers(self.headers.clone()) .send() .await?; @@ -188,9 +183,8 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { .into()); } - let response_json = response.json::().await?; - - Ok(response_json) + let response_proto = response.json::().await?; + Ok(response_proto.into()) } } diff --git a/livekit/src/room/token_source/request_response.rs b/livekit/src/room/token_source/request_response.rs new file mode 100644 index 000000000..e23f1cb60 --- /dev/null +++ b/livekit/src/room/token_source/request_response.rs @@ -0,0 +1,67 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use livekit_protocol as proto; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub struct TokenSourceRequest { + /// The name of the room being requested when generating credentials + pub room_name: Option, + + /// The name of the participant being requested for this client when generating credentials + pub participant_name: Option, + + /// The identity of the participant being requested for this client when generating credentials + pub participant_identity: Option, + + /// Any participant metadata being included along with the credentials generation operation + pub participant_metadata: Option, + + /// Any participant attributes being included along with the credentials generation operation + pub participant_attributes: HashMap, + + /// A RoomConfiguration object can be passed to request extra parameters should be included when + /// generating connection credentials - dispatching agents, defining egress settings, etc + /// More info: + pub room_config: Option, +} + +impl From for proto::TokenSourceRequest { + fn from(value: TokenSourceRequest) -> Self { + proto::TokenSourceRequest { + room_name: value.room_name, + participant_name: value.participant_name, + participant_identity: value.participant_identity, + participant_metadata: value.participant_metadata, + participant_attributes: value.participant_attributes, + room_config: value.room_config, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TokenSourceResponse { + pub server_url: String, + pub participant_token: String, +} + +impl From for TokenSourceResponse { + fn from(value: proto::TokenSourceResponse) -> Self { + TokenSourceResponse { + server_url: value.server_url, + participant_token: value.participant_token, + } + } +} diff --git a/livekit/src/room/token_source/traits.rs b/livekit/src/room/token_source/traits.rs index b0d1e41d8..0afbf0f68 100644 --- a/livekit/src/room/token_source/traits.rs +++ b/livekit/src/room/token_source/traits.rs @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use livekit_protocol::{TokenSourceRequest, TokenSourceResponse}; use std::{ error::Error, future::{ready, Future}, }; -use crate::token_source::TokenSourceFetchOptions; +use crate::token_source::{TokenSourceFetchOptions, TokenSourceResponse}; /// A Fixed TokenSource is a token source that takes no parameters and returns a completely /// independently derived value on each fetch() call. From a3a016d9ae8c1f093e3e3860603120485524cbc9 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 16 Oct 2025 12:54:54 -0400 Subject: [PATCH 22/29] feat: add TokenSourceResult / TokenSourceError for better error handling --- livekit/src/room/token_source/error.rs | 19 ++++++++++++++ livekit/src/room/token_source/mod.rs | 34 ++++++++++++------------- livekit/src/room/token_source/traits.rs | 19 ++++++-------- 3 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 livekit/src/room/token_source/error.rs diff --git a/livekit/src/room/token_source/error.rs b/livekit/src/room/token_source/error.rs new file mode 100644 index 000000000..24905be57 --- /dev/null +++ b/livekit/src/room/token_source/error.rs @@ -0,0 +1,19 @@ +use std::error::Error; + +use livekit_api::access_token::AccessTokenError; + +#[derive(Debug, thiserror::Error)] +pub enum TokenSourceError { + #[error("network error: {0}")] + Reqwest(#[from] reqwest::Error), + #[error( + "Error generating token from endpoint {url}: received {status_code:?} / {raw_body_text}" + )] + TokenGenerationFailed { url: String, status_code: reqwest::StatusCode, raw_body_text: String }, + #[error("access token error: {0}")] + AccessToken(#[from] AccessTokenError), + #[error("Other error: {0}")] + Other(#[from] Box), +} + +pub type TokenSourceResult = Result; diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index 09e9f68b5..621c1dd51 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -14,13 +14,15 @@ use livekit_api::access_token::{self, AccessTokenError}; use livekit_protocol as proto; -use std::{error::Error, future::Future, pin::Pin}; +use std::{future::Future, pin::Pin}; +mod error; mod fetch_options; mod minter_credentials; mod request_response; mod traits; +pub use error::{TokenSourceError, TokenSourceResult}; pub use fetch_options::TokenSourceFetchOptions; pub use minter_credentials::{ MinterCredentials, MinterCredentialsEnvironment, MinterCredentialsSource, @@ -58,7 +60,7 @@ impl TokenSourceLiteral { } impl TokenSourceFixedSynchronous for TokenSourceLiteral { - fn fetch_synchronous(&self) -> Result> { + fn fetch_synchronous(&self) -> TokenSourceResult { Ok(self.generator.apply()) } } @@ -77,7 +79,7 @@ impl TokenSourceConfigurableSynchronous for TokenSou fn fetch_synchronous( &self, options: &TokenSourceFetchOptions, - ) -> Result> { + ) -> TokenSourceResult { let MinterCredentials { url: server_url, api_key, api_secret } = self.credentials_source.get(); @@ -131,7 +133,7 @@ impl< C: MinterCredentialsSource, > TokenSourceFixedSynchronous for TokenSourceCustomMinter { - fn fetch_synchronous(&self) -> Result> { + fn fetch_synchronous(&self) -> TokenSourceResult { let MinterCredentials { url: server_url, api_key, api_secret } = self.credentials_source.get(); @@ -160,7 +162,7 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { async fn fetch( &self, options: &TokenSourceFetchOptions, - ) -> Result> { + ) -> TokenSourceResult { let client = reqwest::Client::new(); let request: TokenSourceRequest = options.clone().into(); @@ -174,13 +176,11 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { .await?; if !response.status().is_success() { - return Err(format!( - "Error generating token from endpoint {}: received {:?} / {}", - self.url, - response.status(), - response.text().await? - ) - .into()); + return Err(TokenSourceError::TokenGenerationFailed { + url: self.url.clone(), + status_code: response.status(), + raw_body_text: response.text().await?, + }); } let response_proto = response.json::().await?; @@ -206,7 +206,7 @@ impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { async fn fetch( &self, options: &TokenSourceFetchOptions, - ) -> Result> { + ) -> TokenSourceResult { self.0.fetch(options).await } } @@ -214,13 +214,13 @@ impl TokenSourceConfigurable for TokenSourceSandboxTokenServer { pub struct TokenSourceCustom< CustomFn: Fn( &TokenSourceFetchOptions, - ) -> Pin>>>>, + ) -> Pin>>>, >(CustomFn); impl< CustomFn: Fn( &TokenSourceFetchOptions, - ) -> Pin>>>>, + ) -> Pin>>>, > TokenSourceCustom { pub fn new(custom_fn: CustomFn) -> Self { @@ -231,13 +231,13 @@ impl< impl< CustomFn: Fn( &TokenSourceFetchOptions, - ) -> Pin>>>>, + ) -> Pin>>>, > TokenSourceConfigurable for TokenSourceCustom { async fn fetch( &self, options: &TokenSourceFetchOptions, - ) -> Result> { + ) -> TokenSourceResult { (self.0)(options).await } } diff --git a/livekit/src/room/token_source/traits.rs b/livekit/src/room/token_source/traits.rs index 0afbf0f68..09c80c2b3 100644 --- a/livekit/src/room/token_source/traits.rs +++ b/livekit/src/room/token_source/traits.rs @@ -12,12 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - error::Error, - future::{ready, Future}, -}; +use std::future::{ready, Future}; -use crate::token_source::{TokenSourceFetchOptions, TokenSourceResponse}; +use crate::token_source::{TokenSourceFetchOptions, TokenSourceResponse, TokenSourceResult}; /// A Fixed TokenSource is a token source that takes no parameters and returns a completely /// independently derived value on each fetch() call. @@ -25,18 +22,18 @@ use crate::token_source::{TokenSourceFetchOptions, TokenSourceResponse}; /// The most common downstream implementer is TokenSourceLiteral. pub trait TokenSourceFixed { // FIXME: what should the error type of the result be? - fn fetch(&self) -> impl Future>>; + fn fetch(&self) -> impl Future>; } /// A helper trait to more easily implement a TokenSourceFixed which not async. pub trait TokenSourceFixedSynchronous { // FIXME: what should the error type of the result be? - fn fetch_synchronous(&self) -> Result>; + fn fetch_synchronous(&self) -> TokenSourceResult; } impl TokenSourceFixed for T { // FIXME: what should the error type of the result be? - fn fetch(&self) -> impl Future>> { + fn fetch(&self) -> impl Future> { ready(self.fetch_synchronous()) } } @@ -55,7 +52,7 @@ pub trait TokenSourceConfigurable { fn fetch( &self, options: &TokenSourceFetchOptions, - ) -> impl Future>>; + ) -> impl Future>; } /// A helper trait to more easily implement a TokenSourceConfigurable which not async. @@ -64,7 +61,7 @@ pub trait TokenSourceConfigurableSynchronous { fn fetch_synchronous( &self, options: &TokenSourceFetchOptions, - ) -> Result>; + ) -> TokenSourceResult; } impl TokenSourceConfigurable for T { @@ -72,7 +69,7 @@ impl TokenSourceConfigurable for T { fn fetch( &self, options: &TokenSourceFetchOptions, - ) -> impl Future>> { + ) -> impl Future> { ready(self.fetch_synchronous(options)) } } From 8b48599b878e99e30228accdaa611a9d941f4d60 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 17 Oct 2025 13:17:01 -0400 Subject: [PATCH 23/29] refactor: drop irrelevant fixme comments --- livekit/src/room/token_source/traits.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/livekit/src/room/token_source/traits.rs b/livekit/src/room/token_source/traits.rs index 09c80c2b3..748b5ee52 100644 --- a/livekit/src/room/token_source/traits.rs +++ b/livekit/src/room/token_source/traits.rs @@ -21,18 +21,15 @@ use crate::token_source::{TokenSourceFetchOptions, TokenSourceResponse, TokenSou /// /// The most common downstream implementer is TokenSourceLiteral. pub trait TokenSourceFixed { - // FIXME: what should the error type of the result be? fn fetch(&self) -> impl Future>; } /// A helper trait to more easily implement a TokenSourceFixed which not async. pub trait TokenSourceFixedSynchronous { - // FIXME: what should the error type of the result be? fn fetch_synchronous(&self) -> TokenSourceResult; } impl TokenSourceFixed for T { - // FIXME: what should the error type of the result be? fn fetch(&self) -> impl Future> { ready(self.fetch_synchronous()) } @@ -48,7 +45,6 @@ impl TokenSourceFixed for T { /// /// A few common downstream implementers are TokenSourceEndpoint and TokenSourceCustom. pub trait TokenSourceConfigurable { - // FIXME: what should the error type of the result be? fn fetch( &self, options: &TokenSourceFetchOptions, @@ -57,7 +53,6 @@ pub trait TokenSourceConfigurable { /// A helper trait to more easily implement a TokenSourceConfigurable which not async. pub trait TokenSourceConfigurableSynchronous { - // FIXME: what should the error type of the result be? fn fetch_synchronous( &self, options: &TokenSourceFetchOptions, @@ -65,7 +60,6 @@ pub trait TokenSourceConfigurableSynchronous { } impl TokenSourceConfigurable for T { - // FIXME: what should the error type of the result be? fn fetch( &self, options: &TokenSourceFetchOptions, From 6047dfaa0f74431f24f53bd4dc28497789d48587 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 17 Oct 2025 16:11:14 -0400 Subject: [PATCH 24/29] feat: add initial TokenSourceCache implementation --- .../src/room/token_source/fetch_options.rs | 2 +- livekit/src/room/token_source/mod.rs | 287 +++++++++++++++++- 2 files changed, 287 insertions(+), 2 deletions(-) diff --git a/livekit/src/room/token_source/fetch_options.rs b/livekit/src/room/token_source/fetch_options.rs index 3b92745fb..d591def0a 100644 --- a/livekit/src/room/token_source/fetch_options.rs +++ b/livekit/src/room/token_source/fetch_options.rs @@ -24,7 +24,7 @@ use crate::token_source::TokenSourceRequest; /// use livekit::token_source::TokenSourceFetchOptions; /// let _ = TokenSourceFetchOptions::default().with_agent_name("my agent name"); /// ``` -#[derive(Clone, Default)] +#[derive(Clone, Default, PartialEq, Eq)] pub struct TokenSourceFetchOptions { room_name: Option, participant_name: Option, diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index 621c1dd51..1bf1f00c7 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -14,7 +14,8 @@ use livekit_api::access_token::{self, AccessTokenError}; use livekit_protocol as proto; -use std::{future::Future, pin::Pin}; +use parking_lot::RwLock; +use std::{future::Future, pin::Pin, sync::Arc}; mod error; mod fetch_options; @@ -241,3 +242,287 @@ impl< (self.0)(options).await } } + + + + + + + +trait TokenResponseCacheValue {} + +impl TokenResponseCacheValue for TokenSourceResponse {} +impl TokenResponseCacheValue for (TokenSourceFetchOptions, TokenSourceResponse) {} + +/// Represents a mechanism by which token responses can be cached +/// +/// When used with a TokenSourceFixed, `Value` is `TokenSourceResponse` +/// When used with a TokenSourceConfigurable, `Value` is `(TokenSourceFetchOptions, TokenSourceResponse)` +trait TokenResponseCache { + fn get(&self) -> Option<&Value>; + fn set(&mut self, value: Value); + fn clear(&mut self); +} + +/// In-memory implementation of [TokenResponseCache] +struct TokenResponseInMemoryCache(Option); +impl TokenResponseInMemoryCache { + pub fn new() -> Self { + Self(None) + } +} + +impl TokenResponseCache for TokenResponseInMemoryCache { + fn get(&self) -> Option<&Value> { + self.0.as_ref() + } + fn set(&mut self, value: Value) { + self.0 = Some(value); + } + fn clear(&mut self) { + self.0 = None; + } +} + + + + + + + + +fn is_response_valid(response: &TokenSourceResponse) -> bool { + // FIXME: write this + true +} + +trait TokenSourceFixedCached { + fn get_response_cache(&self) -> Arc>>; + + async fn update(&self) -> TokenSourceResult; + + async fn fetch_cached(&self) -> TokenSourceResult { + let cache = self.get_response_cache(); + + let cached_response_to_return = { + let cache_read = cache.read(); + let cached_value = cache_read.get(); + + if let Some(cached_response) = cached_value { + if is_response_valid(cached_response) { + Some(cached_response.clone()) + } else { + None + } + } else { + None + } + }; + + if let Some(cached_response) = cached_response_to_return { + Ok(cached_response) + } else { + let response = self.update().await?; + cache.write().set(response.clone()); + Ok(response) + } + } +} + +trait TokenSourceConfigurableCached { + fn get_response_cache(&self) -> Arc>>; + + async fn update(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult; + + async fn fetch_cached( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult { + let cache = self.get_response_cache(); + + let cached_response_to_return = { + let cache_read = cache.read(); + let cached_value = cache_read.get(); + + if let Some((cached_options, cached_response)) = cached_value { + if options == cached_options && is_response_valid(cached_response) { + Some(cached_response.clone()) + } else { + None + } + } else { + None + } + }; + + if let Some(cached_response) = cached_response_to_return { + Ok(cached_response) + } else { + let response = self.update(options).await?; + cache.write().set((options.clone(), response.clone())); + Ok(response) + } + } +} + +// impl TokenSourceConfigurable for T { +// async fn fetch( +// &self, +// options: &TokenSourceFetchOptions, +// ) -> TokenSourceResult { +// self.fetch_cached(options).await +// } +// } + + + + + + +trait TokenSourceCacheType {} + +struct TokenSourceCacheConfigurable(T); +impl TokenSourceCacheType for TokenSourceCacheConfigurable {} + +struct TokenSourceCacheFixed(T); +impl TokenSourceCacheType for TokenSourceCacheFixed {} + +/// A conmposable TokenSource which can wrap either a [TokenSourceFixed] or a [TokenSourceConfigurable] and +/// caches the intermediate value in a [TokenResponseCache]. +struct TokenSourceCache> { + inner: Type, + cache: Arc>, + _v: Value, // FIXME: how do I remove this? `Value` needs to be used in here or I get an error. +} + +impl TokenSourceCache< + TokenSourceCacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + TokenResponseInMemoryCache<(TokenSourceFetchOptions, TokenSourceResponse)> +> { + fn new_configurable(inner_token_source: Inner) -> Self { + TokenSourceCache::new_configurable_with_cache(inner_token_source, TokenResponseInMemoryCache::new()) + } +} + +impl TokenSourceCache< + TokenSourceCacheFixed, + TokenSourceResponse, + TokenResponseInMemoryCache +> { + fn new_fixed(inner_token_source: Inner) -> Self { + TokenSourceCache::new_fixed_with_cache(inner_token_source, TokenResponseInMemoryCache::new()) + } +} + +impl< + Inner: TokenSourceConfigurable, + Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)> +> TokenSourceCache< + TokenSourceCacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + Cache +> { + fn new_configurable_with_cache(inner_token_source: Inner, token_cache: Cache) -> Self { + Self { + inner: TokenSourceCacheConfigurable(inner_token_source), + cache: Arc::new(RwLock::new(token_cache)), + + // FIXME: remove this! + _v: (TokenSourceFetchOptions::default(), TokenSourceResponse { server_url: "".into(), participant_token: "".into() }), + } + } +} + +impl< + Inner: TokenSourceFixed, + Cache: TokenResponseCache +> TokenSourceCache< + TokenSourceCacheFixed, + TokenSourceResponse, + Cache +> { + fn new_fixed_with_cache(inner_token_source: Inner, token_cache: Cache) -> Self { + Self { + inner: TokenSourceCacheFixed(inner_token_source), + cache: Arc::new(RwLock::new(token_cache)), + + // FIXME: remove this! + _v: TokenSourceResponse { server_url: "".into(), participant_token: "".into() }, + } + } +} + + +impl< + Inner: TokenSourceConfigurable, + Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)> +> TokenSourceConfigurableCached for TokenSourceCache< + TokenSourceCacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + Cache, +> { + fn get_response_cache(&self) -> Arc>> { + self.cache.clone() + } + async fn update(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { + self.inner.0.fetch(options).await + } +} + +impl< + Inner: TokenSourceFixed, + Cache: TokenResponseCache +> TokenSourceFixedCached for TokenSourceCache< + TokenSourceCacheFixed, + TokenSourceResponse, + Cache, +> { + fn get_response_cache(&self) -> Arc>> { + self.cache.clone() + } + async fn update(&self) -> TokenSourceResult { + self.inner.0.fetch().await + } +} + + + + +impl< + Inner: TokenSourceConfigurable, + Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)> +> TokenSourceConfigurable for TokenSourceCache< + TokenSourceCacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + Cache, +> { + async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { + self.fetch_cached(options).await + } +} + +impl< + Inner: TokenSourceFixed, + Cache: TokenResponseCache +> TokenSourceFixed for TokenSourceCache< + TokenSourceCacheFixed, + TokenSourceResponse, + Cache, +> { + async fn fetch(&self) -> TokenSourceResult { + self.fetch_cached().await + } +} + + + + + +fn test() { + let a = TokenSourceCache::new_configurable(TokenSourceMinter::default()); + + let minter = TokenSourceMinter::default(); + let cache = TokenResponseInMemoryCache::new(); + let b = TokenSourceCache::new_configurable_with_cache(minter, cache); +} From f0111c865dd97e20b57fbb381dba615d0c292fd5 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 17 Oct 2025 16:31:51 -0400 Subject: [PATCH 25/29] feat: add token validation function and use it in TokenSourceCache --- livekit-api/src/access_token.rs | 23 +++++++++++++++++++++++ livekit/src/room/token_source/mod.rs | 9 ++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/livekit-api/src/access_token.rs b/livekit-api/src/access_token.rs index 1c9d18d0c..d9d3c6acd 100644 --- a/livekit-api/src/access_token.rs +++ b/livekit-api/src/access_token.rs @@ -316,6 +316,29 @@ impl TokenVerifier { } } +/// Given a token, determine if it is currently valid based off data in the claims. +/// +/// NOTE: No token signature validation is being done! To do this, see [TokenVerifier]. +pub fn is_token_valid(token: &str) -> Result { + let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); + validation.validate_exp = true; + validation.validate_nbf = true; + validation.insecure_disable_signature_validation(); + + let result = jsonwebtoken::decode::( + token, + &DecodingKey::from_secret(&[]), + &validation, + ); + + match result { + Ok(_) => Ok(true), + Err(err) if err.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature => Ok(false), + Err(err) if err.kind() == &jsonwebtoken::errors::ErrorKind::ImmatureSignature => Ok(false), + Err(err) => Err(err.into()), + } +} + #[cfg(test)] mod tests { use std::time::Duration; diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index 1bf1f00c7..fec86f86c 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -291,11 +291,6 @@ impl TokenResponseCache for TokenResponse -fn is_response_valid(response: &TokenSourceResponse) -> bool { - // FIXME: write this - true -} - trait TokenSourceFixedCached { fn get_response_cache(&self) -> Arc>>; @@ -309,7 +304,7 @@ trait TokenSourceFixedCached { let cached_value = cache_read.get(); if let Some(cached_response) = cached_value { - if is_response_valid(cached_response) { + if access_token::is_token_valid(&cached_response.participant_token)? { Some(cached_response.clone()) } else { None @@ -345,7 +340,7 @@ trait TokenSourceConfigurableCached { let cached_value = cache_read.get(); if let Some((cached_options, cached_response)) = cached_value { - if options == cached_options && is_response_valid(cached_response) { + if options == cached_options && access_token::is_token_valid(&cached_response.participant_token)? { Some(cached_response.clone()) } else { None From 22570174c7ab06d3b951393eb11172d086586ae0 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 17 Oct 2025 16:35:57 -0400 Subject: [PATCH 26/29] docs: add some fixme notes for next week --- livekit/src/room/token_source/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index fec86f86c..482513de1 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -360,6 +360,7 @@ trait TokenSourceConfigurableCached { } } +// FIXME: Why doesn't this work? // impl TokenSourceConfigurable for T { // async fn fetch( // &self, @@ -395,6 +396,8 @@ impl TokenSourceCache< (TokenSourceFetchOptions, TokenSourceResponse), TokenResponseInMemoryCache<(TokenSourceFetchOptions, TokenSourceResponse)> > { + // FIXME: Is there some way I can make this `new` without requiring something like the below? + // TokenSourceCache::, _, _>::new(...) fn new_configurable(inner_token_source: Inner) -> Self { TokenSourceCache::new_configurable_with_cache(inner_token_source, TokenResponseInMemoryCache::new()) } @@ -405,6 +408,8 @@ impl TokenSourceCache< TokenSourceResponse, TokenResponseInMemoryCache > { + // FIXME: Is there some way I can make this `new` without requiring something like the below? + // TokenSourceCache::, _, _>::new(...) fn new_fixed(inner_token_source: Inner) -> Self { TokenSourceCache::new_fixed_with_cache(inner_token_source, TokenResponseInMemoryCache::new()) } From 6e34fa2ac5a6885bdb48fa8e62c93cc4d536ea98 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 17 Oct 2025 17:00:19 -0400 Subject: [PATCH 27/29] feat: remove test code --- livekit/src/room/token_source/mod.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index 482513de1..d18874cfb 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -514,15 +514,3 @@ impl< self.fetch_cached().await } } - - - - - -fn test() { - let a = TokenSourceCache::new_configurable(TokenSourceMinter::default()); - - let minter = TokenSourceMinter::default(); - let cache = TokenResponseInMemoryCache::new(); - let b = TokenSourceCache::new_configurable_with_cache(minter, cache); -} From c53f3e024c66b2f54a1b33b7f2b834fca4c48ba3 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 17 Oct 2025 17:13:26 -0400 Subject: [PATCH 28/29] refactor: break up cache logic into separate files --- livekit/src/room/token_source/cache.rs | 247 +++++++++++++++ livekit/src/room/token_source/mod.rs | 281 +----------------- .../room/token_source/token_response_cache.rs | 38 +++ 3 files changed, 292 insertions(+), 274 deletions(-) create mode 100644 livekit/src/room/token_source/cache.rs create mode 100644 livekit/src/room/token_source/token_response_cache.rs diff --git a/livekit/src/room/token_source/cache.rs b/livekit/src/room/token_source/cache.rs new file mode 100644 index 000000000..abacaba0f --- /dev/null +++ b/livekit/src/room/token_source/cache.rs @@ -0,0 +1,247 @@ +use livekit_api::access_token; +use parking_lot::RwLock; +use std::sync::Arc; + +use crate::token_source::{ + TokenResponseCache, TokenResponseCacheValue, TokenResponseInMemoryCache, + TokenSourceConfigurable, TokenSourceFetchOptions, TokenSourceFixed, TokenSourceResponse, + TokenSourceResult, +}; + +pub trait TokenSourceFixedCached { + fn get_response_cache(&self) -> Arc>>; + + async fn update(&self) -> TokenSourceResult; + + async fn fetch_cached(&self) -> TokenSourceResult { + let cache = self.get_response_cache(); + + let cached_response_to_return = { + let cache_read = cache.read(); + let cached_value = cache_read.get(); + + if let Some(cached_response) = cached_value { + if access_token::is_token_valid(&cached_response.participant_token)? { + Some(cached_response.clone()) + } else { + None + } + } else { + None + } + }; + + if let Some(cached_response) = cached_response_to_return { + Ok(cached_response) + } else { + let response = self.update().await?; + cache.write().set(response.clone()); + Ok(response) + } + } +} + +pub trait TokenSourceConfigurableCached { + fn get_response_cache( + &self, + ) -> Arc>>; + + async fn update( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult; + + async fn fetch_cached( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult { + let cache = self.get_response_cache(); + + let cached_response_to_return = { + let cache_read = cache.read(); + let cached_value = cache_read.get(); + + if let Some((cached_options, cached_response)) = cached_value { + if options == cached_options + && access_token::is_token_valid(&cached_response.participant_token)? + { + Some(cached_response.clone()) + } else { + None + } + } else { + None + } + }; + + if let Some(cached_response) = cached_response_to_return { + Ok(cached_response) + } else { + let response = self.update(options).await?; + cache.write().set((options.clone(), response.clone())); + Ok(response) + } + } +} + +// FIXME: Why doesn't this work? +// impl TokenSourceConfigurable for T { +// async fn fetch( +// &self, +// options: &TokenSourceFetchOptions, +// ) -> TokenSourceResult { +// self.fetch_cached(options).await +// } +// } + +trait CacheType {} + +pub struct CacheConfigurable(T); +impl CacheType for CacheConfigurable {} + +pub struct CacheFixed(T); +impl CacheType for CacheFixed {} + +/// A conmposable TokenSource which can wrap either a [TokenSourceFixed] or a [TokenSourceConfigurable] and +/// caches the intermediate value in a [TokenResponseCache]. +pub struct TokenSourceCache< + Type: CacheType, + Value: TokenResponseCacheValue, + Cache: TokenResponseCache, +> { + inner: Type, + cache: Arc>, + _v: Value, // FIXME: how do I remove this? `Value` needs to be used in here or I get an error. +} + +impl + TokenSourceCache< + CacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + TokenResponseInMemoryCache<(TokenSourceFetchOptions, TokenSourceResponse)>, + > +{ + // FIXME: Is there some way I can make this `new` without requiring something like the below? + // TokenSourceCache::, _, _>::new(...) + fn new_configurable(inner_token_source: Inner) -> Self { + TokenSourceCache::new_configurable_with_cache( + inner_token_source, + TokenResponseInMemoryCache::new(), + ) + } +} + +impl + TokenSourceCache< + CacheFixed, + TokenSourceResponse, + TokenResponseInMemoryCache, + > +{ + // FIXME: Is there some way I can make this `new` without requiring something like the below? + // TokenSourceCache::, _, _>::new(...) + fn new_fixed(inner_token_source: Inner) -> Self { + TokenSourceCache::new_fixed_with_cache( + inner_token_source, + TokenResponseInMemoryCache::new(), + ) + } +} + +impl< + Inner: TokenSourceConfigurable, + Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)>, + > + TokenSourceCache< + CacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + Cache, + > +{ + fn new_configurable_with_cache(inner_token_source: Inner, token_cache: Cache) -> Self { + Self { + inner: CacheConfigurable(inner_token_source), + cache: Arc::new(RwLock::new(token_cache)), + + // FIXME: remove this! + _v: ( + TokenSourceFetchOptions::default(), + TokenSourceResponse { server_url: "".into(), participant_token: "".into() }, + ), + } + } +} + +impl> + TokenSourceCache, TokenSourceResponse, Cache> +{ + fn new_fixed_with_cache(inner_token_source: Inner, token_cache: Cache) -> Self { + Self { + inner: CacheFixed(inner_token_source), + cache: Arc::new(RwLock::new(token_cache)), + + // FIXME: remove this! + _v: TokenSourceResponse { server_url: "".into(), participant_token: "".into() }, + } + } +} + +impl< + Inner: TokenSourceConfigurable, + Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)>, + > TokenSourceConfigurableCached + for TokenSourceCache< + CacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + Cache, + > +{ + fn get_response_cache( + &self, + ) -> Arc>> { + self.cache.clone() + } + async fn update( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult { + self.inner.0.fetch(options).await + } +} + +impl> TokenSourceFixedCached + for TokenSourceCache, TokenSourceResponse, Cache> +{ + fn get_response_cache(&self) -> Arc>> { + self.cache.clone() + } + async fn update(&self) -> TokenSourceResult { + self.inner.0.fetch().await + } +} + +impl< + Inner: TokenSourceConfigurable, + Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)>, + > TokenSourceConfigurable + for TokenSourceCache< + CacheConfigurable, + (TokenSourceFetchOptions, TokenSourceResponse), + Cache, + > +{ + async fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult { + self.fetch_cached(options).await + } +} + +impl> TokenSourceFixed + for TokenSourceCache, TokenSourceResponse, Cache> +{ + async fn fetch(&self) -> TokenSourceResult { + self.fetch_cached().await + } +} diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs index d18874cfb..8a99753bf 100644 --- a/livekit/src/room/token_source/mod.rs +++ b/livekit/src/room/token_source/mod.rs @@ -14,21 +14,26 @@ use livekit_api::access_token::{self, AccessTokenError}; use livekit_protocol as proto; -use parking_lot::RwLock; -use std::{future::Future, pin::Pin, sync::Arc}; +use std::{future::Future, pin::Pin}; +mod cache; mod error; mod fetch_options; mod minter_credentials; mod request_response; +mod token_response_cache; mod traits; +pub use cache::{CacheConfigurable, CacheFixed, TokenSourceCache}; pub use error::{TokenSourceError, TokenSourceResult}; pub use fetch_options::TokenSourceFetchOptions; pub use minter_credentials::{ MinterCredentials, MinterCredentialsEnvironment, MinterCredentialsSource, }; pub use request_response::{TokenSourceRequest, TokenSourceResponse}; +pub use token_response_cache::{ + TokenResponseCache, TokenResponseCacheValue, TokenResponseInMemoryCache, +}; pub use traits::{ TokenSourceConfigurable, TokenSourceConfigurableSynchronous, TokenSourceFixed, TokenSourceFixedSynchronous, @@ -242,275 +247,3 @@ impl< (self.0)(options).await } } - - - - - - - -trait TokenResponseCacheValue {} - -impl TokenResponseCacheValue for TokenSourceResponse {} -impl TokenResponseCacheValue for (TokenSourceFetchOptions, TokenSourceResponse) {} - -/// Represents a mechanism by which token responses can be cached -/// -/// When used with a TokenSourceFixed, `Value` is `TokenSourceResponse` -/// When used with a TokenSourceConfigurable, `Value` is `(TokenSourceFetchOptions, TokenSourceResponse)` -trait TokenResponseCache { - fn get(&self) -> Option<&Value>; - fn set(&mut self, value: Value); - fn clear(&mut self); -} - -/// In-memory implementation of [TokenResponseCache] -struct TokenResponseInMemoryCache(Option); -impl TokenResponseInMemoryCache { - pub fn new() -> Self { - Self(None) - } -} - -impl TokenResponseCache for TokenResponseInMemoryCache { - fn get(&self) -> Option<&Value> { - self.0.as_ref() - } - fn set(&mut self, value: Value) { - self.0 = Some(value); - } - fn clear(&mut self) { - self.0 = None; - } -} - - - - - - - - -trait TokenSourceFixedCached { - fn get_response_cache(&self) -> Arc>>; - - async fn update(&self) -> TokenSourceResult; - - async fn fetch_cached(&self) -> TokenSourceResult { - let cache = self.get_response_cache(); - - let cached_response_to_return = { - let cache_read = cache.read(); - let cached_value = cache_read.get(); - - if let Some(cached_response) = cached_value { - if access_token::is_token_valid(&cached_response.participant_token)? { - Some(cached_response.clone()) - } else { - None - } - } else { - None - } - }; - - if let Some(cached_response) = cached_response_to_return { - Ok(cached_response) - } else { - let response = self.update().await?; - cache.write().set(response.clone()); - Ok(response) - } - } -} - -trait TokenSourceConfigurableCached { - fn get_response_cache(&self) -> Arc>>; - - async fn update(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult; - - async fn fetch_cached( - &self, - options: &TokenSourceFetchOptions, - ) -> TokenSourceResult { - let cache = self.get_response_cache(); - - let cached_response_to_return = { - let cache_read = cache.read(); - let cached_value = cache_read.get(); - - if let Some((cached_options, cached_response)) = cached_value { - if options == cached_options && access_token::is_token_valid(&cached_response.participant_token)? { - Some(cached_response.clone()) - } else { - None - } - } else { - None - } - }; - - if let Some(cached_response) = cached_response_to_return { - Ok(cached_response) - } else { - let response = self.update(options).await?; - cache.write().set((options.clone(), response.clone())); - Ok(response) - } - } -} - -// FIXME: Why doesn't this work? -// impl TokenSourceConfigurable for T { -// async fn fetch( -// &self, -// options: &TokenSourceFetchOptions, -// ) -> TokenSourceResult { -// self.fetch_cached(options).await -// } -// } - - - - - - -trait TokenSourceCacheType {} - -struct TokenSourceCacheConfigurable(T); -impl TokenSourceCacheType for TokenSourceCacheConfigurable {} - -struct TokenSourceCacheFixed(T); -impl TokenSourceCacheType for TokenSourceCacheFixed {} - -/// A conmposable TokenSource which can wrap either a [TokenSourceFixed] or a [TokenSourceConfigurable] and -/// caches the intermediate value in a [TokenResponseCache]. -struct TokenSourceCache> { - inner: Type, - cache: Arc>, - _v: Value, // FIXME: how do I remove this? `Value` needs to be used in here or I get an error. -} - -impl TokenSourceCache< - TokenSourceCacheConfigurable, - (TokenSourceFetchOptions, TokenSourceResponse), - TokenResponseInMemoryCache<(TokenSourceFetchOptions, TokenSourceResponse)> -> { - // FIXME: Is there some way I can make this `new` without requiring something like the below? - // TokenSourceCache::, _, _>::new(...) - fn new_configurable(inner_token_source: Inner) -> Self { - TokenSourceCache::new_configurable_with_cache(inner_token_source, TokenResponseInMemoryCache::new()) - } -} - -impl TokenSourceCache< - TokenSourceCacheFixed, - TokenSourceResponse, - TokenResponseInMemoryCache -> { - // FIXME: Is there some way I can make this `new` without requiring something like the below? - // TokenSourceCache::, _, _>::new(...) - fn new_fixed(inner_token_source: Inner) -> Self { - TokenSourceCache::new_fixed_with_cache(inner_token_source, TokenResponseInMemoryCache::new()) - } -} - -impl< - Inner: TokenSourceConfigurable, - Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)> -> TokenSourceCache< - TokenSourceCacheConfigurable, - (TokenSourceFetchOptions, TokenSourceResponse), - Cache -> { - fn new_configurable_with_cache(inner_token_source: Inner, token_cache: Cache) -> Self { - Self { - inner: TokenSourceCacheConfigurable(inner_token_source), - cache: Arc::new(RwLock::new(token_cache)), - - // FIXME: remove this! - _v: (TokenSourceFetchOptions::default(), TokenSourceResponse { server_url: "".into(), participant_token: "".into() }), - } - } -} - -impl< - Inner: TokenSourceFixed, - Cache: TokenResponseCache -> TokenSourceCache< - TokenSourceCacheFixed, - TokenSourceResponse, - Cache -> { - fn new_fixed_with_cache(inner_token_source: Inner, token_cache: Cache) -> Self { - Self { - inner: TokenSourceCacheFixed(inner_token_source), - cache: Arc::new(RwLock::new(token_cache)), - - // FIXME: remove this! - _v: TokenSourceResponse { server_url: "".into(), participant_token: "".into() }, - } - } -} - - -impl< - Inner: TokenSourceConfigurable, - Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)> -> TokenSourceConfigurableCached for TokenSourceCache< - TokenSourceCacheConfigurable, - (TokenSourceFetchOptions, TokenSourceResponse), - Cache, -> { - fn get_response_cache(&self) -> Arc>> { - self.cache.clone() - } - async fn update(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { - self.inner.0.fetch(options).await - } -} - -impl< - Inner: TokenSourceFixed, - Cache: TokenResponseCache -> TokenSourceFixedCached for TokenSourceCache< - TokenSourceCacheFixed, - TokenSourceResponse, - Cache, -> { - fn get_response_cache(&self) -> Arc>> { - self.cache.clone() - } - async fn update(&self) -> TokenSourceResult { - self.inner.0.fetch().await - } -} - - - - -impl< - Inner: TokenSourceConfigurable, - Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)> -> TokenSourceConfigurable for TokenSourceCache< - TokenSourceCacheConfigurable, - (TokenSourceFetchOptions, TokenSourceResponse), - Cache, -> { - async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { - self.fetch_cached(options).await - } -} - -impl< - Inner: TokenSourceFixed, - Cache: TokenResponseCache -> TokenSourceFixed for TokenSourceCache< - TokenSourceCacheFixed, - TokenSourceResponse, - Cache, -> { - async fn fetch(&self) -> TokenSourceResult { - self.fetch_cached().await - } -} diff --git a/livekit/src/room/token_source/token_response_cache.rs b/livekit/src/room/token_source/token_response_cache.rs new file mode 100644 index 000000000..0a8e5aaa9 --- /dev/null +++ b/livekit/src/room/token_source/token_response_cache.rs @@ -0,0 +1,38 @@ +use crate::token_source::{TokenSourceFetchOptions, TokenSourceResponse}; + +pub trait TokenResponseCacheValue {} + +impl TokenResponseCacheValue for TokenSourceResponse {} +impl TokenResponseCacheValue for (TokenSourceFetchOptions, TokenSourceResponse) {} + +/// Represents a mechanism by which token responses can be cached +/// +/// When used with a TokenSourceFixed, `Value` is `TokenSourceResponse` +/// When used with a TokenSourceConfigurable, `Value` is `(TokenSourceFetchOptions, TokenSourceResponse)` +pub trait TokenResponseCache { + fn get(&self) -> Option<&Value>; + fn set(&mut self, value: Value); + fn clear(&mut self); +} + +/// In-memory implementation of [TokenResponseCache] +pub struct TokenResponseInMemoryCache(Option); +impl TokenResponseInMemoryCache { + pub fn new() -> Self { + Self(None) + } +} + +impl TokenResponseCache + for TokenResponseInMemoryCache +{ + fn get(&self) -> Option<&Value> { + self.0.as_ref() + } + fn set(&mut self, value: Value) { + self.0 = Some(value); + } + fn clear(&mut self) { + self.0 = None; + } +} From a75174280294a76462759bc5adb20f8be899b731 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 17 Oct 2025 17:21:04 -0400 Subject: [PATCH 29/29] feat: pass through token source cache clear cache method --- livekit/src/room/token_source/cache.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/livekit/src/room/token_source/cache.rs b/livekit/src/room/token_source/cache.rs index abacaba0f..c35a10d91 100644 --- a/livekit/src/room/token_source/cache.rs +++ b/livekit/src/room/token_source/cache.rs @@ -186,6 +186,14 @@ impl> } } +impl> + TokenSourceCache +{ + fn clear_cache(&mut self) { + self.cache.write().clear(); + } +} + impl< Inner: TokenSourceConfigurable, Cache: TokenResponseCache<(TokenSourceFetchOptions, TokenSourceResponse)>,