diff --git a/Editor/TokenSourceComponentConfigEditor.cs b/Editor/TokenSourceComponentConfigEditor.cs index 03650d98..fba7ff16 100644 --- a/Editor/TokenSourceComponentConfigEditor.cs +++ b/Editor/TokenSourceComponentConfigEditor.cs @@ -25,12 +25,12 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty("_token")); break; - case TokenSourceType.Sandbox: + case TokenSourceType.DevelopmentTokenServer: EditorGUILayout.HelpBox( - "Use this for development to create tokens from a sandbox token server. " + - "\nWARNING: ONLY USE THIS OPTION FOR LOCAL DEVELOPMENT, SINCE THE SANDBOX TOKEN SERVER NEEDS NO AUTHENTICATION.", + "Use this for development to create tokens from a development token server. " + + "\nWARNING: ONLY USE THIS OPTION FOR LOCAL DEVELOPMENT, SINCE THE DEVELOPMENT TOKEN SERVER NEEDS NO AUTHENTICATION.", MessageType.Info); - EditorGUILayout.PropertyField(serializedObject.FindProperty("_sandboxId")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("_tokenServerId")); DrawConnectionOptions(); break; diff --git a/README.md b/README.md index 81af3049..284dee4c 100644 --- a/README.md +++ b/README.md @@ -137,11 +137,11 @@ To help getting started with tokens, use `TokenSourceComponent.cs` with a `Token #### 1. Literal Use this to pass a pregenerated server URL and token. Generate tokens via the [LiveKit CLI](https://docs.livekit.io/frontends/build/authentication/custom/#manual-token-creation) or from your [LiveKit Cloud](https://cloud.livekit.io/) project's API key page. -#### 2. Sandbox -For development and testing. Follow the [sandbox token server guide](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) to enable your project's sandbox and get the sandbox ID. Optional connection fields (room name, participant name, agent name, etc.) can be configured in the inspector — leave blank for server defaults. +#### 2. Development Token Server +For development and testing. Follow the [development token server guide](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) to enable your project's development token server and get the token server ID. Optional connection fields (room name, participant name, agent name, etc.) can be configured in the inspector — leave blank for server defaults. #### 3. Endpoint -For production. Point to your own token endpoint URL and add any required authentication headers. Uses the same connection options as Sandbox. See the [endpoint token generation guide](https://docs.livekit.io/frontends/build/authentication/endpoint/). +For production. Point to your own token endpoint URL and add any required authentication headers. Uses the same connection options as Development. See the [endpoint token generation guide](https://docs.livekit.io/frontends/build/authentication/endpoint/). #### Usage @@ -174,20 +174,20 @@ var fetch = _tokenSourceComponent.FetchConnectionDetails(new TokenSourceFetchOpt }); ``` -To skip the ScriptableObject entirely, instantiate a token source directly. Each returns the same `TaskYieldInstruction` from `FetchConnectionDetails`, so it can be yielded, awaited, or `.AsUniTask()`-bridged just like the component: +To skip the ScriptableObject entirely, create a token source at runtime via the `TokenSource` factory methods. Each returns the same `TaskYieldInstruction` from `FetchConnectionDetails`, so it can be yielded, awaited, or `.AsUniTask()`-bridged just like the component: ```cs // Fixed sources take no per-call options: -ITokenSourceFixed source = new TokenSourceLiteral("wss://your.livekit.host", ""); -// or: new TokenSourceCustom(async () => await MyAuthFlow()); +ITokenSourceFixed source = TokenSource.Literal("wss://your.livekit.host", ""); +// or: TokenSource.Custom(async () => await MyAuthFlow()); var fetch = source.FetchConnectionDetails(); yield return fetch; var details = fetch.Result; // Configurable sources accept TokenSourceFetchOptions per call: -ITokenSourceConfigurable configurable = new TokenSourceSandbox(""); -// or: new TokenSourceEndpoint("https://your.token-server/api/token", headers); +ITokenSourceConfigurable configurable = TokenSource.DevelopmentTokenServer(""); +// or: TokenSource.Endpoint("https://your.token-server/api/token", headers); var configurableFetch = configurable.FetchConnectionDetails(new TokenSourceFetchOptions { RoomName = "lobby" }); yield return configurableFetch; diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index 9c9ba09e..24b308cc 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -8,6 +8,196 @@ namespace LiveKit { + /// + /// Factory for the built-in implementations. The concrete implementations + /// are private; obtain them through the factory methods and work with the returned + /// or . + /// + public static class TokenSource + { + public delegate Task CustomTokenFunction(); + + internal const string DevelopmentTokenServerUrl = "https://cloud-api.livekit.io/api/v2/sandbox/connection-details"; + + /// + /// Returns a fixed server URL and participant token. Suitable when credentials are pregenerated + /// (e.g. via the LiveKit CLI or LiveKit Cloud project page). + /// + public static ITokenSourceFixed Literal(string serverUrl, string participantToken) + { + return new TokenSourceLiteral(serverUrl, participantToken); + } + + /// + /// Posts a JSON request to a token-server endpoint and returns the parsed . + /// The body is built from per-call (room name, participant info, + /// agent dispatch, etc.). Use for production token servers — see + /// https://docs.livekit.io/frontends/build/authentication/endpoint/. + /// + public static ITokenSourceConfigurable Endpoint(string endpointUrl, IEnumerable headers) + { + return new TokenSourceEndpoint(endpointUrl, headers); + } + + /// + /// Delegates connection-detail retrieval to a user-supplied async function. Use this when your + /// app already has its own token-fetching code (custom auth flow, cached tokens, etc.). + /// + public static ITokenSourceFixed Custom(CustomTokenFunction customTokenFunction) + { + return new TokenSourceCustom(customTokenFunction); + } + + [Obsolete("Use TokenSource.DevelopmentTokenServer instead")] + public static ITokenSourceConfigurable SandboxTokenServer(string sandboxId) + { + return DevelopmentTokenServer(sandboxId); + } + + /// + /// Convenience preconfigured for LiveKit Cloud development token servers. + /// Intended for development and testing only — see + /// https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/. + /// + public static ITokenSourceConfigurable DevelopmentTokenServer(string tokenServerId) + { + return new TokenSourceEndpoint( + DevelopmentTokenServerUrl, + new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }); + } + + private sealed class TokenSourceLiteral : ITokenSourceFixed + { + private readonly string _serverUrl; + private readonly string _participantToken; + + public TokenSourceLiteral(string serverUrl, string participantToken) + { + _serverUrl = serverUrl; + _participantToken = participantToken; + } + + public TaskYieldInstruction FetchConnectionDetails() + { + var result = new ConnectionDetails { ServerUrl = _serverUrl, ParticipantToken = _participantToken }; + return new TaskYieldInstruction(Task.FromResult(result)); + } + } + + private sealed class TokenSourceCustom : ITokenSourceFixed + { + private readonly CustomTokenFunction _customTokenFunction; + + public TokenSourceCustom(CustomTokenFunction customTokenFunction) + { + _customTokenFunction = customTokenFunction; + } + + public TaskYieldInstruction FetchConnectionDetails() + { + // Route a synchronous throw (or a null return) from the user's function through the + // instruction's IsError/Exception, so callers never have to guard the call itself. + Task task; + try + { + task = _customTokenFunction() + ?? Task.FromException( + new InvalidOperationException("Custom token function returned a null task")); + } + catch (Exception e) + { + task = Task.FromException(e); + } + return new TaskYieldInstruction(task); + } + } + + private sealed class TokenSourceEndpoint : ITokenSourceConfigurable + { + private readonly string _endpointUrl; + private readonly IReadOnlyList _headers; + private static readonly HttpClient HttpClient = new HttpClient(); + + public TokenSourceEndpoint(string endpointUrl, IEnumerable headers) + { + _endpointUrl = endpointUrl; + _headers = headers?.ToList() ?? (IReadOnlyList)Array.Empty(); + } + + public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) + { + // Async methods can't return the (non-awaitable) instruction directly, so the actual + // request lives in the helper below; the returned task carries any synchronous throw. + return new TaskYieldInstruction(FetchConnectionDetailsAsync(options)); + } + + private async Task FetchConnectionDetailsAsync(TokenSourceFetchOptions options) + { + var requestBody = BuildRequest(options); + var jsonBody = JsonConvert.SerializeObject(requestBody); + + using var request = new HttpRequestMessage(HttpMethod.Post, _endpointUrl); + foreach (var header in _headers) + { + if (!string.IsNullOrEmpty(header.key)) + request.Headers.TryAddWithoutValidation(header.key, header.value); + } + var content = new StringContent(jsonBody, System.Text.Encoding.UTF8); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + request.Content = content; + + using var response = await HttpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + throw new InvalidOperationException($"Token server error: {response.StatusCode}, response: {await response.Content.ReadAsStringAsync()}"); + + var jsonContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(jsonContent); + } + + private static TokenSourceRequest BuildRequest(TokenSourceFetchOptions options) + { + var request = new TokenSourceRequest + { + RoomName = NullIfEmpty(options.RoomName), + ParticipantName = NullIfEmpty(options.ParticipantName), + ParticipantIdentity = NullIfEmpty(options.ParticipantIdentity), + ParticipantMetadata = NullIfEmpty(options.ParticipantMetadata), + }; + + if (options.ParticipantAttributes != null && options.ParticipantAttributes.Count > 0) + { + request.ParticipantAttributes = options.ParticipantAttributes + .Where(a => !string.IsNullOrEmpty(a.Key)) + .ToDictionary(a => a.Key, a => a.Value); + if (request.ParticipantAttributes.Count == 0) + request.ParticipantAttributes = null; + } + + if (!string.IsNullOrEmpty(options.AgentName) || !string.IsNullOrEmpty(options.AgentMetadata) || !string.IsNullOrEmpty(options.AgentDeployment)) + { + request.RoomConfig = new RoomConfig + { + Agents = new List + { + new AgentDispatch + { + AgentName = NullIfEmpty(options.AgentName), + Metadata = NullIfEmpty(options.AgentMetadata), + Deployment = NullIfEmpty(options.AgentDeployment) + } + } + }; + } + + return request; + } + + private static string NullIfEmpty(string value) => + string.IsNullOrEmpty(value) ? null : value; + } + } + /// /// Marker interface for any source of LiveKit . /// Implementations are either or . @@ -34,163 +224,61 @@ public interface ITokenSourceConfigurable : ITokenSource public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options); } - /// - /// Returns a fixed server URL and participant token. Suitable when credentials are pregenerated - /// (e.g. via the LiveKit CLI or LiveKit Cloud project page). - /// + #region Old constructors + // For backwards compatibility the old public constructors are still here but deprecated / obsolete. + // Delete when doing a new major release. + + [Obsolete("Use TokenSource.Literal(...) instead")] public class TokenSourceLiteral : ITokenSourceFixed { - private string _serverUrl; - private string _participantToken; + private readonly ITokenSourceFixed _inner; public TokenSourceLiteral(string serverUrl, string participantToken) { - _serverUrl = serverUrl; - _participantToken = participantToken; + _inner = TokenSource.Literal(serverUrl, participantToken); } - public TaskYieldInstruction FetchConnectionDetails() - { - var result = new ConnectionDetails { ServerUrl = _serverUrl, ParticipantToken = _participantToken }; - return new TaskYieldInstruction(Task.FromResult(result)); - } + public TaskYieldInstruction FetchConnectionDetails() => _inner.FetchConnectionDetails(); } - /// - /// Delegates connection-detail retrieval to a user-supplied async function. Use this when your - /// app already has its own token-fetching code (custom auth flow, cached tokens, etc.). - /// + [Obsolete("Use TokenSource.Custom(...) instead")] public class TokenSourceCustom : ITokenSourceFixed { + // v2.0.0 declared the delegate nested here; keep it so explicit + // TokenSourceCustom.CustomTokenFunction references still compile. public delegate Task CustomTokenFunction(); - private CustomTokenFunction _customTokenFunction; + private readonly ITokenSourceFixed _inner; public TokenSourceCustom(CustomTokenFunction customTokenFunction) { - _customTokenFunction = customTokenFunction; + // Lambda (not .Invoke) so a null delegate surfaces at fetch time via + // IsError/Exception, matching v2.0.0 behavior, not as a ctor throw. + _inner = TokenSource.Custom(() => customTokenFunction()); } - public TaskYieldInstruction FetchConnectionDetails() - { - // Route a synchronous throw (or a null return) from the user's function through the - // instruction's IsError/Exception, so callers never have to guard the call itself. - Task task; - try - { - task = _customTokenFunction() - ?? Task.FromException( - new InvalidOperationException("Custom token function returned a null task")); - } - catch (Exception e) - { - task = Task.FromException(e); - } - return new TaskYieldInstruction(task); - } + public TaskYieldInstruction FetchConnectionDetails() => _inner.FetchConnectionDetails(); } - /// - /// Posts a JSON request to a token-server endpoint and returns the parsed . - /// The body is built from per-call (room name, participant info, - /// agent dispatch, etc.). Use for production token servers — see - /// https://docs.livekit.io/frontends/build/authentication/endpoint/. - /// + [Obsolete("Use TokenSource.Endpoint(...) instead")] public class TokenSourceEndpoint : ITokenSourceConfigurable { - private string _endpointUrl; - IEnumerable _headers; - private static readonly HttpClient HttpClient = new HttpClient(); + private readonly ITokenSourceConfigurable _inner; public TokenSourceEndpoint(string endpointUrl, IEnumerable headers) { - _endpointUrl = endpointUrl; - _headers = headers; - } - - public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) - { - // Async methods can't return the (non-awaitable) instruction directly, so the actual - // request lives in the helper below; the returned task carries any synchronous throw. - return new TaskYieldInstruction(FetchConnectionDetailsAsync(options)); + _inner = TokenSource.Endpoint(endpointUrl, headers); } - private async Task FetchConnectionDetailsAsync(TokenSourceFetchOptions options) - { - var requestBody = BuildRequest(options); - var jsonBody = JsonConvert.SerializeObject(requestBody); - - var request = new HttpRequestMessage(HttpMethod.Post, _endpointUrl); - if (_headers != null) - { - foreach (var header in _headers) - { - if (!string.IsNullOrEmpty(header.key)) - request.Headers.TryAddWithoutValidation(header.key, header.value); - } - } - var content = new StringContent(jsonBody, System.Text.Encoding.UTF8); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - request.Content = content; - - var response = await HttpClient.SendAsync(request); - - if (!response.IsSuccessStatusCode) - throw new InvalidOperationException($"Token server error: {response.StatusCode}, response: {await response.Content.ReadAsStringAsync()}"); - - var jsonContent = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(jsonContent); - } - - private static TokenSourceRequest BuildRequest(TokenSourceFetchOptions options) - { - var request = new TokenSourceRequest - { - RoomName = NullIfEmpty(options.RoomName), - ParticipantName = NullIfEmpty(options.ParticipantName), - ParticipantIdentity = NullIfEmpty(options.ParticipantIdentity), - ParticipantMetadata = NullIfEmpty(options.ParticipantMetadata), - }; - - if (options.ParticipantAttributes != null && options.ParticipantAttributes.Count > 0) - { - request.ParticipantAttributes = options.ParticipantAttributes - .Where(a => !string.IsNullOrEmpty(a.Key)) - .ToDictionary(a => a.Key, a => a.Value); - if (request.ParticipantAttributes.Count == 0) - request.ParticipantAttributes = null; - } - - if (!string.IsNullOrEmpty(options.AgentName) || !string.IsNullOrEmpty(options.AgentMetadata) || !string.IsNullOrEmpty(options.AgentDeployment)) - { - request.RoomConfig = new RoomConfig - { - Agents = new List - { - new AgentDispatch - { - AgentName = NullIfEmpty(options.AgentName), - Metadata = NullIfEmpty(options.AgentMetadata), - Deployment = NullIfEmpty(options.AgentDeployment) - } - } - }; - } - - return request; - } - - private static string NullIfEmpty(string value) => - string.IsNullOrEmpty(value) ? null : value; + public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) => _inner.FetchConnectionDetails(options); } - /// - /// Convenience preconfigured for LiveKit Cloud sandbox token servers. - /// Intended for development and testing only — see - /// https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/. - /// + [Obsolete("Use TokenSource.DevelopmentTokenServer(...) instead")] public class TokenSourceSandbox : TokenSourceEndpoint { - public TokenSourceSandbox(string sandboxId) : base("https://cloud-api.livekit.io/api/v2/sandbox/connection-details", new[] { new StringPair { key = "X-Sandbox-ID", value = sandboxId } }) {} + public TokenSourceSandbox(string sandboxId) + : base(TokenSource.DevelopmentTokenServerUrl, new[] { new StringPair { key = "X-Sandbox-ID", value = sandboxId } }) {} } -} \ No newline at end of file + + #endregion +} diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs index ebd104d6..2d80d23a 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs @@ -9,9 +9,10 @@ namespace LiveKit { /// /// MonoBehaviour wrapper that builds an from an inspector-assigned - /// ScriptableObject. To skip the asset entirely, instantiate - /// , , , - /// or directly at runtime. + /// ScriptableObject. To skip the asset entirely, create a + /// source at runtime via the factory methods (, + /// , , + /// or ). /// public class TokenSourceComponent : MonoBehaviour { @@ -34,16 +35,16 @@ public void Awake() switch (_config.TokenSourceType) { - case TokenSourceType.Sandbox: - _tokenSource = new TokenSourceSandbox(_config.SandboxId); + case TokenSourceType.DevelopmentTokenServer: + _tokenSource = TokenSource.DevelopmentTokenServer(_config.TokenServerId); break; case TokenSourceType.Endpoint: - _tokenSource = new TokenSourceEndpoint(_config.EndpointUrl, _config.EndpointHeaders); + _tokenSource = TokenSource.Endpoint(_config.EndpointUrl, _config.EndpointHeaders); break; case TokenSourceType.Literal: - _tokenSource = new TokenSourceLiteral(_config.ServerUrl, _config.Token); + _tokenSource = TokenSource.Literal(_config.ServerUrl, _config.Token); break; default: @@ -55,7 +56,8 @@ public void Awake() /// Fetches connection details, merging per-call over the asset-backed /// . For each field, a value provided on /// overrides the config value (empty strings are treated as unset and fall through to the config). - /// Ignored for fixed token sources (, ). + /// Ignored for fixed token sources (, i.e. those created via + /// or ). /// public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) { diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs index 28f20a5d..0ea4c166 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using UnityEngine; +using UnityEngine.Serialization; namespace LiveKit { public enum TokenSourceType { Literal, - Sandbox, + DevelopmentTokenServer, Endpoint } @@ -27,14 +28,15 @@ public class TokenSourceComponentConfig : ScriptableObject [SerializeField] private string _serverUrl; [SerializeField] private string _token; - // Sandbox fields - [SerializeField] private string _sandboxId; + // Development fields + [FormerlySerializedAs("_sandboxId")] + [SerializeField] private string _tokenServerId; // Endpoint fields [SerializeField] private string _endpointUrl; [SerializeField] private List _endpointHeaders; - // Shared connection options (Sandbox + Endpoint) + // Shared connection options (Development + Endpoint) [SerializeField] private string _roomName; [SerializeField] private string _participantName; [SerializeField] private string _participantIdentity; @@ -50,8 +52,8 @@ public class TokenSourceComponentConfig : ScriptableObject public string ServerUrl => _serverUrl; public string Token => _token; - // Sandbox - public string SandboxId => _sandboxId?.Trim('"'); + // Development + public string TokenServerId => _tokenServerId?.Trim('"'); // Endpoint public string EndpointUrl => _endpointUrl; @@ -70,7 +72,7 @@ public class TokenSourceComponentConfig : ScriptableObject public bool IsValid => _tokenSourceType switch { TokenSourceType.Literal => !string.IsNullOrEmpty(ServerUrl) && ServerUrl.StartsWith("ws") && !string.IsNullOrEmpty(Token), - TokenSourceType.Sandbox => !string.IsNullOrEmpty(SandboxId), + TokenSourceType.DevelopmentTokenServer => !string.IsNullOrEmpty(TokenServerId), TokenSourceType.Endpoint => !string.IsNullOrEmpty(EndpointUrl), _ => false }; diff --git a/Samples~/Agents/README.md b/Samples~/Agents/README.md index a7cf76eb..6ee57f2d 100644 --- a/Samples~/Agents/README.md +++ b/Samples~/Agents/README.md @@ -20,9 +20,9 @@ The app is configured to connect to the LiveKit homepage agent by default, which To switch from the default agent to your own, you first need a LiveKit agent to speak with. For a no-code setup, use the [Agent Builder](https://docs.livekit.io/agents/start/builder/). For more customization, try our starter agent for [Python](https://github.com/livekit-examples/agent-starter-python), [Node.js](https://github.com/livekit-examples/agent-starter-node), or [create your own from scratch](https://docs.livekit.io/agents/start/voice-ai/). -Second, you need a token server. For development, the easiest option is the sandbox token server: enable it from your project's Options on the Settings page in LiveKit Cloud and copy the sandboxId. +Second, you need a token server. For development, the easiest option is the development token server: enable it from your project's Options on the Settings page in LiveKit Cloud and copy the token server ID. -Then create a new TokenSoureComponentConfig asset for your sandbox and reference it in the scene on the `TokenSourceComponent` script instead of the `HomepageAgent.asset`: +Then create a new TokenSourceComponentConfig asset for your development token server and reference it in the scene on the `TokenSourceComponent` script instead of the `HomepageAgent.asset`: ### Common sample package