Repository navigation
Rust core, protocol 1.1, Android camera work, Rust Linux desktop receiver - #3
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
Brought over only app/lib/ui/screens/receiver_screen.dart from origin/test (the rust-test work). Fixed the _Preview constructor, which required a nonexistent 'ystate' field and broke analysis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
Rust crate lenny_core (rlib + cdylib + staticlib) with wire, session, transport, pairing, timing and c_api modules, ported one to one from core/src/*.cpp. The C ABI in include/lenny/lenny.h is unchanged: tools/abi_check.sh regenerates a header with cbindgen and checks struct layouts, constant values and function types against the hand-written one, and the original C++ end-to-end tests (tests/test_session.cpp) pass unmodified when linked against the Rust library. Tests ported to tests/wire.rs (incl. protocol/vectors) and tests/session.rs, plus a struct-layout test. lenny_probe ported to src/bin/lenny_probe.rs. The C++ sources stay until the build integration switches over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
- Android: android_camera's Gradle build runs cargo-ndk (arm64, armv7, x86_64, x86) and the JNI shim links the Rust liblenny_core.so. build.rs sets its SONAME; without it the shim recorded the build machine's path as DT_NEEDED and would not load on a device. - core/CMakeLists.txt now wraps cargo and exposes the same lenny_core target to CMake consumers (windows_receiver; not built on Windows yet). - C++ core sources removed. The C++ session test stays in core/tests/c_abi as a C-caller conformance test against the Rust library. - vcam/: IVirtualCamera trait with V4l2LoopbackCamera and NullVirtualCamera, automatic fallback to null, frame compose/letterbox/placeholder helpers. - CI: fmt, clippy, tests, cbindgen ABI check, C ABI test under ASan/UBSan, cargo-ndk build. - Docs: ADR-0006 (supersedes ADR-0003), README, architecture, testing, CLAUDE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
Wire (additive, gated on the negotiated minor so 1.0 peers see no change): - CAPS lens entries carry their own modes (tag 4) and zoom range (tag 5), so a receiver only offers what the selected lens can stream. - CONTROL pan (21) + controls bit 8: moves the zoomed crop across the lens's field of view; CONTROL_STATE pan_x/pan_y (10, 11). - focus_at now holds until Auto; controls reset to Auto on every reconnect. New vectors control_pan.hex and caps_lens_1_1.hex; tests for gating, including a fake 1.0 phone over raw TCP. C ABI 1.3 (appended, like 1.1/1.2): LENNY_CTL_PAN, LENNY_CAP_PAN, lenny_control_state.pan_x/pan_y, lenny_sender_config.lens_caps + lenny_lens_caps. Rust receivers get the full CAPS via Session::peer_caps(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
Task 3: every lens reports its own modes, built from its StreamConfigurationMap, AE fps ranges and the AVC encoder (no hardcoded size whitelist or 24/30/60 list), and its zoom range (CONTROL_ZOOM_RATIO_RANGE, or max digital zoom before API 30). Sent per lens in CAPS (protocol 1.1). The stream only uses a mode the current lens lists; a lens switch moves to that lens's nearest mode and reports it in STREAM_START instead of silently falling back. Task 4: pan (CONTROL pan) moves SCALER_CROP_REGION over the lens's full field of view on top of CONTROL_ZOOM_RATIO, which still drives sensor switching; the ratio only drops as far as panning toward an edge needs. Pre-API 30 it's crop only. The salvaged Flutter desktop preview's software zoom/pan (InteractiveViewer + BoxFit.cover) is removed. Task 5: Auto | Manual segmented control replaces Auto + Lock focus on the phone and the Flutter desktop. Manual: tap-to-focus (holds), EV slider, exposure lock. Auto sends reset_auto (3A only). Every connect/reconnect starts in Auto. JNI: lens caps and pan in control state. Dart bindings regenerated (ffigen). JVM unit tests for the crop/pan math and mode matching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
A 2x lens of a logical camera at zoom 1.0 is already 2x on the camera's field of view, which is what pan moves over. zoom_range gets a third field (base) and lenny_lens_caps.zoom_base, so a receiver can turn a drag into the right pan change. Vector, docs, JNI and Kotlin updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
lenny_desktop, eframe/egui over winit, linking lenny_core and lenny_vcam directly: - Borderless window with an app-drawn title bar per docs/design.md: minimize, maximize/restore and close send real viewport commands; the bar drags the window, double-click maximizes, window edges resize (BeginResize). - Layout sized from the window every frame (fractions + clamps): preview hero plus a control column when wide, one scrolling column when narrow. - Engine: core receiver session; openh264 decoder thread; virtual camera thread writing a steady 30 fps (live frame, last good frame for 500 ms, then a placeholder with the reason); UDP discovery answers; single-use QR tokens renewed every 80 s and after each pairing; known phones persisted. - Preview sized to the decoded frame's real aspect ratio after rotation (Task 6), never stretched. Manual mode: tap to focus. Zoomed: drag pans the phone's sensor crop, wheel zooms (Task 4, protocol 1.1). - Cards: connection (QR, address chips, known phones, approval dialog), camera (lens, zoom, torch), Auto/Manual, video modes of the selected lens, stream stats and virtual camera status (active vs unavailable here). - fake_phone: synthetic camera -> openh264 -> core sender, for trying the app without a phone; tests/loopback.rs runs the whole pipeline headless. openh264 (built from source by the crate) over FFmpeg: no system libraries, same build on Windows, BSD licensed, and it decodes the Baseline/Constrained High streams Android's encoder produces. Decoded I420 feeds the I420 virtual camera with no conversion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
No emulator in this sandbox (no /dev/kvm; emulator -accel-check: "KVM requires a CPU that supports vmx or svm"), so UI/logic coverage comes from Flutter widget tests (Auto/Manual), JVM unit tests (camera math) and the fake-phone loopback test in desktop/tests. app.yml runs analyze, widget tests, the APK build (cargo-ndk via Gradle) and the JVM tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
…ure and tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
VirtualBox (winget) + Ubuntu 24.04 cloud disk + cloud-init seed ISO built with IMAPI2: Xfce, v4l2loopback (/dev/video10 "Lenny"), OBS, Discord, Chromium, and Lenny Desktop built from the branch. Bridged networking so the phone reaches the VM directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
…tShow + MF backends Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
…eras Byte layout from architecture.md 7.3 (header, 3 seqlock slots, NV12, heartbeat, state), a writer (desktop app) and a bounds-checked reader (the camera DLLs). No dependencies, no OS code; tested here, including a writer/reader race that must never return a torn frame and corrupt headers that must not be read past the mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
…MF camera WindowsCamera implements IVirtualCamera: creates Global\LennyFrames_v1 (DACL from architecture.md 7.3; falls back to Local\ without SeCreateGlobalPrivilege, DirectShow-only), writes I420 frames as NV12, signals the frame event, and on Win11 registers the MF media source via MFCreateVirtualCamera (looked up at runtime). Status text reports what's registered. open_best uses it on Windows. Compile-checked for x86_64 and i686 Windows only; not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
Push source filter reading the lenny_framebuf shared-memory ring: IBaseFilter + one capture pin (IPin, IAMStreamConfig, IKsPropertySet), 1280x720 NV12/YUY2 at 30 fps, allocator negotiation, streaming thread from Pause, graph-clock timestamps while running. Last good frame for 500 ms, then the placeholder. Every COM entry point and the streaming thread are wrapped in catch_unwind; a caught panic switches the filter to placeholder-only. No lenny_core, no network code. regsvr32 registration: COM class (ThreadingModel=Both) plus a VideoInputDeviceCategory entry named "Lenny" (Win10) or "Lenny (Classic)" (Win11). exports.def keeps x86 export names plain. Compile-checked and clippy-clean for x86_64/i686-pc-windows-msvc only; not linked or run yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
… both cameras
Renames vcam/dshow to vcam/com (lenny_vcam_com.dll): one COM DLL with
the DirectShow filter and the MF media source that MFCreateVirtualCamera
points Frame Server at (CLSID {5F0F9024-...}). The MF side follows
Microsoft's SimpleMediaSource shape: IMFActivate (attributes forwarded to
a real store) -> IMFMediaSourceEx + IMFGetService + IKsControl, one
NV12 1280x720 capture stream (IMFMediaStream2 + IKsControl), samples
delivered from a worker thread paced to 30 fps, with the request token.
frames.rs holds the picture choice both cameras share (live, last good
for 500 ms, placeholder). regsvr32 registers both classes.
Compile-checked and clippy-clean for x86_64/i686-pc-windows-msvc only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
vcam/com/tests/com.rs drives the DirectShow filter in a real filter graph (Render + Run) and the MF source through IMFActivate the way Frame Server does (Start, RequestSample x3, paced). The windows job runs clippy and the workspace tests on MSVC x64, the camera tests again as x86, and a regsvr32 register/unregister round trip for both builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
…andoff update Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
…F line endings A Windows checkout converts abi_layout_64.txt to CRLF. The layouts themselves matched. The windows job now runs tests with --no-fail-fast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
spizganed
added a commit
that referenced
this pull request
Sep 26, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Same work as #2, moved to the
rust-rewrite-cloudbranch. It gets tested on a PC and a real phone before merging.Cloud-session work on Tasks 1–8 and the final docs step. Each task has its own commits.
What changed
Task 1: salvaged
app/lib/ui/screens/receiver_screen.dartfromorigin/test(there is norust-testbranch on the remote). It didn't compile because the_Previewconstructor required a field that doesn't exist; fixed that.flutter analyzeandflutter testpass.Task 2: core ported to Rust (
/core, cratelenny_core: cdylib + staticlib + rlib)include/lenny/lenny.his unchanged.core/tools/abi_check.shregenerates the header with cbindgen and compares it withlenny.h: struct layouts, constant values and function types must match. The original C++ end-to-end tests pass unmodified against the Rust library (core/tests/c_abi, run under ASan/UBSan in CI).lenny_probeis ported too..so. The.sosets a SONAME. Without it, the shim recorded the build machine's path and would not have loaded on a phone.lenny_vcam:IVirtualCameratrait with two backends,V4l2LoopbackCameraandNullVirtualCamera, with automatic fallback to null.Tasks 3–5 (Android, Camera2 kept on purpose)
CONTROL_ZOOM_RATIO) and pan (SCALER_CROP_REGION) happen on the camera, not on finished frames. The software zoom/pan in the Flutter desktop preview is removed.pancommand, and pan in CONTROL_STATE, all gated on the negotiated minor version. C ABI 1.3 appends fields to existing structs.Tasks 6–7: Rust desktop receiver (
/desktop, egui, Linux)fake_phonesender and a headless loopback test.Task 8: no emulator in the sandbox (no KVM). Coverage instead comes from JVM, widget and loopback tests; the new
app.ymlworkflow runs them.Test at home before merging
sudo modprobe v4l2loopback exclusive_caps=1 card_label=Lenny, then Chrome, OBS and Zoom see "Lenny" live, with the placeholder when no phone is connected🤖 Generated with Claude Code
https://claude.ai/code/session_01Kb92TxNwgV1nExrrfTzERt
Generated by Claude Code