From 2b473d1ca493ee850bed1dc3f715d8ad8be55d87 Mon Sep 17 00:00:00 2001 From: Ivan Nikolic Date: Wed, 22 Jul 2026 17:30:50 +0300 Subject: [PATCH 1/6] fix: don't respawn ffmpeg decoder on live-stream GOP rollover and ingest gaps Two live-stream (VideoStream) bugs in the video player, each costing a full ffmpeg process kill+respawn on the CLI decoder path: - enqueue_samples treated the playhead crossing into the next GOP as "skipping frames" and reset the decoder. At the live edge the next GOP can never be pre-enqueued (its samples don't exist yet), so this fired on every keyframe (~every 2s), spawning a fresh ffmpeg per GOP. Now the reset only happens when there is an actual gap between the enqueued samples and the requested keyframe's GOP; a contiguous rollover decodes straight through the new IDR. - end-of-video was signaled whenever a stream saw no new samples for 250ms (time_until_video_assumed_ended), a threshold the SDK's 200ms batch tick sits right under. Each misfire forced a decoder reset when the next batch arrived, which under load escalated into a self-reinforcing respawn storm (measured 45 spawns/s). Streams never signal end-of-video anymore; only static assets do. Tradeoff: during a stall the frozen frame is up to ~1s staler because trailing frames stay unflushed. Also adds DIAG debug! lines to every decoder-reset call site (the previously silent branches made this bug near-impossible to attribute) and promotes the out-of-order re-merge trace_once! to a countable debug!. Verified against a live Go2 zenoh H.264 feed (720p15, GOP 30): steady state went from one ffmpeg spawn per 2.1s (storming to ~45/s under load) to a single long-lived process with zero resets after startup. Co-Authored-By: Claude Fable 5 --- .../re_video/src/decode/ffmpeg_cli/ffmpeg.rs | 2 +- crates/utils/re_video/src/player/mod.rs | 68 +++++++++++++++---- crates/viewer/re_renderer/src/video/mod.rs | 3 + .../src/cache/video_stream_cache.rs | 4 +- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs index 1e66f4b2a1fa..946647f0a3df 100644 --- a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs +++ b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs @@ -990,7 +990,7 @@ impl AsyncDecoder for FFmpegCliDecoder { fn reset(&mut self, video_descr: &VideoDataDescription) -> crate::decode::Result<()> { re_tracing::profile_function!(); - re_log::trace!("Resetting ffmpeg decoder {}", self.debug_name); + re_log::debug!("DIAG Resetting ffmpeg decoder {}", self.debug_name); self.ffmpeg = FFmpegProcessAndListener::new( &self.debug_name, self.output_sender.clone(), diff --git a/crates/utils/re_video/src/player/mod.rs b/crates/utils/re_video/src/player/mod.rs index 0209f2cf3aee..1daad001f209 100644 --- a/crates/utils/re_video/src/player/mod.rs +++ b/crates/utils/re_video/src/player/mod.rs @@ -655,17 +655,38 @@ impl VideoPlayer { && let Some(keyframe_idx) = video_description.sample_keyframe_idx(last_enqueued) { if keyframe_idx < requested_keyframe_idx { - // Need to reset if we're skipping frames. - self.reset(video_description)?; - // Skip forward and just enqueue the requested keyframe. - self.enqueue_keyframe_range( - video_description, - requested_keyframe_idx, - requested_sample_idx, - get_video_buffer, - )?; - - requested_keyframe_idx + let requested_gop_start = video_description + .keyframe_indices + .get(requested_keyframe_idx) + .copied(); + + if requested_gop_start.is_some_and(|gop_start| last_enqueued + 1 >= gop_start) { + // The enqueued samples lead contiguously into the requested keyframe's GOP. + // This is the normal per-GOP rollover on live streams (the next GOP can never + // be pre-enqueued there, since its samples don't exist yet): the decoder can + // decode straight through the new IDR, no reset needed. The enqueue loop + // below picks up the new keyframe range. + re_log::debug!( + "DIAG GOP rollover without reset (last_enqueued={last_enqueued}, requested_keyframe={requested_keyframe_idx})" + ); + keyframe_idx + } else { + // Actual gap between the enqueued samples and the requested keyframe's GOP — + // we're skipping frames, so reset. + re_log::debug!( + "DIAG video reset[skip]: gap to requested keyframe (last_enqueued={last_enqueued}, gop_start={requested_gop_start:?})" + ); + self.reset(video_description)?; + // Skip forward and just enqueue the requested keyframe. + self.enqueue_keyframe_range( + video_description, + requested_keyframe_idx, + requested_sample_idx, + get_video_buffer, + )?; + + requested_keyframe_idx + } } else { keyframe_idx } @@ -768,8 +789,19 @@ impl VideoPlayer { // Signal the end of the video if we reached it. // This is important for some decoders to flush out all the frames. + // + // Never signal it for stream-delivered video: a live stream has no end, and a + // brief ingest gap (>250ms, easily hit under load with a 200ms batcher tick) + // would otherwise be mistaken for the end of the video. Once new samples then + // arrive, the player has to fully reset the decoder — which for the ffmpeg CLI + // decoder means killing and respawning the process, once per gap. + // The tradeoff is that during a stall the last few frames stay unflushed in the + // decoder, so the frozen image is slightly staler. if !self.signaled_end_of_video - && !treat_video_as_live_stream(&self.config, video_description) + && matches!( + video_description.delivery_method, + crate::VideoDeliveryMethod::Static { .. } + ) && self.enqueued_last_sample_of_video(video_description) { re_log::trace!("Signaling end of video"); @@ -797,6 +829,7 @@ impl VideoPlayer { ) -> Result<(), VideoPlayerError> { // If we haven't decoded anything at all yet, reset the decoder. let Some(last_requested) = self.last_requested else { + re_log::debug!("DIAG video reset[1]: nothing requested yet (fresh start or follow-up to a prior reset)"); return self.reset(video_description); }; @@ -812,6 +845,13 @@ impl VideoPlayer { // For each new (!) error after entering the error state, we reset the decoder. // This way, it might later recover from the error as we progress in the video. + re_log::debug!( + "DIAG video reset[2]: decode error: {}", + self.last_error + .as_ref() + .map(|e| e.latest_error.to_string()) + .unwrap_or_default() + ); self.reset(video_description)?; } // Reset if seeking forward by more than one GOP @@ -821,6 +861,10 @@ impl VideoPlayer { enqueued_idx < video_description.keyframe_indices[last_keyframe] }) { + re_log::debug!( + "DIAG video reset[3]: seek forward >1 GOP (last_enqueued={:?}, requested={requested}, requested_keyframe={requested_keyframe})", + self.last_enqueued + ); self.reset(video_description)?; } // Previously signaled the end of the video, but encountering frames that are newer than the last enqueued. diff --git a/crates/viewer/re_renderer/src/video/mod.rs b/crates/viewer/re_renderer/src/video/mod.rs index 04aa4a8a099b..809bec2339e0 100644 --- a/crates/viewer/re_renderer/src/video/mod.rs +++ b/crates/viewer/re_renderer/src/video/mod.rs @@ -299,6 +299,9 @@ impl Video { }; if needs_reset { + re_log::debug!( + "DIAG video reset[ooo]: sample insertion overlapping active GOP (change {change_start}..{change_end}, delta {size_delta})" + ); player.reset(&self.video_description).ok_or_log_error_once(); } else { // Shift player indices to the new index space. diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs index f21a08345d23..5b9623e7c111 100644 --- a/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs @@ -333,9 +333,7 @@ impl VideoStreamCache { match result { Err(VideoStreamProcessingError::OutOfOrderSamples) => { - re_log::trace_once!( - "Out-of-order chunk detected, performing delta re-merge" - ); + re_log::debug!("DIAG Out-of-order chunk detected, performing delta re-merge"); match handle_out_of_order_chunk( video_data, &mut entry.known_chunk_ranges, From 65cf98b38f2aa73b3e867a38e5d6105e0409ae64 Mon Sep 17 00:00:00 2001 From: Ivan Nikolic Date: Wed, 22 Jul 2026 17:49:47 +0300 Subject: [PATCH 2/6] feat: single-threaded ffmpeg decode for live streams cuts latency ~1s ffmpeg's frame-multithreading adds roughly one frame of output delay per thread and defaults to the core count: measured 17 frames of steady-state holdback (~1.1s at 15fps) with default threading vs 3 frames with -threads 1 on a 720p15 H.264 stream. That holdback was the visible native-viewer video delay vs the WebCodecs-based web viewer. Stream-delivered video now decodes with -threads 1 (one decode thread is ample for typical live-stream resolutions); static assets keep default threading for throughput. Also tried -fflags nobuffer / -flags low_delay: both measured *worse* (32 frames held), so they stay out. Co-Authored-By: Claude Fable 5 --- .../re_video/src/decode/ffmpeg_cli/ffmpeg.rs | 15 +++++++++++++++ crates/utils/re_video/src/decode/mod.rs | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs index 946647f0a3df..5a4d2dc3de18 100644 --- a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs +++ b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs @@ -245,6 +245,7 @@ impl FFmpegProcessAndListener { encoding_details: Option<&VideoEncodingDetails>, ffmpeg_path: Option<&std::path::Path>, codec: &crate::VideoCodec, + low_latency: bool, ) -> Result { re_tracing::profile_function!(); @@ -311,6 +312,15 @@ impl FFmpegProcessAndListener { "-analyzeduration", "0", ]) + // Frame-multithreading adds roughly one frame of output delay per thread and + // ffmpeg defaults to the core count. For live streams a single decode thread + // cuts the holdback from ~17 frames to ~3 (measured, 720p H.264), and decode + // throughput of one thread is ample for typical stream resolutions. + .args(if low_latency { + &["-threads", "1"][..] + } else { + &[][..] + }) // Keep in mind that all arguments that are about the input, need to go before! .format(codec_str) // TODO(andreas): should we check ahead of time whether this is available? //.fps_mode("0") @@ -899,6 +909,7 @@ pub struct FFmpegCliDecoder { output_sender: Sender, ffmpeg_path: Option, codec: crate::VideoCodec, + low_latency: bool, } impl FFmpegCliDecoder { @@ -908,6 +919,7 @@ impl FFmpegCliDecoder { output_sender: Sender, ffmpeg_path: Option, codec: &crate::VideoCodec, + low_latency: bool, ) -> Result { re_tracing::profile_function!(); @@ -926,6 +938,7 @@ impl FFmpegCliDecoder { encoding_details, ffmpeg_path.as_deref(), codec, + low_latency, )?; Ok(Self { @@ -934,6 +947,7 @@ impl FFmpegCliDecoder { output_sender, ffmpeg_path, codec: codec.clone(), + low_latency, }) } } @@ -997,6 +1011,7 @@ impl AsyncDecoder for FFmpegCliDecoder { video_descr.encoding_details.as_ref(), self.ffmpeg_path.as_deref(), &self.codec, + self.low_latency, )?; Ok(()) } diff --git a/crates/utils/re_video/src/decode/mod.rs b/crates/utils/re_video/src/decode/mod.rs index 7b110d04f60f..1ffd2e5db98e 100644 --- a/crates/utils/re_video/src/decode/mod.rs +++ b/crates/utils/re_video/src/decode/mod.rs @@ -310,6 +310,14 @@ pub fn new_decoder( output_sender, decode_settings.ffmpeg_path.clone(), &video.codec, + // Live streams want minimal decode latency: ffmpeg's frame-multithreading + // adds roughly one frame of delay per thread (measured: 17 frames held back + // at default/auto threads vs 3 with a single thread). For static assets + // keep the default for decode throughput. + matches!( + video.delivery_method, + crate::VideoDeliveryMethod::Stream { .. } + ), )?)), crate::VideoCodec::ImageSequence(codec) => { From fee27309373b201f9ff2985242cd617302eea3e6 Mon Sep 17 00:00:00 2001 From: Ivan Nikolic Date: Wed, 22 Jul 2026 17:53:51 +0300 Subject: [PATCH 3/6] chore: reword diagnostic reset logging Co-Authored-By: Claude Fable 5 --- crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs | 2 +- crates/utils/re_video/src/player/mod.rs | 10 +++++----- crates/viewer/re_renderer/src/video/mod.rs | 2 +- .../re_viewer_context/src/cache/video_stream_cache.rs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs index 5a4d2dc3de18..a33e72d4b0de 100644 --- a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs +++ b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs @@ -1004,7 +1004,7 @@ impl AsyncDecoder for FFmpegCliDecoder { fn reset(&mut self, video_descr: &VideoDataDescription) -> crate::decode::Result<()> { re_tracing::profile_function!(); - re_log::debug!("DIAG Resetting ffmpeg decoder {}", self.debug_name); + re_log::debug!("Resetting ffmpeg decoder {}", self.debug_name); self.ffmpeg = FFmpegProcessAndListener::new( &self.debug_name, self.output_sender.clone(), diff --git a/crates/utils/re_video/src/player/mod.rs b/crates/utils/re_video/src/player/mod.rs index 1daad001f209..ab96aaa56a5f 100644 --- a/crates/utils/re_video/src/player/mod.rs +++ b/crates/utils/re_video/src/player/mod.rs @@ -667,14 +667,14 @@ impl VideoPlayer { // decode straight through the new IDR, no reset needed. The enqueue loop // below picks up the new keyframe range. re_log::debug!( - "DIAG GOP rollover without reset (last_enqueued={last_enqueued}, requested_keyframe={requested_keyframe_idx})" + "Continuing video decoder across GOP boundary (last enqueued {last_enqueued}, requested keyframe {requested_keyframe_idx})" ); keyframe_idx } else { // Actual gap between the enqueued samples and the requested keyframe's GOP — // we're skipping frames, so reset. re_log::debug!( - "DIAG video reset[skip]: gap to requested keyframe (last_enqueued={last_enqueued}, gop_start={requested_gop_start:?})" + "Resetting video decoder: gap between enqueued samples and requested keyframe (last enqueued {last_enqueued}, GOP start {requested_gop_start:?})" ); self.reset(video_description)?; // Skip forward and just enqueue the requested keyframe. @@ -829,7 +829,7 @@ impl VideoPlayer { ) -> Result<(), VideoPlayerError> { // If we haven't decoded anything at all yet, reset the decoder. let Some(last_requested) = self.last_requested else { - re_log::debug!("DIAG video reset[1]: nothing requested yet (fresh start or follow-up to a prior reset)"); + re_log::debug!("Resetting video decoder: nothing requested yet"); return self.reset(video_description); }; @@ -846,7 +846,7 @@ impl VideoPlayer { // For each new (!) error after entering the error state, we reset the decoder. // This way, it might later recover from the error as we progress in the video. re_log::debug!( - "DIAG video reset[2]: decode error: {}", + "Resetting video decoder after decode error: {}", self.last_error .as_ref() .map(|e| e.latest_error.to_string()) @@ -862,7 +862,7 @@ impl VideoPlayer { }) { re_log::debug!( - "DIAG video reset[3]: seek forward >1 GOP (last_enqueued={:?}, requested={requested}, requested_keyframe={requested_keyframe})", + "Resetting video decoder: seeking forward by more than one GOP (last enqueued {:?}, requested sample {requested}, requested keyframe {requested_keyframe})", self.last_enqueued ); self.reset(video_description)?; diff --git a/crates/viewer/re_renderer/src/video/mod.rs b/crates/viewer/re_renderer/src/video/mod.rs index 809bec2339e0..adfea56283ef 100644 --- a/crates/viewer/re_renderer/src/video/mod.rs +++ b/crates/viewer/re_renderer/src/video/mod.rs @@ -300,7 +300,7 @@ impl Video { if needs_reset { re_log::debug!( - "DIAG video reset[ooo]: sample insertion overlapping active GOP (change {change_start}..{change_end}, delta {size_delta})" + "Resetting video decoder: out-of-order sample insertion within the active GOP (change {change_start}..{change_end}, delta {size_delta})" ); player.reset(&self.video_description).ok_or_log_error_once(); } else { diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs index 5b9623e7c111..5b856508b3ff 100644 --- a/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs @@ -333,7 +333,7 @@ impl VideoStreamCache { match result { Err(VideoStreamProcessingError::OutOfOrderSamples) => { - re_log::debug!("DIAG Out-of-order chunk detected, performing delta re-merge"); + re_log::debug!("Out-of-order chunk detected, performing delta re-merge"); match handle_out_of_order_chunk( video_data, &mut entry.known_chunk_ranges, From 6e230899afb4cf0c47beeb6fdb7190c15d815f13 Mon Sep 17 00:00:00 2001 From: Ivan Nikolic Date: Wed, 22 Jul 2026 17:55:49 +0300 Subject: [PATCH 4/6] chore: bump dimos-viewer to 0.32.0a2 Co-Authored-By: Claude Fable 5 --- Cargo.lock | 2 +- dimos/Cargo.toml | 2 +- dimos/pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d049c42e43aa..877613d6192e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3043,7 +3043,7 @@ dependencies = [ [[package]] name = "dimos-viewer" -version = "0.32.0-alpha.1" +version = "0.32.0-alpha.2" dependencies = [ "bincode", "clap", diff --git a/dimos/Cargo.toml b/dimos/Cargo.toml index db8b34bf5e43..2a085820c9a1 100644 --- a/dimos/Cargo.toml +++ b/dimos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dimos-viewer" -version = "0.32.0-alpha.1" +version = "0.32.0-alpha.2" edition = "2024" rust-version = "1.92" license = "MIT OR Apache-2.0" diff --git a/dimos/pyproject.toml b/dimos/pyproject.toml index 81fa2e3e4d4f..0eddfa08e55b 100644 --- a/dimos/pyproject.toml +++ b/dimos/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "dimos-viewer" -version = "0.32.0a1" +version = "0.32.0a2" description = "Interactive Rerun viewer for DimOS with click-to-navigate support" readme = "README.md" requires-python = ">=3.10" From ead4debe63f6290ae666bdad7f0903340817d90a Mon Sep 17 00:00:00 2001 From: Ivan Nikolic Date: Wed, 22 Jul 2026 17:59:17 +0300 Subject: [PATCH 5/6] chore: taplo-format dimos/pyproject.toml Co-Authored-By: Claude Fable 5 --- dimos/pyproject.toml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/dimos/pyproject.toml b/dimos/pyproject.toml index 0eddfa08e55b..6c682bdb2a89 100644 --- a/dimos/pyproject.toml +++ b/dimos/pyproject.toml @@ -8,18 +8,18 @@ version = "0.32.0a2" description = "Interactive Rerun viewer for DimOS with click-to-navigate support" readme = "README.md" requires-python = ">=3.10" -license = {text = "MIT OR Apache-2.0"} -authors = [{name = "Dimensional Inc.", email = "engineering@dimensional.com"}] +license = { text = "MIT OR Apache-2.0" } +authors = [{ name = "Dimensional Inc.", email = "engineering@dimensional.com" }] classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Visualization", - "Programming Language :: Rust", - "License :: OSI Approved :: MIT License", - "License :: OSI Approved :: Apache Software License", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Visualization", + "Programming Language :: Rust", + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", ] [project.urls] From b9c0e33db50bcf7b62b8935e1c0cc92721c6b7e6 Mon Sep 17 00:00:00 2001 From: Ivan Nikolic Date: Wed, 22 Jul 2026 18:02:09 +0300 Subject: [PATCH 6/6] chore: fix pre-existing rerun-lint errors in dimos interaction code Co-Authored-By: Claude Fable 5 --- dimos/src/interaction/handle.rs | 4 ++-- dimos/src/interaction/keyboard.rs | 16 ++++++++-------- dimos/src/interaction/ws.rs | 3 ++- docs/websockets.md | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dimos/src/interaction/handle.rs b/dimos/src/interaction/handle.rs index 0f71a6f11fd6..4b112652235c 100644 --- a/dimos/src/interaction/handle.rs +++ b/dimos/src/interaction/handle.rs @@ -34,8 +34,8 @@ impl InteractionHandle { is_2d, }; - if let Err(e) = self.tx.send(event) { - eprintln!("Failed to send click event: {}", e); + if let Err(err) = self.tx.send(event) { + eprintln!("Failed to send click event: {err}"); } } } diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index 291586df7024..b81fd8731ee4 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -93,8 +93,8 @@ impl KeyboardHandler { // If not engaged, don't capture any keys if !self.engaged { if self.was_active { - if let Err(e) = self.publish_stop() { - re_log::warn!("Failed to send stop on disengage: {e}"); + if let Err(err) = self.publish_stop() { + re_log::warn!("Failed to send stop on disengage: {err}"); } self.was_active = false; } @@ -107,8 +107,8 @@ impl KeyboardHandler { // Check for emergency stop (Space key pressed - one-shot action) if ctx.input(|i| i.key_pressed(egui::Key::Space)) { self.state.reset(); - if let Err(e) = self.publish_stop() { - re_log::warn!("Failed to send emergency stop: {e}"); + if let Err(err) = self.publish_stop() { + re_log::warn!("Failed to send emergency stop: {err}"); } self.was_active = false; self.estop_flash = true; @@ -117,13 +117,13 @@ impl KeyboardHandler { // Publish twist command if keys are active, or stop if just released if self.state.any_active() { - if let Err(e) = self.publish_twist() { - re_log::warn!("Failed to publish twist command: {e}"); + if let Err(err) = self.publish_twist() { + re_log::warn!("Failed to publish twist command: {err}"); } self.was_active = true; } else if self.was_active { - if let Err(e) = self.publish_stop() { - re_log::warn!("Failed to send stop on key release: {e}"); + if let Err(err) = self.publish_stop() { + re_log::warn!("Failed to send stop on key release: {err}"); } self.was_active = false; } diff --git a/dimos/src/interaction/ws.rs b/dimos/src/interaction/ws.rs index a05636b5488b..18fc5bb80268 100644 --- a/dimos/src/interaction/ws.rs +++ b/dimos/src/interaction/ws.rs @@ -24,6 +24,7 @@ use tokio::sync::mpsc; pub enum SendError { /// The send queue is full; the event was dropped. QueueFull, + /// Failed to serialize the event to JSON. Serialize(String), } @@ -136,7 +137,7 @@ impl WsPublisher { } fn broadcast(&self, event: WsEvent) -> Result<(), SendError> { - let json = serde_json::to_string(&event).map_err(|e| SendError::Serialize(e.to_string()))?; + let json = serde_json::to_string(&event).map_err(|err| SendError::Serialize(err.to_string()))?; // Non-blocking: error if the channel is full rather than block the UI thread. self.tx.try_send(json).map_err(|_| SendError::QueueFull) } diff --git a/docs/websockets.md b/docs/websockets.md index ee0c5e7f5edc..840a76e1ff3e 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -1,4 +1,4 @@ -# dimos-viewer WebSocket Event Stream +# Dimos-viewer WebSocket event stream When `dimos-viewer` is started with `--connect`, LCM multicast is not available (LCM uses UDP multicast which is limited to the local machine or subnet). Instead,