Skip to content

Drone demo: real-time 2D game with physics#1171

Merged
kixelated merged 10 commits into
mainfrom
robo-demo
Mar 31, 2026
Merged

Drone demo: real-time 2D game with physics#1171
kixelated merged 10 commits into
mainfrom
robo-demo

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

  • Replace pre-recorded fMP4 video with a real-time rendered 2D drone game using tiny-skia (rendering) and Rapier2D (physics)
  • Drone on a 5x5 grid: move with arrow keys, grab/drop a ball with spacebar, auto-dock on low battery
  • Dual rendition encoding: 720p (500kbps) + 360p (200kbps) with resolution labels burned in
  • Rename robodrone throughout (crate, package, paths, docs)
  • Extract publishing recipes into dev/pub module, add flat justfile shortcuts (just drone, just pub)

What's in the game

  • Spring-physics wire for grab/drop (ball swings naturally)
  • Battery drains while flying, recharges on dock, auto-docks at 10%
  • Web viewer with d-pad controls, keyboard input (arrows + space + escape)
  • Sensor HUD synced with game battery
  • Multi-instance support (just drone 3 spawns 3 drones)
  • "No drones online" empty state in viewer

Test plan

  • just drone — starts relay + drone + viewer, video plays in browser
  • Arrow keys move drone, spacebar grabs/drops ball
  • Battery bar drains, auto-docks at 10%, recharges on dock
  • just drone 3 — three drone cards appear in grid
  • Click card to expand, controls appear in side panel
  • 720p/360p label visible in bottom-right of video
  • just dev — web demo still works (pub module extracted correctly)
  • just check passes

🤖 Generated with Claude Code

kixelated and others added 6 commits March 23, 2026 11:27
- Add moq-robo: Rust robot publisher with ffmpeg-next video pipeline,
  fake sensor telemetry, and viewer command handling
- Add web viewer: TypeScript app with robot discovery grid, video player
  with sensor HUD overlay, and multi-viewer control panel
- Move js/demo to demo/web, new robot demo at demo/robot
- Both Rust and TypeScript source coexist in demo/robot/src/
- Update all doc/config references to new demo/web path
- Remove --silent from justfile bun commands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add moq_mux::import::Avc1 for AVCC-formatted H.264 (avc1 style with
  out-of-band description). Parses SPS for width/height, auto-detects
  keyframes from NAL types, mirrors the Avc3 API (initialize/decode).
- Robot publisher produces two renditions: HD via Avc1 transmux and
  240p preview via Avc3 transcode with burned-in "240p" label.
- JS viewer uses Watch API directly with pixel budget switching:
  240p for thumbnails, HD when expanded. Non-expanded cards disabled
  to save bandwidth.
- Rename robot → robo everywhere (directory, module, struct, paths).
- Restructure justfiles: nested modules (dev::robo, dev::web) with
  default recipes, just dev::robo::add to spawn extra publishers.
- Fix announce flapping: separate publish/consume origins.
- Add controller alert overlay (yellow/red based on count).
- Random robot ID if --id not provided.
- Real-time pacing for video pipeline.
- Update all docs for just dev → just, dev/ path restructure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace angle-switching with an action-based state machine:
- idle/dead states loop their video files
- 5 named actions (cup, wrench, ball, cone, box) play once then return to idle
- Actions queue when one is already playing; kill interrupts immediately
- Status track advertises available actions, current state, and queued action
- Viewer dynamically renders action buttons with active/queued highlighting
- SD preview matches source aspect ratio instead of hardcoded 16:9
- CLI uses --media-dir with convention-based file discovery

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace ffmpeg demuxer with mp4_atom for fMP4 parsing. All media files
are preloaded into RAM at startup and parsed into H.264 samples. The
playback loop is fully async with tokio::time::sleep for pacing.

The 240p transcoder (ffmpeg decode/scale/encode) runs on a dedicated
std::thread, communicating via tokio::sync::mpsc channels. No unsafe
Send impls or block_in_place needed — ffmpeg types never cross thread
boundaries.

Also adds anyhow::Context convention to CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the robo demo's fMP4 video playback with a real-time rendered
drone game using tiny-skia (2D rendering) and Rapier2D (physics).

The game features a drone on a 5x5 grid that can move, grab/drop a ball
via a spring-physics wire, and auto-dock when battery runs low. Video is
encoded to H.264 at two renditions (720p + 360p) with resolution labels.

Also renames robo→drone throughout, extracts publishing recipes into
dev/pub module, and adds flat justfile shortcuts (just drone, just pub).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 555cba95-9b2a-41f0-aa1b-d59f49f03300

📥 Commits

Reviewing files that changed from the base of the PR and between f04ba65 and 0162176.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • dev/drone/package.json
  • dev/drone/tsconfig.json
  • dev/web/tsconfig.json
✅ Files skipped from review due to trivial changes (3)
  • dev/web/tsconfig.json
  • dev/drone/tsconfig.json
  • dev/drone/package.json

Walkthrough

The PR reorganizes demos from js/demo/ into dev/web/, adds a new dev/drone/ demo with a Rust publisher (physics-driven 2D drone simulation, video encoding, sensor telemetry) and a TypeScript/Solid.js viewer, and converts top-level justfile commands into modular dev/* justfiles. It adds workspace entries for dev/drone and dev/web, various dev tooling/config (Vite, tsconfig, package.json, Cargo manifests), new docs for the drone and web demos, relay/dev orchestration scripts, and updates CLAUDE.md with repository structure and Rust error-handling conventions.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly summarizes the main change: introducing a real-time 2D drone game with physics as a replacement for the static demo.
Description check ✅ Passed The pull request description is comprehensive and directly related to the changeset, covering the game implementation, features, module reorganization, and test plan.
Docstring Coverage ✅ Passed Docstring coverage is 84.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch robo-demo
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch robo-demo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
doc/setup/dev.md (1)

17-21: ⚠️ Potential issue | 🟡 Minor

Documentation inconsistency: just no longer lists commands.

Line 18 shows just under the comment "List all available commands", but with the new default target, running just will start the demo instead. To list commands, use just --list or just -l.

📝 Suggested fix
 # List all available commands
-just
+just --list

 # Run the demo
 just
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/setup/dev.md` around lines 17 - 21, The documentation incorrectly states
that running "just" will list all commands; update the text in doc/setup/dev.md
to reflect the new default target by replacing the first "just" example with the
correct command to list tasks ("just --list" or "just -l") and keep the second
"just" example as the command to run the demo so readers know "just" now starts
the demo while "just --list" / "just -l" shows available commands.
🧹 Nitpick comments (11)
dev/drone/justfile (1)

7-10: Consider removing redundant sleep calls.

The sleep 1 and sleep 2 before just wait appear redundant since wait already polls repeatedly. This pattern differs from dev/justfile which uses only wait. Unless there's a specific reason for the staggered delays, simplifying to just just wait would be more consistent.

♻️ Suggested simplification
 	bun run concurrently --kill-others --prefix-colors auto \
 		"just relay" \
-		"sleep 1 && just wait && just start {{count}}" \
-		"sleep 2 && just wait && just web"
+		"just wait && just start {{count}}" \
+		"just wait && just web"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/justfile` around lines 7 - 10, Remove the redundant leading sleep
invocations in the bun run concurrently command: replace the "sleep 1 && just
wait" and "sleep 2 && just wait" entries with simply "just wait" so all
concurrent tasks use the same polling wait behavior (leave "just relay", "just
wait", "just start {{count}}", "just web" as the commands run by concurrently).
This aligns with the dev/justfile pattern and avoids unnecessary staggered
delays introduced by the "sleep 1" and "sleep 2" prefixes.
dev/drone/src/index.html (1)

172-174: Consider moving inline styles to the style block.

The footer has extensive inline styles while the rest of the document uses a dedicated <style> block. For consistency and maintainability, consider moving these styles to the existing CSS section.

♻️ Suggested refactor

Add to the <style> block:

footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 0.4rem 1rem;
  background: `#111`;
  border-top: 1px solid `#222`;
  font-family: monospace;
  font-size: 0.65rem;
  color: `#555`;
  text-align: center;
}
footer code { color: `#888`; }

Then simplify the HTML:

-<footer style="position:fixed;bottom:0;left:0;right:0;padding:0.4rem 1rem;background:`#111`;border-top:1px solid `#222`;font-family:monospace;font-size:0.65rem;color:`#555`;text-align:center;">
-  Run <code style="color:`#888`">just dev::drone::start <em>N</em></code> to add more drones
+<footer>
+  Run <code>just dev::drone::start <em>N</em></code> to add more drones
 </footer>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/index.html` around lines 172 - 174, Move the inline styling
from the <footer> element into the document's existing <style> block: create CSS
rules targeting footer (and footer code for the code color) with the position,
bottom/left/right, padding, background, border-top, font-family, font-size,
color, and text-align properties, then remove the style attribute from the
<footer> element in the HTML so the footer markup uses the new CSS rules
instead.
dev/drone/package.json (1)

1-28: LGTM overall. The package configuration is well-structured with appropriate workspace dependencies and dev tooling. The comment syntax using "//" key is a pragmatic workaround for JSON's lack of comments.

Note: Vite 8.0.3 is now available (latest as of March 2026), but the current constraint ^7.3.1 is valid. Consider upgrading to Vite 8 if there are no compatibility concerns with other workspace packages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/package.json` around lines 1 - 28, Update the Vite dependency in
devDependencies from "vite": "^7.3.1" to the desired newer version (e.g.,
"^8.0.3") and then run local builds/tests (the "dev" script and "build" script
referencing vite) to verify compatibility with the workspace packages
(`@moq/hang`, `@moq/lite`, `@moq/watch`, `@moq/signals`); if any incompatibilities
appear, either pin a compatible Vite version or revert to "^7.3.1" and document
the decision.
dev/drone/src/index.ts (2)

179-183: Extract pixel budget constants.

The pixel budget values are magic numbers. Consider extracting them as named constants for clarity.

📝 Proposed fix
+const PIXEL_BUDGET_EXPANDED = 1920 * 1080;
+const PIXEL_BUDGET_THUMBNAIL = 478 * 360;
+
 // ... in the effect:
-			const pixels = exp === droneId ? 1920 * 1080 : 478 * 360;
+			const pixels = exp === droneId ? PIXEL_BUDGET_EXPANDED : PIXEL_BUDGET_THUMBNAIL;

As per coding guidelines: "Avoid using magic numbers; use named constants instead"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/index.ts` around lines 179 - 183, The code uses magic numbers
for pixel budgets inside the this.#signals.run callback; extract 1920 * 1080 and
478 * 360 into named constants (e.g., FULL_HD_PIXELS and THUMBNAIL_PIXELS) and
replace the inline expressions in the branch that computes pixels (where
effect.get(expanded) is compared to droneId and videoSource.target.set({ pixels
}) is called) so the intent is clear and maintainable.

77-299: Consider splitting DroneCard constructor.

The DroneCard constructor handles video setup, sensor subscription, status tracking, controller alerts, and command publishing—all in ~220 lines. Consider extracting logical sections into private helper methods for improved readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/index.ts` around lines 77 - 299, The DroneCard constructor is
too large and should be split into focused private helpers; extract logical
sections (video setup, sensor subscription, status tracking, controller alert
logic, keyboard handling, and command publishing) from the constructor into
clearly named private methods such as setupVideo(video-related args),
setupSensorHud(broadcast,hud), setupStatusTracking(broadcast,status,controls),
setupAlertUpdater(status,alert), setupKeyboardHandlers(droneId) and
setupCommandPublishing(connection, expanded, droneId) and call those from the
constructor; ensure each helper registers/returns any created resources or
accepts this.#signals to register cleanup (and moves the keyHandler
registration/removal into setupKeyboardHandlers) so cleanup remains centralized
via this.#signals.cleanup and existing symbols (DroneCard constructor,
this.#signals.run, videoSource, videoDecoder, broadcast, status, commandTrack)
can be referenced to relocate logic without changing behavior.
doc/setup/demo/drone.md (1)

51-62: Add language specification to fenced code block.

The fenced code block should have a language specified to avoid lint warnings. For text-based diagrams, use text or plaintext.

📝 Proposed fix
-```
+```text
 drone/
   {id}/                           ← drone broadcast
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/setup/demo/drone.md` around lines 51 - 62, The fenced code block in
doc/setup/demo/drone.md lacks a language spec which triggers lint warnings;
update the opening fence to specify a plain-text language (e.g., use ```text or
```plaintext) for the block that starts with "drone/" so the diagram is treated
as text and the linter stops complaining.
dev/drone/src/main.rs (1)

77-80: std::process::exit(0) bypasses async cleanup.

Using std::process::exit(0) on Ctrl-C terminates immediately without running destructors or awaiting pending futures. For a demo this is acceptable, but be aware that graceful shutdown (e.g., closing MoQ sessions cleanly) won't occur.

Also applies to: 93-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/main.rs` around lines 77 - 80, The current
tokio::signal::ctrl_c() arm calls std::process::exit(0) which aborts async
cleanup; change this to trigger a graceful shutdown instead: have main create a
shutdown mechanism (e.g., a CancellationToken or a
tokio::sync::oneshot/broadcast channel) that you pass into run_one (and any
other components like MoQ sessions) and, in the ctrl_c arm, send/trigger that
shutdown signal or simply return so the program can await run_one's completion
and let destructors run; apply the same change to the other ctrl_c usage noted
(the block that spans the later occurrence).
dev/drone/src/drone.rs (1)

115-118: Consider handling poisoned mutex gracefully.

lock().unwrap() will panic if another thread panicked while holding the lock. While unlikely in this demo, using lock().ok() or lock().expect("...") with a clear message would be more defensive.

🛡️ Defensive alternative
         let json = {
-            let state = inner.state.lock().unwrap();
+            let Ok(state) = inner.state.lock() else {
+                // Mutex poisoned, skip this tick
+                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+                continue;
+            };
             serde_json::to_string(&*state)?
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/drone.rs` around lines 115 - 118, The current code calls
inner.state.lock().unwrap() which will panic on a poisoned mutex; change this to
handle the PoisonError and return a proper error instead of panicking (e.g.
match inner.state.lock() or use inner.state.lock().map_err(|e| /* convert to
your function's error with a clear message */)? ), then pass the guarded state
to serde_json::to_string(&*state)? as before; reference inner.state.lock() and
serde_json::to_string to locate the spot and ensure you propagate or convert the
poisoned-lock error with a clear message rather than using unwrap().
dev/drone/src/video.rs (2)

100-106: Thread join in Drop may cause shutdown delays.

The Drop implementation joins the encoder thread synchronously. If the encoder thread is blocked (e.g., on blocking_recv waiting for frames), this could delay cleanup. Consider closing the channel sender first or using a timeout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/video.rs` around lines 100 - 106, The Drop impl for
EncoderHandle currently does a synchronous join on self.thread which can block
shutdown if the encoder thread is waiting; modify drop to first close the frame
sender to unblock the encoder (drop or take the sender field, e.g.,
EncoderHandle.sender) and only then join the thread, or avoid blocking join by
detaching the thread instead (do not call JoinHandle::join in
EncoderHandle::drop) or implement a timed/shutdown handshake before calling
JoinHandle::join; ensure you reference EncoderHandle, its drop method, the
thread field (self.thread) and the channel sender field when making the change.

154-159: Consider avoiding frame clone for HD encoding.

yuv_hd.clone() duplicates the YUV frame buffer before burning the label. Since burn_label modifies the frame in-place, you could reorder operations to burn the HD label, encode HD, then downscale from the original (unlabeled) frame—avoiding the clone entirely.

♻️ Proposed optimization
-        // Burn "720p" label and encode HD.
-        let mut hd_frame = yuv_hd.clone();
-        burn_label(&mut hd_frame, "720p");
-        if let Err(e) = hd_encoder.encode_yuv(&hd_frame, ts, &mut hd) {
+        // Downscale first (before burning label on HD).
+        let mut sd_frame = ffmpeg_next::frame::Video::empty();
+        if let Err(e) = downscaler.run(&yuv_hd, &mut sd_frame) {
+            tracing::error!(error = %e, "downscale error");
+            continue;
+        }
+
+        // Now burn label on HD and encode.
+        burn_label(&mut yuv_hd, "720p");
+        if let Err(e) = hd_encoder.encode_yuv(&yuv_hd, ts, &mut hd) {
             tracing::error!(error = %e, "HD encode error");
         }

-        // Downscale to 360p, burn label, encode SD.
-        let mut sd_frame = ffmpeg_next::frame::Video::empty();
-        if let Err(e) = downscaler.run(&yuv_hd, &mut sd_frame) {
-            tracing::error!(error = %e, "downscale error");
-            continue;
-        }
+        // Burn label on SD and encode.
         burn_label(&mut sd_frame, "360p");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/video.rs` around lines 154 - 159, Avoid cloning yuv_hd: instead
of let mut hd_frame = yuv_hd.clone(), call burn_label on the HD buffer directly
(burn_label(&mut yuv_hd, "720p")) then encode with
hd_encoder.encode_yuv(&yuv_hd, ts, &mut hd); create the low-res buffer by
downscaling from the original unlabeled source frame (the pre-burn variable you
already have) and burn the low-res label there—this removes the yuv_hd.clone()
and keeps burn_label and encode_yuv usage on the appropriate buffers (refer to
burn_label, yuv_hd, hd_encoder, encode_yuv, ts, and hd).
dev/drone/src/game.rs (1)

33-54: Use one source of truth for action names.

dev/drone/src/drone.rs:197 validates commands against action_names(), while dev/drone/src/video.rs:41 parses them with GameAction::from_str(). Keeping two hard-coded lists here makes it easy for a future action to be accepted upstream but silently dropped in the game loop.

♻️ Suggested refactor
+const ACTIONS: &[(&str, GameAction)] = &[
+    ("left", GameAction::Left),
+    ("right", GameAction::Right),
+    ("up", GameAction::Up),
+    ("down", GameAction::Down),
+    ("grab", GameAction::Grab),
+    ("drop", GameAction::Drop),
+    ("dock", GameAction::Dock),
+];
+
 impl GameAction {
     pub fn from_str(s: &str) -> Option<Self> {
-        match s {
-            "left" => Some(Self::Left),
-            "right" => Some(Self::Right),
-            "up" => Some(Self::Up),
-            "down" => Some(Self::Down),
-            "grab" => Some(Self::Grab),
-            "drop" => Some(Self::Drop),
-            "dock" => Some(Self::Dock),
-            _ => None,
-        }
+        ACTIONS
+            .iter()
+            .find_map(|(name, action)| (*name == s).then_some(action.clone()))
     }
 }
@@
 pub fn action_names() -> Vec<String> {
-    ["left", "right", "up", "down", "grab", "drop", "dock"]
-        .iter()
-        .map(|s| s.to_string())
-        .collect()
+    ACTIONS.iter().map(|(name, _)| (*name).to_string()).collect()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/game.rs` around lines 33 - 54, The two hard-coded lists of
action strings are duplicated between GameAction::from_str and action_names;
consolidate to a single source of truth by deriving the list from the GameAction
enum (or a single const array used by both). Update GameAction::from_str to
parse by consulting that single list (or implement a matching helper built from
the enum variants), and change action_names() to generate its Vec<String> from
the same source (e.g., a const ACTION_STRS or an iterator over GameAction
variants), ensuring both parsing and validation use the identical set of action
names (referencing GameAction::from_str and action_names in your changes).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dev/drone/Cargo.toml`:
- Line 21: Update the dependency pins in Cargo.toml so they explicitly reference
the latest stable patch versions: change ffmpeg-next from "8" to "8.1" and
add/replace the rapier2d and tiny-skia entries to pin them to "0.32" and "0.12"
respectively; ensure the dependency keys are exactly ffmpeg-next, rapier2d, and
tiny-skia so Cargo uses the specified versions.

In `@dev/drone/justfile`:
- Around line 17-18: The VITE_RELAY_URL environment assignment in the "web"
justfile target uses an unquoted {{url}} which is unsafe for shell parsing;
update the command in the web target so VITE_RELAY_URL is quoted (e.g.,
VITE_RELAY_URL="{{url}}") to ensure any special characters in {{url}} (like & or
spaces) are handled correctly—modify the "web" target where VITE_RELAY_URL is
set and leave the existing url='http://localhost:4443/anon' declaration intact.

In `@dev/drone/src/game.rs`:
- Around line 267-295: start_dock can leave a stale self.target when the drone
is already in the dock tile but off-center because self.dock_path stays empty;
update start_dock so that if self.dock_path.first() is None you explicitly
retarget to the dock center (e.g. set self.target = Some((self.dock.0 as f32,
self.dock.1 as f32)) or to (gx, gy)) or clear the target as appropriate,
ensuring self.docking remains true; modify the block that currently sets
self.target only when first exists to handle the empty-path case using the
identifiers start_dock, self.dock_path, self.target, gx/gy and self.dock.
- Around line 417-424: reset_to_dock currently only calls do_drop() when
self.carrying is true, so if a reset happens while self.grabbing is still true
the spring joint/wire remains attached; modify reset_to_dock to ensure the
tether is detached in mid-grab by invoking the drop/detach logic whenever
grabbing is true (e.g., check self.grabbing and call self.do_drop() or
unconditionally call self.do_drop() before clearing dock state), and also
clear/reset any related fields like self.grabbing and the spring/joint state so
the ball is no longer tethered.

In `@dev/drone/src/index.html`:
- Around line 80-98: The D-pad button elements styled by .dpad-btn / .dpad-up /
.dpad-left / .dpad-right / .dpad-down lack accessible labels; update the code
that creates or renders these buttons to add descriptive aria-label attributes
(e.g., "Up", "Left", "Right", "Down") or aria-labelledby references so screen
readers can identify each control, and ensure any dynamic creation logic sets
the aria-label on the element before inserting into the DOM.

In `@dev/pub/justfile`:
- Around line 95-106: The publish invocation is using a non-existent flag
"--format annex-b" instead of the publish subcommand; update the pipeline that
runs cargo run --bin moq-cli -- publish to replace the flag style with the avc3
subcommand so the command becomes: call the publish subcommand with avc3 (i.e.,
use "publish ... --url/--name ... avc3 {{args}}") — locate the publish
invocation in the h264 recipe that pipes ffmpeg into cargo run and swap the
"--format annex-b" token for the "avc3" subcommand.

In `@dev/web/justfile`:
- Around line 3-5: Fix the typo in the top comment: change "targetting" to
"targeting" in the comment above the default justfile target so the line reads
"Run the web server targeting the specified relay"; the change is near the
comment that describes the default target which uses the variable url and target
name default.

In `@doc/setup/demo/web.md`:
- Line 13: Replace the namespace-qualified just invocation with the top-level
alias: change the command string "just dev::web" to "just web" so it matches the
root justfile module alias (mod web 'dev/web') and the rest of the
documentation; update the occurrence in the demo web doc where the command is
shown.

In `@rs/moq-mux/src/import/avc1.rs`:
- Around line 143-170: The is_keyframe method currently handles length_size
values 1, 2, and 4 but returns false for length_size == 3; update the match in
is_keyframe to support a 3-byte big-endian length (compute nal_len from
data[offset..offset+3] as a 24-bit BE integer) so NAL unit parsing works for
length_size == 3, keeping the same bounds checks around offset and nal_len and
preserving the existing logic that checks nal_type == 5 for IDR slices.

In `@rs/moq-relay/README.md`:
- Line 55: Clarify that the example key path "key = \"dev/relay/root.jwk\"" is
interpreted relative to the process working directory; update the README.md text
near that snippet to explicitly state that relative paths are resolved from
where moq-relay is launched (or recommend using an absolute path or environment
variable for determinism), and show a brief note or example demonstrating an
absolute path alternative.

---

Outside diff comments:
In `@doc/setup/dev.md`:
- Around line 17-21: The documentation incorrectly states that running "just"
will list all commands; update the text in doc/setup/dev.md to reflect the new
default target by replacing the first "just" example with the correct command to
list tasks ("just --list" or "just -l") and keep the second "just" example as
the command to run the demo so readers know "just" now starts the demo while
"just --list" / "just -l" shows available commands.

---

Nitpick comments:
In `@dev/drone/justfile`:
- Around line 7-10: Remove the redundant leading sleep invocations in the bun
run concurrently command: replace the "sleep 1 && just wait" and "sleep 2 &&
just wait" entries with simply "just wait" so all concurrent tasks use the same
polling wait behavior (leave "just relay", "just wait", "just start {{count}}",
"just web" as the commands run by concurrently). This aligns with the
dev/justfile pattern and avoids unnecessary staggered delays introduced by the
"sleep 1" and "sleep 2" prefixes.

In `@dev/drone/package.json`:
- Around line 1-28: Update the Vite dependency in devDependencies from "vite":
"^7.3.1" to the desired newer version (e.g., "^8.0.3") and then run local
builds/tests (the "dev" script and "build" script referencing vite) to verify
compatibility with the workspace packages (`@moq/hang`, `@moq/lite`, `@moq/watch`,
`@moq/signals`); if any incompatibilities appear, either pin a compatible Vite
version or revert to "^7.3.1" and document the decision.

In `@dev/drone/src/drone.rs`:
- Around line 115-118: The current code calls inner.state.lock().unwrap() which
will panic on a poisoned mutex; change this to handle the PoisonError and return
a proper error instead of panicking (e.g. match inner.state.lock() or use
inner.state.lock().map_err(|e| /* convert to your function's error with a clear
message */)? ), then pass the guarded state to serde_json::to_string(&*state)?
as before; reference inner.state.lock() and serde_json::to_string to locate the
spot and ensure you propagate or convert the poisoned-lock error with a clear
message rather than using unwrap().

In `@dev/drone/src/game.rs`:
- Around line 33-54: The two hard-coded lists of action strings are duplicated
between GameAction::from_str and action_names; consolidate to a single source of
truth by deriving the list from the GameAction enum (or a single const array
used by both). Update GameAction::from_str to parse by consulting that single
list (or implement a matching helper built from the enum variants), and change
action_names() to generate its Vec<String> from the same source (e.g., a const
ACTION_STRS or an iterator over GameAction variants), ensuring both parsing and
validation use the identical set of action names (referencing
GameAction::from_str and action_names in your changes).

In `@dev/drone/src/index.html`:
- Around line 172-174: Move the inline styling from the <footer> element into
the document's existing <style> block: create CSS rules targeting footer (and
footer code for the code color) with the position, bottom/left/right, padding,
background, border-top, font-family, font-size, color, and text-align
properties, then remove the style attribute from the <footer> element in the
HTML so the footer markup uses the new CSS rules instead.

In `@dev/drone/src/index.ts`:
- Around line 179-183: The code uses magic numbers for pixel budgets inside the
this.#signals.run callback; extract 1920 * 1080 and 478 * 360 into named
constants (e.g., FULL_HD_PIXELS and THUMBNAIL_PIXELS) and replace the inline
expressions in the branch that computes pixels (where effect.get(expanded) is
compared to droneId and videoSource.target.set({ pixels }) is called) so the
intent is clear and maintainable.
- Around line 77-299: The DroneCard constructor is too large and should be split
into focused private helpers; extract logical sections (video setup, sensor
subscription, status tracking, controller alert logic, keyboard handling, and
command publishing) from the constructor into clearly named private methods such
as setupVideo(video-related args), setupSensorHud(broadcast,hud),
setupStatusTracking(broadcast,status,controls), setupAlertUpdater(status,alert),
setupKeyboardHandlers(droneId) and setupCommandPublishing(connection, expanded,
droneId) and call those from the constructor; ensure each helper
registers/returns any created resources or accepts this.#signals to register
cleanup (and moves the keyHandler registration/removal into
setupKeyboardHandlers) so cleanup remains centralized via this.#signals.cleanup
and existing symbols (DroneCard constructor, this.#signals.run, videoSource,
videoDecoder, broadcast, status, commandTrack) can be referenced to relocate
logic without changing behavior.

In `@dev/drone/src/main.rs`:
- Around line 77-80: The current tokio::signal::ctrl_c() arm calls
std::process::exit(0) which aborts async cleanup; change this to trigger a
graceful shutdown instead: have main create a shutdown mechanism (e.g., a
CancellationToken or a tokio::sync::oneshot/broadcast channel) that you pass
into run_one (and any other components like MoQ sessions) and, in the ctrl_c
arm, send/trigger that shutdown signal or simply return so the program can await
run_one's completion and let destructors run; apply the same change to the other
ctrl_c usage noted (the block that spans the later occurrence).

In `@dev/drone/src/video.rs`:
- Around line 100-106: The Drop impl for EncoderHandle currently does a
synchronous join on self.thread which can block shutdown if the encoder thread
is waiting; modify drop to first close the frame sender to unblock the encoder
(drop or take the sender field, e.g., EncoderHandle.sender) and only then join
the thread, or avoid blocking join by detaching the thread instead (do not call
JoinHandle::join in EncoderHandle::drop) or implement a timed/shutdown handshake
before calling JoinHandle::join; ensure you reference EncoderHandle, its drop
method, the thread field (self.thread) and the channel sender field when making
the change.
- Around line 154-159: Avoid cloning yuv_hd: instead of let mut hd_frame =
yuv_hd.clone(), call burn_label on the HD buffer directly (burn_label(&mut
yuv_hd, "720p")) then encode with hd_encoder.encode_yuv(&yuv_hd, ts, &mut hd);
create the low-res buffer by downscaling from the original unlabeled source
frame (the pre-burn variable you already have) and burn the low-res label
there—this removes the yuv_hd.clone() and keeps burn_label and encode_yuv usage
on the appropriate buffers (refer to burn_label, yuv_hd, hd_encoder, encode_yuv,
ts, and hd).

In `@doc/setup/demo/drone.md`:
- Around line 51-62: The fenced code block in doc/setup/demo/drone.md lacks a
language spec which triggers lint warnings; update the opening fence to specify
a plain-text language (e.g., use ```text or ```plaintext) for the block that
starts with "drone/" so the diagram is treated as text and the linter stops
complaining.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78d35ac1-2f6f-4bd0-9f49-93abcf1eb4e3

📥 Commits

Reviewing files that changed from the base of the PR and between e674949 and c6736bd.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • bun.lock is excluded by !**/*.lock
  • dev/web/src/favicon.svg is excluded by !**/*.svg
📒 Files selected for processing (58)
  • CLAUDE.md
  • Cargo.toml
  • README.md
  • dev/drone/Cargo.toml
  • dev/drone/justfile
  • dev/drone/package.json
  • dev/drone/src/drone.rs
  • dev/drone/src/game.rs
  • dev/drone/src/index.html
  • dev/drone/src/index.ts
  • dev/drone/src/main.rs
  • dev/drone/src/sensor.rs
  • dev/drone/src/video.rs
  • dev/drone/tsconfig.json
  • dev/drone/vite.config.ts
  • dev/justfile
  • dev/pub/justfile
  • dev/pub/media/.gitignore
  • dev/relay/justfile
  • dev/relay/leaf0.toml
  • dev/relay/leaf1.toml
  • dev/relay/localhost.toml
  • dev/relay/prod.toml
  • dev/relay/root.toml
  • dev/throttle/enable
  • dev/web/.env
  • dev/web/README.md
  • dev/web/justfile
  • dev/web/package.json
  • dev/web/src/discover.ts
  • dev/web/src/highlight.ts
  • dev/web/src/index.css
  • dev/web/src/index.html
  • dev/web/src/index.ts
  • dev/web/src/mse.html
  • dev/web/src/publish.html
  • dev/web/src/publish.ts
  • dev/web/src/vite-env.d.ts
  • dev/web/tailwind.config.js
  • dev/web/tsconfig.json
  • dev/web/vite.config.ts
  • doc/.vitepress/config.ts
  • doc/app/web.md
  • doc/concept/standard/interop.md
  • doc/index.md
  • doc/js/@moq/lite.md
  • doc/js/index.md
  • doc/setup/demo/drone.md
  • doc/setup/demo/web.md
  • doc/setup/dev.md
  • doc/setup/index.md
  • js/demo/tsconfig.json
  • justfile
  • package.json
  • rs/moq-mux/src/import/avc1.rs
  • rs/moq-mux/src/import/decoder.rs
  • rs/moq-mux/src/import/mod.rs
  • rs/moq-relay/README.md
💤 Files with no reviewable changes (1)
  • js/demo/tsconfig.json

Comment thread dev/drone/Cargo.toml
anyhow = { version = "1", features = ["backtrace"] }
bytes = "1"
clap = { version = "4", features = ["derive"] }
ffmpeg-next = "8"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What are the latest versions of the Rust crates ffmpeg-next, rapier2d, and tiny-skia on crates.io?

💡 Result:

The latest versions on crates.io as of 2026-03-28 are: - ffmpeg-next: 8.1.0 (published 2026-03-18) - rapier2d: 0.32.0 (published 2026-01-09) - tiny-skia: 0.12.0 (published 2026-02-02)

Citations:


🏁 Script executed:

sed -n '20,35p' dev/drone/Cargo.toml | cat -n

Repository: moq-dev/moq

Length of output: 586


🏁 Script executed:

find . -name "CLAUDE.md" -o -name "claude.md" | head -5

Repository: moq-dev/moq

Length of output: 66


🏁 Script executed:

cat ./CLAUDE.md

Repository: moq-dev/moq

Length of output: 6332


Update dependency versions to explicitly pin the latest stable releases.

The current semver ranges permit the latest versions, but per CLAUDE.md guidelines to "use the newest stable version" for new dependencies, these should be pinned to the actual latest available:

  • ffmpeg-next = "8.1" (currently 8.1.0)
  • rapier2d = "0.32" (currently 0.32.0)
  • tiny-skia = "0.12" (currently 0.12.0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/Cargo.toml` at line 21, Update the dependency pins in Cargo.toml so
they explicitly reference the latest stable patch versions: change ffmpeg-next
from "8" to "8.1" and add/replace the rapier2d and tiny-skia entries to pin them
to "0.32" and "0.12" respectively; ensure the dependency keys are exactly
ffmpeg-next, rapier2d, and tiny-skia so Cargo uses the specified versions.

Comment thread dev/drone/justfile Outdated
Comment thread dev/drone/src/game.rs
Comment thread dev/drone/src/game.rs
Comment thread dev/drone/src/index.html
Comment on lines +80 to +98
/* D-pad: 3x3 CSS grid */
.dpad {
display: grid;
grid-template-columns: 56px 56px 56px;
grid-template-rows: 56px 56px 56px;
gap: 4px;
}
.dpad-btn {
background: #222; color: #e0e0e0; border: 1px solid #555;
border-radius: 6px; cursor: pointer; font-size: 1.2rem;
display: flex; align-items: center; justify-content: center;
transition: all 0.15s;
}
.dpad-btn:hover { background: #333; border-color: #4ade80; }
.dpad-btn:active { background: #444; transform: scale(0.95); }
.dpad-up { grid-column: 2; grid-row: 1; }
.dpad-left { grid-column: 1; grid-row: 2; }
.dpad-right { grid-column: 3; grid-row: 2; }
.dpad-down { grid-column: 2; grid-row: 3; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add accessible labels to D-pad buttons.

The D-pad buttons lack accessible labels, making them unusable for screen reader users. Consider adding aria-label attributes when these buttons are created in JavaScript, or document that accessibility labels should be added to the button elements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/index.html` around lines 80 - 98, The D-pad button elements
styled by .dpad-btn / .dpad-up / .dpad-left / .dpad-right / .dpad-down lack
accessible labels; update the code that creates or renders these buttons to add
descriptive aria-label attributes (e.g., "Up", "Left", "Right", "Down") or
aria-labelledby references so screen readers can identify each control, and
ensure any dynamic creation logic sets the aria-label on the element before
inserting into the DOM.

Comment thread dev/pub/justfile Outdated
Comment thread dev/web/justfile Outdated
Comment thread doc/setup/demo/web.md Outdated
Comment thread rs/moq-mux/src/import/avc1.rs
Comment thread rs/moq-relay/README.md Outdated
kixelated and others added 2 commits March 28, 2026 14:27
The toml files and justfile recipes referenced relay/ prefixed paths
which were correct when running from dev/ but wrong now that the
working directory is dev/relay/ itself.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix start_dock empty path: target dock center when already on dock tile
- Fix reset_to_dock: also detach wire when grabbing (not just carrying)
- Fix pub h264 recipe: --format annex-b → avc3 subcommand
- Fix avc1 is_keyframe: add length_size == 3 support
- Quote VITE_RELAY_URL in drone justfile for shell safety
- Add aria-labels to d-pad buttons
- Fix web.md: just dev::web → just web
- Fix dev.md: just now runs demo, just --list lists commands
- Fix drone.md: add language spec to fenced code block
- Fix web justfile typo: targetting → targeting
- Clarify relay README key path is relative to working directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
dev/drone/src/video.rs (3)

35-43: Consider handling channel disconnection explicitly.

The try_recv() loop treats both Empty and Disconnected as non-fatal. If the command sender is dropped, this loop will silently continue without commands. For a demo this is acceptable, but you may want to log or handle disconnection if commands are expected to remain available.

Optional: Handle disconnection explicitly
         // Drain all pending commands (non-blocking).
-        while let Ok(name) = cmd_rx.try_recv() {
-            if let Some(action) = game::GameAction::from_str(&name) {
-                game.apply_action(action);
+        loop {
+            match cmd_rx.try_recv() {
+                Ok(name) => {
+                    if let Some(action) = game::GameAction::from_str(&name) {
+                        game.apply_action(action);
+                    }
+                }
+                Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
+                Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
+                    tracing::debug!("command channel disconnected");
+                    break;
+                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/video.rs` around lines 35 - 43, The loop draining commands
currently ignores a Disconnected result from cmd_rx.try_recv(), so if the sender
is dropped the loop will silently spin; update the try_recv handling on cmd_rx
to match on Err(e) and explicitly detect and handle TryRecvError::Disconnected
(e.g., log a warning via your logger or break/stop the loop) while preserving
the current behavior for TryRecvError::Empty, and keep the existing flow that
converts names with game::GameAction::from_str and calls
game.apply_action(action).

100-106: Thread panic information is silently discarded.

If the encoder thread panics, thread.join() returns Err containing the panic payload, but this is ignored. For debugging, consider logging the panic.

Optional: Log thread panic on drop
 impl Drop for EncoderHandle {
     fn drop(&mut self) {
         if let Some(thread) = self.thread.take() {
-            let _ = thread.join();
+            if let Err(panic_payload) = thread.join() {
+                tracing::error!("encoder thread panicked: {:?}", panic_payload);
+            }
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/video.rs` around lines 100 - 106, In the Drop implementation
for EncoderHandle, do not ignore the Result from thread.join(); instead match on
the join outcome and log any panic payload. In the impl Drop for EncoderHandle
(fn drop), replace the unused let _ = thread.join() with code that matches
thread.join() -> Ok(_) => (), Err(payload) => { extract payload as &str or
String via downcast_ref::<&str>() / downcast_ref::<String>() and log it (e.g.
log::error! or eprintln!) with contextual text like "encoder thread panicked" }.
Ensure you reference EncoderHandle, the Drop::drop method, and the call to
thread.join() when making the change.

180-186: Frame cloning is necessary but consider documenting the reason.

The yuv_hd.clone() is required because the original frame is needed for downscaling while a modified copy is used for HD encoding. This is correct but a brief comment would clarify the intent.

Optional: Add clarifying comment
         // Burn "720p" label and encode HD.
+        // Clone because we need the unmodified yuv_hd for SD downscaling.
         let mut hd_frame = yuv_hd.clone();
         burn_label(&mut hd_frame, "720p");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/drone/src/video.rs` around lines 180 - 186, Add a short clarifying
comment above the cloning line explaining that yuv_hd.clone() is required
because burn_label(&mut hd_frame, "720p") mutates the frame and we must preserve
the original yuv_hd for subsequent downscaling/SD processing; reference the
symbols yuv_hd.clone(), burn_label, and hd_encoder.encode_yuv so the intent and
necessity of the mutable copy are clear to future readers.
rs/moq-mux/src/import/avc1.rs (1)

53-65: SPS parsing failures are silently ignored.

If ebsp_to_rbsp or Sps::parse fails, width/height remain 0 and become None in the config. This is acceptable for fault tolerance, but logging a debug message on parse failure would help diagnose malformed streams.

Optional: Log SPS parse failures
 			if offset + sps_len <= avcc.len() && !avcc[offset..].is_empty() {
 				let sps_nalu = &avcc[offset..offset + sps_len];
 				let rbsp = h264_parser::nal::ebsp_to_rbsp(&sps_nalu[1..]);
-				if let Ok(sps) = h264_parser::Sps::parse(&rbsp) {
-					width = sps.width;
-					height = sps.height;
+				match h264_parser::Sps::parse(&rbsp) {
+					Ok(sps) => {
+						width = sps.width;
+						height = sps.height;
+					}
+					Err(e) => {
+						tracing::debug!(error = %e, "failed to parse SPS for dimensions");
+					}
 				}
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rs/moq-mux/src/import/avc1.rs` around lines 53 - 65, The SPS parsing failures
in avc1.rs are currently swallowed (when calling h264_parser::nal::ebsp_to_rbsp
and h264_parser::Sps::parse), leaving width/height at 0; update the block that
processes sps_nalu so that parse errors are logged at debug level (include the
sps_nalu length and error details). Specifically, inside the branch that builds
rbsp from sps_nalu and calls h264_parser::Sps::parse, add a debug log on Err
from ebsp_to_rbsp and on Err from Sps::parse (referencing sps_nalu, rbsp
conversion failure, and the parse error) so failures are visible while
preserving the existing fallback behavior that leaves width/height unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dev/pub/justfile`:
- Around line 133-138: The cleanup() trap is missing EXIT and currently masks
the original exit status; update the trap to include EXIT (trap cleanup SIGINT
SIGTERM EXIT) and modify cleanup() to preserve the original exit code (save $?
into a local variable at start of cleanup, kill the backgrounded jobs/ffmpeg as
it already does, then exit with the saved status) so a crashed Python server or
port error doesn't leave ffmpeg orphaned and the original error code isn't lost;
locate the cleanup() function and the trap line and make these changes, and
ensure the backgrounded ffmpeg invocation referenced in the diff remains
targeted by kill $(jobs -p).

---

Nitpick comments:
In `@dev/drone/src/video.rs`:
- Around line 35-43: The loop draining commands currently ignores a Disconnected
result from cmd_rx.try_recv(), so if the sender is dropped the loop will
silently spin; update the try_recv handling on cmd_rx to match on Err(e) and
explicitly detect and handle TryRecvError::Disconnected (e.g., log a warning via
your logger or break/stop the loop) while preserving the current behavior for
TryRecvError::Empty, and keep the existing flow that converts names with
game::GameAction::from_str and calls game.apply_action(action).
- Around line 100-106: In the Drop implementation for EncoderHandle, do not
ignore the Result from thread.join(); instead match on the join outcome and log
any panic payload. In the impl Drop for EncoderHandle (fn drop), replace the
unused let _ = thread.join() with code that matches thread.join() -> Ok(_) =>
(), Err(payload) => { extract payload as &str or String via
downcast_ref::<&str>() / downcast_ref::<String>() and log it (e.g. log::error!
or eprintln!) with contextual text like "encoder thread panicked" }. Ensure you
reference EncoderHandle, the Drop::drop method, and the call to thread.join()
when making the change.
- Around line 180-186: Add a short clarifying comment above the cloning line
explaining that yuv_hd.clone() is required because burn_label(&mut hd_frame,
"720p") mutates the frame and we must preserve the original yuv_hd for
subsequent downscaling/SD processing; reference the symbols yuv_hd.clone(),
burn_label, and hd_encoder.encode_yuv so the intent and necessity of the mutable
copy are clear to future readers.

In `@rs/moq-mux/src/import/avc1.rs`:
- Around line 53-65: The SPS parsing failures in avc1.rs are currently swallowed
(when calling h264_parser::nal::ebsp_to_rbsp and h264_parser::Sps::parse),
leaving width/height at 0; update the block that processes sps_nalu so that
parse errors are logged at debug level (include the sps_nalu length and error
details). Specifically, inside the branch that builds rbsp from sps_nalu and
calls h264_parser::Sps::parse, add a debug log on Err from ebsp_to_rbsp and on
Err from Sps::parse (referencing sps_nalu, rbsp conversion failure, and the
parse error) so failures are visible while preserving the existing fallback
behavior that leaves width/height unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ab2b4251-5c11-404a-8457-1521c008385a

📥 Commits

Reviewing files that changed from the base of the PR and between 51266c7 and 0b1fac7.

📒 Files selected for processing (12)
  • dev/drone/justfile
  • dev/drone/src/game.rs
  • dev/drone/src/index.ts
  • dev/drone/src/sensor.rs
  • dev/drone/src/video.rs
  • dev/pub/justfile
  • dev/web/justfile
  • doc/setup/demo/drone.md
  • doc/setup/demo/web.md
  • doc/setup/dev.md
  • rs/moq-mux/src/import/avc1.rs
  • rs/moq-relay/README.md
✅ Files skipped from review due to trivial changes (6)
  • doc/setup/demo/web.md
  • doc/setup/dev.md
  • rs/moq-relay/README.md
  • dev/drone/justfile
  • dev/web/justfile
  • doc/setup/demo/drone.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • dev/drone/src/sensor.rs
  • dev/drone/src/index.ts
  • dev/drone/src/game.rs

Comment thread dev/pub/justfile
Comment on lines +133 to +138
cleanup() {
echo "Shutting down..."
kill $(jobs -p) 2>/dev/null || true
exit 0
}
trap cleanup SIGINT SIGTERM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent cleanup: missing EXIT trap may leave ffmpeg orphaned.

Unlike the hls recipe (line 86) which traps SIGINT SIGTERM EXIT, this recipe only traps SIGINT SIGTERM. If the Python server exits unexpectedly (port conflict, crash), the backgrounded ffmpeg process (line 153) will continue running as an orphan.

Additionally, the exit 0 inside cleanup() masks any error exit status.

Suggested fix for consistent cleanup
 cleanup() {
     echo "Shutting down..."
     kill $(jobs -p) 2>/dev/null || true
-    exit 0
 }
-trap cleanup SIGINT SIGTERM
+trap cleanup SIGINT SIGTERM EXIT
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cleanup() {
echo "Shutting down..."
kill $(jobs -p) 2>/dev/null || true
exit 0
}
trap cleanup SIGINT SIGTERM
cleanup() {
echo "Shutting down..."
kill $(jobs -p) 2>/dev/null || true
}
trap cleanup SIGINT SIGTERM EXIT
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/pub/justfile` around lines 133 - 138, The cleanup() trap is missing EXIT
and currently masks the original exit status; update the trap to include EXIT
(trap cleanup SIGINT SIGTERM EXIT) and modify cleanup() to preserve the original
exit code (save $? into a local variable at start of cleanup, kill the
backgrounded jobs/ffmpeg as it already does, then exit with the saved status) so
a crashed Python server or port error doesn't leave ffmpeg orphaned and the
original error code isn't lost; locate the cleanup() function and the trap line
and make these changes, and ensure the backgrounded ffmpeg invocation referenced
in the diff remains targeted by kill $(jobs -p).

@kixelated kixelated changed the base branch from dev to main March 31, 2026 18:09
kixelated and others added 2 commits March 31, 2026 11:22
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kixelated kixelated enabled auto-merge (squash) March 31, 2026 21:28
@kixelated kixelated merged commit e2259ae into main Mar 31, 2026
2 checks passed
@kixelated kixelated deleted the robo-demo branch March 31, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant