Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/src/selftest/playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ mod fixture {
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
}],
transitions: Vec::new(),
zoom_segments: Vec::new(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ async fn load_recording(
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
}]
}
StudioRecordingMeta::MultipleSegments { inner } => inner
Expand All @@ -152,6 +153,7 @@ async fn load_recording(
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
})
})
.collect(),
Expand Down
137 changes: 135 additions & 2 deletions apps/desktop/src-tauri/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ fn full_timeline_for_segments(
end: duration,
name: None,
speed_audio_mode: None,
audio_muted: false,
})
})
.collect()
Expand Down Expand Up @@ -358,6 +359,7 @@ fn full_timeline_for_source_segments(
end: duration,
name: None,
speed_audio_mode: None,
audio_muted: false,
})
})
.collect()
Expand Down Expand Up @@ -920,6 +922,7 @@ fn source_timeline_segments_for_import(
end,
name: None,
speed_audio_mode: None,
audio_muted: segment.audio_muted,
});
}

Expand Down Expand Up @@ -1718,6 +1721,7 @@ async fn append_mp4_to_editor_project(
end: duration,
name: None,
speed_audio_mode: None,
audio_muted: false,
});
add_clip_configs(
&mut config,
Expand Down Expand Up @@ -1767,13 +1771,21 @@ async fn append_cap_project_to_editor_project(
};
};

append_studio_project_to_editor_project(target_project_path, &source_meta, source_studio_meta)
}

fn append_studio_project_to_editor_project(
target_project_path: PathBuf,
source_meta: &RecordingMeta,
source_studio_meta: &StudioRecordingMeta,
) -> Result<usize, String> {
let source_segments = studio_segments_for_import(source_studio_meta);
if source_segments.is_empty() {
return Err("Source Cap project has no recording segments".to_string());
}

let source_timeline = source_timeline_segments_for_import(&source_meta, &source_segments)?;
let source_cursors = match source_studio_meta.as_ref() {
let source_timeline = source_timeline_segments_for_import(source_meta, &source_segments)?;
let source_cursors = match source_studio_meta {
StudioRecordingMeta::MultipleSegments { inner } => Some(&inner.cursors),
StudioRecordingMeta::SingleSegment { .. } => None,
};
Expand Down Expand Up @@ -1849,6 +1861,7 @@ async fn append_cap_project_to_editor_project(
end: source_segment.end,
name: None,
speed_audio_mode: source_segment.speed_audio_mode,
audio_muted: source_segment.audio_muted,
});
}
}
Expand Down Expand Up @@ -2083,6 +2096,126 @@ pub async fn check_import_ready(project_path: PathBuf) -> Result<bool, String> {
mod tests {
use super::*;

#[test]
fn cap_project_import_preserves_segment_mute() {
let _ = ffmpeg::init();
let source_project = tempfile::tempdir().unwrap();
let target_project = tempfile::tempdir().unwrap();
let display_path = RelativePathBuf::from("content/segments/segment-0/display.mp4");
let absolute_display_path = display_path.to_path(source_project.path());
std::fs::create_dir_all(absolute_display_path.parent().unwrap()).unwrap();
std::fs::write(
&absolute_display_path,
include_bytes!("../../../media-server/src/__tests__/fixtures/test-no-audio.mp4"),
)
.unwrap();

let source_segment = MultipleSegment {
display: VideoMeta {
path: display_path,
fps: 30,
start_time: Some(0.0),
device_id: None,
},
camera: None,
mic: None,
system_audio: None,
cursor: None,
keyboard: None,
display_notch: None,
};
let source_meta = RecordingMeta {
platform: Some(Platform::default()),
project_path: source_project.path().to_path_buf(),
pretty_name: "Muted import fixture".to_string(),
sharing: None,
inner: RecordingMetaInner::Studio(Box::new(StudioRecordingMeta::MultipleSegments {
inner: MultipleSegments {
segments: vec![source_segment],
cursors: Cursors::default(),
status: Some(StudioRecordingStatus::Complete),
},
})),
upload: None,
};
source_meta.save_for_project().unwrap();
ProjectConfiguration {
timeline: Some(TimelineConfiguration {
segments: vec![TimelineSegment {
recording_clip: 0,
timescale: 1.0,
start: 0.0,
end: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: true,
}],
transitions: Vec::new(),
zoom_segments: Vec::new(),
scene_segments: Vec::new(),
mask_segments: Vec::new(),
text_segments: Vec::new(),
caption_segments: Vec::new(),
keyboard_segments: Vec::new(),
audio_segments: Vec::new(),
camera3d_segments: Vec::new(),
}),
..Default::default()
}
.write(source_project.path())
.unwrap();
let source_meta = RecordingMeta::load_for_project(source_project.path()).unwrap();

RecordingMeta {
platform: Some(Platform::default()),
project_path: target_project.path().to_path_buf(),
pretty_name: "Target import fixture".to_string(),
sharing: None,
inner: RecordingMetaInner::Studio(Box::new(StudioRecordingMeta::MultipleSegments {
inner: MultipleSegments {
segments: Vec::new(),
cursors: Cursors::default(),
status: Some(StudioRecordingStatus::Complete),
},
})),
upload: None,
}
.save_for_project()
.unwrap();

let RecordingMetaInner::Studio(source_studio) = &source_meta.inner else {
panic!("expected Studio source metadata");
};
let imported = append_studio_project_to_editor_project(
target_project.path().to_path_buf(),
&source_meta,
source_studio,
)
.unwrap();

let target_config = ProjectConfiguration::load(target_project.path()).unwrap();
let target_timeline = target_config.timeline.unwrap();
let target_meta = RecordingMeta::load_for_project(target_project.path()).unwrap();
let RecordingMetaInner::Studio(target_studio) = target_meta.inner else {
panic!("expected Studio recording metadata");
};
let StudioRecordingMeta::MultipleSegments { inner } = *target_studio else {
panic!("expected multiple recording segments");
};

assert_eq!(imported, 1);
assert_eq!(target_timeline.segments.len(), 1);
assert!(target_timeline.segments[0].audio_muted);
assert_eq!(inner.segments.len(), 1);
assert!(
inner.segments[0]
.display
.path
.to_path(target_project.path())
.is_file()
);
}

#[test]
fn source_asset_path_allows_file_inside_source_project() {
let source_project = tempfile::tempdir().unwrap();
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3844,6 +3844,7 @@ fn project_config_from_recording(
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
})
.collect::<Vec<_>>();

Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/routes/editor/ConfigSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5606,6 +5606,20 @@ function ClipSegmentConfig(props: {
</p>
</div>

<Field name="Audio" icon={<IconLucideVolume2 class="size-4" />}>
<Subfield name="Mute Audio">
<Toggle
checked={props.segment.audioMuted}
onChange={(audioMuted) =>
projectActions.setClipSegmentAudioMuted(
props.segmentIndex,
audioMuted,
)
}
/>
</Subfield>
</Field>

<Field name="Speed" icon={<IconLucideFastForward class="size-4" />}>
<KRadioGroup
class="flex flex-row gap-1.5 -mt-1"
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/routes/editor/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,9 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider(
speedAudioMode,
);
},
setClipSegmentAudioMuted: (index: number, audioMuted: boolean) => {
setProject("timeline", "segments", index, "audioMuted", audioMuted);
},
};

let projectSaveTimeout: number | undefined;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1068,7 +1068,7 @@ export type SystemDiagnostics = { macosVersion: MacOSVersionInfo | null; availab
export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null }
export type TextSegment = { start: number; end: number; track?: number; enabled?: boolean; content?: string; center?: XY<number>; size?: XY<number>; fontFamily?: string; fontSize?: number; fontWeight?: number; italic?: boolean; color?: string; fadeDuration?: number }
export type TimelineConfiguration = { segments: TimelineSegment[]; transitions: ClipTransition[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[]; maskSegments?: MaskSegment[]; textSegments?: TextSegment[]; captionSegments?: CaptionTrackSegment[]; keyboardSegments?: KeyboardTrackSegment[]; audioSegments?: AudioTrackSegment[]; camera3dSegments?: Camera3DSegment[] }
export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number; name?: string | null; speedAudioMode?: ClipSpeedAudioMode | null }
export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number; name?: string | null; speedAudioMode?: ClipSpeedAudioMode | null; audioMuted?: boolean }
export type TranscriptionEngine = "Whisper" | "Parakeet"
export type Trigger = "screenshotTaken" | "studioRecordingFinished" | "instantRecordingFinished" | "recordingStarted" | "uploadCompleted" | "videoImported" | "recordingDeleted"
export type UpdateChannel = "stable" | "nightly"
Expand Down
2 changes: 2 additions & 0 deletions crates/editor/examples/editor-playback-benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ async fn load_recording(
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
}]
}
StudioRecordingMeta::MultipleSegments { inner } => inner
Expand All @@ -372,6 +373,7 @@ async fn load_recording(
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
})
})
.collect(),
Expand Down
2 changes: 2 additions & 0 deletions crates/editor/examples/playback-pipeline-benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ async fn load_recording(
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
}]
}
StudioRecordingMeta::MultipleSegments { inner } => inner
Expand All @@ -298,6 +299,7 @@ async fn load_recording(
timescale: 1.0,
name: None,
speed_audio_mode: None,
audio_muted: false,
})
})
.collect(),
Expand Down
Loading