feat: recording pipeline, .rec project format, and editor shell#1
Conversation
Replace rec_format/ui crates with core/capture/codec/render/app layout. Workspace resolver 3, fmt+clippy+test CI on Linux with macOS/Windows check jobs.
Directory package with manifest recovery flag and atomic JSON writes. Serde model for takes, non-destructive timeline, and style. Append-only JSONL input events.
VideoEncoder/AudioEncoder over ffmpeg-sidecar, confined to backends/ffmpeg.rs so no ffmpeg types leak into public API. Fragmented MP4 (frag_keyframe+empty_moov), libx264 ultrafast, keyframe every second, AAC 192k.
Recorder drives pinray (display + system mix) into breez-codec encoders on a capture thread. CFR by frame duplication, audio gap silence fill, lazy encoder init, rdev input logger (skipped on Wayland). is_finished() lets the UI notice a self-terminated capture thread.
Frameless eframe window with Geist theme tokens and custom titlebar. Record view (pill bar, timer) wired to breez-capture: record, stop, auto-open editor. Editor chrome: tool rail, tool panel stubs, canvas stage with take metadata, static inspector, timeline transport/ruler/lanes. Painter-based widget set. Recordings land in ~/Videos/Breez.
There was a problem hiding this comment.
9 issues found across 61 files
You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/core/src/project.rs">
<violation number="1" location="crates/core/src/project.rs:62">
P2: Timeline numeric fields have implicit semantic constraints (e.g. `speed > 0`, `src_out_ns >= src_in_ns`, normalized `anchor` values, `gain >= 0`) but are deserialized without any validation. A corrupted or hand-edited `project.json` can silently produce negative-duration clips, zero speed, or out-of-range anchors that propagate into the editor UI and future render/export logic.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:7">
P2: The README's product description and workflow list present trim, zoom & pan, styling, music, and export as current capabilities, but the roadmap section in the same document marks all of those features as unimplemented. This creates a misleading user expectation — readers may try to use features that don't exist yet. Consider qualifying the 'Workflow' and capability text to reflect the current state (record + editor shell only), or reword the roadmap to avoid the direct contradiction.</violation>
</file>
<file name="crates/codec/src/encoder.rs">
<violation number="1" location="crates/codec/src/encoder.rs:37">
P2: The frame-length calculation `config.width as usize * config.height as usize * 4` in `VideoEncoder::create` can silently overflow on pathological dimensions (especially on 32-bit targets). After overflow, `push_frame` will validate against the wrong byte length. Consider computing it with `checked_mul` so it returns `CodecError::BadInput` on overflow instead of panicking or wrapping.</violation>
</file>
<file name="crates/core/src/events.rs">
<violation number="1" location="crates/core/src/events.rs:51">
P1: The `write()` method buffers events through `BufWriter` without flushing to the underlying file, and the capture thread only calls `flush()` at `stop()`. A `kill -9` mid-recording loses all buffered events in the `BufWriter`'s internal buffer, violating the crash-safety expectation for the append-only input event log. Consider adding periodic `flush()` calls (e.g., every N events or every second) during recording, or using unbuffered writes if throughput allows.</violation>
</file>
<file name="crates/codec/src/backends/ffmpeg.rs">
<violation number="1" location="crates/codec/src/backends/ffmpeg.rs:51">
P2: The crop filter silently reduces odd capture dimensions by one pixel, but the original dimensions are still persisted in `Take.width`/`Take.height` and returned in `TakeSummary`. Downstream code that reads the project metadata will expect a different resolution than what the encoded MP4 actually contains, which can shift cursor coordinates or canvas sizing by one pixel.</violation>
</file>
<file name="crates/app/src/ui/widgets/slider.rs">
<violation number="1" location="crates/app/src/ui/widgets/slider.rs:12">
P2: Reversed or degenerate ranges (`max <= min`) produce inconsistent behavior between interaction and rendering. For `max < min`, dragging still updates the underlying value via `new = min + t * (max - min)`, but rendering hardcodes `t = 0.0` so the knob and fill never move. This creates a silent state mutation with no visual feedback. Consider normalizing `min` and `max` so the widget always operates with `max >= min`.</violation>
</file>
<file name="crates/app/src/ui/editor/inspector/audio.rs">
<violation number="1" location="crates/app/src/ui/editor/inspector/audio.rs:14">
P2: The `system_audio_gain` slider does not clamp or sanitize the value when it is loaded from the project. The custom `slider` widget only bounds the value during pointer drag/click; if the deserialized `f32` is already out of range, it persists in the data model while the knob is drawn at the edge. Consider clamping the value on load or inside `slider_row` so the model and UI stay consistent.</violation>
</file>
<file name="crates/app/src/ui/titlebar.rs">
<violation number="1" location="crates/app/src/ui/titlebar.rs:167">
P2: On macOS, egui-winit intentionally avoids populating `viewport().maximized` at runtime to prevent a deadlock, leaving it as `None`. The `unwrap_or(false)` here always yields `false`, so once the window is maximized the titlebar can never send `Maximized(false)` to restore it. Consider tracking the maximized state in app state instead of relying solely on the optional viewport info, so the toggle works reliably across all desktop platforms.</violation>
</file>
<file name="crates/app/src/ui/editor/toolrail.rs">
<violation number="1" location="crates/app/src/ui/editor/toolrail.rs:86">
P2: The rail does not defend against narrow window heights. The top stack of six buttons plus gaps occupies about 304px, while the Plugins button is pinned absolutely at `rail.max.y - 54.0`. When the available height drops below roughly 358px, the bottom button overlaps the top items, and lower items become clipped because there is neither a minimum-height constraint nor a scroll area.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }) | ||
| } | ||
|
|
||
| pub fn write(&mut self, event: &InputEvent) -> Result<(), PackageError> { |
There was a problem hiding this comment.
P1: The write() method buffers events through BufWriter without flushing to the underlying file, and the capture thread only calls flush() at stop(). A kill -9 mid-recording loses all buffered events in the BufWriter's internal buffer, violating the crash-safety expectation for the append-only input event log. Consider adding periodic flush() calls (e.g., every N events or every second) during recording, or using unbuffered writes if throughput allows.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/core/src/events.rs, line 51:
<comment>The `write()` method buffers events through `BufWriter` without flushing to the underlying file, and the capture thread only calls `flush()` at `stop()`. A `kill -9` mid-recording loses all buffered events in the `BufWriter`'s internal buffer, violating the crash-safety expectation for the append-only input event log. Consider adding periodic `flush()` calls (e.g., every N events or every second) during recording, or using unbuffered writes if throughput allows.</comment>
<file context>
@@ -0,0 +1,114 @@
+ })
+ }
+
+ pub fn write(&mut self, event: &InputEvent) -> Result<(), PackageError> {
+ serde_json::to_writer(&mut self.out, event)?;
+ self.out.write_all(b"\n")?;
</file context>
| "-i", | ||
| "pipe:0", | ||
| // yuv420p needs even dimensions; crop a stray odd row/column. | ||
| "-vf", |
There was a problem hiding this comment.
P2: The crop filter silently reduces odd capture dimensions by one pixel, but the original dimensions are still persisted in Take.width/Take.height and returned in TakeSummary. Downstream code that reads the project metadata will expect a different resolution than what the encoded MP4 actually contains, which can shift cursor coordinates or canvas sizing by one pixel.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/codec/src/backends/ffmpeg.rs, line 51:
<comment>The crop filter silently reduces odd capture dimensions by one pixel, but the original dimensions are still persisted in `Take.width`/`Take.height` and returned in `TakeSummary`. Downstream code that reads the project metadata will expect a different resolution than what the encoded MP4 actually contains, which can shift cursor coordinates or canvas sizing by one pixel.</comment>
<file context>
@@ -0,0 +1,131 @@
+ "-i",
+ "pipe:0",
+ // yuv420p needs even dimensions; crop a stray odd row/column.
+ "-vf",
+ "crop=trunc(iw/2)*2:trunc(ih/2)*2",
+ "-c:v",
</file context>
| } | ||
| }); | ||
| // Plugins pins to the rail bottom. | ||
| let bottom = Rect::from_min_size( |
There was a problem hiding this comment.
P2: The rail does not defend against narrow window heights. The top stack of six buttons plus gaps occupies about 304px, while the Plugins button is pinned absolutely at rail.max.y - 54.0. When the available height drops below roughly 358px, the bottom button overlaps the top items, and lower items become clipped because there is neither a minimum-height constraint nor a scroll area.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/app/src/ui/editor/toolrail.rs, line 86:
<comment>The rail does not defend against narrow window heights. The top stack of six buttons plus gaps occupies about 304px, while the Plugins button is pinned absolutely at `rail.max.y - 54.0`. When the available height drops below roughly 358px, the bottom button overlaps the top items, and lower items become clipped because there is neither a minimum-height constraint nor a scroll area.</comment>
<file context>
@@ -0,0 +1,162 @@
+ }
+ });
+ // Plugins pins to the rail bottom.
+ let bottom = Rect::from_min_size(
+ pos2(rail.min.x + (rail.width() - 44.0) / 2.0, rail.max.y - 54.0),
+ vec2(44.0, 46.0),
</file context>
Foundation of Breez: a crash-safe capture pipeline, the
.recproject format, and the egui app shell with a working record -> edit loop.breez-core
.recdirectory package: manifest with crash-recovery flag, atomic JSON writesproject.jsonv1 schema: takes, non-destructive timeline, stylebreez-codec
VideoEncoder/AudioEncoderover ffmpeg-sidecar, confined tobackends/ffmpeg.rs(no ffmpeg types in public API)breez-capture
Recorder: pinray display + system-audio session feeding the encoders on a capture threadbreez (app)
Verification
kill -9mid-record: take decodes cleanly to the last fragment, recovery flag persistscargo clippy --workspace --all-targetszero warnings; fmt clean; unit tests passKnown gaps (intentional, tracked for later phases)
Summary by cubic
Adds a crash‑safe recording pipeline, the
.recproject format, and aneguieditor shell with a working record → edit loop. Uses fragmented MP4 and append‑only input logs, with a frameless app wired to start/stop recording; CI now locks dependencies.New Features
.recpackage: manifest with recovery flag, atomicproject.jsonv1 (takes, timeline, style), append‑only JSONL input event log.breez-codec:VideoEncoder/AudioEncoderoverffmpeg-sidecar(fragmented MP4, libx264 ultrafast, AAC 192k).breez-capture:Recorderfor display + system audio viapinray, CFR pacing by frame dup, silence fill for audio gaps; globalrdevlistener shared across recordings with timestamps anchored to the first frame (Wayland skipped).breez): framelesseframewindow with Geist theme; Record view wired to capture; stop opens Editor; editor chrome (tool rail, canvas, inspector, timeline) with unified aspect‑ratio options.Refactors
crates/core,crates/codec,crates/capture,crates/render,crates/app; removedrec_formatandui.--locked; build checks on macOS/Windows.ffmpeg-sidecar,pinray,egui/eframe,rdev; added Geist font assets.Written for commit dca8896. Summary will update on new commits.