Skip to content

Commit c1e481a

Browse files
Fix the 5 min studio export limit being enforced incorrectly (#1117)
* wip * use ffmpeg for duration * remove all `ffmpeg::init` calls * fix * fix * use pts for both start and end times in avfoundation encoder * clippy --------- Co-authored-by: Brendan Allan <[email protected]>
1 parent bb2479f commit c1e481a

File tree

5 files changed

+53
-55
lines changed

5 files changed

+53
-55
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ specta-typescript = "0.0.7"
5353
tokio.workspace = true
5454
uuid = { version = "1.10.0", features = ["v4"] }
5555
image = "0.25.2"
56-
mp4 = "0.14.0"
5756
futures-intrusive = "0.5.0"
5857
anyhow.workspace = true
5958
futures = { workspace = true }

apps/desktop/src-tauri/src/lib.rs

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ use cap_rendering::{ProjectRecordingsMeta, RenderedFrame};
4545
use clipboard_rs::common::RustImage;
4646
use clipboard_rs::{Clipboard, ClipboardContext};
4747
use editor_window::{EditorInstances, WindowEditorInstance};
48+
use ffmpeg::ffi::AV_TIME_BASE;
4849
use general_settings::GeneralSettingsStore;
4950
use kameo::{Actor, actor::ActorRef};
50-
use mp4::Mp4Reader;
5151
use notifications::NotificationType;
5252
use png::{ColorType, Encoder};
5353
use recording::InProgressRecording;
@@ -64,7 +64,7 @@ use std::{
6464
collections::BTreeMap,
6565
fs::File,
6666
future::Future,
67-
io::{BufReader, BufWriter},
67+
io::BufWriter,
6868
marker::PhantomData,
6969
path::{Path, PathBuf},
7070
process::Command,
@@ -421,11 +421,6 @@ async fn create_screenshot(
421421
println!("Creating screenshot: input={input:?}, output={output:?}, size={size:?}");
422422

423423
let result: Result<(), String> = tokio::task::spawn_blocking(move || -> Result<(), String> {
424-
ffmpeg::init().map_err(|e| {
425-
eprintln!("Failed to initialize ffmpeg: {e}");
426-
e.to_string()
427-
})?;
428-
429424
let mut ictx = ffmpeg::format::input(&input).map_err(|e| {
430425
eprintln!("Failed to create input context: {e}");
431426
e.to_string()
@@ -584,11 +579,11 @@ async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(
584579
return Err(format!("Source file {src} does not exist"));
585580
}
586581

587-
if !is_screenshot && !is_gif && !is_valid_mp4(src_path) {
582+
if !is_screenshot && !is_gif && !is_valid_video(src_path) {
588583
let mut attempts = 0;
589584
while attempts < 10 {
590585
std::thread::sleep(std::time::Duration::from_secs(1));
591-
if is_valid_mp4(src_path) {
586+
if is_valid_video(src_path) {
592587
break;
593588
}
594589
attempts += 1;
@@ -631,8 +626,8 @@ async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(
631626
continue;
632627
}
633628

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

685-
pub fn is_valid_mp4(path: &std::path::Path) -> bool {
686-
if let Ok(file) = std::fs::File::open(path) {
687-
let file_size = match file.metadata() {
688-
Ok(metadata) => metadata.len(),
689-
Err(_) => return false,
690-
};
691-
let reader = std::io::BufReader::new(file);
692-
Mp4Reader::read_header(reader, file_size).is_ok()
693-
} else {
694-
false
680+
pub fn is_valid_video(path: &std::path::Path) -> bool {
681+
match ffmpeg::format::input(path) {
682+
Ok(input_context) => {
683+
// Check if we have at least one video stream
684+
input_context
685+
.streams()
686+
.any(|stream| stream.parameters().medium() == ffmpeg::media::Type::Video)
687+
}
688+
Err(_) => false,
695689
}
696690
}
697691

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

879873
fn get_duration_for_path(path: PathBuf) -> Result<f64, String> {
880-
let reader = BufReader::new(
881-
File::open(&path).map_err(|e| format!("Failed to open video file: {e}"))?,
882-
);
883-
let file_size = path
884-
.metadata()
885-
.map_err(|e| format!("Failed to get file metadata: {e}"))?
886-
.len();
887-
888-
let current_duration = match Mp4Reader::read_header(reader, file_size) {
889-
Ok(mp4) => mp4.duration().as_secs_f64(),
890-
Err(e) => {
891-
println!("Failed to read MP4 header: {e}. Falling back to default duration.");
892-
0.0_f64
893-
}
894-
};
874+
let input =
875+
ffmpeg::format::input(&path).map_err(|e| format!("Failed to open video file: {e}"))?;
876+
877+
let raw_duration = input.duration();
878+
if raw_duration <= 0 {
879+
return Err(format!(
880+
"Unknown or invalid duration for video file: {:?}",
881+
path
882+
));
883+
}
895884

896-
Ok(current_duration)
885+
let duration = raw_duration as f64 / AV_TIME_BASE as f64;
886+
Ok(duration)
897887
}
898888

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

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

18421835
#[cfg_attr(mobile, tauri::mobile_entry_point)]
18431836
pub async fn run(recording_logging_handle: LoggingHandle) {
1837+
ffmpeg::init()
1838+
.map_err(|e| {
1839+
error!("Failed to initialize ffmpeg: {e}");
1840+
})
1841+
.ok();
1842+
18441843
let tauri_context = tauri::generate_context!();
18451844

18461845
let specta_builder = tauri_specta::Builder::new()

apps/desktop/src/routes/editor/ExportDialog.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import {
4747
topSlideAnimateClasses,
4848
} from "./ui";
4949

50+
class SilentError extends Error {}
51+
5052
export const COMPRESSION_OPTIONS: Array<{
5153
label: string;
5254
value: ExportCompression;
@@ -319,9 +321,9 @@ export function ExportDialog() {
319321
if (!canShare.allowed) {
320322
if (canShare.reason === "upgrade_required") {
321323
await commands.showWindow("Upgrade");
322-
throw new Error(
323-
"Upgrade required to share recordings longer than 5 minutes",
324-
);
324+
// The window takes a little to show and this prevents the user seeing it glitch
325+
await new Promise((resolve) => setTimeout(resolve, 1000));
326+
throw new SilentError();
325327
}
326328
}
327329

@@ -376,9 +378,11 @@ export function ExportDialog() {
376378
},
377379
onError: (error) => {
378380
console.error(error);
379-
commands.globalMessageDialog(
380-
error instanceof Error ? error.message : "Failed to upload recording",
381-
);
381+
if (!(error instanceof SilentError)) {
382+
commands.globalMessageDialog(
383+
error instanceof Error ? error.message : "Failed to upload recording",
384+
);
385+
}
382386

383387
setExportState(reconcile({ type: "idle" }));
384388
},

crates/enc-avfoundation/src/mp4.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@ pub struct MP4Encoder {
88
#[allow(unused)]
99
tag: &'static str,
1010
#[allow(unused)]
11-
last_pts: Option<i64>,
12-
#[allow(unused)]
1311
config: VideoInfo,
1412
asset_writer: arc::R<av::AssetWriter>,
1513
video_input: arc::R<av::AssetWriterInput>,
1614
audio_input: Option<arc::R<av::AssetWriterInput>>,
1715
start_time: cm::Time,
1816
first_timestamp: Option<cm::Time>,
1917
segment_first_timestamp: Option<cm::Time>,
20-
last_timestamp: Option<cm::Time>,
18+
last_pts: Option<cm::Time>,
2119
is_writing: bool,
2220
is_paused: bool,
2321
elapsed_duration: cm::Time,
@@ -177,14 +175,13 @@ impl MP4Encoder {
177175

178176
Ok(Self {
179177
tag,
180-
last_pts: None,
181178
config: video_config,
182179
audio_input,
183180
asset_writer,
184181
video_input,
185182
first_timestamp: None,
186183
segment_first_timestamp: None,
187-
last_timestamp: None,
184+
last_pts: None,
188185
is_writing: false,
189186
is_paused: false,
190187
start_time: cm::Time::zero(),
@@ -227,7 +224,7 @@ impl MP4Encoder {
227224

228225
self.first_timestamp.get_or_insert(time);
229226
self.segment_first_timestamp.get_or_insert(time);
230-
self.last_timestamp = Some(time);
227+
self.last_pts = Some(new_pts);
231228

232229
self.video_frames_appended += 1;
233230

@@ -322,7 +319,7 @@ impl MP4Encoder {
322319
.elapsed_duration
323320
.add(time.sub(self.segment_first_timestamp.unwrap()));
324321
self.segment_first_timestamp = None;
325-
self.last_timestamp = None;
322+
self.last_pts = None;
326323
self.is_paused = true;
327324
}
328325

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

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

356353
debug!("First video timestamp: {:?}", self.first_timestamp);
357-
debug!("Last video timestamp: {:?}", self.last_timestamp);
354+
debug!("Last video timestamp: {:?}", self.last_pts);
358355

359356
info!("Finished writing");
360357
}

0 commit comments

Comments
 (0)