Skip to content
Draft
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
16 changes: 16 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3046,6 +3046,22 @@ async fn create_editor_instance_impl(
) -> Result<Arc<EditorInstance>, String> {
let app = app.clone();

if RecordingMeta::needs_recovery(&path) {
let pretty_name = path
.file_name()
.and_then(|s| s.to_str())
.map(|s| s.trim_end_matches(".cap").to_string())
.unwrap_or_else(|| "Recovered Recording".to_string());

tracing::info!("Attempting to recover incomplete recording: {:?}", path);
match RecordingMeta::try_recover(&path, pretty_name) {
Ok(_) => tracing::info!("Successfully recovered recording: {:?}", path),
Err(e) => {
return Err(format!("Failed to recover recording: {e}"));
}
}
}

let instance = {
let app = app.clone();
EditorInstance::new(
Expand Down
169 changes: 169 additions & 0 deletions crates/enc-ffmpeg/src/mux/fragmented_mp4.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use cap_media_info::RawVideoFormat;
use ffmpeg::{format, frame};
use std::{path::PathBuf, time::Duration};
use tracing::*;

use crate::{
audio::AudioEncoder,
h264,
video::h264::{H264Encoder, H264EncoderError},
};

pub struct FragmentedMP4File {
#[allow(unused)]
tag: &'static str,
output: format::context::Output,
video: H264Encoder,
audio: Option<Box<dyn AudioEncoder + Send>>,
is_finished: bool,
}

#[derive(thiserror::Error, Debug)]
pub enum InitError {
#[error("{0:?}")]
Ffmpeg(ffmpeg::Error),
#[error("Video/{0}")]
VideoInit(H264EncoderError),
#[error("Audio/{0}")]
AudioInit(Box<dyn std::error::Error>),
}

#[derive(thiserror::Error, Debug)]
pub enum FinishError {
#[error("Already finished")]
AlreadyFinished,
#[error("{0}")]
WriteTrailerFailed(ffmpeg::Error),
}

pub struct FinishResult {
pub video_finish: Result<(), ffmpeg::Error>,
pub audio_finish: Result<(), ffmpeg::Error>,
}

impl FragmentedMP4File {
pub fn init(
tag: &'static str,
mut output_path: PathBuf,
video: impl FnOnce(&mut format::context::Output) -> Result<H264Encoder, H264EncoderError>,
audio: impl FnOnce(
&mut format::context::Output,
)
-> Option<Result<Box<dyn AudioEncoder + Send>, Box<dyn std::error::Error>>>,
) -> Result<Self, InitError> {
output_path.set_extension("mp4");

if let Some(parent) = output_path.parent() {
let _ = std::fs::create_dir_all(parent);
}

let mut output = format::output(&output_path).map_err(InitError::Ffmpeg)?;

trace!("Preparing encoders for fragmented mp4 file");

let video = video(&mut output).map_err(InitError::VideoInit)?;
let audio = audio(&mut output)
.transpose()
.map_err(InitError::AudioInit)?;

info!("Prepared encoders for fragmented mp4 file");

let mut opts = ffmpeg::Dictionary::new();
opts.set("movflags", "frag_keyframe+empty_moov+default_base_moof");
opts.set("frag_duration", "1000000");

output.write_header_with(opts).map_err(InitError::Ffmpeg)?;

Ok(Self {
tag,
output,
video,
audio,
is_finished: false,
})
}

pub fn video_format() -> RawVideoFormat {
RawVideoFormat::YUYV420
}

pub fn queue_video_frame(
&mut self,
frame: frame::Video,
timestamp: Duration,
) -> Result<(), h264::QueueFrameError> {
if self.is_finished {
return Ok(());
}

self.video.queue_frame(frame, timestamp, &mut self.output)
}

pub fn queue_audio_frame(&mut self, frame: frame::Audio) {
if self.is_finished {
return;
}

let Some(audio) = &mut self.audio else {
return;
};

audio.send_frame(frame, &mut self.output);
}

pub fn finish(&mut self) -> Result<FinishResult, FinishError> {
if self.is_finished {
return Err(FinishError::AlreadyFinished);
}

self.is_finished = true;

tracing::info!("FragmentedMP4Encoder: Finishing encoding");

let video_finish = self.video.flush(&mut self.output).inspect_err(|e| {
error!("Failed to finish video encoder: {e:#}");
});

let audio_finish = self
.audio
.as_mut()
.map(|enc| {
tracing::info!("FragmentedMP4Encoder: Flushing audio encoder");
enc.flush(&mut self.output).inspect_err(|e| {
error!("Failed to finish audio encoder: {e:#}");
})
})
.unwrap_or(Ok(()));

tracing::info!("FragmentedMP4Encoder: Writing trailer");
self.output
.write_trailer()
.map_err(FinishError::WriteTrailerFailed)?;

Ok(FinishResult {
video_finish,
audio_finish,
})
}

pub fn video(&self) -> &H264Encoder {
&self.video
}

pub fn video_mut(&mut self) -> &mut H264Encoder {
&mut self.video
}
}

impl Drop for FragmentedMP4File {
fn drop(&mut self) {
let _ = self.finish();
}
}

pub struct FragmentedMP4Input {
pub video: frame::Video,
pub audio: Option<frame::Audio>,
}

unsafe impl Send for FragmentedMP4File {}
1 change: 1 addition & 0 deletions crates/enc-ffmpeg/src/mux/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod fragmented_mp4;
pub mod mp4;
pub mod ogg;
66 changes: 66 additions & 0 deletions crates/project/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,72 @@ impl CursorEvents {
serde_json::from_reader(file).map_err(|e| format!("Failed to parse cursor data: {e}"))
}

pub fn load_from_stream(path: &Path) -> Result<Self, String> {
use std::io::{BufRead, BufReader};

let file = File::open(path).map_err(|e| format!("Failed to open cursor stream: {e}"))?;
let reader = BufReader::new(file);

let mut all_moves = Vec::new();
let mut all_clicks = Vec::new();

for line in reader.lines() {
let line = line.map_err(|e| format!("Failed to read line: {e}"))?;
if line.trim().is_empty() {
continue;
}

#[derive(serde::Deserialize)]
struct Batch {
moves: Vec<CursorMoveEvent>,
clicks: Vec<CursorClickEvent>,
}

match serde_json::from_str::<Batch>(&line) {
Ok(batch) => {
all_moves.extend(batch.moves);
all_clicks.extend(batch.clicks);
}
Err(e) => {
tracing::warn!("Failed to parse cursor batch: {}", e);
}
}
}

Ok(Self {
moves: all_moves,
clicks: all_clicks,
})
}

pub fn load_with_fallback(dir: &Path) -> Self {
let stream_path = dir.join("cursor-stream.jsonl");
let json_path = dir.join("cursor.json");

if stream_path.exists() {
match Self::load_from_stream(&stream_path) {
Ok(events) if !events.moves.is_empty() || !events.clicks.is_empty() => {
return events;
}
Ok(_) => {}
Err(e) => {
tracing::warn!("Failed to load cursor stream: {}", e);
}
}
}

if json_path.exists() {
match Self::load_from_file(&json_path) {
Ok(events) => return events,
Err(e) => {
tracing::warn!("Failed to load cursor json: {}", e);
}
}
}

Self::default()
}

pub fn stabilize_short_lived_cursor_shapes(
&mut self,
pointer_ids: Option<&HashSet<String>>,
Expand Down
Loading
Loading