Skip to content

feat: recording pipeline, .rec project format, and editor shell#1

Merged
Itz-Agasta merged 7 commits into
mainfrom
core
Jul 10, 2026
Merged

feat: recording pipeline, .rec project format, and editor shell#1
Itz-Agasta merged 7 commits into
mainfrom
core

Conversation

@Itz-Agasta

@Itz-Agasta Itz-Agasta commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Foundation of Breez: a crash-safe capture pipeline, the .rec project format, and the egui app shell with a working record -> edit loop.

breez-core

  • .rec directory package: manifest with crash-recovery flag, atomic JSON writes
  • project.json v1 schema: takes, non-destructive timeline, style
  • Append-only JSONL input event log

breez-codec

  • VideoEncoder/AudioEncoder over ffmpeg-sidecar, confined to backends/ffmpeg.rs (no ffmpeg types in public API)
  • Fragmented MP4 (playable up to the last fragment after a crash), libx264 ultrafast, AAC 192k

breez-capture

  • Recorder: pinray display + system-audio session feeding the encoders on a capture thread
  • CFR pacing by frame duplication, silence fill for audio gaps, lazy encoder init
  • rdev input logger (X11; skipped on Wayland by design)

breez (app)

  • Frameless window, Geist theme tokens, custom titlebar with Record/Edit toggle
  • Record view: source pills, timer, record button wired to the capture thread; stop opens the editor with the take
  • Editor chrome: tool rail, canvas stage, inspector (background/zoom/cursor/audio), timeline transport + ruler + lanes
  • Painter-based widget set (segmented, switch, slider, anchor grid, swatches)

Verification

  • 10s Wayland (Hyprland portal) recording: 1080p60 H.264 + 48k stereo AAC, A/V delta 28ms
  • kill -9 mid-record: take decodes cleanly to the last fragment, recovery flag persists
  • cargo clippy --workspace --all-targets zero warnings; fmt clean; unit tests pass

Known gaps (intentional, tracked for later phases)

  • Playback/trim/export land in Phase 3+
  • Hardware encode detection deferred (libx264 only)
  • X11 path untested (dev machine is Wayland)
  • Window/Area capture deferred; display capture only

Summary by cubic

Adds a crash‑safe recording pipeline, the .rec project format, and an egui editor 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

    • .rec package: manifest with recovery flag, atomic project.json v1 (takes, timeline, style), append‑only JSONL input event log.
    • breez-codec: VideoEncoder/AudioEncoder over ffmpeg-sidecar (fragmented MP4, libx264 ultrafast, AAC 192k).
    • breez-capture: Recorder for display + system audio via pinray, CFR pacing by frame dup, silence fill for audio gaps; global rdev listener shared across recordings with timestamps anchored to the first frame (Wayland skipped).
    • App (breez): frameless eframe window with Geist theme; Record view wired to capture; stop opens Editor; editor chrome (tool rail, canvas, inspector, timeline) with unified aspect‑ratio options.
  • Refactors

    • Workspace reshaped to crates/core, crates/codec, crates/capture, crates/render, crates/app; removed rec_format and ui.
    • CI: fmt + clippy + tests on Linux with --locked; build checks on macOS/Windows.
    • New deps: ffmpeg-sidecar, pinray, egui/eframe, rdev; added Geist font assets.

Written for commit dca8896. Summary will update on new commits.

Review in cubic

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread crates/app/src/ui/editor/timeline/ruler.rs
Comment thread crates/core/src/events.rs
})
}

pub fn write(&mut self, event: &InputEvent) -> Result<(), PackageError> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread crates/capture/src/input.rs Outdated
Comment thread crates/capture/src/recorder.rs
Comment thread crates/capture/src/recorder.rs
Comment thread crates/core/src/package.rs Outdated
Comment thread crates/app/src/ui/editor/canvas.rs Outdated
Comment thread crates/app/src/ui/editor/canvas.rs
"-i",
"pipe:0",
// yuv420p needs even dimensions; crop a stray odd row/column.
"-vf",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

@Itz-Agasta Itz-Agasta left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

LGTM

@Itz-Agasta Itz-Agasta merged commit cb1ed53 into main Jul 10, 2026
7 checks passed
@Itz-Agasta Itz-Agasta deleted the core branch July 10, 2026 20:37
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