Skip to content
Merged
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
components: clippy

- name: Rust cache
uses: swatinem/rust-cache@v2
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ specta-typescript = "0.0.7"
tokio.workspace = true
uuid = { version = "1.10.0", features = ["v4"] }
image = "0.25.2"
mp4 = "0.14.0"
futures-intrusive = "0.5.0"
anyhow.workspace = true
futures = { workspace = true }
Expand Down
75 changes: 37 additions & 38 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ use cap_rendering::{ProjectRecordingsMeta, RenderedFrame};
use clipboard_rs::common::RustImage;
use clipboard_rs::{Clipboard, ClipboardContext};
use editor_window::{EditorInstances, WindowEditorInstance};
use ffmpeg::ffi::AV_TIME_BASE;
use general_settings::GeneralSettingsStore;
use kameo::{Actor, actor::ActorRef};
use mp4::Mp4Reader;
use notifications::NotificationType;
use png::{ColorType, Encoder};
use recording::InProgressRecording;
Expand All @@ -64,7 +64,7 @@ use std::{
collections::BTreeMap,
fs::File,
future::Future,
io::{BufReader, BufWriter},
io::BufWriter,
marker::PhantomData,
path::{Path, PathBuf},
process::Command,
Expand Down Expand Up @@ -421,11 +421,6 @@ async fn create_screenshot(
println!("Creating screenshot: input={input:?}, output={output:?}, size={size:?}");

let result: Result<(), String> = tokio::task::spawn_blocking(move || -> Result<(), String> {
ffmpeg::init().map_err(|e| {
eprintln!("Failed to initialize ffmpeg: {e}");
e.to_string()
})?;

let mut ictx = ffmpeg::format::input(&input).map_err(|e| {
eprintln!("Failed to create input context: {e}");
e.to_string()
Expand Down Expand Up @@ -584,11 +579,11 @@ async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(
return Err(format!("Source file {src} does not exist"));
}

if !is_screenshot && !is_gif && !is_valid_mp4(src_path) {
if !is_screenshot && !is_gif && !is_valid_video(src_path) {
let mut attempts = 0;
while attempts < 10 {
std::thread::sleep(std::time::Duration::from_secs(1));
if is_valid_mp4(src_path) {
if is_valid_video(src_path) {
break;
}
attempts += 1;
Expand Down Expand Up @@ -631,8 +626,8 @@ async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(
continue;
}

if !is_screenshot && !is_gif && !is_valid_mp4(std::path::Path::new(&dst)) {
last_error = Some("Destination file is not a valid MP4".to_string());
if !is_screenshot && !is_gif && !is_valid_video(std::path::Path::new(&dst)) {
last_error = Some("Destination file is not a valid".to_string());
let _ = tokio::fs::remove_file(&dst).await;
attempts += 1;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
Expand Down Expand Up @@ -682,16 +677,15 @@ async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(
Err(last_error.unwrap_or_else(|| "Maximum retry attempts exceeded".to_string()))
}

pub fn is_valid_mp4(path: &std::path::Path) -> bool {
if let Ok(file) = std::fs::File::open(path) {
let file_size = match file.metadata() {
Ok(metadata) => metadata.len(),
Err(_) => return false,
};
let reader = std::io::BufReader::new(file);
Mp4Reader::read_header(reader, file_size).is_ok()
} else {
false
pub fn is_valid_video(path: &std::path::Path) -> bool {
match ffmpeg::format::input(path) {
Ok(input_context) => {
// Check if we have at least one video stream
input_context
.streams()
.any(|stream| stream.parameters().medium() == ffmpeg::media::Type::Video)
}
Err(_) => false,
}
}

Expand Down Expand Up @@ -877,23 +871,19 @@ async fn get_video_metadata(path: PathBuf) -> Result<VideoRecordingMetadata, Str
let recording_meta = RecordingMeta::load_for_project(&path).map_err(|v| v.to_string())?;

fn get_duration_for_path(path: PathBuf) -> Result<f64, String> {
let reader = BufReader::new(
File::open(&path).map_err(|e| format!("Failed to open video file: {e}"))?,
);
let file_size = path
.metadata()
.map_err(|e| format!("Failed to get file metadata: {e}"))?
.len();

let current_duration = match Mp4Reader::read_header(reader, file_size) {
Ok(mp4) => mp4.duration().as_secs_f64(),
Err(e) => {
println!("Failed to read MP4 header: {e}. Falling back to default duration.");
0.0_f64
}
};
let input =
ffmpeg::format::input(&path).map_err(|e| format!("Failed to open video file: {e}"))?;

let raw_duration = input.duration();
if raw_duration <= 0 {
return Err(format!(
"Unknown or invalid duration for video file: {:?}",
path
));
}

Ok(current_duration)
let duration = raw_duration as f64 / AV_TIME_BASE as f64;
Ok(duration)
}

let display_paths = match &recording_meta.inner {
Expand All @@ -915,7 +905,10 @@ async fn get_video_metadata(path: PathBuf) -> Result<VideoRecordingMetadata, Str
let duration = display_paths
.into_iter()
.map(get_duration_for_path)
.sum::<Result<_, _>>()?;
.try_fold(0f64, |acc, item| -> Result<f64, String> {
let d = item?;
Ok(acc + d)
})?;

let (width, height) = (1920, 1080);
let fps = 30;
Expand Down Expand Up @@ -1841,6 +1834,12 @@ type LoggingHandle = tracing_subscriber::reload::Handle<Option<DynLoggingLayer>,

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub async fn run(recording_logging_handle: LoggingHandle) {
ffmpeg::init()
.map_err(|e| {
error!("Failed to initialize ffmpeg: {e}");
})
.ok();

let tauri_context = tauri::generate_context!();

let specta_builder = tauri_specta::Builder::new()
Expand Down
16 changes: 10 additions & 6 deletions apps/desktop/src/routes/editor/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import {
topSlideAnimateClasses,
} from "./ui";

class SilentError extends Error {}

export const COMPRESSION_OPTIONS: Array<{
label: string;
value: ExportCompression;
Expand Down Expand Up @@ -319,9 +321,9 @@ export function ExportDialog() {
if (!canShare.allowed) {
if (canShare.reason === "upgrade_required") {
await commands.showWindow("Upgrade");
throw new Error(
"Upgrade required to share recordings longer than 5 minutes",
);
// The window takes a little to show and this prevents the user seeing it glitch
await new Promise((resolve) => setTimeout(resolve, 1000));
throw new SilentError();
}
}

Expand Down Expand Up @@ -376,9 +378,11 @@ export function ExportDialog() {
},
onError: (error) => {
console.error(error);
commands.globalMessageDialog(
error instanceof Error ? error.message : "Failed to upload recording",
);
if (!(error instanceof SilentError)) {
commands.globalMessageDialog(
error instanceof Error ? error.message : "Failed to upload recording",
);
}

setExportState(reconcile({ type: "idle" }));
},
Expand Down
15 changes: 6 additions & 9 deletions crates/enc-avfoundation/src/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ pub struct MP4Encoder {
#[allow(unused)]
tag: &'static str,
#[allow(unused)]
last_pts: Option<i64>,
#[allow(unused)]
config: VideoInfo,
asset_writer: arc::R<av::AssetWriter>,
video_input: arc::R<av::AssetWriterInput>,
audio_input: Option<arc::R<av::AssetWriterInput>>,
start_time: cm::Time,
first_timestamp: Option<cm::Time>,
segment_first_timestamp: Option<cm::Time>,
last_timestamp: Option<cm::Time>,
last_pts: Option<cm::Time>,
is_writing: bool,
is_paused: bool,
elapsed_duration: cm::Time,
Expand Down Expand Up @@ -177,14 +175,13 @@ impl MP4Encoder {

Ok(Self {
tag,
last_pts: None,
config: video_config,
audio_input,
asset_writer,
video_input,
first_timestamp: None,
segment_first_timestamp: None,
last_timestamp: None,
last_pts: None,
is_writing: false,
is_paused: false,
start_time: cm::Time::zero(),
Expand Down Expand Up @@ -227,7 +224,7 @@ impl MP4Encoder {

self.first_timestamp.get_or_insert(time);
self.segment_first_timestamp.get_or_insert(time);
self.last_timestamp = Some(time);
self.last_pts = Some(new_pts);

self.video_frames_appended += 1;

Expand Down Expand Up @@ -322,7 +319,7 @@ impl MP4Encoder {
.elapsed_duration
.add(time.sub(self.segment_first_timestamp.unwrap()));
self.segment_first_timestamp = None;
self.last_timestamp = None;
self.last_pts = None;
self.is_paused = true;
}

Expand All @@ -342,7 +339,7 @@ impl MP4Encoder {
self.is_writing = false;

self.asset_writer
.end_session_at_src_time(self.last_timestamp.unwrap_or(cm::Time::zero()));
.end_session_at_src_time(self.last_pts.unwrap_or(cm::Time::zero()));
self.video_input.mark_as_finished();
if let Some(i) = self.audio_input.as_mut() {
i.mark_as_finished()
Expand All @@ -354,7 +351,7 @@ impl MP4Encoder {
debug!("Appended {} audio frames", self.audio_frames_appended);

debug!("First video timestamp: {:?}", self.first_timestamp);
debug!("Last video timestamp: {:?}", self.last_timestamp);
debug!("Last video timestamp: {:?}", self.last_pts);

info!("Finished writing");
}
Expand Down
Loading