Conversation
4609f6b to
46fbe57
Compare
46fbe57 to
dd905a9
Compare
|
@claude review |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
390bb2e to
66e96cc
Compare
43e595a to
ccdc25a
Compare
|
@claude review |
|
Claude finished @m3taphysics's task in 2m 53s —— View job Code Review
Issues Found🔴 Performance —
|
| } | ||
| } | ||
|
|
||
| if (!anyDied && audioSources.Count > 0) return; |
There was a problem hiding this comment.
Performance bug — participants lock acquired every frame when room has no audio
When audioSources.Count == 0 (e.g. room has no audio tracks yet, or after Stop()), anyDied stays false, so !anyDied && audioSources.Count > 0 evaluates to false and the method always falls through to ReleaseAllAudioSources() + OpenAllAudioStreams(). OpenAllAudioStreams acquires lock(room.Participants) and iterates all participants — every frame, for the lifetime of the player when no audio is present.
Fix: track a separate flag for "attempted to open audio, found nothing".
| if (!anyDied && audioSources.Count > 0) return; | |
| if (!anyDied && audioSources.Count > 0) return; | |
| if (audioSources.Count == 0 && !anyDied) | |
| { | |
| // No sources and nothing died — room has no audio tracks. | |
| // Only re-check when the frame budget allows (or when first opened). | |
| OpenAllAudioStreams(); | |
| return; | |
| } |
Simpler fix: add an audioInitialized bool that is set to true in OpenMedia/OpenAllAudioStreams, and only skip the redundant re-poll when audioInitialized && audioSources.Count == 0.
| /// Finds the audio track paired to a specific video track from the same participant. | ||
| /// Available for future targeted audio scenarios. | ||
| /// </summary> | ||
| private Weak<AudioStream> FindPairedAudio(string identity, string videoSid) |
There was a problem hiding this comment.
Dead code — FindPairedAudio is never called
This private method has no callers. Per the project's code standards: "Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements."
The docs note it's reserved for "future targeted audio scenarios", but shipping dead code adds maintenance burden and cognitive noise. Remove it until it's actually needed.
There was a problem hiding this comment.
I kept it in case it will be required in the future, as livekit sends Video and Audio in separate tracks, this utility helps to match the participant Video with their respective Audio.
| // If a specific user stream died, fallback to current-stream (first available track) | ||
| if (playingAddress.Value.IsUserStream(out _)) | ||
| { | ||
| OpenMedia(LivekitAddress.CurrentStream()); |
There was a problem hiding this comment.
Audio disruption on every video fallback
OpenMedia(LivekitAddress.CurrentStream()) calls CloseCurrentStream() first, which calls ReleaseAllAudioSources() — releasing and re-pooling all audio MonoBehaviour objects — then immediately re-acquires them in OpenAllAudioStreams(). This causes a brief audio dropout on every video recovery cycle, even when the audio streams are perfectly healthy.
Consider splitting the video and audio reset paths so a dying video track doesn't interrupt audio that is still alive:
| OpenMedia(LivekitAddress.CurrentStream()); | |
| // Video stream died — swap to first available track without resetting audio | |
| if (playingAddress.Value.IsUserStream(out _)) | |
| { | |
| currentVideoIdentity = null; | |
| currentVideoStream = FirstVideoTrackingIdentity(); | |
| playingAddress = LivekitAddress.CurrentStream(); | |
| videoSwitchedAtTime = UnityEngine.Time.realtimeSinceStartup; | |
| playerState = PlayerState.PLAYING; | |
| return; | |
| } |
| if (!string.IsNullOrEmpty(displayName)) | ||
| return displayName; | ||
| } | ||
| catch (JsonException) { } |
There was a problem hiding this comment.
Silent exception swallowing — invalid metadata is silently ignored
catch (JsonException) { } discards all parse failures without any trace. If a participant sends malformed metadata this will silently fall through to the Name/Identity fallback, making it very hard to diagnose in the field.
;
| catch (JsonException) { } | |
| catch (JsonException e) { ReportHub.LogWarning(ReportCategory.COMMS, $"Failed to parse participant metadata for display name: {e.Message}"); } |
1366c4f to
125a030
Compare
Hey @cyaiox, I see the intention. It can be useful for prod, but I would exclude it for development for sure, otherwise that catch silently swallows the data that could be important for the context and traceability. |
NickKhalow
left a comment
There was a problem hiding this comment.
Thank you for your updates! Please address the comments
| /// internal depth stack corrupted. The allocation is negligible — this | ||
| /// runs only on error paths that already allocated an exception object. | ||
| /// </summary> | ||
| private void ResetWriter() |
There was a problem hiding this comment.
I got the idea, makes a sense. Would be great to enforce the invariant in the code, because there is always a chance that new method may forget to reset the JTW.
WDYT?
UPD: addressed
| private PlayerState playerState; | ||
| private LivekitAddress? playingAddress; | ||
|
|
||
| private LivekitAddress? currentVideo; |
There was a problem hiding this comment.
second LivekitAddress is redundant due the following:
FromIdentity is used only in LiveKitPlayer. LiveKitPlayer is NOT responsible for issuing the LiveKitAddress by design. It only executes the address like a command and never creates that internally
Please remove FromIdentity method. IsPresentationBotIdentity should be placed in LivekitAddress.New method, it will handle the union resolution and LivekitPlayer never needs to create a value.
| } | ||
| catch (InvalidOperationException) | ||
| { | ||
| // Collection modified during snapshot; retry next frame. |
There was a problem hiding this comment.
It would be better to avoid such defensive programming in concurrent scenarios and solve the root cause in place. Would it be possible to address the issue here?
UPD: addressed
…rack-selector # Conflicts: # Explorer/Assets/DCL/Infrastructure/Utility/DecentralandUrls/DecentralandUrl.cs # Explorer/Assets/DCL/NetworkDefinitions/Browser/DecentralandUrlsSource.cs




Pull Request Description
What does this PR change?
Enhances the LiveKit media player to support full Cast scenarios with multiple participants, automatic speaker tracking, presentation bot priority, data messaging between scenes and participants, and self-healing stream recovery.
Video routing
CurrentStreammode, video automatically follows the dominant speaker usingroom.ActiveSpeakerswith a 1.5s debounce to prevent flickeringpresentation-bot:are always prioritized — once locked on, speaker tracking is suppressed until the bot leavesTrackPublication.Mutedper frame and renders a black texture when mutedCurrentStream(first available track)Audio routing
AUDIO_RESCAN_INTERVAL_SECONDS), or immediately when a dead track is detectedData messaging API
PublishData(topic, data)— Send messages to a topic (rate-limited to 10 msg/s, 16KB cap)SubscribeToTopic(topic)— Subscribe to receive messages on a topicConsumeMessages(topic)— Consume buffered messages for a topicUpdateMetadata(metadata)— Update local participant metadata (broadcast by LiveKit)DataPipegracefully (InvalidProtocolBufferException)Stream metadata
getActiveVideoStreamsresponse withdisplayName,speaking,trackName,width,heightdisplayNamefield), falling back toNamethenIdentityDocumentation
docs/cast.mdcovering all new featuresTest Instructions
Prerequisites
Streaming Theatreto .zone/.orgTest Steps — Video routing
Test Steps — Presentation bot
Test Steps — Audio
Test Steps — Data messaging
SubscribeToTopic("test")PublishData("test", "hello")ConsumeMessages("test")— verify the message is returned with sender identityTest Steps — Screen share recovery
Additional Testing Notes
Quality Checklist
Code Review Reference
Please review our Code Review Standards before submitting.
Screenshots