Skip to content

feat(recording): add AudioOnly capture target and recording pipeline …#1881

Open
ManthanNimodiya wants to merge 12 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/audio-only-recording
Open

feat(recording): add AudioOnly capture target and recording pipeline …#1881
ManthanNimodiya wants to merge 12 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/audio-only-recording

Conversation

@ManthanNimodiya

@ManthanNimodiya ManthanNimodiya commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds AudioOnly variant to ScreenCaptureTarget, the source of truth for what gets captured, propagated through every pipeline match
  • Wires AudioOnly through instant and studio recording pipelines, skipping screen/camera capture entirely and building audio-only output
  • Makes display: Option in SingleSegment and MultipleSegments, backwards-compatible via serde default, existing recordings unaffected
  • Makes screen: Option in studio Pipeline, audio-only recordings have no display track
  • Adds audio_only: bool to RecordingMeta so the editor and share page can read it from recording-meta.json
  • Tauri command layer: skips screen capture permission check and shareable content acquisition for AudioOnly

What's not in this PR

(Phase 6-7)
Desktop UI (mode selector), desktop editor (waveform view), and share page (AudioPlayer fallback) are follow-up PRs that build on this foundation.

Test plan

  • cargo clippy --workspace --all-targets -- -D warnings passes clean
  • Old recording-meta.json files without display or audio_only deserialize correctly via serde defaults
  • Existing screen/camera/camera-only recordings behave unchanged (manual verification on desktop app)

Greptile Summary

This PR wires AudioOnly as a first-class ScreenCaptureTarget through the full recording pipeline. The second-round re-review shows that all seven previously flagged blocking issues (missing AudioOnly pipeline arms, premature audio-pipeline cancellation, audio_only flag always written as false, cross-track AV sync regression, crash-recovery data loss, incorrect CurrentRecordingTarget::Camera mapping, and needs_fragment_remux false-positive) have been correctly resolved.

  • SingleSegment.display and MultipleSegment.display are now Option<VideoMeta> with serde defaults, leaving existing recordings fully backward-compatible.
  • persist_final_recording_meta and write_in_progress_meta now read the existing audio_only flag from disk rather than hardcoding false.
  • One remaining issue: EditorInstance::create (line 243) still calls ProjectRecordingsMeta::new without a has_display guard; the rendering crate's SegmentRecordings.display is still a non-optional Video, so the editor window fails to open for any audio-only recording with an opaque "missing display" error.

Confidence Score: 4/5

Safe to merge for the recording-pipeline scope of this PR; the editor path for audio-only recordings will still fail to open.

All previously flagged recording-pipeline defects are resolved. The one remaining defect is in EditorInstance::create: it calls ProjectRecordingsMeta::new without guarding for audio-only, and SegmentRecordings.display in the rendering crate is still non-optional, so opening the editor for an audio-only recording returns a hard error to the user. No UI can create audio-only recordings yet, so the path cannot be triggered today — but the fix is a one-liner guard analogous to the one already added in recording.rs.

crates/editor/src/editor_instance.rs (unconditional ProjectRecordingsMeta::new call) and crates/rendering/src/project_recordings.rs (SegmentRecordings.display non-optional)

Important Files Changed

Filename Overview
crates/recording/src/instant_recording.rs Adds a dedicated ScreenCaptureTarget::AudioOnly arm; previously it fell into the screen-capture _ arm and failed. Audio-only pipeline now writes to content/output.mp4 via Mp4Muxer.
crates/recording/src/studio_recording.rs Adds audio_only branch to create_segment_pipeline, makes Pipeline.screen optional, fixes cancel-guard early-return, restores cross-track AV-sync, and reads audio_only flag from disk in both persist_final_recording_meta and write_in_progress_meta.
crates/recording/src/recovery.rs Fixes audio-only crash recovery: the continue that discarded segments with no display fragments is now guarded by !meta.audio_only; recovered meta correctly omits display for audio-only segments.
apps/desktop/src-tauri/src/recording.rs Adds has_display guard around ProjectRecordingsMeta::new in finalization path; fixes needs_fragment_remux for audio-only; sets audio_only: true in initial meta; audio-only no longer triggers camera window or screen-permission check.
crates/rendering/src/project_recordings.rs Converts .expect() panics to proper Err returns for missing display, but SegmentRecordings.display remains non-optional (Video not Option<Video>), so ProjectRecordingsMeta::new still errors for audio-only recordings — callers outside recording.rs are unguarded.
crates/project/src/meta.rs Makes SingleSegment.display and MultipleSegment.display Option<VideoMeta> with serde defaults for backward compatibility; adds audio_only: bool field to RecordingMeta; updates min_fps/max_fps to return 0 when no display segments exist.
apps/desktop/src-tauri/src/lib.rs Fixes ScreenCaptureTarget::AudioOnly mapping from CurrentRecordingTarget::Camera to CurrentRecordingTarget::Audio, so the frontend state correctly reflects an audio-only session.
crates/editor/src/editor_instance.rs Updates timeline-generation to handle display: Option, but still calls ProjectRecordingsMeta::new unconditionally at line 243 — this will fail for audio-only recordings with a missing-display error, crashing editor load.
apps/cli/src/project.rs Guards displayVideo file check with !meta.audio_only and handles display: Option<VideoMeta> correctly for both SingleSegment and MultipleSegments.

Comments Outside Diff (8)

  1. crates/recording/src/studio_recording.rs, line 1615-1636 (link)

    P1 audio_only flag always written as false by the studio pipeline

    persist_final_recording_meta hardcodes audio_only: false in the RecordingMeta it writes to disk. For audio-only studio recordings, start_recording correctly writes audio_only: true initially, but this function overwrites the file at the end of the recording, so downstream consumers (editor, share page) will always read audio_only: false. The same problem exists in write_in_progress_meta (line 1656), which runs before recording even begins and overwrites the initial value. Both functions need to either accept the capture target as a parameter or read and preserve the existing audio_only value from disk.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/recording/src/studio_recording.rs
    Line: 1615-1636
    
    Comment:
    **`audio_only` flag always written as `false` by the studio pipeline**
    
    `persist_final_recording_meta` hardcodes `audio_only: false` in the `RecordingMeta` it writes to disk. For audio-only studio recordings, `start_recording` correctly writes `audio_only: true` initially, but this function overwrites the file at the end of the recording, so downstream consumers (editor, share page) will always read `audio_only: false`. The same problem exists in `write_in_progress_meta` (line 1656), which runs before recording even begins and overwrites the initial value. Both functions need to either accept the capture target as a parameter or read and preserve the existing `audio_only` value from disk.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/desktop/src-tauri/src/recording.rs, line 853-869 (link)

    P2 Audio-only mode incorrectly triggers the camera window

    The AudioOnly target enters the same branch as CameraOnly here, which calls ShowCapWindow::Camera { centered: true } and sets was_camera_only_recording = true. For an audio-only recording there is no camera feed, so this opens a camera preview window with nothing to show and attaches incorrect state metadata. If the camera permission has not been granted, this could also produce an unexpected permission prompt. The AudioOnly case should likely skip this block entirely.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/recording.rs
    Line: 853-869
    
    Comment:
    **Audio-only mode incorrectly triggers the camera window**
    
    The `AudioOnly` target enters the same branch as `CameraOnly` here, which calls `ShowCapWindow::Camera { centered: true }` and sets `was_camera_only_recording = true`. For an audio-only recording there is no camera feed, so this opens a camera preview window with nothing to show and attaches incorrect state metadata. If the camera permission has not been granted, this could also produce an unexpected permission prompt. The `AudioOnly` case should likely skip this block entirely.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. apps/desktop/src-tauri/src/recording.rs, line 2749-2750 (link)

    P1 Audio-only studio recordings cannot complete finalization

    SegmentRecordings.display is a non-optional Video, so ProjectRecordingsMeta::new returns Err("SingleSegment/MultipleSegment missing display") whenever display is None. At both this call site (line 2749) and handle_recording_finish (line 2506), the result is propagated with ?. For audio-only studio recordings this means neither config.write nor any downstream steps run — the recording's project config is never written and the recording appears incomplete from the editor's perspective.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/recording.rs
    Line: 2749-2750
    
    Comment:
    **Audio-only studio recordings cannot complete finalization**
    
    `SegmentRecordings.display` is a non-optional `Video`, so `ProjectRecordingsMeta::new` returns `Err("SingleSegment/MultipleSegment missing display")` whenever display is `None`. At both this call site (line 2749) and `handle_recording_finish` (line 2506), the result is propagated with `?`. For audio-only studio recordings this means neither `config.write` nor any downstream steps run — the recording's project config is never written and the recording appears incomplete from the editor's perspective.
    
    How can I resolve this? If you propose a fix, please make it concise.
  4. crates/recording/src/studio_recording.rs, line 595-609 (link)

    P1 Cancel-guard immediately terminates audio pipelines for audio-only recordings

    When screen is None (audio-only), screen_done is None, so the if let Some(done) = screen_done guard is skipped and the spawned task proceeds directly to calling mic_cancel.cancel() (and cam_cancel, sys_cancel). Because the task is spawned but not immediately polled, it fires at the very next async yield point after recording starts — effectively cancelling the microphone pipeline before meaningful audio is captured. Every audio-only studio recording would produce an empty or near-empty output.

    The cancellation task should only be spawned when there is an actual screen pipeline to act as the trigger. When screen is absent, the audio pipelines should run until Pipeline::stop is called explicitly.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/recording/src/studio_recording.rs
    Line: 595-609
    
    Comment:
    **Cancel-guard immediately terminates audio pipelines for audio-only recordings**
    
    When `screen` is `None` (audio-only), `screen_done` is `None`, so the `if let Some(done) = screen_done` guard is skipped and the spawned task proceeds directly to calling `mic_cancel.cancel()` (and `cam_cancel`, `sys_cancel`). Because the task is spawned but not immediately polled, it fires at the very next async yield point after recording starts — effectively cancelling the microphone pipeline before meaningful audio is captured. Every audio-only studio recording would produce an empty or near-empty output.
    
    The cancellation task should only be spawned when there is an actual screen pipeline to act as the trigger. When `screen` is absent, the audio pipelines should run until `Pipeline::stop` is called explicitly.
    
    How can I resolve this? If you propose a fix, please make it concise.
  5. crates/recording/src/recovery.rs, line 226-232 (link)

    P1 Audio-only crash recovery silently discards all data

    analyze_incomplete requires display fragments before a segment can be considered recoverable. For audio-only recordings there is no display file, so display_fragments is always empty and every segment is skipped by this continue. The result is that recoverable_segments stays empty, inspect_recording returns None, and the recovery system treats every crashed audio-only recording as if nothing happened — all captured audio is silently discarded.

    Because audio-only studio recordings do use FragmentedAudioMuxer when crash recovery (fragmented = true) is enabled, this is not a theoretical edge case: it affects every audio-only recording that ends with a crash or force-quit.

    A minimal fix is to treat a segment as recoverable when mic_fragments (or system_audio_fragments) are present even with no display, and relax the RecoverableSegment type so display_fragments becomes Option<Vec<PathBuf>> for audio-only paths.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/recording/src/recovery.rs
    Line: 226-232
    
    Comment:
    **Audio-only crash recovery silently discards all data**
    
    `analyze_incomplete` requires display fragments before a segment can be considered recoverable. For audio-only recordings there is no display file, so `display_fragments` is always empty and every segment is skipped by this `continue`. The result is that `recoverable_segments` stays empty, `inspect_recording` returns `None`, and the recovery system treats every crashed audio-only recording as if nothing happened — all captured audio is silently discarded.
    
    Because audio-only studio recordings do use `FragmentedAudioMuxer` when crash recovery (`fragmented = true`) is enabled, this is not a theoretical edge case: it affects every audio-only recording that ends with a crash or force-quit.
    
    A minimal fix is to treat a segment as recoverable when `mic_fragments` (or `system_audio_fragments`) are present even with no display, and relax the `RecoverableSegment` type so `display_fragments` becomes `Option<Vec<PathBuf>>` for audio-only paths.
    
    How can I resolve this? If you propose a fix, please make it concise.
  6. crates/recording/src/studio_recording.rs, line 1415-1498 (link)

    P1 Missing AudioOnly branch — studio recording fails at pipeline creation

    create_segment_pipeline checks camera_only and, when false, falls through to the else branch that calls target_to_display_and_crop (line 1497). That function now returns Err("Camera-only target has no display") for AudioOnly. On macOS the else branch also calls .ok_or_else(|| anyhow!("Missing shareable content"))? (line 1523), which fails because shareable_content is None for AudioOnly. As a result, any attempt to start an audio-only studio recording returns an error before a single byte of audio is captured.

    The audio_only flag defined at line 1410 is used to skip camera and cursor setup but never applied to the screen pipeline branch. An else if audio_only { (None, None, None) } arm — parallel to the existing if camera_only arm — is required to skip screen capture entirely for AudioOnly targets.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/recording/src/studio_recording.rs
    Line: 1415-1498
    
    Comment:
    **Missing AudioOnly branch — studio recording fails at pipeline creation**
    
    `create_segment_pipeline` checks `camera_only` and, when false, falls through to the `else` branch that calls `target_to_display_and_crop` (line 1497). That function now returns `Err("Camera-only target has no display")` for `AudioOnly`. On macOS the else branch also calls `.ok_or_else(|| anyhow!("Missing shareable content"))?` (line 1523), which fails because `shareable_content` is `None` for `AudioOnly`. As a result, any attempt to start an audio-only studio recording returns an error before a single byte of audio is captured.
    
    The `audio_only` flag defined at line 1410 is used to skip camera and cursor setup but never applied to the screen pipeline branch. An `else if audio_only { (None, None, None) }` arm — parallel to the existing `if camera_only` arm — is required to skip screen capture entirely for `AudioOnly` targets.
    
    How can I resolve this? If you propose a fix, please make it concise.
  7. crates/recording/src/instant_recording.rs, line 610-641 (link)

    P1 AudioOnly falls into the screen-capture _ arm — instant recording also fails

    spawn_instant_recording_actor matches CameraOnly explicitly and handles everything else with _ =>. For AudioOnly, the _ => arm calls target_to_display_and_crop (line 614), which returns Err("Camera-only target has no display"). On macOS the arm also calls .ok_or_else(|| anyhow::anyhow!("Missing shareable content"))? (line 635) against a None value, so the actor fails to build and returns an error before any audio pipeline is created.

    AudioOnly needs its own match arm that sets up an audio-only pipeline (mic + system audio) and sets video_info: None, analogous to the CameraOnly arm.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/recording/src/instant_recording.rs
    Line: 610-641
    
    Comment:
    **`AudioOnly` falls into the screen-capture `_` arm — instant recording also fails**
    
    `spawn_instant_recording_actor` matches `CameraOnly` explicitly and handles everything else with `_ =>`. For `AudioOnly`, the `_ =>` arm calls `target_to_display_and_crop` (line 614), which returns `Err("Camera-only target has no display")`. On macOS the arm also calls `.ok_or_else(|| anyhow::anyhow!("Missing shareable content"))?` (line 635) against a `None` value, so the actor fails to build and returns an error before any audio pipeline is created.
    
    `AudioOnly` needs its own match arm that sets up an audio-only pipeline (mic + system audio) and sets `video_info: None`, analogous to the `CameraOnly` arm.
    
    How can I resolve this? If you propose a fix, please make it concise.
  8. crates/editor/src/editor_instance.rs, line 243-246 (link)

    P1 ProjectRecordingsMeta::new fails unconditionally for audio-only recordings

    EditorInstance::create calls ProjectRecordingsMeta::new without any audio-only guard. For audio-only recordings the function returns Err("SingleSegment/MultipleSegment missing display") (because SegmentRecordings.display is still non-optional in the rendering crate) and the ? propagates that error up, causing the editor window to fail to open.

    recording.rs added a has_display guard before the same call, but this site was not updated. Any recording that reaches the editor path — for example if a user opens an audio-only .cap from the recordings overlay — will produce an error dialog instead of an editor.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/editor/src/editor_instance.rs
    Line: 243-246
    
    Comment:
    **`ProjectRecordingsMeta::new` fails unconditionally for audio-only recordings**
    
    `EditorInstance::create` calls `ProjectRecordingsMeta::new` without any audio-only guard. For audio-only recordings the function returns `Err("SingleSegment/MultipleSegment missing display")` (because `SegmentRecordings.display` is still non-optional in the rendering crate) and the `?` propagates that error up, causing the editor window to fail to open.
    
    `recording.rs` added a `has_display` guard before the same call, but this site was not updated. Any recording that reaches the editor path — for example if a user opens an audio-only `.cap` from the recordings overlay — will produce an error dialog instead of an editor.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/editor/src/editor_instance.rs:243-246
**`ProjectRecordingsMeta::new` fails unconditionally for audio-only recordings**

`EditorInstance::create` calls `ProjectRecordingsMeta::new` without any audio-only guard. For audio-only recordings the function returns `Err("SingleSegment/MultipleSegment missing display")` (because `SegmentRecordings.display` is still non-optional in the rendering crate) and the `?` propagates that error up, causing the editor window to fail to open.

`recording.rs` added a `has_display` guard before the same call, but this site was not updated. Any recording that reaches the editor path — for example if a user opens an audio-only `.cap` from the recordings overlay — will produce an error dialog instead of an editor.

Reviews (7): Last reviewed commit: "fix(audio-only): wire up recovery displa..." | Re-trigger Greptile

Comment thread crates/rendering/src/project_recordings.rs Outdated
Comment thread apps/desktop/src-tauri/src/lib.rs Outdated
@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review

Comment thread crates/recording/src/studio_recording.rs Outdated
@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review

Comment thread apps/desktop/src-tauri/src/recording.rs
@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@richiemcilroy, can you have a quick look and lmk if I can go for the phase 6 and 7, no hurry just in case this gets buried

@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

Comment on lines 1139 to 1147
let needs_remux = if fragmented {
segment_metas.iter().any(|seg| {
let display_path = seg.display.path.to_path(&recording_dir);
let display_path = seg
.display
.as_ref()
.map(|d| d.path.to_path(&recording_dir))
.unwrap_or_default();
display_path.is_dir()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

unwrap_or_default() here will treat display: None like an empty path; is_dir() can easily become true (e.g. current dir), which would incorrectly mark audio-only recordings as needing remux.

Suggested change
let needs_remux = if fragmented {
segment_metas.iter().any(|seg| {
let display_path = seg.display.path.to_path(&recording_dir);
let display_path = seg
.display
.as_ref()
.map(|d| d.path.to_path(&recording_dir))
.unwrap_or_default();
display_path.is_dir()
})
let needs_remux = if fragmented {
segment_metas.iter().any(|seg| {
seg.display
.as_ref()
.is_some_and(|d| d.path.to_path(&recording_dir).is_dir())
})
} else {
false
};

sharing: None,
inner: RecordingMetaInner::Studio(Box::new(studio_meta.clone())),
upload: None,
audio_only: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This hardcodes audio_only: false when persisting the final meta. If recording-meta.json was initially written with audio_only: true, this will overwrite it at the end (same issue in write_in_progress_meta).

Suggested change
audio_only: false,
audio_only: RecordingMeta::load_for_project(recording_dir)
.ok()
.map(|m| m.audio_only)
.unwrap_or(false),

Comment thread apps/desktop/src-tauri/src/import.rs Outdated
.enumerate()
.map(|(index, segment)| {
let duration = get_video_duration_secs(&segment.display.path.to_path(project_path))?;
let duration = get_video_duration_secs(&segment.display.as_ref().map(|d| d.path.clone()).unwrap_or_default().to_path(project_path))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Using unwrap_or_default() here means a missing display falls back to project_path and you’ll try to read a directory as a video. I think this should stay an explicit error (like the full_timeline_for_source_segments path).

Suggested change
let duration = get_video_duration_secs(&segment.display.as_ref().map(|d| d.path.clone()).unwrap_or_default().to_path(project_path))?;
let display = segment.display.as_ref().ok_or("Missing display video")?;
let duration = get_video_duration_secs(&display.path.to_path(project_path))?;

let display = s
.display
.as_ref()
.ok_or_else(|| "SingleSegment missing display".to_string())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Now that display is optional, this returns an error whenever it’s None. If studio audio-only recordings are expected to finalize, callers need to skip ProjectRecordingsMeta::new (or this type needs to represent display as optional too) so audio-only paths don’t fail here.

Comment on lines +610 to +613
tokio::spawn(async move {
// When screen (video) finishes, cancel the other pipelines
let _ = screen_done.await;
if let Some(done) = screen_done {
let _ = done.await;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This still cancels the other pipelines immediately for audio-only (screen_done = None). If the intent is “cancel others when screen finishes”, the task should exit early when there’s no screen pipeline.

Suggested change
tokio::spawn(async move {
// When screen (video) finishes, cancel the other pipelines
let _ = screen_done.await;
if let Some(done) = screen_done {
let _ = done.await;
}
tokio::spawn(async move {
let Some(done) = screen_done else {
return;
};
let _ = done.await;

Comment thread apps/desktop/src-tauri/src/recording.rs Outdated
let display_output_path = match &updated_studio_meta {
StudioRecordingMeta::SingleSegment { segment } => {
segment.display.path.to_path(&recording_dir)
segment.display.as_ref().map(|d| d.path.clone()).unwrap_or_default().to_path(&recording_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor footgun with the unwrap_or_default() change: for audio-only, this resolves to recording_dir and then gets passed into create_screenshot a few lines later. Might be worth guarding the screenshot generation behind if segment.display.is_some() (and same for the multiple-segment case) so audio-only doesn’t try to thumbnail a directory.

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai @tembo please re-review

@tembo

tembo Bot commented Jun 18, 2026

Copy link
Copy Markdown

@greptileai @tembo please re-review

re-reviewed pr #1881 at the latest commit (b04c68d) and posted my findings as a pr comment.

two of the four flagged issues are fixed:

  • cancel-guard no longer kills audio-only pipelines in spawn_watcher (early-returns when there's no screen track)
  • audio-only finalization no longer fails (inline finish path is now guarded by has_display, and the remux path isn't reachable for audio-only)

two are still open:

  • p1: audio_only is still hardcoded false in the studio meta writers (studio_recording.rs:1782 and :1812), so editor/share will never see audio-only studio recordings as audio-only
  • p2: AudioOnly still opens the camera window because it's bundled with CameraOnly in recording.rs:1235-1252 (empty camera preview + possible camera permission prompt)

comment with details and concrete fix suggestions: #1881 (comment)

want me to push a fix for the two remaining issues?

@tembo

tembo Bot commented Jun 18, 2026

Copy link
Copy Markdown

Re-review (commit b04c68d8)

Re-checked the four issues flagged in the last round. Two are resolved, two are still open.

✅ Fixed

Cancel-guard no longer kills audio-only pipelinescrates/recording/src/studio_recording.rs:609-624
The spawned cancel task now early-returns when there's no screen pipeline:

let screen_done = self.screen.as_ref().map(|s| s.done_fut());
tokio::spawn(async move {
    let Some(done) = screen_done else { return; };
    let _ = done.await;
    // ...cancel mic/cam/sys
});

For audio-only (screen == None), screen_done is None, so mic/cam/sys are never cancelled and run until Pipeline::stop. Correct.

Audio-only finalization no longer failsapps/desktop/src-tauri/src/recording.rs:2832-2858
The inline finish path now guards on has_display, building empty ProjectRecordingsMeta { segments: vec![] } instead of calling ::new (which still returns Err(...missing display) for None display), and the display screenshot is only spawned when a display path exists. Audio-only recordings reach this path because needs_fragment_remux (recording.rs:3418) returns false when segments have no display, so the unguarded finalize_studio_recording path is never hit for audio-only.

❌ Still open

1. audio_only always written as false by the studio pipeline (P1)crates/recording/src/studio_recording.rs:1782 and :1812
Both persist_final_recording_meta and write_in_progress_meta still hardcode audio_only: false. For an audio-only studio recording, the final recording-meta.json (and the in-progress one) will report audio_only: false, so the editor/share page can't detect it. Note the instant path (recording.rs:1409) sets this correctly, but the studio path overwrites with false.

Both functions have what they need in scope to fix this:

  • write_in_progress_meta is called from spawn_studio_recording_actor (:865), which has base_inputs.capture_target — pass matches!(base_inputs.capture_target, ScreenCaptureTarget::AudioOnly).
  • persist_final_recording_meta takes studio_meta, so audio_only can be derived (segments non-empty and all display are None), or read+preserve the existing value already on disk.

2. AudioOnly opens the camera window (P2)apps/desktop/src-tauri/src/recording.rs:1235-1252
AudioOnly is still bundled with CameraOnly:

if matches!(inputs.capture_target,
    ScreenCaptureTarget::CameraOnly | ScreenCaptureTarget::AudioOnly) {
    inputs.capture_system_audio = false;
    app_state.was_camera_only_recording = true;
    ShowCapWindow::Camera { centered: true }.show(&app).await?;
}

For audio-only there's no camera feed, so this shows an empty camera preview, sets was_camera_only_recording = true, and may trigger an unexpected camera permission prompt. AudioOnly should skip the camera-window block (the capture_system_audio = false line, if intended for audio-only, can be split out).


Summary: the two P1 regressions from the studio pipeline are addressed; the remaining blockers are the hardcoded audio_only: false (P1) and the camera window for audio-only (P2). Happy to push a fix for both if you'd like.

let recordings = if has_display {
ProjectRecordingsMeta::new(&recording_dir, &updated_studio_meta)?
} else {
ProjectRecordingsMeta { segments: vec![] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One concern with the audio-only fallback here: ProjectRecordingsMeta { segments: vec![] } means project_config_from_recording will still persist timeline: Some with 0 segments, which can block later auto-timeline generation (it only runs when timeline is None).

Might be worth explicitly leaving timeline unset for audio-only (or gating timeline creation on !recordings.segments.is_empty()).

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@tembo please re-review

@tembo

tembo Bot commented Jun 18, 2026

Copy link
Copy Markdown

@tembo please re-review

re-reviewed pr #1881 at the latest commit (b22097e) and posted a review on github.

good progress since last round — most previously-flagged issues are fixed:

  • needs_remux no longer mis-flags audio-only segments
  • cancel-guard now early-returns when there's no screen pipeline (was killing audio pipelines instantly)
  • cross-track av-sync snapping is preserved for screen recordings
  • empty timeline no longer persisted for audio-only (the new commit handles this)
  • ProjectRecordingsMeta::new is guarded by has_display, so no more panic/err on finalization

still open (left inline comments):

  1. (p1) audio_only flag is hardcoded to false in both persist_final_recording_meta and write_in_progress_meta in studio_recording.rs — overwrites the true written at start, so editor/share page always read false. suggested a one-line fix using RecordingMeta::load_for_project.
  2. (p2) AudioOnly still shares the CameraOnly branch in recording.rs and opens a camera preview window (+ possible camera permission prompt). should skip that block.

flagged as follow-up (not a regression here): recovery.rs analyze_incomplete skips any segment without display fragments, so a crashed audio-only studio recording is silently discarded. recovery.rs isn't touched by this pr, so it's a pre-existing gap audio-only newly exposes — fine to handle in a later phase but worth tracking.

once 1 and 2 are addressed it looks ready to go.

@tembo tembo 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.

Re-review @ b22097e

Good progress since the last round — most of the previously-flagged issues are resolved. Summary below.

✅ Fixed

  • needs_fragment_remux / needs_remux — audio-only segments (display: None) no longer resolve to recording_dir and get mis-flagged as fragmented; the empty-path fallback is false under is_dir(). (studio_recording.rs:1143)
  • Cancel-guard — the spawned task now early-returns when screen_done is None, so audio pipelines are no longer cancelled immediately for audio-only recordings. (studio_recording.rs:609)
  • Cross-track AV syncCROSS_TRACK_SNAP_SECS snapping is preserved for screen recordings; the raw-fallback only fires when screen is None. (studio_recording.rs:978)
  • Empty timelineproject_config_from_recording now gates config.timeline on !timeline_segments.is_empty(), so audio-only recordings no longer persist a 0-segment timeline that would block auto-timeline generation. (recording.rs:3328, commit b22097e)
  • ProjectRecordingsMeta::new — callers guard with has_display before calling it, so audio-only finalization no longer hits the Err/panic path. (recording.rs:2853)

❌ Still open

  1. audio_only flag clobbered to false (P1) — persist_final_recording_meta (line 1782) and write_in_progress_meta (line 1812) both hardcode audio_only: false, overwriting the true written by start_recording. Downstream consumers always read false. Inline comment with suggested fix.
  2. Camera window opened for AudioOnly (P2) — AudioOnly shares the CameraOnly branch and shows ShowCapWindow::Camera, opening an empty preview and possibly prompting for camera permission. Inline comment at recording.rs:1237.

⚠️ Follow-up worth tracking

  • Audio-only crash recoveryRecoveryManager::analyze_incomplete (crates/recording/src/recovery.rs:226) continues on any segment without display fragments, so a crashed audio-only studio recording is silently discarded. recovery.rs is not touched by this PR, so this is a pre-existing limitation that audio-only newly exposes rather than a regression here. Given the UI to trigger audio-only studio recordings is a follow-up phase, this is reasonable to address in a later PR — but it should be tracked so audio-only doesn't ship with silent data loss on crash.

Once (1) and (2) are addressed this looks ready. Nice work on the sync/cancel/timeline fixes.

sharing: None,
inner: RecordingMetaInner::Studio(Box::new(studio_meta.clone())),
upload: None,
audio_only: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still open (re-review): audio_only is hardcoded to false here. start_recording writes audio_only: true for an AudioOnly studio recording, but this final persist overwrites recording-meta.json at the end, so the editor/share page always read false. write_in_progress_meta (line 1812) has the same hardcode and runs before recording starts, clobbering the initial true as well.

Since RecordingMeta::load_for_project exists, the minimal fix is to read and preserve the existing value:

audio_only: RecordingMeta::load_for_project(recording_dir)
    .ok()
    .map(|m| m.audio_only)
    .unwrap_or(false),

Comment thread apps/desktop/src-tauri/src/recording.rs Outdated
if matches!(inputs.capture_target, ScreenCaptureTarget::CameraOnly) {
if matches!(
inputs.capture_target,
ScreenCaptureTarget::CameraOnly | ScreenCaptureTarget::AudioOnly

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still open (re-review): AudioOnly shares this CameraOnly branch, so it calls ShowCapWindow::Camera { centered: true } and sets was_camera_only_recording = true. An audio-only recording has no camera feed, so this opens an empty camera preview window, attaches incorrect state metadata, and may trigger a camera permission prompt that audio-only users shouldn't see.

AudioOnly should skip the camera-window block. The capture_system_audio = false line is also worth a second look for audio-only — disabling system audio limits audio-only to mic-only capture, which may not be the intended behavior (vs. CameraOnly where it makes sense).

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@tembo @greptileai please re-review

@tembo

tembo Bot commented Jun 18, 2026

Copy link
Copy Markdown

@tembo @greptileai please re-review

re-reviewed pr #1881 at commit 801e689 and posted my review: #1881 (review)

most of the round-1 issues are now fixed:

  • audio_only flag is preserved in the meta writes (reads existing value first)
  • camera window no longer opens for AudioOnly
  • cancel-guard early-returns for audio-only instead of killing the mic/cam/sys pipelines
  • finalization is guarded with has_display and skips timeline gen when there are no segments

but i found a blocker that greptile missed: AudioOnly never actually reaches an audio-only pipeline. neither instant nor studio has a branch for it, so it falls through to screen-capture code that calls target_to_display_and_crop, which returns Err("Camera-only target has no display") for AudioOnly (capture_pipeline.rs:553). result: an audio-only recording errors out at startup before any audio is captured, in both modes. the audio_only var in studio's create_segment_pipeline is even computed but never used in the screen tuple. this contradicts the pr's stated goal of building audio-only output.

still open from round 1:

  • recovery.rs:226-232 still discards all audio-only data on crash (skips segments with no display fragments)
  • a second ProjectRecordingsMeta::new call in finalize_studio_recording (recording.rs:3105) is unguarded — latent, only bites once audio-only remux/recovery is wired up

also worth noting: no tests exercise an actual audio-only recording start or audio-only crash recovery, so these slip past clippy/compile.

happy to open a follow-up pr with the audio-only pipeline branches + the recovery fix if you want me to.

Comment thread apps/cli/src/project.rs Outdated
"displayVideo",
meta.path(&segment.display.path),
));
if let Some(display) = &segment.display {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should this be gated on meta.audio_only rather than segment.display.is_some()? As-is, a corrupted/non-audio-only studio recording with missing display metadata would get reported as valid since the required check is skipped.

Comment thread crates/rendering/src/main.rs Outdated
.display
.as_ref()
.map(|d| recording_meta.path(&d.path))
.unwrap_or_default(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

unwrap_or_default() here turns a missing display track into an empty path (often treated as .), which leads to pretty confusing decoder errors. Since this binary can’t render without a display track anyway, failing fast seems clearer.

Suggested change
.unwrap_or_default(),
.context("Missing display video")?,

Comment thread crates/rendering/src/main.rs Outdated
.display
.as_ref()
.map(|d| recording_meta.path(&d.path))
.unwrap_or_default(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same idea as above for the multi-segment path — avoids passing an empty path into the screen decoder.

Suggested change
.unwrap_or_default(),
.context("Missing display video")?,

Comment thread crates/rendering/src/lib.rs Outdated
StudioRecordingMeta::MultipleSegments { inner, .. } => {
inner.segments[segment_i].display.fps
StudioRecordingMeta::SingleSegment { segment } => {
segment.display.as_ref().map(|d| d.fps).unwrap_or(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Defaulting screen_fps to 0 makes downstream failures harder to diagnose (and risks odd behavior if it slips through). Returning a clear error here keeps the invariant “rendering requires display metadata” explicit.

Suggested change
segment.display.as_ref().map(|d| d.fps).unwrap_or(0)
segment
.display
.as_ref()
.map(|d| d.fps)
.ok_or_else(|| "Display metadata missing".to_string())?

Comment thread crates/rendering/src/lib.rs Outdated
.display
.as_ref()
.map(|d| d.fps)
.unwrap_or(0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same for the multi-segment case.

Suggested change
.unwrap_or(0),
.map(|d| d.fps)
.ok_or_else(|| "Display metadata missing".to_string())?,

@tembo tembo 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.

Re-review (commit 801e689)

Re-reviewed after the two follow-up commits. Good progress — most of the previously-flagged issues are now resolved, but there's a core-functionality bug that blocks the whole feature plus the recovery gap that's still open.

✅ Fixed since last review

  • audio_only flag no longer overwrittenpersist_final_recording_meta / write_in_progress_meta now read the existing value via RecordingMeta::load_for_project before re-writing (studio_recording.rs:1775, 1802). 👍
  • Camera window no longer opened for AudioOnlystart_recording now matches CameraOnly only (recording.rs:1235). 👍
  • Cancel-guard early-returns for audio-only — the spawned task bails when screen_done is None instead of cancelling the mic/cam/sys pipelines (studio_recording.rs:610-613). 👍
  • Finalization guardedhandle_recording_finish uses has_display and substitutes an empty ProjectRecordingsMeta instead of calling ProjectRecordingsMeta::new for audio-only (recording.rs:2851); timeline generation is skipped when there are no segments (recording.rs:3328). 👍

🔴 Blocker (new) — AudioOnly never reaches an audio-only pipeline; it errors out at startup

Neither pipeline has a branch for AudioOnly, so it falls through to screen-capture code that calls target_to_display_and_crop, which returns Err("Camera-only target has no display") for AudioOnly (capture_pipeline.rs:553-555). Both recording modes fail before any audio is captured:

  • Studiocreate_segment_pipeline only special-cases camera_only; audio_only is computed (studio_recording.rs:1410) but never used in the screen tuple, so it hits the else branch and errors at studio_recording.rs:1497-1498.
  • Instantmatch inputs.capture_target has a CameraOnly arm but no AudioOnly arm, so AudioOnly hits _ => and errors at instant_recording.rs:614.

This contradicts the PR's stated goal of "skipping screen/camera capture entirely and building audio-only output." Each pipeline needs a dedicated audio-only arm (build mic + optional system-audio pipelines, set screen/display to None, skip target_to_display_and_crop). Worth adding a smoke test that actually starts an audio-only recording in each mode — clippy/compile won't catch this since the failure is at runtime.

🔴 Still open — audio-only crash recovery silently discards all data

recovery.rs:226-232 still continues whenever display_fragments is empty, so every audio-only segment is skipped, recoverable_segments stays empty, and inspect_recording returns None. Any audio-only studio recording that ends in a crash/force-quit loses all captured audio. Fix as previously suggested: treat a segment as recoverable when mic_fragments/system_audio_fragments are present, and make display_fragments optional on RecoverableSegment. (No test added for this path either — tests/recovery.rs only covers display recordings.)

🟡 Latent — second ProjectRecordingsMeta::new call site is unguarded

finalize_studio_recording (the fragmented-remux path) still calls ProjectRecordingsMeta::new(...)? unconditionally at recording.rs:3105. It's currently unreachable for audio-only because needs_fragment_remux returns false when no segment has a display dir — but the moment audio-only recovery/remux is wired up (see the recovery bug above), this becomes a real finalization failure. Mirror the has_display guard used at recording.rs:2851 here too.


Net: the meta/camera/cancel/finalize fixes are correct, but as it stands an audio-only recording can't actually start in either mode, and the recovery path would drop data if it did. Happy to open a PR with the pipeline branches + recovery fix if useful.

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@tembo please re-review

@tembo

tembo Bot commented Jun 18, 2026

Copy link
Copy Markdown

@tembo please re-review

re-reviewed pr #1881 at the latest commit (d7f2fb0) and posted a review on github.

the new commit since my last pass only hardens the renderer/cli to fail-fast on missing display — good cleanup, but the core blocker is still there.

still blocking:

  • AudioOnly never reaches an audio-only pipeline. studio (studio_recording.rs:1415) falls into the else screen-capture branch and instant (instant_recording.rs:610) falls into the _ => arm — both call target_to_display_and_crop which returns Err for AudioOnly (and on macos require shareable_content that's None). the audio_only flag computed at studio:1410 is used to skip camera/cursor but never applied to the screen tuple. so an audio-only recording errors at startup before any audio is captured, in both modes. greptile flagged the same two spots.

still open from round 1:

  • recovery.rs:226-231 still discards audio-only data on crash (skips segments with no display fragments) — pre-existing gap, recovery.rs isn't touched by this pr, fine to defer
  • recording.rs:3105 second unguarded ProjectRecordingsMeta::new in finalize_studio_recording — latent, only bites once remux/recovery is wired for audio-only

confirmed still fixed: audio_only flag preserved in meta writes, camera window skipped for AudioOnly, cancel-guard early-returns with no screen track, finalization guarded by has_display.

the two studio/instant pipeline branches are the gating items. happy to push a fix for those + the recovery handling if you want.

@tembo tembo 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.

Re-review (commit d7f2fb08e)

Re-reviewed against my last pass (801e689). The new commit hardens the renderer/CLI to fail-fast on missing display (crates/rendering/src/{lib,main}.rs, apps/cli/src/project.rs now gate on audio_only) — good hygiene, but it does not touch the core blocker.

❌ Still blocking — AudioOnly never reaches an audio-only pipeline

Unchanged since my last review. Neither recording mode has a branch for AudioOnly, so it falls through to screen-capture code that errors out before any audio is captured. Greptile flagged the same two spots (its comments 6 & 7).

Studiocrates/recording/src/studio_recording.rs:1415
The screen tuple is if camera_only { … } else { … }. AudioOnly hits the else, which calls target_to_display_and_crop at line 1498 → Err("Camera-only target has no display") (capture_pipeline.rs:553), and on macOS also requires shareable_content at line 1523 (None for audio-only). The audio_only flag computed at line 1410 is applied to the camera (1559) and cursor (1698) skips but never to the screen tuple. Needs an else if audio_only { (None, None, None) } arm parallel to camera_only.

Instantcrates/recording/src/instant_recording.rs:610
spawn_instant_recording_actor matches CameraOnly explicitly; AudioOnly falls into _ =>, calling target_to_display_and_crop at 614 (same Err) and requiring shareable_content at 635. Needs its own AudioOnly arm that builds a mic + system-audio pipeline with video_info: None, like the CameraOnly arm.

Net effect: starting an audio-only recording fails at startup in both modes, which contradicts the PR's stated "building audio-only output". Note there's no UI path to trigger AudioOnly yet (phase 6), so existing flows are unaffected — but the new capability is currently non-functional and not exercised by any test, so it slips past clippy/compile.

❌ Still open (from round 1)

  • recovery.rs:226-231analyze_incomplete still does if display_fragments.is_empty() { continue; }, so a crashed audio-only studio recording is silently discarded (mic/system-audio fragments are never considered recoverable). recovery.rs isn't touched by this PR, so this is a pre-existing gap that audio-only newly exposes — fine to defer to a later phase, but worth tracking.
  • recording.rs:3105 — the second ProjectRecordingsMeta::new in finalize_studio_recording is still unguarded (the one in handle_recording_finish:2851 is correctly gated on has_display). Latent: only bites once audio-only remux/recovery is wired up.

✅ Confirmed still fixed

audio_only flag preserved in the studio meta writers (reads existing value via RecordingMeta::load_for_project), camera window skipped for AudioOnly, cancel-guard early-returns when there's no screen pipeline, and finalization is guarded by has_display with timeline gen skipped for empty segments.


The two studio/instant pipeline branches are the gating items. Happy to push a fix for those + the recovery handling if you'd like.

Comment thread apps/cli/src/project.rs
let path = segment
.display
.as_ref()
.map_or_else(PathBuf::new, |d| meta.path(&d.path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PathBuf::new() here is a bit risky: an empty path often resolves to . so exists() can return true and accidentally mark a non-audio-only project as valid even when display metadata is missing.

Maybe prefer branching on the option and forcing exists: false when display is None:

Suggested change
.map_or_else(PathBuf::new, |d| meta.path(&d.path));
let path = match segment.display.as_ref() {
Some(d) => meta.path(&d.path),
None => PathBuf::from("<missing display metadata>"),
};

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@tembo please re-review

@tembo

tembo Bot commented Jun 18, 2026

Copy link
Copy Markdown

@tembo please re-review

re-reviewed pr #1881 at commit 863fe1f2 and posted a review on github.

the headline blocker is finally fixed — AudioOnly now reaches a real audio-only pipeline in both modes:

  • studio: new else if audio_only { (None, None, None) } branch so it no longer falls into the screen-capture path / target_to_display_and_crop that errored for audio-only. mic pipeline still built, segment writer already handles screen == None.
  • instant: dedicated AudioOnly arm building an audio-only output.mp4 via the NoVideo builder + Mp4Muxer (verified the muxer tolerates no video track and the builder requires ≥1 audio source). clear error if no mic.
  • the second unguarded ProjectRecordingsMeta::new in finalize_studio_recording is now guarded by has_display.
  • still-confirmed fixes from prior rounds: audio_only flag preserved in meta writes, camera window skipped, cancel-guard early-returns, macos shareable-content skipped.

one new issue worth fixing before merge: recovery is only half-wired. recovery.rs:226 now keeps audio-only segments, but build_recovered_meta (recovery.rs:1278) still unconditionally writes display: Some(display.mp4) — so a recovered audio-only studio recording references a non-existent display track. either gate the display field on having display fragments, or hold the :226 change until recovery is fully wired.

minor follow-ups (non-blocking): linux camera branch doesn't exclude audio_only; studio audio-only has no mic-presence guard (instant does); no tests exercise an actual audio-only start or recovery.

happy to push a fix for the recovery half-wiring if you want.

review: #1881 (review)

bounds: LogicalBounds,
},
CameraOnly,
AudioOnly,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With ScreenCaptureTarget::AudioOnly added, the #[cfg(target_os = "linux")] LinuxCaptureSource::from_target match now needs an AudioOnly arm too, otherwise this won’t compile on Linux (non-exhaustive match). Probably map it the same way as CameraOnly (or explicitly return an error, depending on desired behavior).

}
}
ScreenCaptureTarget::CameraOnly => {
ScreenCaptureTarget::CameraOnly | ScreenCaptureTarget::AudioOnly => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: this CameraOnly | AudioOnly arm is effectively unreachable right now because the function calls target.display()? before the match, and both variants return None there. If you want the more specific error, consider early-returning on these variants before the display lookup.

Also the error text says “Camera-only…” but now includes AudioOnly.

@tembo tembo 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.

Re-review (commit 863fe1f2)

The headline blocker from the last few rounds is now fixed: AudioOnly finally reaches a real audio-only pipeline in both modes.

✅ Resolved

  • Studio pipeline branchstudio_recording.rs:1491 now has an else if audio_only { (None, None, None) } arm, so it no longer falls into the screen-capture branch / target_to_display_and_crop (which returned Err for AudioOnly). The mic pipeline is still built from base_inputs.mic_feed, and the segment writer already handles screen == None (display: None, falling back to mic_start_time). The audio_only flag is now actually consumed (camera/cursor skip + screen tuple).
  • Instant pipeline branchinstant_recording.rs:610 adds a dedicated AudioOnly arm building an audio-only output.mp4 via OutputPipelineBuilder<NoVideo>::build::<Mp4Muxer>. Verified this path is valid: Mp4Muxer::setup tolerates video_config: None, and the NoVideo builder requires ≥1 audio source (which is provided). Missing mic returns a clear error.
  • finalize_studio_recording — the second ProjectRecordingsMeta::new at recording.rs:3104 is now guarded by has_display, building an empty segments: vec![] otherwise. The latent panic I flagged last round is closed.
  • Still-confirmed from prior rounds: audio_only flag preserved in meta writes, camera window skipped for AudioOnly, cancel-guard early-returns with no screen track, macOS shareable-content acquisition skipped for AudioOnly.

❌ New issue — recovery is only half-wired

recovery.rs:226 now keeps audio-only segments (&& !meta.audio_only), but the downstream build_recovered_meta still unconditionally fabricates a display track:

// recovery.rs:1278-1286
MultipleSegment {
    display: Some(VideoMeta {
        path: RelativePathBuf::from(format!("{segment_base}/display.mp4")), // file never exists for audio-only
        fps,                                                                // get_video_fps falls back to 30
        start_time: display_start_time,
        ...
    }),

So recovering a crashed audio-only studio recording now produces a recording-meta.json that references a non-existent display.mp4 (and contributes no duration, since estimate_fragments_duration runs on the empty display_fragments). Before this commit the segment was silently dropped; now it's kept but points at a phantom display track, which the editor will fail to load. Either gate the display field on !display_fragments.is_empty() (mirroring the segment writer), or hold the :226 change until recovery is fully wired. As-is the two halves disagree.

Minor / follow-up (non-blocking)

  1. Linux camera not excluded for audio-onlystudio_recording.rs:1622 guards the camera pipeline on camera_only only; macOS/Windows use camera_only || audio_only. On Linux an audio-only recording with a camera feed selected would still record camera.
  2. No mic-presence guard in studio audio-only — instant returns a friendly "requires a microphone" error; the studio audio_only branch doesn't, so an audio-only studio recording with no mic yields a fully-empty segment (screen/mic/cam/sys all None) rather than a clear error.
  3. No tests exercise an actual audio-only recording start (studio or instant) or audio-only recovery, so these slip past clippy/compile.
  4. Several matches!(.., ScreenCaptureTarget::CameraOnly) sites (telemetry, target mapping, upload mode) don't yet account for AudioOnly — fine while the UI mode selector is still phase 6-7, but worth auditing when that lands.

The pipeline blocker is genuinely closed — nice work. The recovery inconsistency (#1 above) is the one item I'd want addressed (or reverted) before merge; the rest are follow-ups. Happy to push a fix for the recovery half-wiring if useful.

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@tembo please re-review

@tembo

tembo Bot commented Jun 19, 2026

Copy link
Copy Markdown

@tembo please re-review

re-reviewed pr #1881 at the latest commit (c915932) and posted a review on github.

the new commit since my last pass (c915932) closes every item i flagged last round:

  • recovery is now fully wired for audio-only (was the last blocker): analyze_incomplete keeps no-display segments when audio_only, recover only finalizes display.mp4 when display fragments exist, build_recovered_meta sets display: None instead of pointing at a non-existent display.mp4, and create_project_config derives the timeline duration from the mic track.
  • studio audio-only now has a mic-presence guard (matches the instant path).
  • linux camera branch excludes audio_only.
  • extra hardening: AudioOnly treated like CameraOnly for background snapshot, instant upload mode, and presentation defaults (+ a new unit test).

still confirmed from prior rounds: audio_only flag preserved in meta writes, camera window skipped, cancel-guard early-returns, finalization guarded by has_display, both modes reach a real audio-only pipeline.

non-blocking notes: no end-to-end test for an actual audio-only start or crash recovery yet; the two leftover audio_only: false literals are in non-audio-only contexts and are harmless.

ci note: typecheck is failing but only on unrelated web files (Footer.tsx, caption-tracks.test.ts) — pre-existing, not from this pr. the rust clippy jobs were still running at review time, worth confirming they go green before merge.

verdict: all four issues from my last review are resolved and i don't see a new blocker. the recording-pipeline foundation looks ready to merge once clippy is green (ui/editor/share-page work is explicitly deferred to phases 6–7).

review: #1881 (review)

StudioRecordingMeta::SingleSegment { segment } => {
segment.display.path.to_path(&recovered.project_path)
}
StudioRecordingMeta::SingleSegment { segment } => segment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: if segment.display is None (audio-only, or corrupted meta), this builds a default/empty display_output_path and then create_screenshot just logs an error. Gating the screenshot spawn on display.is_some() keeps recovery quieter and avoids confusing errors.

Suggested change
StudioRecordingMeta::SingleSegment { segment } => segment
let display_output_path = match &recovered.meta {
StudioRecordingMeta::SingleSegment { segment } => segment
.display
.as_ref()
.map(|d| d.path.to_path(&recovered.project_path)),
StudioRecordingMeta::MultipleSegments { inner, .. } => inner.segments[0]
.display
.as_ref()
.map(|d| d.path.to_path(&recovered.project_path)),
};
if let Some(display_output_path) = display_output_path {
let screenshots_dir = recovered.project_path.join("screenshots");
std::fs::create_dir_all(&screenshots_dir)
.map_err(|e| format!("Failed to create screenshots directory: {e}"))?;
let display_screenshot = screenshots_dir.join("display.jpg");
tokio::spawn(async move {
if let Err(e) = create_screenshot(display_output_path, display_screenshot, None).await {
tracing::error!("Failed to create screenshot during recovery: {}", e);
}
});
}

@tembo tembo 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.

Re-review (commit c9159323)

The new commit since my last pass (c915932 — wire up recovery display gating, mic guard, and remaining AudioOnly call sites) closes every item I flagged last round. Nice — this addresses the recovery half-wiring that was the last real blocker.

✅ Fixed since last review

1. Recovery is now fully wired for audio-only (was half-wired)

  • analyze_incomplete (recovery.rs:226) keeps segments with no display fragments when meta.audio_only, instead of continue-ing past them.
  • recover (recovery.rs:744–788) only finalizes a display.mp4 when display_fragments is non-empty; mic/system-audio tracks are muxed independently.
  • build_recovered_meta (recovery.rs:1279) now sets display: None when display_fragments.is_empty() instead of unconditionally pointing at a non-existent display.mp4. This was the specific bug I called out last time.
  • create_project_config (recovery.rs:1399–1417) derives the timeline duration from the mic track when there's no display, so a recovered audio-only recording gets a valid timeline.

2. Studio audio-only mic guard added (studio_recording.rs:1491–1497) — the else if audio_only arm now errors early with a clear message if no mic is selected, matching the instant path (instant_recording.rs:611).

3. Linux camera branch (studio_recording.rs:1568/1599/1630) now excludes audio_only, so no camera pipeline is built for audio-only on Linux.

4. Extra hardeningAudioOnly is now treated like CameraOnly (skips screen-specific behavior) in spawn_current_desktop_background_snapshot, the instant upload_mode (desktopMP4), and apply_screen_recording_presentation_defaults, with a new unit test covering the presentation-defaults case.

✅ Still confirmed from prior rounds

  • audio_only flag preserved in both studio meta writers via RecordingMeta::load_for_project (studio_recording.rs:1784/1811).
  • Camera window no longer opens for AudioOnly (recording.rs:1238 matches CameraOnly only).
  • Cancel-guard early-returns when there's no screen track.
  • Finalization guarded by has_display at both call sites (recording.rs:2857 and :3110).
  • Both studio and instant reach a real audio-only pipeline; macOS shareable-content acquisition skipped.

Minor / non-blocking

  • The two remaining audio_only: false literals in recording.rs (:2512, :3231) are in non-audio-only contexts — a screenshot-import SingleSegment that always has a display, and an in-memory temp RecordingMeta used only for zoom-segment generation. Both harmless; not regressions.
  • No automated test exercises an actual audio-only recording start or audio-only crash recovery end-to-end (the new test only covers presentation defaults). Worth adding in a follow-up, but these paths are now logically wired.
  • analyze_incomplete will keep an audio-only segment dir even if it has neither display nor audio fragments; recover just produces nothing for it. Negligible edge case.

CI note

Typecheck is failing, but on web-only files unrelated to this PR (apps/web/app/(site)/Footer.tsx, apps/web/__tests__/unit/caption-tracks.test.ts) — pre-existing, not introduced here. The Rust Clippy jobs (the relevant ones) were still in progress at review time; worth confirming they go green before merge.

Verdict

All four issues from my last review are resolved and I don't see a new blocker. With the desktop UI / editor / share-page work explicitly deferred to phases 6–7, the recording-pipeline foundation in this PR looks ready to merge once Clippy is green.

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review

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