Skip to content

Commit 9f7cd6f

Browse files
cyaioxNickKhalowCopilot
authored
feat: livekit cast improvements (#7347)
* feat: enrich getActiveVideoStreams response and auto-resolve audio for livekit user streams Add participant metadata (name, speaking, trackName, width, height) to GetActiveVideoStreamsResponse JSON output. Enable automatic audio track resolution in LivekitPlayer when opening a user stream by pairing video source to the correct audio source (camera→microphone, screenshare→screenshare audio). * fix: resolve display name from participant metadata for castV2 support Parse displayName from participant.Metadata JSON (used by unauthenticated castV2 users), falling back to participant.Name then participant.Identity. * feat: auto-recover livekit player when specific track dies When a specific user stream track (livekit-video://identity/sid) is unpublished, fallback to current-stream instead of retrying the same dead address every frame. This lets EnsureVideoIsPlaying and EnsureAudioIsPlaying self-heal by scanning for any available track. * feat: play all audio tracks from livekit room in media player Instead of pairing one audio track to the selected video track, LivekitPlayer now plays every audio track in the room — so all participants are heard regardless of which video is displayed. * feat: auto-switch video to active speaker in CurrentStream mode When LivekitPlayer plays in CurrentStream mode, the video now follows the dominant speaker using room.ActiveSpeakers with a 1.5s debounce to prevent flickering. UserStream mode remains pinned to the specified participant. Also adds docs/cast.md documenting the full cast feature. * fix: prevent audio disruption on video recovery, throttle empty-audio rescan, log metadata parse failures - Add ReopenVideoStream() to recover video without tearing down audio - Throttle empty-audio rescan to 2s interval to avoid per-frame lock - Replace silent JsonException catch with ReportHub.LogWarning * fix: make audio discovery additive to support late-joining participants OpenAllAudioStreams now skips tracks that already have a source, so it can be called periodically without disrupting healthy audio. Dead sources are removed individually. EnsureVideoIsPlaying also drives audio discovery since PBVideoPlayer entities don't trigger UpdateAudioStream. * feat: expose LiveKit data messaging to scenes via CommsApi Add PublishData, SubscribeToTopic, and ConsumeMessages methods to CommsApiWrap, enabling scenes to send commands and receive state updates through LiveKit data channels on arbitrary topics. Includes rate limiting (10 msg/s per topic), 16KB payload cap, thread-safe buffering for network-thread DataReceived events, and proper cleanup on dispose. * feat: expose LiveKit data messaging and metadata to scenes via CommsApi Add PublishData, SubscribeToTopic, ConsumeMessages, and UpdateMetadata methods to CommsApiWrap with corresponding JS module bindings, enabling scenes to send commands, receive state updates, and set participant roles through LiveKit data channels on arbitrary topics. Includes rate limiting (10 msg/s per topic), 16KB payload cap, thread-safe buffering for network-thread DataReceived events, and proper cleanup on dispose. Also handle non-protobuf data on the shared DataPipe gracefully in MessagePipe by catching InvalidProtocolBufferException silently, since JSON data from CommsApi messaging is now expected on the pipe. * feat: prioritize presentation bot video track over active speaker In CurrentStream mode, automatically lock onto a presentation bot's video track when one is in the room. The bot is identified by the identity prefix 'presentation-bot:'. Active speaker switching is suppressed while a presentation bot is active, and resumes when the bot leaves. * docs: update cast.md with latest LiveKit features Document presentation bot priority, audio rescan throttling, video muted state tracking, resolution capping, data messaging API, and wallet address identity format. * refactor: LivekitPlayer review feedback — Weak.Null, hasLiveAudio, flip conditions - Replace nullable Weak<IVideoStream>? with Weak<T>.Null struct pattern - Rename tempStreamKeys to streamKeysBuffer - Replace O(n) .Exists() with tracked hasLiveAudio boolean - Flip negated if/else to positive condition first - Clarify video-only audio discovery comment - Remove unused IsVideoTrackMuted, videoTrackMuted, CheckVideoTrackMuted - Mark class as main-thread only * refactor: CommsApi review feedback — ConcurrentDictionary, ITypedArray, ctor subscription - Subscribe to IDataPipe in constructor, make field readonly - Replace Dictionary+List+bufferLock with ConcurrentDictionary+ConcurrentQueue - PublishData accepts ITypedArray<byte> instead of string - Use IJsOperations.LIVEKIT_MAX_SIZE for message size limit - Name magic number as RATE_LIMIT_WINDOW_MS, remove unnecessary unchecked - Simplify DisplayNameOf: remove unused metadata JSON parsing - Flip ternary, extract computation to separate line - Update MessagePipe catch comment with IMessagePipe migration TODO * refactor: CommsApi.js — add type documentation, publishData passes Uint8Array - Add parameter type comments for all exported functions - publishData now passes Uint8Array directly to match ITypedArray<byte> C# signature * fix: clarify EnsureAudioIsPlaying comment in LivekitPlayer * feat: add gatekeeper mode dropdown to debug settings Adds a GatekeeperMode selector (Org, Zone, Today, Localhost, Custom) to DebugSettings, injecting --gatekeeper-url into app parameters. The custom URL field is only shown when mode is set to Custom. * feat: raise LiveKit video resolution cap to 2048x2048 * refactor: remove UpdateMetadata from CommsApi Scene room metadata is managed internally by RemoteMetadata and would be overwritten, making scene-set metadata unreliable. * chore: reorder constants and remove dead code per coding standards - Move private const fields before readonly fields in LivekitPlayer, UpdateMediaPlayerSystem, DebugSettings - Remove unused FindPairedAudio() from LivekitPlayer (speculative code for hypothetical future) * refactor: route CommsApi through ISceneCommunicationPipe with MsgType.CommsData - Replace direct IDataPipe access with ISceneCommunicationPipe routing - Add MsgType.CommsData = 3 for topic-based pub/sub data - Wire format: [MsgType=3][topicLen 2B LE][topic UTF-8][data UTF-8] - Move ISceneCommunicationPipe to CRDT.ECS.Bridge assembly (resolve circular dependency) - Thread ISceneCommunicationPipe + ISceneData through RegisterAll/RegisterCommsApi - PublishData now takes string instead of ITypedArray<byte> - Update CommsApi.js to handle both string and Uint8Array from scenes * fix: update MessagePipe catch comment and CommsApi.js publishData to string - Update InvalidProtocolBufferException comment (cast2 webapp, not CommsApi) - CommsApi.js publishData expects string data directly * test: add unit tests for CommsApiWrap - 12 tests covering: handler lifecycle, wire format, round-trip, topic buffering, input validation, rate limiting, malformed message handling * fix: rename stale test method to match MsgType.CommsData * chore: add missing meta file for CommsApiWrapShould test * refactor: move gatekeeper URL override from app args to URL source layer DebugSettings no longer injects --gatekeeper-url into AppParameters. Instead, GatekeeperMode and CustomGatekeeperUrl are exposed as properties and resolved in MainSceneLoader into a base URL override that is passed to DecentralandUrlsSource. All composed gatekeeper URLs (SceneAdapter, ChatAdapter, Status, BannedUsers, and LocalGateKeeperSceneAdapter) now inherit the override, ensuring consistent cross-environment debugging. * fix: align CommsApiWrap tests with wire format and JSON escaping - PublishDataWithMsgTypeCommsDataPrefix: decode topic header + data instead of treating bytes after MsgType as raw data - SimulateIncomingMessage: encode wire format [topicLen][topic][data] so OnDataReceived can parse incoming messages - DrainBufferOnConsume: check sender identities instead of inner JSON which gets escaped by JsonTextWriter - PublishAndReceiveRoundTrip: relax assertion to account for JSON-escaped inner data string * fix: address code review — race condition, stackalloc overflow, misleading sentinel - Wrap ActiveSpeakers iteration in try-catch for thread safety (LivekitPlayer) - Cap PublishData payload at LIVEKIT_MAX_SIZE before stackalloc (CommsApiWrap) - Replace unused tuple sentinel with bool currentWritten (CommsApiWrap) - Add RejectOversizedData test * refactor: use LKParticipant type alias in GetActiveVideoStreamsResponse * fix: remove stale #if guard on cross-platform media throttle fields * fix: address remaining code review items — TOCTOU race, writer recovery, null-conditional - Snapshot ActiveSpeakers into a local list before iterating to eliminate the TOCTOU race between Count and foreach (LivekitPlayer) - Add ResetWriter() to recover JsonTextWriter state after mid-write exceptions corrupt its internal depth stack (CommsApiWrap) - Simplify confusing !livekitPlayer?.IsVideoOpened ?? false expression to explicit null check (UpdateMediaPlayerSystem) * refactor: address PR review — encapsulate video sourcing, dedup audio sources - LivekitPlayer: introduce VideoEntity domain object + PresentationBotStream variant on LivekitAddress; replace currentVideoIdentity/string-prefix probes with exhaustive Match dispatch. - Swap audioSources to Dictionary<StreamKey, ...> to collapse the O(N^2) HasAudioSourceForKey scan and drop the streamKeysBuffer field. - Drop hasLiveAudio flag; isAudioOpened now reads audioSources.Count directly. - Simplify TryFollowActiveSpeaker (foreach/break to index access; is { } pattern replaces null-forgiving operator). - Collapse EnsureVideoIsPlaying else branch to a single CurrentStream reopen. - CommsApiWrap: extract wire-format byte widths into named constants. - LiveKitMediaExtensions: move PRESENTATION_BOT_IDENTITY_PREFIX out of LivekitPlayer; add IsPresentationBotIdentity extension. * fix: address code review — clarify MessagePipe catch and ResetWriter XML doc MessagePipe InvalidProtocolBufferException handler now reads as an explicit defensive fallback for non-DCL participants publishing non-protobuf data on the shared IDataPipe. DCL clients (cast2 included) speak protobuf and CommsApi traffic routes via MsgType.CommsData, so neither reaches this catch. ResetWriter XML doc expanded to explain JsonTextWriter's internal depth/token stack: an exception thrown between WriteStart*/WriteEnd* leaves the stack unbalanced, corrupting subsequent writes. Recreating on the exception path is cheaper than wrapping each Write* call in try/catch on the happy path. No behaviour change. * refactor: address LiveKit review — AnyExposedAudioSource rename, fold VideoEntity into LivekitAddress - Rename ExposedAudioSource → AnyExposedAudioSource across LivekitPlayer, MultiMediaPlayer and MediaPlayerComponent. The underlying storage moved from a single field to a Dictionary so iteration order is unspecified (and can reshuffle after audioSources.Remove), making "First" misleading. Restore the original inline comments on the method and expand the XMLDoc to call out the multi-source / non-deterministic semantics. - Fix the "referene" typo in MultiMediaPlayer's XMLDoc while there. - Eliminate VideoEntity: its IsPresentationBot and Identity accessors and the FromIdentity factory move directly onto LivekitAddress, which already discriminates over the UserStream / PresentationBotStream / CurrentStream union. Migrate all 6 call sites in LivekitPlayer. * clarify #7347 (comment) * address https://github.com/decentraland/unity-explorer/pull/7347\#discussion_r3132329943 * register new category for reporting * address https://github.com/decentraland/unity-explorer/pull/7347\#discussion_r3132469058 * allocation free write, address https://github.com/decentraland/unity-explorer/pull/7347\#discussion_r3132270763 and https://github.com/decentraland/unity-explorer/pull/7347\#discussion_r3132314939 * submit todo for futher optimisations * remove redundant const * implement message count bouding, add explicit comments to js contract, address https://github.com/decentraland/unity-explorer/pull/7347\#discussion_r3132503328 and https://github.com/decentraland/unity-explorer/pull/7347\#discussion_r3132457159 * implement ListPool * link livekit package * mitigate InvalidOperationException and ActiveSpeakers thread-safe access * remove redundant locks, simplify "switch to current" method * update package lock * unify OpenVideoStream * address LivekitPlayer * remove redundant code * remove redundant null check * update comment * align livekit deps * chore: Add id-token permission Co-authored-by: Copilot <copilot@github.com> * fix: remove added prop --------- Co-authored-by: NickKhalow <nickhalow@gmail.com> Co-authored-by: Copilot <copilot@github.com>
1 parent a9b84c6 commit 9f7cd6f

25 files changed

Lines changed: 1317 additions & 128 deletions

File tree

Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/Communications/ISceneCommunicationPipe.cs renamed to Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/ISceneCommunicationPipe.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using DCL.Multiplayer.Connections.Messaging;
2-
using Decentraland.Kernel.Comms.Rfc4;
3-
using System;
1+
using System;
42
using System.Threading;
53

64
namespace CrdtEcsBridge.JsModulesImplementation.Communications
@@ -11,6 +9,7 @@ public enum MsgType
119
{
1210
String = 1, // SDK scenes MessageBus messages
1311
Uint8Array = 2,
12+
CommsData = 3, // CommsApi publish/subscribe topic-based data
1413
}
1514

1615
public enum ConnectivityAssertiveness

Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/Communications/ISceneCommunicationPipe.cs.meta renamed to Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/ISceneCommunicationPipe.cs.meta

File renamed without changes.

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DebugSettings/DebugSettings.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55

66
namespace Global.Dynamic.DebugSettings
77
{
8+
public enum GatekeeperMode
9+
{
10+
Org = 0,
11+
Zone = 1,
12+
Today = 2,
13+
Localhost = 3,
14+
Custom = 4,
15+
}
16+
817
[Serializable]
918
public class DebugSettings
1019
{
@@ -34,6 +43,13 @@ [SerializeField] [Tooltip("Make sure the ENS put here will be loaded as a Global
3443
private bool overrideConnectionQuality;
3544
[SerializeField]
3645
private LKConnectionQuality connectionQuality;
46+
[Space]
47+
[Header("Comms Gatekeeper")]
48+
[SerializeField]
49+
private GatekeeperMode gatekeeperMode;
50+
[SerializeField] [Tooltip("Base gatekeeper URL used only when Gatekeeper Mode is set to Custom (e.g. http://localhost:3000)")]
51+
private string customGatekeeperUrl = string.Empty;
52+
[Space]
3753
[SerializeField]
3854
private string[] appParameters;
3955

@@ -52,6 +68,8 @@ public static DebugSettings Release() =>
5268
connectionQuality = LKConnectionQuality.QualityExcellent,
5369
enableRemotePortableExperiences = true,
5470
emotesToAddToUserProfile = null,
71+
gatekeeperMode = GatekeeperMode.Org,
72+
customGatekeeperUrl = string.Empty,
5573
appParameters = Array.Empty<string>(),
5674
};
5775

@@ -68,6 +86,8 @@ public static DebugSettings Release() =>
6886
public bool EnableEmulateNoLivekitConnection => Application.isEditor? this.enableEmulateNoLivekitConnection : RELEASE_SETTINGS.enableEmulateNoLivekitConnection;
6987
public bool OverrideConnectionQuality => Application.isEditor ? this.overrideConnectionQuality : RELEASE_SETTINGS.overrideConnectionQuality;
7088
public LKConnectionQuality ConnectionQuality => Application.isEditor ? this.connectionQuality : RELEASE_SETTINGS.connectionQuality;
71-
public string[] AppParameters => appParameters;
89+
public GatekeeperMode GatekeeperMode => Application.isEditor ? gatekeeperMode : RELEASE_SETTINGS.gatekeeperMode;
90+
public string CustomGatekeeperUrl => Application.isEditor ? customGatekeeperUrl : RELEASE_SETTINGS.customGatekeeperUrl;
91+
public string[] AppParameters => Application.isEditor ? appParameters : RELEASE_SETTINGS.appParameters;
7292
}
7393
}

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
using ECS.StreamableLoading.Common.Components;
4646
using Newtonsoft.Json.Linq;
4747
using Global.AppArgs;
48+
using Global.Dynamic.DebugSettings;
4849
using Global.Dynamic.RealmUrl;
4950
using Global.Dynamic.RealmUrl.Names;
5051
using Global.Versioning;
@@ -187,7 +188,9 @@ private async UniTask InitializeFlowAsync(CancellationToken ct)
187188
World world = World.Create();
188189

189190
var realmData = new RealmData();
190-
var decentralandUrlsSource = new GatewayUrlsSource(decentralandEnvironment, realmData, launchSettings);
191+
string? gatekeeperBaseOverride = ResolveGatekeeperBaseOverride(debugSettings.GatekeeperMode, debugSettings.CustomGatekeeperUrl);
192+
ReportHub.Log(ReportCategory.STARTUP, $"Gatekeeper mode: {debugSettings.GatekeeperMode}, base override: {gatekeeperBaseOverride ?? "(default)"}");
193+
var decentralandUrlsSource = new GatewayUrlsSource(decentralandEnvironment, realmData, launchSettings, gatekeeperBaseOverride);
191194
DiagnosticInfoUtils.LogEnvironment(decentralandUrlsSource);
192195

193196
var assetsProvisioner = new AddressablesProvisioner();
@@ -802,6 +805,17 @@ public void Dispose()
802805
}
803806
}
804807

808+
private static string? ResolveGatekeeperBaseOverride(GatekeeperMode mode, string customUrl) =>
809+
mode switch
810+
{
811+
GatekeeperMode.Org => null,
812+
GatekeeperMode.Zone => "https://comms-gatekeeper.decentraland.zone",
813+
GatekeeperMode.Today => "https://comms-gatekeeper.decentraland.today",
814+
GatekeeperMode.Localhost => "http://localhost:3000",
815+
GatekeeperMode.Custom => string.IsNullOrEmpty(customUrl) ? null : customUrl,
816+
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
817+
};
818+
805819
[Serializable]
806820
public class SplashScreenRef : ComponentReference<SplashScreen>
807821
{

Explorer/Assets/DCL/Infrastructure/Global/Editor/DebugSettingsDrawer.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using UnityEditor;
77
using UnityEngine;
8+
using UnityEngine.Assertions;
89

910
namespace Global.Editor
1011
{
@@ -157,11 +158,15 @@ private static Rect DrawDefaultProperties(Rect position, SerializedProperty prop
157158
SerializedProperty endProperty = property.GetEndProperty();
158159

159160
bool enterChildren = true;
161+
bool isCustomGatekeeper = IsCustomGatekeeperMode(property);
160162

161163
while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, endProperty))
162164
{
163165
enterChildren = false;
164166

167+
if (iterator.name == "customGatekeeperUrl" && !isCustomGatekeeper)
168+
continue;
169+
165170
float propertyHeight = EditorGUI.GetPropertyHeight(iterator, true);
166171
position.height = propertyHeight;
167172
EditorGUI.PropertyField(position, iterator, true);
@@ -178,16 +183,28 @@ private static float GetDefaultPropertiesHeight(SerializedProperty property)
178183
SerializedProperty endProperty = property.GetEndProperty();
179184

180185
bool enterChildren = true;
186+
bool isCustomGatekeeper = IsCustomGatekeeperMode(property);
181187

182188
while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, endProperty))
183189
{
184190
enterChildren = false;
191+
192+
if (iterator.name == "customGatekeeperUrl" && !isCustomGatekeeper)
193+
continue;
194+
185195
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
186196
}
187197

188198
return height;
189199
}
190200

201+
private static bool IsCustomGatekeeperMode(SerializedProperty property)
202+
{
203+
SerializedProperty gatekeeperMode = property.FindPropertyRelative("gatekeeperMode");
204+
Assert.IsNotNull(gatekeeperMode, "Failed to find 'gatekeeperMode' property.");
205+
return gatekeeperMode.intValue == (int)GatekeeperMode.Custom;
206+
}
207+
191208
private static string? FindCreatorHubPath()
192209
{
193210
// Check default path

Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneFactory.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ private async UniTask<ISceneFacade> CreateSceneAsync(ISceneData sceneData, IJsAp
243243
sceneData,
244244
realmData,
245245
portableExperiencesController,
246-
remoteMetadata
246+
remoteMetadata,
247+
messagePipesHub
247248
);
248249
}
249250
else
@@ -271,7 +272,8 @@ private async UniTask<ISceneFacade> CreateSceneAsync(ISceneData sceneData, IJsAp
271272
sceneData,
272273
realmData,
273274
portableExperiencesController,
274-
remoteMetadata
275+
remoteMetadata,
276+
messagePipesHub
275277
);
276278
}
277279

0 commit comments

Comments
 (0)