From d02308ff00915d9ee0cb892725005d21c96f6964 Mon Sep 17 00:00:00 2001 From: konsalex Date: Sun, 25 Jan 2026 22:00:07 +0100 Subject: [PATCH 1/4] fix: sync state for remote control --- core/src/room_service.rs | 114 +++++++++++++++++- tauri/src/components/ui/call-banner.tsx | 2 +- tauri/src/components/ui/call-center.tsx | 2 +- .../ui/participant-row-wo-livekit.tsx | 2 +- tauri/src/lib/deepLinkUtils.ts | 2 +- tauri/src/windows/main-window/tabs/Debug.tsx | 2 +- tauri/src/windows/main-window/tabs/Rooms.tsx | 2 +- 7 files changed, 117 insertions(+), 9 deletions(-) diff --git a/core/src/room_service.rs b/core/src/room_service.rs index 7bdda978..1b04bf87 100644 --- a/core/src/room_service.rs +++ b/core/src/room_service.rs @@ -80,6 +80,12 @@ struct RoomServiceInner { // TODO: See if we can use a sync::Mutex instead of tokio::sync::Mutex room: Mutex>, buffer_source: Arc>>, + /// Whether this instance is the sharer (has published screen share track). + /// Used to determine if we should rebroadcast remote control state on participant join. + is_sharer: std::sync::Mutex, + /// Current remote control enabled state. Updated when sharer toggles remote control. + /// Used for rebroadcasting to late joiners. + remote_control_enabled: std::sync::Mutex, } /// RoomService is a wrapper around the LiveKit room, on creation it @@ -129,6 +135,8 @@ impl RoomService { let inner = Arc::new(RoomServiceInner { room: Mutex::new(None), buffer_source: Arc::new(std::sync::Mutex::new(None)), + is_sharer: std::sync::Mutex::new(false), + remote_control_enabled: std::sync::Mutex::new(true), // Default to true, sharer will broadcast actual state }); let (service_command_tx, service_command_rx) = mpsc::unbounded_channel(); let (service_command_res_tx, service_command_res_rx) = std::sync::mpsc::channel(); @@ -431,7 +439,12 @@ async fn room_service_commands( let user_sid = room.local_participant().sid().as_str().to_string(); // TODO: Check if this will need cleanup /* Spawn thread for handling livekit data events. */ - tokio::spawn(handle_room_events(rx, event_loop_proxy, user_sid)); + tokio::spawn(handle_room_events( + rx, + event_loop_proxy, + user_sid, + inner.clone(), + )); let mut inner_room = inner.room.lock().await; *inner_room = Some(room); @@ -503,8 +516,45 @@ async fn room_service_commands( continue; } - let mut inner_buffer_source = inner.buffer_source.lock().unwrap(); - *inner_buffer_source = Some(buffer_source); + // Scope the buffer_source lock so it's dropped before async operations + { + let mut inner_buffer_source: std::sync::MutexGuard< + '_, + Option, + > = inner.buffer_source.lock().unwrap(); + *inner_buffer_source = Some(buffer_source); + } + + // Mark this instance as the sharer authority + { + let mut is_sharer = inner.is_sharer.lock().unwrap(); + *is_sharer = true; + } + + // Publish initial remote control state so controllers know current state + let rc_enabled = { + let rc_state = inner.remote_control_enabled.lock().unwrap(); + *rc_state + }; + let local_participant = room.local_participant(); + let res = local_participant + .publish_data(DataPacket { + payload: serde_json::to_vec(&ClientEvent::RemoteControlEnabled( + RemoteControlEnabled { + enabled: rc_enabled, + }, + )) + .unwrap(), + reliable: true, + topic: Some(TOPIC_REMOTE_CONTROL_ENABLED.to_string()), + ..Default::default() + }) + .await; + if let Err(e) = res { + log::warn!( + "room_service_commands: Failed to publish initial remote control state: {e:?}" + ); + } let res = tx.send(RoomServiceCommandResult::Success); if let Err(e) = res { @@ -516,6 +566,16 @@ async fn room_service_commands( task.abort(); } + // Reset sharer state + { + let mut is_sharer = inner.is_sharer.lock().unwrap(); + *is_sharer = false; + } + { + let mut rc_state = inner.remote_control_enabled.lock().unwrap(); + *rc_state = true; // Reset to default + } + let room = { let mut inner_room = inner.room.lock().await; if inner_room.is_none() { @@ -561,6 +621,12 @@ async fn room_service_commands( ); } RoomServiceCommand::PublishControllerCursorEnabled(enabled) => { + // Update internal state for late joiner rebroadcast + { + let mut rc_state = inner.remote_control_enabled.lock().unwrap(); + *rc_state = enabled; + } + let inner_room = inner.room.lock().await; if inner_room.is_none() { log::warn!("room_service_commands: Room doesn't exist"); @@ -867,6 +933,7 @@ async fn handle_room_events( mut receiver: mpsc::UnboundedReceiver, event_loop_proxy: EventLoopProxy, user_sid: String, + inner: Arc, ) { while let Some(msg) = receiver.recv().await { match msg { @@ -997,6 +1064,47 @@ async fn handle_room_events( continue; } + // If we are the sharer, rebroadcast current remote control state to the new joiner + let is_sharer = { + let is_sharer_guard = inner.is_sharer.lock().unwrap(); + *is_sharer_guard + }; + if is_sharer { + let rc_enabled = { + let rc_state = inner.remote_control_enabled.lock().unwrap(); + *rc_state + }; + + // Get the room and publish the current state + let room_guard = inner.room.lock().await; + if let Some(room) = room_guard.as_ref() { + let local_participant = room.local_participant(); + let res = local_participant + .publish_data(DataPacket { + payload: serde_json::to_vec(&ClientEvent::RemoteControlEnabled( + RemoteControlEnabled { + enabled: rc_enabled, + }, + )) + .unwrap(), + reliable: true, + topic: Some(TOPIC_REMOTE_CONTROL_ENABLED.to_string()), + ..Default::default() + }) + .await; + if let Err(e) = res { + log::warn!( + "handle_room_events: Failed to rebroadcast remote control state: {e:?}" + ); + } else { + log::info!( + "handle_room_events: Rebroadcast remote control state ({}) to late joiner", + rc_enabled + ); + } + } + } + if let Err(e) = event_loop_proxy.send_event(UserEvent::ParticipantConnected(ParticipantData { name, diff --git a/tauri/src/components/ui/call-banner.tsx b/tauri/src/components/ui/call-banner.tsx index 7ba09197..853aa4eb 100644 --- a/tauri/src/components/ui/call-banner.tsx +++ b/tauri/src/components/ui/call-banner.tsx @@ -64,7 +64,7 @@ export const CallBanner = ({ callerId, toastId }: { callerId: string; toastId: s hasAudioEnabled: true, hasCameraEnabled: false, role: ParticipantRole.NONE, - isRemoteControlEnabled: true, + isRemoteControlEnabled: false, cameraTrackId: null, cameraWindowOpen: false, krispToggle: true, diff --git a/tauri/src/components/ui/call-center.tsx b/tauri/src/components/ui/call-center.tsx index da1c71f3..ddc7519e 100644 --- a/tauri/src/components/ui/call-center.tsx +++ b/tauri/src/components/ui/call-center.tsx @@ -400,7 +400,7 @@ function ScreenShareIcon({ setCallTokens({ ...callTokens, role: ParticipantRole.NONE, - isRemoteControlEnabled: true, + isRemoteControlEnabled: false, }); tauriUtils.stopSharing(); } diff --git a/tauri/src/components/ui/participant-row-wo-livekit.tsx b/tauri/src/components/ui/participant-row-wo-livekit.tsx index c0b7a3d5..4dabd35e 100644 --- a/tauri/src/components/ui/participant-row-wo-livekit.tsx +++ b/tauri/src/components/ui/participant-row-wo-livekit.tsx @@ -149,7 +149,7 @@ export const ParticipantRow = (props: { user: components["schemas"]["BaseUser"] hasAudioEnabled: true, hasCameraEnabled: false, role: ParticipantRole.NONE, - isRemoteControlEnabled: true, + isRemoteControlEnabled: false, cameraTrackId: null, cameraWindowOpen: false, krispToggle: true, diff --git a/tauri/src/lib/deepLinkUtils.ts b/tauri/src/lib/deepLinkUtils.ts index c9fc7c55..ec021b0a 100644 --- a/tauri/src/lib/deepLinkUtils.ts +++ b/tauri/src/lib/deepLinkUtils.ts @@ -109,7 +109,7 @@ export const handleJoinSessionDeepLink = async (sessionId: string): Promise { // hasAudioEnabled: true, hasAudioEnabled: false, hasCameraEnabled: false, - isRemoteControlEnabled: true, + isRemoteControlEnabled: false, cameraTrackId: null, }); }} diff --git a/tauri/src/windows/main-window/tabs/Rooms.tsx b/tauri/src/windows/main-window/tabs/Rooms.tsx index cde3ec44..3f8dc325 100644 --- a/tauri/src/windows/main-window/tabs/Rooms.tsx +++ b/tauri/src/windows/main-window/tabs/Rooms.tsx @@ -287,7 +287,7 @@ export const Rooms = () => { hasAudioEnabled: true, hasCameraEnabled: false, role: ParticipantRole.NONE, - isRemoteControlEnabled: true, + isRemoteControlEnabled: false, cameraTrackId: null, room: room, cameraWindowOpen: false, From 3493c5eb18e323d6ab3ce4cea8bd20ee1185acbb Mon Sep 17 00:00:00 2001 From: konsalex Date: Sun, 25 Jan 2026 22:07:02 +0100 Subject: [PATCH 2/4] chore: default to true --- tauri/src/components/ui/call-banner.tsx | 2 +- tauri/src/components/ui/call-center.tsx | 2 +- tauri/src/components/ui/participant-row-wo-livekit.tsx | 2 +- tauri/src/lib/deepLinkUtils.ts | 2 +- tauri/src/windows/main-window/tabs/Debug.tsx | 2 +- tauri/src/windows/main-window/tabs/Rooms.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tauri/src/components/ui/call-banner.tsx b/tauri/src/components/ui/call-banner.tsx index 853aa4eb..7ba09197 100644 --- a/tauri/src/components/ui/call-banner.tsx +++ b/tauri/src/components/ui/call-banner.tsx @@ -64,7 +64,7 @@ export const CallBanner = ({ callerId, toastId }: { callerId: string; toastId: s hasAudioEnabled: true, hasCameraEnabled: false, role: ParticipantRole.NONE, - isRemoteControlEnabled: false, + isRemoteControlEnabled: true, cameraTrackId: null, cameraWindowOpen: false, krispToggle: true, diff --git a/tauri/src/components/ui/call-center.tsx b/tauri/src/components/ui/call-center.tsx index ddc7519e..da1c71f3 100644 --- a/tauri/src/components/ui/call-center.tsx +++ b/tauri/src/components/ui/call-center.tsx @@ -400,7 +400,7 @@ function ScreenShareIcon({ setCallTokens({ ...callTokens, role: ParticipantRole.NONE, - isRemoteControlEnabled: false, + isRemoteControlEnabled: true, }); tauriUtils.stopSharing(); } diff --git a/tauri/src/components/ui/participant-row-wo-livekit.tsx b/tauri/src/components/ui/participant-row-wo-livekit.tsx index 4dabd35e..c0b7a3d5 100644 --- a/tauri/src/components/ui/participant-row-wo-livekit.tsx +++ b/tauri/src/components/ui/participant-row-wo-livekit.tsx @@ -149,7 +149,7 @@ export const ParticipantRow = (props: { user: components["schemas"]["BaseUser"] hasAudioEnabled: true, hasCameraEnabled: false, role: ParticipantRole.NONE, - isRemoteControlEnabled: false, + isRemoteControlEnabled: true, cameraTrackId: null, cameraWindowOpen: false, krispToggle: true, diff --git a/tauri/src/lib/deepLinkUtils.ts b/tauri/src/lib/deepLinkUtils.ts index ec021b0a..c9fc7c55 100644 --- a/tauri/src/lib/deepLinkUtils.ts +++ b/tauri/src/lib/deepLinkUtils.ts @@ -109,7 +109,7 @@ export const handleJoinSessionDeepLink = async (sessionId: string): Promise { // hasAudioEnabled: true, hasAudioEnabled: false, hasCameraEnabled: false, - isRemoteControlEnabled: false, + isRemoteControlEnabled: true, cameraTrackId: null, }); }} diff --git a/tauri/src/windows/main-window/tabs/Rooms.tsx b/tauri/src/windows/main-window/tabs/Rooms.tsx index 3f8dc325..cde3ec44 100644 --- a/tauri/src/windows/main-window/tabs/Rooms.tsx +++ b/tauri/src/windows/main-window/tabs/Rooms.tsx @@ -287,7 +287,7 @@ export const Rooms = () => { hasAudioEnabled: true, hasCameraEnabled: false, role: ParticipantRole.NONE, - isRemoteControlEnabled: false, + isRemoteControlEnabled: true, cameraTrackId: null, room: room, cameraWindowOpen: false, From a779121f6b132d9b0ddbcfa3d2b8acdbea9b8130 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sat, 31 Jan 2026 13:24:23 +0000 Subject: [PATCH 3/4] fix: remove slop --- core/src/room_service.rs | 119 +++++++++++---------------------------- 1 file changed, 32 insertions(+), 87 deletions(-) diff --git a/core/src/room_service.rs b/core/src/room_service.rs index 1b04bf87..35c60c72 100644 --- a/core/src/room_service.rs +++ b/core/src/room_service.rs @@ -80,9 +80,6 @@ struct RoomServiceInner { // TODO: See if we can use a sync::Mutex instead of tokio::sync::Mutex room: Mutex>, buffer_source: Arc>>, - /// Whether this instance is the sharer (has published screen share track). - /// Used to determine if we should rebroadcast remote control state on participant join. - is_sharer: std::sync::Mutex, /// Current remote control enabled state. Updated when sharer toggles remote control. /// Used for rebroadcasting to late joiners. remote_control_enabled: std::sync::Mutex, @@ -135,8 +132,7 @@ impl RoomService { let inner = Arc::new(RoomServiceInner { room: Mutex::new(None), buffer_source: Arc::new(std::sync::Mutex::new(None)), - is_sharer: std::sync::Mutex::new(false), - remote_control_enabled: std::sync::Mutex::new(true), // Default to true, sharer will broadcast actual state + remote_control_enabled: std::sync::Mutex::new(true), }); let (service_command_tx, service_command_rx) = mpsc::unbounded_channel(); let (service_command_res_tx, service_command_res_rx) = std::sync::mpsc::channel(); @@ -516,45 +512,11 @@ async fn room_service_commands( continue; } - // Scope the buffer_source lock so it's dropped before async operations - { - let mut inner_buffer_source: std::sync::MutexGuard< - '_, - Option, - > = inner.buffer_source.lock().unwrap(); - *inner_buffer_source = Some(buffer_source); - } - - // Mark this instance as the sharer authority - { - let mut is_sharer = inner.is_sharer.lock().unwrap(); - *is_sharer = true; - } + let mut inner_buffer_source = inner.buffer_source.lock().unwrap(); + *inner_buffer_source = Some(buffer_source); - // Publish initial remote control state so controllers know current state - let rc_enabled = { - let rc_state = inner.remote_control_enabled.lock().unwrap(); - *rc_state - }; - let local_participant = room.local_participant(); - let res = local_participant - .publish_data(DataPacket { - payload: serde_json::to_vec(&ClientEvent::RemoteControlEnabled( - RemoteControlEnabled { - enabled: rc_enabled, - }, - )) - .unwrap(), - reliable: true, - topic: Some(TOPIC_REMOTE_CONTROL_ENABLED.to_string()), - ..Default::default() - }) - .await; - if let Err(e) = res { - log::warn!( - "room_service_commands: Failed to publish initial remote control state: {e:?}" - ); - } + let mut rc_state = inner.remote_control_enabled.lock().unwrap(); + *rc_state = true; let res = tx.send(RoomServiceCommandResult::Success); if let Err(e) = res { @@ -566,16 +528,6 @@ async fn room_service_commands( task.abort(); } - // Reset sharer state - { - let mut is_sharer = inner.is_sharer.lock().unwrap(); - *is_sharer = false; - } - { - let mut rc_state = inner.remote_control_enabled.lock().unwrap(); - *rc_state = true; // Reset to default - } - let room = { let mut inner_room = inner.room.lock().await; if inner_room.is_none() { @@ -1064,44 +1016,37 @@ async fn handle_room_events( continue; } - // If we are the sharer, rebroadcast current remote control state to the new joiner - let is_sharer = { - let is_sharer_guard = inner.is_sharer.lock().unwrap(); - *is_sharer_guard + let rc_enabled = { + let rc_state = inner.remote_control_enabled.lock().unwrap(); + *rc_state }; - if is_sharer { - let rc_enabled = { - let rc_state = inner.remote_control_enabled.lock().unwrap(); - *rc_state - }; - - // Get the room and publish the current state - let room_guard = inner.room.lock().await; - if let Some(room) = room_guard.as_ref() { - let local_participant = room.local_participant(); - let res = local_participant - .publish_data(DataPacket { - payload: serde_json::to_vec(&ClientEvent::RemoteControlEnabled( - RemoteControlEnabled { - enabled: rc_enabled, - }, - )) - .unwrap(), - reliable: true, - topic: Some(TOPIC_REMOTE_CONTROL_ENABLED.to_string()), - ..Default::default() - }) - .await; - if let Err(e) = res { - log::warn!( - "handle_room_events: Failed to rebroadcast remote control state: {e:?}" - ); - } else { - log::info!( + + // Get the room and publish the current state + let room_guard = inner.room.lock().await; + if let Some(room) = room_guard.as_ref() { + let local_participant = room.local_participant(); + let res = local_participant + .publish_data(DataPacket { + payload: serde_json::to_vec(&ClientEvent::RemoteControlEnabled( + RemoteControlEnabled { + enabled: rc_enabled, + }, + )) + .unwrap(), + reliable: true, + topic: Some(TOPIC_REMOTE_CONTROL_ENABLED.to_string()), + ..Default::default() + }) + .await; + if let Err(e) = res { + log::warn!( + "handle_room_events: Failed to rebroadcast remote control state: {e:?}" + ); + } else { + log::info!( "handle_room_events: Rebroadcast remote control state ({}) to late joiner", rc_enabled ); - } } } From 3d4ed8c5d4b0c6a40b35b7e843bf63ade43943a3 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sat, 31 Jan 2026 13:37:28 +0000 Subject: [PATCH 4/4] fix: don't spam with updates when the state doesn't change --- .../components/SharingScreen/SharingScreen.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tauri/src/components/SharingScreen/SharingScreen.tsx b/tauri/src/components/SharingScreen/SharingScreen.tsx index 052f0fbe..04304962 100644 --- a/tauri/src/components/SharingScreen/SharingScreen.tsx +++ b/tauri/src/components/SharingScreen/SharingScreen.tsx @@ -3,7 +3,14 @@ import Draggable from "react-draggable"; import { throttle } from "lodash"; import { RiDraggable } from "react-icons/ri"; import { HiPencil } from "react-icons/hi2"; -import { LiveKitRoom, useDataChannel, useLocalParticipant, useRoomContext, useTracks, VideoTrack } from "@livekit/components-react"; +import { + LiveKitRoom, + useDataChannel, + useLocalParticipant, + useRoomContext, + useTracks, + VideoTrack, +} from "@livekit/components-react"; import { ConnectionState, DataPublishOptions, LocalParticipant, Track } from "livekit-client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { resizeWindow } from "./utils"; @@ -123,7 +130,14 @@ const ConsumerComponent = React.memo(() => { useDataChannel("remote_control_enabled", (msg) => { const decoder = new TextDecoder(); const payload: TPRemoteControlEnabled = JSON.parse(decoder.decode(msg.payload)); - if (payload.payload.enabled == false) { + const newValue = payload.payload.enabled; + + // Skip if value matches current state + if (newValue === isRemoteControlEnabled) { + return; + } + + if (newValue === false) { updateCallTokens({ isRemoteControlEnabled: false, });