diff --git a/.changeset/extract_livekit_region_crate.md b/.changeset/extract_livekit_region_crate.md new file mode 100644 index 000000000..016eaa446 --- /dev/null +++ b/.changeset/extract_livekit_region_crate.md @@ -0,0 +1,9 @@ +--- +livekit-api: patch +livekit: patch +livekit-ffi: patch +livekit-region: patch +--- + +Moves the internal region-discovery cache into a new `livekit-region` crate. No +public API or behaviour change. diff --git a/.changeset/extract_livekit_signaling_crate.md b/.changeset/extract_livekit_signaling_crate.md new file mode 100644 index 000000000..0f17f7e38 --- /dev/null +++ b/.changeset/extract_livekit_signaling_crate.md @@ -0,0 +1,14 @@ +--- +livekit-api: patch +livekit: patch +livekit-ffi: patch +livekit-signaling: patch +--- + +Moves the signalling client into a new `livekit-signaling` crate. livekit-api +re-exports it under the historical `livekit_api::signal_client` path, now marked +deprecated: it is internal SDK API, and dependents should use livekit-signaling +directly. livekit-api no longer depends on livekit-net. + +Also drops two dependencies that were declared but never used: `scopeguard` and +`bytes`. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 374d5248f..6254ee13c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,3 +10,4 @@ /livekit-wakeword/ @pham-tuan-binh /livekit-net/ @jhugman /livekit-token-source/ @MaxHeimbrock +/livekit-signaling/ @lukasIO diff --git a/.github/workflows/test-api.yml b/.github/workflows/test-api.yml index 113e5647b..b8167516f 100644 --- a/.github/workflows/test-api.yml +++ b/.github/workflows/test-api.yml @@ -28,8 +28,12 @@ jobs: # Exercise every runtime backend so a regression in one (e.g. the async/isahc # server API silently failing to compile) is caught. Each leg pins a single # runtime via --no-default-features; the mock server backs the legs that make - # real requests (services-*), and is a harmless no-op for the signal-client + # real requests (services-*), and is a harmless no-op for the livekit-signaling # legs, which spin up their own ephemeral listeners. + # + # The job name stays `livekit-api` even though it now also covers + # livekit-signaling: it is a required status check, and renaming it would break + # branch protection. livekit-api: runs-on: ubuntu-latest strategy: @@ -40,10 +44,10 @@ jobs: cmd: cargo test -p livekit-api --no-default-features --features services-tokio,access-token --lib services::api_test -- --nocapture - name: services (async / isahc) cmd: cargo test -p livekit-api --no-default-features --features services-async,access-token --test services_async -- --nocapture - - name: signal-client (tokio) - cmd: cargo test -p livekit-api --no-default-features --features signal-client-tokio --lib signal_client -- --nocapture - - name: signal-client (async) - cmd: cargo test -p livekit-api --no-default-features --features signal-client-async --lib signal_client -- --nocapture + - name: livekit-signaling (tokio) + cmd: cargo test -p livekit-signaling --no-default-features --features tokio -- --nocapture + - name: livekit-signaling (async) + cmd: cargo test -p livekit-signaling --no-default-features --features async -- --nocapture services: mock-server: image: livekit/test-server:latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5ab0bd9d0..c1404b345 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -181,4 +181,4 @@ jobs: shell: bash run: | cargo test --verbose --target ${{ matrix.target }} -p livekit-net --features native-tokio - cargo test --verbose --target ${{ matrix.target }} -p livekit-api --features signal-client-tokio \ No newline at end of file + cargo test --verbose --target ${{ matrix.target }} -p livekit-signaling --features native-tokio \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index bfa1b206e..8ec800716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3928,27 +3928,20 @@ dependencies = [ name = "livekit-api" version = "0.6.3" dependencies = [ - "async-trait", "base64 0.21.7", - "bytes", - "device-info", - "flate2", "futures", "http 1.4.2", "isahc", - "livekit-common", - "livekit-net", "livekit-protocol", + "livekit-region", "livekit-runtime", + "livekit-signaling", "livekit-token", "log", - "os_info", - "parking_lot", "pbjson-types", "prost 0.12.6", "rand 0.9.5", "reqwest", - "scopeguard", "serde", "serde_json", "sha2", @@ -4069,6 +4062,13 @@ dependencies = [ "serde", ] +[[package]] +name = "livekit-region" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "livekit-runtime" version = "0.4.0" @@ -4081,6 +4081,30 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "livekit-signaling" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.21.7", + "device-info", + "flate2", + "http 1.4.2", + "livekit-common", + "livekit-net", + "livekit-protocol", + "livekit-region", + "livekit-runtime", + "log", + "os_info", + "parking_lot", + "prost 0.12.6", + "serde_json", + "thiserror 2.0.19", + "tokio", + "url", +] + [[package]] name = "livekit-token" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7c64dfa64..5c5c8c539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "livekit", "livekit-api", "livekit-protocol", + "livekit-region", "livekit-common", "livekit-data-stream", "livekit-ffi", @@ -14,6 +15,7 @@ members = [ "livekit-ffi-node-bindings", "livekit-net", "livekit-runtime", + "livekit-signaling", "livekit-wakeword", "libwebrtc", "soxr-sys", @@ -55,12 +57,14 @@ livekit = { version = "0.8.3", path = "livekit" } livekit-api = { version = "0.6.3", path = "livekit-api" } livekit-ffi = { version = "0.12.75", path = "livekit-ffi" } livekit-datatrack = { version = "0.1.13", path = "livekit-datatrack" } +livekit-signaling = { version = "0.1.0", path = "livekit-signaling" } livekit-token = { version = "0.1.0", path = "livekit-token" } livekit-token-source = { version = "0.1.1", path = "livekit-token-source" } livekit-common = { version = "0.1.1", path = "livekit-common" } livekit-data-stream = { version = "0.1.2", path = "livekit-data-stream" } livekit-net = { version = "0.1.2", path = "livekit-net" } livekit-protocol = { version = "0.7.12", path = "livekit-protocol" } +livekit-region = { version = "0.1.0", path = "livekit-region" } # default-features off so each consumer selects its runtime explicitly # (livekit-runtime/tokio | /async | /dispatcher); otherwise the default `tokio` # feature is forced on everywhere and collides with `async`/`dispatcher` builds. diff --git a/knope.toml b/knope.toml index 4034078e6..2689c692f 100644 --- a/knope.toml +++ b/knope.toml @@ -94,6 +94,24 @@ versioned_files = [ changelog = "livekit-token/CHANGELOG.md" scopes = ["livekit-token"] +[packages.livekit-region] +versioned_files = [ + "livekit-region/Cargo.toml", + "Cargo.lock", + { path = "Cargo.toml", dependency = "livekit-region" }, +] +changelog = "livekit-region/CHANGELOG.md" +scopes = ["livekit-region"] + +[packages.livekit-signaling] +versioned_files = [ + "livekit-signaling/Cargo.toml", + "Cargo.lock", + { path = "Cargo.toml", dependency = "livekit-signaling" }, +] +changelog = "livekit-signaling/CHANGELOG.md" +scopes = ["livekit-signaling"] + [packages.libwebrtc] versioned_files = [ "libwebrtc/Cargo.toml", diff --git a/livekit-api/Cargo.toml b/livekit-api/Cargo.toml index 64323f21b..e55ca1f71 100644 --- a/livekit-api/Cargo.toml +++ b/livekit-api/Cargo.toml @@ -11,28 +11,19 @@ readme = "README.md" # By default ws TLS is not enabled default = ["services-tokio", "access-token", "webhooks"] -# Signalling client, blind to the transport backend. Pulls livekit-net (no -# backend), the tokio crate (signal_client uses tokio::sync directly), and -# protobuf helpers. The livekit-runtime flavor comes from the signal-client- -# wrappers below or the top crate — not here, so async/dispatcher don't collide -# with tokio under the one-runtime guard. Needs a runtime flavor to compile, and -# a registered livekit_net::transport() at runtime. -signal-client = [ - "dep:livekit-net", - "dep:livekit-runtime", - "dep:tokio", - "dep:base64", - "dep:flate2", - "dep:bytes", - "dep:serde_json", -] +# Signalling client. This is an internal compatibility shim: prefer depending on +# the livekit-signaling crate directly. Enabling it here only re-exports that +# crate under the historical `livekit_api::signal_client` path, which is marked +# deprecated. Resolution is unchanged from when signal_client lived here: +# livekit-signaling/native- forwards to livekit-net/native-, which +# already folds in livekit-runtime/. +signal-client = ["dep:livekit-signaling"] +signal-client-tokio = ["signal-client", "livekit-signaling/native-tokio"] +signal-client-async = ["signal-client", "livekit-signaling/native-async"] +signal-client-dispatcher = ["signal-client", "livekit-signaling/native-dispatcher"] -signal-client-tokio = ["signal-client", "livekit-net/native-tokio", "livekit-runtime/tokio"] -signal-client-async = ["signal-client", "livekit-net/native-async", "livekit-runtime/async"] -signal-client-dispatcher = ["signal-client", "livekit-net/native-dispatcher", "livekit-runtime/dispatcher"] - -services-tokio = ["access-token", "dep:reqwest", "dep:tokio", "tokio/time", "dep:livekit-runtime", "livekit-runtime/tokio"] -services-async = ["access-token", "dep:isahc", "dep:livekit-runtime", "livekit-runtime/async"] +services-tokio = ["access-token", "dep:livekit-region", "dep:reqwest", "dep:tokio", "tokio/time", "dep:livekit-runtime", "livekit-runtime/tokio"] +services-async = ["access-token", "dep:livekit-region", "dep:isahc", "dep:livekit-runtime", "livekit-runtime/async"] access-token = ["dep:livekit-token"] webhooks = ["access-token", "dep:serde_json", "dep:base64"] @@ -57,28 +48,27 @@ webhooks = ["access-token", "dep:serde_json", "dep:base64"] # the embedded certificates can expire, requiring a fresh build to update them. # Uses the platform's native TLS implementation (OpenSSL on Linux, Secure Transport on macOS, SChannel on Windows) -native-tls = ["livekit-net?/native-tls", "reqwest?/native-tls"] +native-tls = ["livekit-signaling?/native-tls", "reqwest?/native-tls"] # Same as native-tls but compiles OpenSSL from source (useful for cross-compilation) -native-tls-vendored = ["livekit-net?/native-tls-vendored", "reqwest?/native-tls-vendored"] +native-tls-vendored = ["livekit-signaling?/native-tls-vendored", "reqwest?/native-tls-vendored"] # Uses rustls with the operating system's CA certificate store. # Requires ca-certificates to be installed in container environments. -rustls-tls-native-roots = ["livekit-net?/rustls-tls-native-roots", "reqwest?/rustls-tls-native-roots"] +rustls-tls-native-roots = ["livekit-signaling?/rustls-tls-native-roots", "reqwest?/rustls-tls-native-roots"] # Uses rustls with Mozilla's bundled root certificates. # RECOMMENDED for container deployments - no system CA certificates required. -rustls-tls-webpki-roots = ["livekit-net?/rustls-tls-webpki-roots", "reqwest?/rustls-tls-webpki-roots"] -__rustls-tls = ["livekit-net?/__rustls-tls", "reqwest?/__rustls"] +rustls-tls-webpki-roots = ["livekit-signaling?/rustls-tls-webpki-roots", "reqwest?/rustls-tls-webpki-roots"] +__rustls-tls = ["livekit-signaling?/__rustls-tls", "reqwest?/__rustls"] [dependencies] -livekit-net = { workspace = true, optional = true } +livekit-signaling = { workspace = true, optional = true } livekit-protocol = { workspace = true } +livekit-region = { workspace = true, optional = true } livekit-token = { workspace = true, optional = true } -livekit-common = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } sha2 = "0.10" url = "2.3" log = { workspace = true } -parking_lot = { workspace = true } prost = "0.12" pbjson-types = "0.6" @@ -95,15 +85,9 @@ http = "1.1" reqwest = { version = "0.12", default-features = false, features = [ "json" ], optional = true } isahc = { version = "1.7.2", default-features = false, features = [ "json", "text-decoding" ], optional = true } -flate2 = { version = "1", optional = true } -bytes = { workspace = true, optional = true } -scopeguard = "1.2.0" rand = { workspace = true } -os_info = "3.14.0" -device-info = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["rt", "rt-multi-thread", "net", "time", "macros", "io-util"] } # Minimal executor to drive the runtime-agnostic `services-async` (isahc) tests. futures = "0.3" -async-trait = "0.1" diff --git a/livekit-api/src/lib.rs b/livekit-api/src/lib.rs index d953e5e1e..c61091b2b 100644 --- a/livekit-api/src/lib.rs +++ b/livekit-api/src/lib.rs @@ -24,17 +24,23 @@ pub use livekit_token as access_token; #[cfg(any(feature = "services-tokio", feature = "services-async"))] pub mod services; +// The signalling client lives in the livekit-signaling crate. Unlike +// `access_token`, this path is NOT supported API: nothing in the workspace uses +// it any more and it exists only so existing dependents keep compiling. +// +// The deprecation fires on `use livekit_api::signal_client;` but not on +// `use livekit_api::signal_client::{Item}` — rustc only lints a deprecated +// module when it is the final path segment. `#[doc(hidden)]` keeps it out of the +// published docs so it stops reading as blessed API. #[cfg(feature = "signal-client")] -pub mod signal_client; +#[deprecated(note = "internal SDK API; depend on livekit-signaling directly")] +#[doc(hidden)] +pub mod signal_client { + pub use livekit_signaling::*; +} #[cfg(any(feature = "services-tokio", feature = "services-async"))] mod http_client; -// Region-discovery helpers shared by the signaling region provider -// (signal_client::region_url_provider) and the API failover region cache -// (services::failover). -#[cfg(any(feature = "signal-client", feature = "services-tokio", feature = "services-async"))] -mod region; - #[cfg(feature = "webhooks")] pub mod webhooks; diff --git a/livekit-api/src/services/failover.rs b/livekit-api/src/services/failover.rs index ab45a6bf3..3d3d63b24 100644 --- a/livekit-api/src/services/failover.rs +++ b/livekit-api/src/services/failover.rs @@ -24,7 +24,7 @@ use std::{sync::OnceLock, time::Duration}; use http::header::{HeaderMap, CONTENT_LENGTH, CONTENT_TYPE}; use url::Url; -use crate::region::{is_cloud_host, parse_max_age, Cached, RegionCache, RegionsResponse}; +use livekit_region::{is_cloud_host, parse_max_age, Cached, RegionCache, RegionsResponse}; /// Total attempts (the original request plus fallback regions) and the base /// retry backoff are fixed, not user-configurable, so retries can't be tuned to @@ -119,7 +119,7 @@ pub(crate) async fn backoff_sleep(d: Duration) { const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(2); /// Process-wide region cache for the API failover path. Owns the API instance of -/// the shared [`RegionCache`] (which stores `http(s)` URLs; see [`crate::region`]). +/// the shared [`RegionCache`] (which stores `http(s)` URLs; see [`livekit_region`]). fn region_cache() -> &'static RegionCache { static CACHE: OnceLock = OnceLock::new(); CACHE.get_or_init(|| RegionCache::new(RegionCache::DEFAULT_TTL)) diff --git a/livekit-region/Cargo.toml b/livekit-region/Cargo.toml new file mode 100644 index 000000000..67cbbfb06 --- /dev/null +++ b/livekit-region/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "livekit-region" +version = "0.1.0" +license.workspace = true +description = "Internal region-discovery cache shared by the LiveKit signalling and server-API clients" +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +serde = { workspace = true, features = ["derive"] } diff --git a/livekit-region/README.md b/livekit-region/README.md new file mode 100644 index 000000000..2220b829e --- /dev/null +++ b/livekit-region/README.md @@ -0,0 +1,8 @@ +# LiveKit Region + +An internal crate holding the region-discovery cache shared by the LiveKit +signalling client and the server-API failover path: the `/settings/regions` +response types, the cloud-host and `Cache-Control` helpers, and `RegionCache`. + +To build applications with LiveKit, please use the public APIs provided by the +[livekit](../livekit) and [livekit-api](../livekit-api) crates. diff --git a/livekit-api/src/region.rs b/livekit-region/src/lib.rs similarity index 89% rename from livekit-api/src/region.rs rename to livekit-region/src/lib.rs index e0e9848c2..bcc54b2d5 100644 --- a/livekit-api/src/region.rs +++ b/livekit-region/src/lib.rs @@ -13,16 +13,16 @@ // limitations under the License. //! Region discovery shared by the two `/settings/regions` consumers: the -//! signaling region provider ([`crate::signal_client::region_url_provider`]) and -//! the API failover ([`crate::services::failover`]). +//! signalling region provider in `livekit-signaling`, and the API failover in +//! `livekit-api`. //! //! This holds the runtime-independent pieces both need: the response types, the //! cloud-host / `Cache-Control` helpers, and the [`RegionCache`] (TTL lookups, //! stale fallback, pruning). Each consumer owns its own cache instance — they -//! store URLs in different schemes (`wss://` for signaling, `http(s)` for the API) -//! — but the caching logic lives here once. The network fetch stays with each -//! consumer: they authenticate differently (bearer token vs. forwarded headers) -//! and run on different HTTP clients / feature islands. +//! store URLs in different schemes (`wss://` for signalling, `http(s)` for the +//! API) — but the caching logic lives here once. The network fetch stays with +//! each consumer: they authenticate differently (bearer token vs. forwarded +//! headers) and run on different HTTP clients / feature islands. use std::{ collections::HashMap, @@ -37,26 +37,26 @@ use serde::Deserialize; /// body missing `regions` is rejected so a malformed discovery response surfaces /// as an error rather than an empty region list. #[derive(Debug, Deserialize)] -pub(crate) struct RegionsResponse { +pub struct RegionsResponse { pub regions: Vec, } #[derive(Debug, Deserialize)] -pub(crate) struct RegionInfo { +pub struct RegionInfo { pub url: String, } /// Reports whether `host` belongs to a LiveKit Cloud project — a /// `*.livekit.cloud` or `*.livekit.run` subdomain. Region discovery and API /// failover only engage for these hosts. -pub(crate) fn is_cloud_host(host: &str) -> bool { +pub fn is_cloud_host(host: &str) -> bool { host.ends_with(".livekit.cloud") || host.ends_with(".livekit.run") } /// Parses the `max-age` directive (in seconds) out of a `Cache-Control` header /// value, e.g. `"public, max-age=300"` -> `Some(300s)`. Returns `None` when the /// directive is absent or unparseable. Directive names are case-insensitive. -pub(crate) fn parse_max_age(cache_control: &str) -> Option { +pub fn parse_max_age(cache_control: &str) -> Option { cache_control.split(',').find_map(|directive| { let (name, value) = directive.split_once('=')?; name.trim().eq_ignore_ascii_case("max-age").then_some(())?; @@ -73,7 +73,7 @@ struct CachedRegions { } /// Outcome of a [`RegionCache::get`] lookup. -pub(crate) enum Cached { +pub enum Cached { /// Entry exists and is within the TTL — safe to use without re-fetching. Fresh(Vec), /// Entry exists but is older than the TTL — the caller should re-fetch, but @@ -88,7 +88,7 @@ pub(crate) enum Cached { /// TTL derived from the server's `Cache-Control: max-age`, fresh/stale/miss /// lookups (stale entries are retained for fallback), and pruning of failed /// regions. -pub(crate) struct RegionCache { +pub struct RegionCache { entries: Mutex>, default_ttl: Duration, } @@ -96,15 +96,15 @@ pub(crate) struct RegionCache { impl RegionCache { /// Fallback entry lifetime, used when the region response carries no /// `Cache-Control: max-age`. Matches client-sdk-js's `DEFAULT_MAX_AGE_MS`. - pub(crate) const DEFAULT_TTL: Duration = Duration::from_secs(5); + pub const DEFAULT_TTL: Duration = Duration::from_secs(5); - pub(crate) fn new(default_ttl: Duration) -> Self { + pub fn new(default_ttl: Duration) -> Self { Self { entries: Mutex::new(HashMap::new()), default_ttl } } /// Looks up the cached region URLs for `host`, reporting whether the entry is /// fresh (within its TTL), stale, or absent. - pub(crate) fn get(&self, host: &str) -> Cached { + pub fn get(&self, host: &str) -> Cached { let entries = self.entries.lock().unwrap(); match entries.get(host) { Some(e) if e.fetched_at.elapsed() < e.ttl => Cached::Fresh(e.urls.clone()), @@ -116,7 +116,7 @@ impl RegionCache { /// Stores `urls` for `host`, honouring the server's `Cache-Control: max-age` /// (`max_age`) as the entry's TTL and falling back to [`Self::DEFAULT_TTL`] /// when the header is absent. - pub(crate) fn insert(&self, host: String, urls: Vec, max_age: Option) { + pub fn insert(&self, host: String, urls: Vec, max_age: Option) { let ttl = max_age.unwrap_or(self.default_ttl); self.entries .lock() @@ -128,8 +128,7 @@ impl RegionCache { /// out again. If that empties the list, the entry is dropped entirely, /// forcing a re-fetch on the next lookup. // Only the signaling consumer prunes individual failed regions. - #[allow(dead_code)] - pub(crate) fn mark_failed(&self, host: &str, failed_url: &str) { + pub fn mark_failed(&self, host: &str, failed_url: &str) { let mut entries = self.entries.lock().unwrap(); if let Some(entry) = entries.get_mut(host) { entry.urls.retain(|u| u != failed_url); @@ -140,14 +139,12 @@ impl RegionCache { } /// Drops the cached entry for `host`, forcing a re-fetch on the next lookup. - #[allow(dead_code)] - pub(crate) fn invalidate(&self, host: &str) { + pub fn invalidate(&self, host: &str) { self.entries.lock().unwrap().remove(host); } /// Drops every cached entry. - #[allow(dead_code)] - pub(crate) fn clear(&self) { + pub fn clear(&self) { self.entries.lock().unwrap().clear(); } } diff --git a/livekit-signaling/Cargo.toml b/livekit-signaling/Cargo.toml new file mode 100644 index 000000000..95a4d641c --- /dev/null +++ b/livekit-signaling/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "livekit-signaling" +version = "0.1.0" +license.workspace = true +description = "Internal LiveKit signalling client, blind to the transport backend" +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[features] +default = [] + +# Runtime flavour only — transport stays blind; the host must register a +# livekit_net transport (set_ws_client / set_http_client) before connecting. +tokio = ["livekit-net/tokio", "livekit-runtime/tokio"] +async = ["livekit-net/async", "livekit-runtime/async"] +dispatcher = ["livekit-net/dispatcher", "livekit-runtime/dispatcher"] + +# Flavour plus livekit-net's bundled native backend. +native-tokio = ["tokio", "livekit-net/native-tokio"] +native-async = ["async", "livekit-net/native-async"] +native-dispatcher = ["dispatcher", "livekit-net/native-dispatcher"] + +# TLS, forwarded to livekit-net (only meaningful with a native backend). +native-tls = ["livekit-net/native-tls"] +native-tls-vendored = ["livekit-net/native-tls-vendored"] +rustls-tls-native-roots = ["livekit-net/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["livekit-net/rustls-tls-webpki-roots"] +__rustls-tls = ["livekit-net/__rustls-tls"] + +[dependencies] +livekit-net = { workspace = true } +livekit-protocol = { workspace = true } +livekit-common = { workspace = true } +livekit-region = { workspace = true } +# Inherited so the workspace entry's default-features = false is honored, which +# keeps livekit-runtime's one-runtime compile_error! guard satisfied. +livekit-runtime = { workspace = true } +device-info = { workspace = true } +thiserror = { workspace = true } +log = { workspace = true } +parking_lot = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, default-features = false, features = ["sync", "macros", "signal", "io-util", "net"] } +base64 = { version = "0.21", features = ["std"] } +flate2 = "1" +http = "1.1" +os_info = "3.14.0" +prost = "0.12" +url = "2.3" + +[dev-dependencies] +async-trait = "0.1" +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "net", "time", "macros", "io-util"] } diff --git a/livekit-signaling/README.md b/livekit-signaling/README.md new file mode 100644 index 000000000..6451906c1 --- /dev/null +++ b/livekit-signaling/README.md @@ -0,0 +1,9 @@ +# LiveKit Signaling + +An internal crate holding the LiveKit signalling client: the WebSocket signal +stream, reconnect/resume handling, and LiveKit Cloud region discovery. Transport +is provided by [livekit-net](../livekit-net), so this crate is blind to the +backend. + +To build applications with LiveKit, please use the public APIs provided by the +[livekit](../livekit) crate. diff --git a/livekit-api/src/signal_client/mod.rs b/livekit-signaling/src/lib.rs similarity index 98% rename from livekit-api/src/signal_client/mod.rs rename to livekit-signaling/src/lib.rs index 472642332..04e1caae5 100644 --- a/livekit-api/src/signal_client/mod.rs +++ b/livekit-signaling/src/lib.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![doc = include_str!("../README.md")] + use std::{ borrow::Cow, fmt::Debug, @@ -33,7 +35,7 @@ use prost::Message; use thiserror::Error; use tokio::sync::{mpsc, Mutex as AsyncMutex, RwLock as AsyncRwLock}; -use crate::signal_client::signal_stream::SignalStream; +use crate::signal_stream::SignalStream; use livekit_net::HttpClientExt; mod region_url_provider; @@ -978,20 +980,20 @@ fn get_livekit_url( } /// Build the `Authorization: Bearer ` header vec used by HTTP/WS callers. -pub(super) fn bearer_headers(token: &str) -> Vec { +pub(crate) fn bearer_headers(token: &str) -> Vec { vec![livekit_net::Header { name: "Authorization".into(), value: format!("Bearer {token}") }] } /// Resolve the registered WebSocket client, or a permanent /// [`SignalError::TransportNotConfigured`] if none has been set. Centralises the /// lookup so callers share one error rather than each inventing a string. -pub(super) fn require_ws_client() -> SignalResult> { +pub(crate) fn require_ws_client() -> SignalResult> { livekit_net::ws_client().ok_or(SignalError::TransportNotConfigured) } /// Resolve the registered HTTP client, or a permanent /// [`SignalError::TransportNotConfigured`] if none has been set. -pub(super) fn require_http_client() -> SignalResult> { +pub(crate) fn require_http_client() -> SignalResult> { livekit_net::http_client().ok_or(SignalError::TransportNotConfigured) } @@ -1000,7 +1002,7 @@ pub(super) fn require_http_client() -> SignalResult SignalResult<()> { +pub(crate) fn check_token_format(token: &str) -> SignalResult<()> { http::HeaderValue::from_str(&format!("Bearer {token}")) .map(|_| ()) .map_err(|_| SignalError::TokenFormat) @@ -1147,7 +1149,7 @@ mod tests { }) } - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn send_queues_queueable_signals_during_reconnect() { let inner = make_stub_inner(); @@ -1177,7 +1179,7 @@ mod tests { assert_eq!(queue.len(), 3, "all three queueable signals should be buffered"); } - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn send_does_not_queue_pass_through_signals_during_reconnect() { let inner = make_stub_inner(); @@ -1203,7 +1205,7 @@ mod tests { assert!(queue.is_empty(), "pass-through signals must not be queued, got {}", queue.len()); } - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn set_reconnected_drains_queue_and_clears_flag() { let inner = make_stub_inner(); @@ -1462,10 +1464,10 @@ mod tests { // Region + validate + stream behaviour, driven by the shared mock transport. - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn region_fetch_via_mock_transport_parses_urls() { - use crate::signal_client::test_transport::install_mock_transport; + use crate::test_transport::install_mock_transport; install_mock_transport(); // fetch_from_endpoint bypasses the is_cloud gate; the mock serves canned @@ -1484,10 +1486,10 @@ mod tests { /// The mock returns HTTP 401 when the `Authorization: Bearer ` header /// is absent; `validate()` maps 401 to `SignalError::Client`, so `is_ok()` /// passes only when `validate()` forwarded a valid Bearer token. - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn validate_via_mock_transport_succeeds() { - use crate::signal_client::test_transport::install_mock_transport; + use crate::test_transport::install_mock_transport; install_mock_transport(); let ws_url = url::Url::parse("ws://mock.livekit.cloud/rtc").unwrap(); @@ -1513,10 +1515,10 @@ mod tests { /// transport layer it must return `Ok(())` so the caller surfaces the original /// connection error, never masking it. The mock returns a `Connection` error /// for URLs marked `connrefused`. - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn validate_swallows_transport_error_to_avoid_masking() { - use crate::signal_client::test_transport::install_mock_transport; + use crate::test_transport::install_mock_transport; install_mock_transport(); let ws_url = url::Url::parse("wss://connrefused.livekit.cloud/rtc").unwrap(); @@ -1531,10 +1533,10 @@ mod tests { /// SignalStream delivers the first protobuf frame from the transport to the /// events channel. The mock returns one canned Pong frame then closes. - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn stream_delivers_first_frame() { - use crate::signal_client::test_transport::install_mock_transport; + use crate::test_transport::install_mock_transport; install_mock_transport(); let (_stream, mut events) = SignalStream::connect( @@ -1552,10 +1554,10 @@ mod tests { /// `SignalError::RegionError`. The mock returns /// `TransportError::Connection(..connection refused..)` for URLs marked /// `connrefused`. - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn region_fetch_connection_refused_includes_error_chain() { - use crate::signal_client::test_transport::install_mock_transport; + use crate::test_transport::install_mock_transport; install_mock_transport(); let endpoint = "http://mock.test/connrefused/settings/regions"; @@ -1574,10 +1576,10 @@ mod tests { /// A non-JSON region body yields a descriptive `RegionError`. The mock /// returns a 200 with a non-JSON body for URLs marked `badjson`. - #[cfg(feature = "signal-client-tokio")] + #[cfg(feature = "tokio")] #[tokio::test] async fn region_fetch_invalid_json_includes_error_chain() { - use crate::signal_client::test_transport::install_mock_transport; + use crate::test_transport::install_mock_transport; install_mock_transport(); let endpoint = "http://mock.test/badjson"; diff --git a/livekit-api/src/signal_client/region_url_provider.rs b/livekit-signaling/src/region_url_provider.rs similarity index 98% rename from livekit-api/src/signal_client/region_url_provider.rs rename to livekit-signaling/src/region_url_provider.rs index 1c0fffba6..acffcd22e 100644 --- a/livekit-api/src/signal_client/region_url_provider.rs +++ b/livekit-signaling/src/region_url_provider.rs @@ -22,7 +22,7 @@ use std::{ use parking_lot::Mutex; use tokio::sync::Mutex as AsyncMutex; -use crate::region::{is_cloud_host, parse_max_age, Cached, RegionCache, RegionsResponse}; +use livekit_region::{is_cloud_host, parse_max_age, Cached, RegionCache, RegionsResponse}; use super::{SignalError, SignalResult, REGION_FETCH_TIMEOUT}; use livekit_net::HttpClientExt; @@ -374,7 +374,7 @@ mod tests { fn fetch_lock_is_shared_per_host() { // Same host hands back the same lock, so concurrent callers contend on a // single fetch; distinct hosts get independent locks. (RegionCache's own - // caching behavior is unit-tested in crate::region.) + // caching behavior is unit-tested in the livekit-region crate.) let a1 = fetch_lock("a.livekit.cloud"); let a2 = fetch_lock("a.livekit.cloud"); let b = fetch_lock("b.livekit.cloud"); diff --git a/livekit-api/src/signal_client/signal_stream.rs b/livekit-signaling/src/signal_stream.rs similarity index 100% rename from livekit-api/src/signal_client/signal_stream.rs rename to livekit-signaling/src/signal_stream.rs diff --git a/livekit-api/src/signal_client/test_transport.rs b/livekit-signaling/src/test_transport.rs similarity index 100% rename from livekit-api/src/signal_client/test_transport.rs rename to livekit-signaling/src/test_transport.rs