diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d53554a4da..e6847dc72d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,6 +103,7 @@ jobs: uses: swatinem/rust-cache@v2 with: shared-key: ${{ matrix.settings.target }} + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Create .env file in root run: | @@ -169,6 +170,7 @@ jobs: uses: swatinem/rust-cache@v2 with: shared-key: ${{ matrix.settings.target }} + save-if: ${{ github.ref == 'refs/heads/main' }} - uses: ./.github/actions/setup-js diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a934217e57..e3d835697c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -173,6 +173,7 @@ jobs: uses: swatinem/rust-cache@v2 with: shared-key: ${{ matrix.settings.target }} + save-if: ${{ github.ref == 'refs/heads/main' }} - uses: ./.github/actions/setup-js diff --git a/Cargo.lock b/Cargo.lock index 69d71f73d4..21775e6804 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1496,6 +1496,7 @@ dependencies = [ "objc2-app-kit", "relative-path", "replace_with", + "retry", "ringbuf", "scap-cpal", "scap-direct3d", @@ -7517,6 +7518,15 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "retry" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e211f878258887b3e65dd3c8ff9f530fe109f441a117ee0cdc27f341355032" +dependencies = [ + "rand 0.9.2", +] + [[package]] name = "rfd" version = "0.15.4" @@ -7857,6 +7867,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation 0.3.1", "scap-targets", + "tokio", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index bacc8022da..1dfe748dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ percent-encoding = "2.3.1" [workspace.lints.rust] deprecated = "allow" unexpected_cfgs = "allow" +unused_must_use = "deny" [workspace.lints.clippy] dbg_macro = "deny" diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index efd1e68e8c..d8ccf834d8 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -196,15 +196,15 @@ pub async fn focus_window(window_id: WindowId) -> Result<(), String> { if GetWindowPlacement(hwnd, &mut wp).is_ok() { // Restore using the previous placement to avoid resizing wp.showCmd = SW_RESTORE.0 as u32; - SetWindowPlacement(hwnd, &wp); + let _ = SetWindowPlacement(hwnd, &wp); } else { // Fallback to simple restore if placement fails - ShowWindow(hwnd, SW_RESTORE); + let _ = ShowWindow(hwnd, SW_RESTORE); } } // Always try to bring to foreground - SetForegroundWindow(hwnd); + let _ = SetForegroundWindow(hwnd); } } diff --git a/crates/audio/src/latency.rs b/crates/audio/src/latency.rs index e39e2dca20..899d88e669 100644 --- a/crates/audio/src/latency.rs +++ b/crates/audio/src/latency.rs @@ -230,6 +230,7 @@ const WARMUP_GUARD_SAMPLES: u32 = 3; const WARMUP_SPIKE_RATIO: f64 = 50.0; #[cfg(not(target_os = "macos"))] const FALLBACK_WIRED_LATENCY_SECS: f64 = 0.03; +#[cfg(target_os = "macos")] const WIRELESS_FALLBACK_LATENCY_SECS: f64 = 0.20; const WIRELESS_MIN_LATENCY_SECS: f64 = 0.12; @@ -615,6 +616,7 @@ mod macos { } #[cfg(test)] +#[allow(clippy::unchecked_duration_subtraction)] mod tests { use super::*; use std::time::Instant; diff --git a/crates/camera-directshow/src/lib.rs b/crates/camera-directshow/src/lib.rs index 9e29cdf457..40690d8299 100644 --- a/crates/camera-directshow/src/lib.rs +++ b/crates/camera-directshow/src/lib.rs @@ -10,7 +10,7 @@ use std::{ ptr::{self, null, null_mut}, time::{Duration, Instant}, }; -use tracing::{trace, warn}; +use tracing::*; use windows::{ Win32::{ Foundation::*, diff --git a/crates/camera-windows/src/lib.rs b/crates/camera-windows/src/lib.rs index 51500743f3..785a09df4d 100644 --- a/crates/camera-windows/src/lib.rs +++ b/crates/camera-windows/src/lib.rs @@ -6,7 +6,7 @@ use std::{ ffi::{OsStr, OsString}, fmt::{Debug, Display}, ops::Deref, - time::{Duration, Instant}, + time::Duration, }; use windows::Win32::Media::{DirectShow::*, KernelStreaming::*, MediaFoundation::*}; diff --git a/crates/enc-avfoundation/src/mp4.rs b/crates/enc-avfoundation/src/mp4.rs index a4ded9825a..d06648aa20 100644 --- a/crates/enc-avfoundation/src/mp4.rs +++ b/crates/enc-avfoundation/src/mp4.rs @@ -2,7 +2,7 @@ use cap_media_info::{AudioInfo, VideoInfo}; use cidre::{cm::SampleTimingInfo, objc::Obj, *}; use ffmpeg::frame; use std::{ops::Sub, path::PathBuf, time::Duration}; -use tracing::{debug, error, info, trace}; +use tracing::*; // before pausing at all, subtract 0. // on pause, record last frame time. @@ -48,23 +48,23 @@ pub enum InitError { } #[derive(thiserror::Error, Debug)] -pub enum QueueVideoFrameError { +pub enum QueueFrameError { #[error("AppendError/{0}")] AppendError(arc::R), #[error("Failed")] Failed, + #[error("Construct/{0}")] + Construct(cidre::os::Error), + #[error("NotReadyForMore")] + NotReadyForMore, } #[derive(thiserror::Error, Debug)] -pub enum QueueAudioFrameError { - #[error("No audio input")] - NoAudioInput, - #[error("Not ready")] - NotReady, - #[error("Setup/{0}")] - Setup(cidre::os::Error), - #[error("AppendError/{0}")] - AppendError(&'static cidre::ns::Exception), +pub enum FinishError { + #[error("NotWriting")] + NotWriting, + #[error("NoFrames")] + NoFrames, #[error("Failed")] Failed, } @@ -219,11 +219,15 @@ impl MP4Encoder { &mut self, frame: arc::R, timestamp: Duration, - ) -> Result<(), QueueVideoFrameError> { - if self.is_paused || !self.video_input.is_ready_for_more_media_data() { + ) -> Result<(), QueueFrameError> { + if self.is_paused { return Ok(()); }; + if !self.video_input.is_ready_for_more_media_data() { + return Err(QueueFrameError::NotReadyForMore); + } + if !self.is_writing { self.is_writing = true; self.asset_writer @@ -270,10 +274,7 @@ impl MP4Encoder { timing.pts = cm::Time::new(pts_duration.as_millis() as i64, 1_000); let frame = frame.copy_with_new_timing(&[timing]).unwrap(); - self.video_input - .append_sample_buf(&frame) - .map_err(|e| QueueVideoFrameError::AppendError(e.retained())) - .and_then(|v| v.then_some(()).ok_or(QueueVideoFrameError::Failed))?; + append_sample_buf(&mut self.video_input, &self.asset_writer, &frame)?; self.video_frames_appended += 1; self.last_timestamp = Some(timestamp); @@ -285,13 +286,17 @@ impl MP4Encoder { /// in the timebase of 1 / sample rate pub fn queue_audio_frame( &mut self, - frame: frame::Audio, + frame: &frame::Audio, timestamp: Duration, - ) -> Result<(), QueueAudioFrameError> { + ) -> Result<(), QueueFrameError> { if self.is_paused || !self.is_writing { return Ok(()); } + let Some(audio_input) = &mut self.audio_input else { + return Err(QueueFrameError::Failed); + }; + if let Some(pause_timestamp) = self.pause_timestamp && let Some(gap) = timestamp.checked_sub(pause_timestamp) { @@ -299,12 +304,8 @@ impl MP4Encoder { self.pause_timestamp = None; } - let Some(audio_input) = &mut self.audio_input else { - return Err(QueueAudioFrameError::NoAudioInput); - }; - if !audio_input.is_ready_for_more_media_data() { - return Ok(()); + return Err(QueueFrameError::NotReadyForMore); } let audio_desc = cat::audio::StreamBasicDesc::common_f32( @@ -316,11 +317,11 @@ impl MP4Encoder { let total_data = frame.samples() * frame.channels() as usize * frame.format().bytes(); let mut block_buf = - cm::BlockBuf::with_mem_block(total_data, None).map_err(QueueAudioFrameError::Setup)?; + cm::BlockBuf::with_mem_block(total_data, None).map_err(QueueFrameError::Construct)?; let block_buf_slice = block_buf .as_mut_slice() - .map_err(QueueAudioFrameError::Setup)?; + .map_err(QueueFrameError::Construct)?; if frame.is_planar() { let mut offset = 0; @@ -335,7 +336,7 @@ impl MP4Encoder { } let format_desc = - cm::AudioFormatDesc::with_asbd(&audio_desc).map_err(QueueAudioFrameError::Setup)?; + cm::AudioFormatDesc::with_asbd(&audio_desc).map_err(QueueFrameError::Construct)?; let mut pts_duration = timestamp .checked_sub(self.timestamp_offset) @@ -344,7 +345,7 @@ impl MP4Encoder { if let Some(last_pts) = self.last_audio_pts && pts_duration <= last_pts { - let frame_duration = Self::audio_frame_duration(&frame); + let frame_duration = Self::audio_frame_duration(frame); let adjusted_pts = last_pts + frame_duration; trace!( @@ -383,12 +384,9 @@ impl MP4Encoder { }], &[], ) - .map_err(QueueAudioFrameError::Setup)?; + .map_err(QueueFrameError::Construct)?; - audio_input - .append_sample_buf(&buffer) - .map_err(QueueAudioFrameError::AppendError) - .and_then(|v| v.then_some(()).ok_or(QueueAudioFrameError::Failed))?; + append_sample_buf(audio_input, &self.asset_writer, &buffer)?; self.audio_frames_appended += 1; self.last_timestamp = Some(timestamp); @@ -449,13 +447,14 @@ impl MP4Encoder { self.is_paused = false; } - pub fn finish(&mut self, timestamp: Option) { + pub fn finish(&mut self, timestamp: Option) -> Result<(), FinishError> { if !self.is_writing { - return; + return Err(FinishError::NotWriting); } let Some(mut most_recent_frame) = self.most_recent_frame.take() else { - return; + warn!("Encoder attempted to finish with no frame"); + return Err(FinishError::NoFrames); }; // We extend the video to the provided timestamp if possible @@ -489,16 +488,17 @@ impl MP4Encoder { debug!("Appended {} video frames", self.video_frames_appended); debug!("Appended {} audio frames", self.audio_frames_appended); - // debug!("First video timestamp: {:?}", self.first_timestamp); - // debug!("Last video timestamp: {:?}", self.last_pts); + wait_for_writer_finished(&self.asset_writer).map_err(|_| FinishError::Failed)?; info!("Finished writing"); + + Ok(()) } } impl Drop for MP4Encoder { fn drop(&mut self) { - self.finish(None); + let _ = self.finish(None); } } @@ -621,3 +621,35 @@ impl SampleBufExt for cm::SampleBuf { } } } + +fn append_sample_buf( + input: &mut av::AssetWriterInput, + writer: &av::AssetWriter, + frame: &cm::SampleBuf, +) -> Result<(), QueueFrameError> { + match input.append_sample_buf(frame) { + Ok(true) => {} + Ok(false) => { + if writer.status() == av::asset::writer::Status::Failed { + return Err(QueueFrameError::Failed); + } + if writer.status() == av::asset::writer::Status::Writing { + return Err(QueueFrameError::NotReadyForMore); + } + } + Err(e) => return Err(QueueFrameError::AppendError(e.retained())), + } + + Ok(()) +} + +fn wait_for_writer_finished(writer: &av::AssetWriter) -> Result<(), ()> { + use av::asset::writer::Status; + loop { + match writer.status() { + Status::Completed | Status::Cancelled => return Ok(()), + Status::Failed | Status::Unknown => return Err(()), + Status::Writing => std::thread::sleep(Duration::from_millis(2)), + } + } +} diff --git a/crates/enc-ffmpeg/src/audio/aac.rs b/crates/enc-ffmpeg/src/audio/aac.rs index be47f4ebc0..85bcfd92cd 100644 --- a/crates/enc-ffmpeg/src/audio/aac.rs +++ b/crates/enc-ffmpeg/src/audio/aac.rs @@ -104,8 +104,8 @@ impl AACEncoder { self.base.send_frame(frame, timestamp, output) } - pub fn finish(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { - self.base.finish(output) + pub fn flush(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { + self.base.flush(output) } } @@ -114,7 +114,7 @@ impl AudioEncoder for AACEncoder { let _ = self.send_frame(frame, Duration::MAX, output); } - fn finish(&mut self, output: &mut format::context::Output) { - let _ = self.finish(output); + fn flush(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { + self.flush(output) } } diff --git a/crates/enc-ffmpeg/src/audio/audio_encoder.rs b/crates/enc-ffmpeg/src/audio/audio_encoder.rs index 81d1f00082..118b8b5127 100644 --- a/crates/enc-ffmpeg/src/audio/audio_encoder.rs +++ b/crates/enc-ffmpeg/src/audio/audio_encoder.rs @@ -9,5 +9,5 @@ pub trait AudioEncoder { } fn send_frame(&mut self, frame: frame::Audio, output: &mut format::context::Output); - fn finish(&mut self, output: &mut format::context::Output); + fn flush(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error>; } diff --git a/crates/enc-ffmpeg/src/audio/base.rs b/crates/enc-ffmpeg/src/audio/base.rs index b15954a3d0..5f352188db 100644 --- a/crates/enc-ffmpeg/src/audio/base.rs +++ b/crates/enc-ffmpeg/src/audio/base.rs @@ -36,7 +36,7 @@ impl AudioEncoderBase { Ok(()) } - pub fn finish(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { + pub fn flush(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { while let Some(frame) = self.resampler.flush(self.encoder.frame_size() as usize) { self.inner.send_frame(&frame, output, &mut self.encoder)?; } diff --git a/crates/enc-ffmpeg/src/audio/mod.rs b/crates/enc-ffmpeg/src/audio/mod.rs index 377eb21df3..1eaea74225 100644 --- a/crates/enc-ffmpeg/src/audio/mod.rs +++ b/crates/enc-ffmpeg/src/audio/mod.rs @@ -1,10 +1,9 @@ mod audio_encoder; -mod base; -mod buffered_resampler; pub use audio_encoder::*; -mod opus; -pub use opus::*; +mod base; + +pub mod buffered_resampler; -mod aac; -pub use aac::*; +pub mod aac; +pub mod opus; diff --git a/crates/enc-ffmpeg/src/audio/opus.rs b/crates/enc-ffmpeg/src/audio/opus.rs index de82272653..1b9c144624 100644 --- a/crates/enc-ffmpeg/src/audio/opus.rs +++ b/crates/enc-ffmpeg/src/audio/opus.rs @@ -8,8 +8,9 @@ use ffmpeg::{ threading::Config, }; -use super::AudioEncoder; -use crate::audio::{base::AudioEncoderBase, buffered_resampler::BufferedResampler}; +use crate::audio::{ + audio_encoder::AudioEncoder, base::AudioEncoderBase, buffered_resampler::BufferedResampler, +}; pub struct OpusEncoder { base: AudioEncoderBase, @@ -97,8 +98,8 @@ impl OpusEncoder { self.base.send_frame(frame, timestamp, output) } - pub fn finish(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { - self.base.finish(output) + pub fn flush(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { + self.base.flush(output) } } @@ -110,6 +111,16 @@ fn select_output_rate(input_rate: i32, supported_rates: &[i32]) -> Option { .or_else(|| supported_rates.iter().copied().max()) } +impl AudioEncoder for OpusEncoder { + fn send_frame(&mut self, frame: frame::Audio, output: &mut format::context::Output) { + let _ = self.queue_frame(frame, Duration::MAX, output); + } + + fn flush(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { + self.flush(output) + } +} + #[cfg(test)] mod tests { use super::select_output_rate; @@ -132,13 +143,3 @@ mod tests { assert_eq!(select_output_rate(4_000, &supported), Some(8_000)); } } - -impl AudioEncoder for OpusEncoder { - fn send_frame(&mut self, frame: frame::Audio, output: &mut format::context::Output) { - let _ = self.queue_frame(frame, Duration::MAX, output); - } - - fn finish(&mut self, output: &mut format::context::Output) { - let _ = self.finish(output); - } -} diff --git a/crates/enc-ffmpeg/src/lib.rs b/crates/enc-ffmpeg/src/lib.rs index 08812ebc64..d07e15e4a9 100644 --- a/crates/enc-ffmpeg/src/lib.rs +++ b/crates/enc-ffmpeg/src/lib.rs @@ -1,5 +1,6 @@ -mod audio; mod base; + +mod audio; pub use audio::*; mod video; diff --git a/crates/enc-ffmpeg/src/mux/mod.rs b/crates/enc-ffmpeg/src/mux/mod.rs index 09078ff0fd..280593d90f 100644 --- a/crates/enc-ffmpeg/src/mux/mod.rs +++ b/crates/enc-ffmpeg/src/mux/mod.rs @@ -1,5 +1,2 @@ -mod mp4; -pub use mp4::*; - -mod ogg; -pub use ogg::*; +pub mod mp4; +pub mod ogg; diff --git a/crates/enc-ffmpeg/src/mux/mp4.rs b/crates/enc-ffmpeg/src/mux/mp4.rs index 9d152ccbb1..3c9e31f055 100644 --- a/crates/enc-ffmpeg/src/mux/mp4.rs +++ b/crates/enc-ffmpeg/src/mux/mp4.rs @@ -1,12 +1,12 @@ use cap_media_info::RawVideoFormat; use ffmpeg::{format, frame}; use std::{path::PathBuf, time::Duration}; -use tracing::{info, trace}; +use tracing::*; use crate::{ audio::AudioEncoder, h264, - video::{H264Encoder, H264EncoderError}, + video::h264::{H264Encoder, H264EncoderError}, }; pub struct MP4File { @@ -28,6 +28,19 @@ pub enum InitError { AudioInit(Box), } +#[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 MP4File { pub fn init( tag: &'static str, @@ -95,26 +108,39 @@ impl MP4File { audio.send_frame(frame, &mut self.output); } - pub fn finish(&mut self) { + pub fn finish(&mut self) -> Result { if self.is_finished { - return; + return Err(FinishError::AlreadyFinished); } self.is_finished = true; tracing::info!("MP4Encoder: Finishing encoding"); - self.video.finish(&mut self.output); - - if let Some(audio) = &mut self.audio { - tracing::info!("MP4Encoder: Flushing audio encoder"); - audio.finish(&mut self.output); - } + 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!("MP4Encoder: Flushing audio encoder"); + enc.flush(&mut self.output).inspect_err(|e| { + error!("Failed to finish audio encoder: {e:#}"); + }) + }) + .unwrap_or(Ok(())); tracing::info!("MP4Encoder: Writing trailer"); - if let Err(e) = self.output.write_trailer() { - tracing::error!("Failed to write MP4 trailer: {:?}", e); - } + self.output + .write_trailer() + .map_err(FinishError::WriteTrailerFailed)?; + + Ok(FinishResult { + video_finish, + audio_finish, + }) } pub fn video(&self) -> &H264Encoder { @@ -128,7 +154,7 @@ impl MP4File { impl Drop for MP4File { fn drop(&mut self) { - self.finish(); + let _ = self.finish(); } } diff --git a/crates/enc-ffmpeg/src/mux/ogg.rs b/crates/enc-ffmpeg/src/mux/ogg.rs index 03073aa30c..aae6ea8bbf 100644 --- a/crates/enc-ffmpeg/src/mux/ogg.rs +++ b/crates/enc-ffmpeg/src/mux/ogg.rs @@ -1,7 +1,7 @@ use ffmpeg::{format, frame}; use std::{path::PathBuf, time::Duration}; -use crate::audio::{OpusEncoder, OpusEncoderError}; +use crate::audio::opus::{OpusEncoder, OpusEncoderError}; pub struct OggFile { encoder: OpusEncoder, @@ -9,6 +9,14 @@ pub struct OggFile { finished: bool, } +#[derive(thiserror::Error, Debug)] +pub enum FinishError { + #[error("Already finished")] + AlreadyFinished, + #[error("{0}")] + WriteTrailerFailed(ffmpeg::Error), +} + impl OggFile { pub fn init( mut output: PathBuf, @@ -33,21 +41,32 @@ impl OggFile { &self.encoder } - pub fn queue_frame(&mut self, frame: frame::Audio, timestamp: Duration) { - let _ = self.encoder.queue_frame(frame, timestamp, &mut self.output); + pub fn queue_frame( + &mut self, + frame: frame::Audio, + timestamp: Duration, + ) -> Result<(), ffmpeg::Error> { + self.encoder.queue_frame(frame, timestamp, &mut self.output) } - pub fn finish(&mut self) { - if !self.finished { - let _ = self.encoder.finish(&mut self.output); - self.output.write_trailer().unwrap(); - self.finished = true; + pub fn finish(&mut self) -> Result, FinishError> { + if self.finished { + return Err(FinishError::AlreadyFinished); } + + self.finished = true; + + let flush_result = self.encoder.flush(&mut self.output); + self.output + .write_trailer() + .map_err(FinishError::WriteTrailerFailed)?; + + Ok(flush_result) } } impl Drop for OggFile { fn drop(&mut self) { - self.finish(); + let _ = self.finish(); } } diff --git a/crates/enc-ffmpeg/src/video/h264.rs b/crates/enc-ffmpeg/src/video/h264.rs index 99c6751289..a338229cb7 100644 --- a/crates/enc-ffmpeg/src/video/h264.rs +++ b/crates/enc-ffmpeg/src/video/h264.rs @@ -262,10 +262,8 @@ impl H264Encoder { Ok(()) } - pub fn finish(&mut self, output: &mut format::context::Output) { - if let Err(e) = self.base.process_eof(output, &mut self.encoder) { - tracing::error!("Failed to send EOF to encoder: {:?}", e); - } + pub fn flush(&mut self, output: &mut format::context::Output) -> Result<(), ffmpeg::Error> { + self.base.process_eof(output, &mut self.encoder) } } diff --git a/crates/enc-ffmpeg/src/video/mod.rs b/crates/enc-ffmpeg/src/video/mod.rs index 28f9a889e8..f61e796943 100644 --- a/crates/enc-ffmpeg/src/video/mod.rs +++ b/crates/enc-ffmpeg/src/video/mod.rs @@ -1,2 +1 @@ pub mod h264; -pub use h264::*; diff --git a/crates/enc-mediafoundation/src/lib.rs b/crates/enc-mediafoundation/src/lib.rs index f1d1eb7571..630da66d6c 100644 --- a/crates/enc-mediafoundation/src/lib.rs +++ b/crates/enc-mediafoundation/src/lib.rs @@ -2,8 +2,7 @@ pub mod d3d; pub mod media; -mod mft; -mod unsafe_send; +pub mod mft; pub mod video; pub use video::H264Encoder; diff --git a/crates/enc-mediafoundation/src/unsafe_send.rs b/crates/enc-mediafoundation/src/unsafe_send.rs deleted file mode 100644 index a4e831982a..0000000000 --- a/crates/enc-mediafoundation/src/unsafe_send.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::{fmt::Display, ops::Deref}; - -#[derive(Debug)] -pub struct UnsafeSend(pub T); - -unsafe impl Send for UnsafeSend {} -unsafe impl Sync for UnsafeSend {} - -impl Deref for UnsafeSend { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for UnsafeSend { - fn from(inner: T) -> Self { - Self(inner) - } -} - -impl Clone for UnsafeSend -where - T: Clone, -{ - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Display for UnsafeSend -where - T: std::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} diff --git a/crates/enc-mediafoundation/src/video/h264.rs b/crates/enc-mediafoundation/src/video/h264.rs index 24d5633fa6..266c117d89 100644 --- a/crates/enc-mediafoundation/src/video/h264.rs +++ b/crates/enc-mediafoundation/src/video/h264.rs @@ -60,8 +60,6 @@ pub struct H264Encoder { output_stream_id: u32, output_type: IMFMediaType, bitrate: u32, - - first_time: Option, } #[derive(Clone, Debug, thiserror::Error)] @@ -298,7 +296,6 @@ impl H264Encoder { bitrate, output_type, - first_time: None, }) } diff --git a/crates/export/src/mp4.rs b/crates/export/src/mp4.rs index e50a5441b1..79edbffb46 100644 --- a/crates/export/src/mp4.rs +++ b/crates/export/src/mp4.rs @@ -1,6 +1,6 @@ use crate::ExporterBase; use cap_editor::{AudioRenderer, get_audio_segments}; -use cap_enc_ffmpeg::{AACEncoder, AudioEncoder, H264Encoder, MP4File, MP4Input}; +use cap_enc_ffmpeg::{AudioEncoder, aac::AACEncoder, h264::H264Encoder, mp4::*}; use cap_media_info::{RawVideoFormat, VideoInfo}; use cap_project::XY; use cap_rendering::{ProjectUniforms, RenderSegment, RenderedFrame}; @@ -111,7 +111,16 @@ impl Mp4ExportSettings { info!("Encoded {encoded_frames} video frames"); - encoder.finish(); + let res = encoder + .finish() + .map_err(|e| format!("Failed to finish encoding: {e}"))?; + + if let Err(e) = res.video_finish { + return Err(format!("Video encoding failed: {e}")); + } + if let Err(e) = res.audio_finish { + return Err(format!("Audio encoding failed: {e}")); + } Ok::<_, String>(base.output_path) }) diff --git a/crates/mediafoundation-ffmpeg/src/h264.rs b/crates/mediafoundation-ffmpeg/src/h264.rs index 4cab09a6c1..bf12d502c6 100644 --- a/crates/mediafoundation-ffmpeg/src/h264.rs +++ b/crates/mediafoundation-ffmpeg/src/h264.rs @@ -1,6 +1,6 @@ use cap_mediafoundation_utils::*; use ffmpeg::{Rational, ffi::av_rescale_q, packet}; -use tracing::{info, trace}; +use tracing::*; use windows::Win32::Media::MediaFoundation::{IMFSample, MFSampleExtension_CleanPoint}; /// Configuration for H264 muxing diff --git a/crates/recording/Cargo.toml b/crates/recording/Cargo.toml index 83558ee348..201b87afbe 100644 --- a/crates/recording/Cargo.toml +++ b/crates/recording/Cargo.toml @@ -48,6 +48,7 @@ indexmap = "2.10.0" kameo = "0.17.2" inquire = "0.7.5" replace_with = "0.1.8" +retry = "2.1.0" [target.'cfg(target_os = "macos")'.dependencies] cidre = { workspace = true } diff --git a/crates/recording/examples/camera.rs b/crates/recording/examples/camera.rs index d4de350fdf..4b418f3434 100644 --- a/crates/recording/examples/camera.rs +++ b/crates/recording/examples/camera.rs @@ -1,27 +1,35 @@ -use std::fmt::Display; - -use cap_recording::{CameraFeed, feeds::camera::DeviceOrModelID}; +use cap_recording::{ + CameraFeed, + feeds::camera::{self, DeviceOrModelID}, +}; use ffmpeg::format::Pixel; use image::{ColorType, codecs::jpeg}; +use kameo::Actor; +use std::fmt::Display; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); - let cameras = CameraFeed::list_cameras() - .into_iter() - .map(CameraSelection) - .collect(); + let cameras = cap_camera::list_cameras().map(CameraSelection).collect(); let device = inquire::Select::new("Select a device", cameras) .prompt() .unwrap(); - let feed = CameraFeed::init(DeviceOrModelID::from_info(&device.0)) - .await - .unwrap(); + let feed = CameraFeed::spawn(CameraFeed::default()); + feed.ask(camera::SetInput { + id: DeviceOrModelID::from_info(&device.0), + }) + .await + .unwrap() + .await + .unwrap(); + let (tx, rx) = flume::bounded(1); - feed.attach(tx); - let frame = rx.recv_async().await.unwrap().frame; + + feed.ask(camera::AddSender(tx)).await.unwrap(); + + let frame = rx.recv_async().await.unwrap().inner; frame.format(); frame.width(); frame.height(); diff --git a/crates/recording/src/instant_recording.rs b/crates/recording/src/instant_recording.rs index 787a4cd490..703616c147 100644 --- a/crates/recording/src/instant_recording.rs +++ b/crates/recording/src/instant_recording.rs @@ -12,7 +12,6 @@ use cap_media_info::{AudioInfo, VideoInfo}; use cap_project::InstantRecordingMeta; use cap_utils::ensure_dir; use kameo::{Actor as _, prelude::*}; -use scap_targets::WindowId; use std::{ path::PathBuf, sync::Arc, @@ -253,7 +252,7 @@ pub struct ActorBuilder { mic_feed: Option>, max_output_size: Option, #[cfg(target_os = "macos")] - excluded_windows: Vec, + excluded_windows: Vec, } impl ActorBuilder { @@ -285,7 +284,7 @@ impl ActorBuilder { } #[cfg(target_os = "macos")] - pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self { + pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self { self.excluded_windows = excluded_windows; self } diff --git a/crates/recording/src/lib.rs b/crates/recording/src/lib.rs index c2ca40f73a..d417c5f087 100644 --- a/crates/recording/src/lib.rs +++ b/crates/recording/src/lib.rs @@ -12,7 +12,7 @@ pub use sources::screen_capture; use cap_media::MediaError; use feeds::microphone::MicrophoneFeedLock; -use scap_targets::{WindowId, bounds::LogicalBounds}; +use scap_targets::bounds::LogicalBounds; use serde::{Deserialize, Serialize}; use std::sync::Arc; use thiserror::Error; @@ -48,7 +48,7 @@ pub struct RecordingBaseInputs { #[cfg(target_os = "macos")] pub shareable_content: cidre::arc::R, #[cfg(target_os = "macos")] - pub excluded_windows: Vec, + pub excluded_windows: Vec, } #[derive(specta::Type, Serialize, Deserialize, Clone, Debug)] diff --git a/crates/recording/src/output_pipeline/core.rs b/crates/recording/src/output_pipeline/core.rs index cbd00941b7..f0e1f047c7 100644 --- a/crates/recording/src/output_pipeline/core.rs +++ b/crates/recording/src/output_pipeline/core.rs @@ -13,6 +13,7 @@ use std::{ any::Any, future, marker::PhantomData, + ops::Deref, path::{Path, PathBuf}, sync::{ Arc, @@ -361,7 +362,13 @@ async fn finish_build( let _ = done_tx.send(match (res, muxer_res) { (Err(e), _) | (_, Err(e)) => Err(e), - _ => Ok(()), + (_, Ok(muxer_streams_res)) => { + if let Err(e) = muxer_streams_res { + warn!("Muxer streams had failure: {e:#}"); + } + + Ok(()) + } }); }), ); @@ -696,6 +703,14 @@ impl AudioFrame { } } +impl Deref for AudioFrame { + type Target = ffmpeg::frame::Audio; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + pub trait VideoSource: Send + 'static { type Config; type Frame: VideoFrame; @@ -789,7 +804,7 @@ pub trait Muxer: Send + 'static { fn stop(&mut self) {} - fn finish(&mut self, timestamp: Duration) -> anyhow::Result<()>; + fn finish(&mut self, timestamp: Duration) -> anyhow::Result>; } pub trait AudioMuxer: Muxer { diff --git a/crates/recording/src/output_pipeline/ffmpeg.rs b/crates/recording/src/output_pipeline/ffmpeg.rs index 5276902142..8453479de1 100644 --- a/crates/recording/src/output_pipeline/ffmpeg.rs +++ b/crates/recording/src/output_pipeline/ffmpeg.rs @@ -3,7 +3,7 @@ use crate::{ output_pipeline::{AudioFrame, AudioMuxer, Muxer, VideoFrame, VideoMuxer}, }; use anyhow::{Context, anyhow}; -use cap_enc_ffmpeg::*; +use cap_enc_ffmpeg::{aac::AACEncoder, h264::*, ogg::*, opus::OpusEncoder}; use cap_media_info::{AudioInfo, VideoInfo}; use cap_timestamp::Timestamp; use std::{ @@ -65,18 +65,28 @@ impl Muxer for Mp4Muxer { }) } - fn finish(&mut self, _: Duration) -> anyhow::Result<()> { - if let Some(video_encoder) = self.video_encoder.as_mut() { - video_encoder.finish(&mut self.output); - } + fn finish(&mut self, _: Duration) -> anyhow::Result> { + let video_result = self + .video_encoder + .as_mut() + .map(|enc| enc.flush(&mut self.output)) + .unwrap_or(Ok(())); - if let Some(audio_encoder) = self.audio_encoder.as_mut() { - audio_encoder.finish(&mut self.output); - } + let audio_result = self + .audio_encoder + .as_mut() + .map(|enc| enc.flush(&mut self.output)) + .unwrap_or(Ok(())); - self.output.write_trailer()?; + self.output.write_trailer().context("write_trailer")?; - Ok(()) + if video_result.is_ok() && audio_result.is_ok() { + return Ok(Ok(())); + } + + Ok(Err(anyhow!( + "Video: {video_result:#?}, Audio: {audio_result:#?}" + ))) } } @@ -89,7 +99,7 @@ impl VideoMuxer for Mp4Muxer { timestamp: Duration, ) -> anyhow::Result<()> { if let Some(video_encoder) = self.video_encoder.as_mut() { - video_encoder.queue_frame(frame.inner, timestamp, &mut self.output); + video_encoder.queue_frame(frame.inner, timestamp, &mut self.output)?; } Ok(()) @@ -99,7 +109,7 @@ impl VideoMuxer for Mp4Muxer { impl AudioMuxer for Mp4Muxer { fn send_audio_frame(&mut self, frame: AudioFrame, timestamp: Duration) -> anyhow::Result<()> { if let Some(audio_encoder) = self.audio_encoder.as_mut() { - audio_encoder.send_frame(frame.inner, timestamp, &mut self.output); + audio_encoder.send_frame(frame.inner, timestamp, &mut self.output)?; } Ok(()) @@ -131,15 +141,16 @@ impl Muxer for OggMuxer { )) } - fn finish(&mut self, _: Duration) -> anyhow::Result<()> { - self.0.finish(); - Ok(()) + fn finish(&mut self, _: Duration) -> anyhow::Result> { + self.0 + .finish() + .map_err(Into::into) + .map(|r| r.map_err(Into::into)) } } impl AudioMuxer for OggMuxer { fn send_audio_frame(&mut self, frame: AudioFrame, timestamp: Duration) -> anyhow::Result<()> { - self.0.queue_frame(frame.inner, timestamp); - Ok(()) + Ok(self.0.queue_frame(frame.inner, timestamp)?) } } diff --git a/crates/recording/src/output_pipeline/macos.rs b/crates/recording/src/output_pipeline/macos.rs index 4c17e9c94d..d0623014e9 100644 --- a/crates/recording/src/output_pipeline/macos.rs +++ b/crates/recording/src/output_pipeline/macos.rs @@ -3,7 +3,9 @@ use crate::{ sources::screen_capture, }; use anyhow::anyhow; +use cap_enc_avfoundation::QueueFrameError; use cap_media_info::{AudioInfo, VideoInfo}; +use retry::{OperationResult, delay::Fixed}; use std::{ path::PathBuf, sync::{Arc, Mutex, atomic::AtomicBool}, @@ -49,12 +51,13 @@ impl Muxer for AVFoundationMp4Muxer { )) } - fn finish(&mut self, timestamp: Duration) -> anyhow::Result<()> { - self.0 + fn finish(&mut self, timestamp: Duration) -> anyhow::Result> { + Ok(self + .0 .lock() .map_err(|e| anyhow!("{e}"))? - .finish(Some(timestamp)); - Ok(()) + .finish(Some(timestamp)) + .map(Ok)?) } } @@ -74,17 +77,32 @@ impl VideoMuxer for AVFoundationMp4Muxer { mp4.resume(); } - mp4.queue_video_frame(frame.sample_buf, timestamp) - .map_err(|e| anyhow!("QueueVideoFrame/{e}")) + retry::retry(Fixed::from_millis(3).take(3), || { + match mp4.queue_video_frame(frame.sample_buf.clone(), timestamp) { + Ok(v) => OperationResult::Ok(v), + Err(QueueFrameError::NotReadyForMore) => { + OperationResult::Retry(QueueFrameError::NotReadyForMore) + } + Err(e) => OperationResult::Err(e), + } + }) + .map_err(|e| anyhow!("send_video_frame/{e}")) } } impl AudioMuxer for AVFoundationMp4Muxer { fn send_audio_frame(&mut self, frame: AudioFrame, timestamp: Duration) -> anyhow::Result<()> { - self.0 - .lock() - .map_err(|e| anyhow!("{e}"))? - .queue_audio_frame(frame.inner, timestamp) - .map_err(|e| anyhow!("{e}")) + let mut mp4 = self.0.lock().map_err(|e| anyhow!("{e}"))?; + + retry::retry(Fixed::from_millis(3).take(3), || { + match mp4.queue_audio_frame(&frame.inner, timestamp) { + Ok(v) => OperationResult::Ok(v), + Err(QueueFrameError::NotReadyForMore) => { + OperationResult::Retry(QueueFrameError::NotReadyForMore) + } + Err(e) => OperationResult::Err(e), + } + }) + .map_err(|e| anyhow!("send_audio_frame/{e}")) } } diff --git a/crates/recording/src/output_pipeline/win.rs b/crates/recording/src/output_pipeline/win.rs index 0ab39862da..5306b73ce2 100644 --- a/crates/recording/src/output_pipeline/win.rs +++ b/crates/recording/src/output_pipeline/win.rs @@ -1,6 +1,6 @@ use crate::{AudioFrame, AudioMuxer, Muxer, TaskPool, VideoMuxer, screen_capture}; use anyhow::{Context, anyhow}; -use cap_enc_ffmpeg::AACEncoder; +use cap_enc_ffmpeg::aac::AACEncoder; use cap_media_info::{AudioInfo, VideoInfo}; use futures::channel::oneshot; use std::{ @@ -106,7 +106,7 @@ impl Muxer for WindowsMuxer { } }; - cap_enc_ffmpeg::H264Encoder::builder(video_config) + cap_enc_ffmpeg::h264::H264Encoder::builder(video_config) .with_output_size(fallback_width, fallback_height) .and_then(|builder| builder.build(&mut *output_guard)) .map(either::Right) @@ -272,12 +272,20 @@ impl Muxer for WindowsMuxer { let _ = self.video_tx.send(None); } - fn finish(&mut self, _: Duration) -> anyhow::Result<()> { - let mut output = self.output.lock().unwrap(); - if let Some(audio_encoder) = self.audio_encoder.as_mut() { - let _ = audio_encoder.finish(&mut output); - } - Ok(output.write_trailer()?) + fn finish(&mut self, _: Duration) -> anyhow::Result> { + let mut output = self + .output + .lock() + .map_err(|_| anyhow!("Failed to lock output"))?; + let audio_result = self + .audio_encoder + .as_mut() + .map(|enc| enc.flush(&mut output)) + .unwrap_or(Ok(())); + + output.write_trailer()?; + + Ok(audio_result.map_err(Into::into)) } } diff --git a/crates/recording/src/sources/audio_mixer.rs b/crates/recording/src/sources/audio_mixer.rs index 3a706e5d1b..b493379c8a 100644 --- a/crates/recording/src/sources/audio_mixer.rs +++ b/crates/recording/src/sources/audio_mixer.rs @@ -479,6 +479,8 @@ fn duration_from_samples(samples: usize, rate: i32) -> Duration { #[cfg(test)] mod test { + use futures::{SinkExt, StreamExt}; + use super::*; const SAMPLE_RATE: u32 = 48_000; @@ -490,29 +492,31 @@ mod test { const ONE_SECOND: Duration = Duration::from_secs(1); const SAMPLES_SECOND: usize = SOURCE_INFO.rate() as usize; - #[test] - fn mix_sources() { - let (tx, output_rx) = flume::bounded(4); - let mut mixer = AudioMixerBuilder::new(tx); + #[tokio::test] + async fn mix_sources() { + let (tx, mut output_rx) = mpsc::channel(4); + let mut mixer = AudioMixerBuilder::new(); - let (tx1, rx) = flume::bounded(4); + let (mut tx1, rx) = mpsc::channel(4); mixer.add_source(SOURCE_INFO, rx); - let (tx2, rx) = flume::bounded(4); + let (mut tx2, rx) = mpsc::channel(4); mixer.add_source(SOURCE_INFO, rx); - let mut mixer = mixer.build().unwrap(); + let mut mixer = mixer.build(tx).unwrap(); let start = mixer.timestamps; - tx1.send(( - SOURCE_INFO.wrap_frame(&vec![128, 255, 255, 255]), + tx1.send(AudioFrame::new( + SOURCE_INFO.wrap_frame(&[128, 255, 255, 255]), Timestamp::Instant(start.instant()), )) + .await .unwrap(); - tx2.send(( - SOURCE_INFO.wrap_frame(&vec![128, 128, 1, 255]), + tx2.send(AudioFrame::new( + SOURCE_INFO.wrap_frame(&[128, 128, 1, 255]), Timestamp::Instant(start.instant()), )) + .await .unwrap(); let _ = mixer.tick( @@ -520,7 +524,7 @@ mod test { Timestamp::Instant(start.instant() + Duration::from_secs_f64(4.0 / SAMPLE_RATE as f64)), ); - let (frame, _) = output_rx.recv().expect("No output frame"); + let frame = output_rx.next().await.expect("No output frame"); let byte_count = frame.samples() * frame.channels() as usize; let samples: &[f32] = unsafe { std::mem::transmute(&frame.data(0)[0..byte_count]) }; @@ -535,132 +539,135 @@ mod test { mod source_buffer { use super::*; - #[test] - fn single_frame() { - let (output_tx, _) = flume::bounded(4); - let mut mixer = AudioMixerBuilder::new(output_tx); + #[tokio::test] + async fn single_frame() { + let (output_tx, _) = mpsc::channel::(4); + let mut mixer = AudioMixerBuilder::new(); let start = Timestamps::now(); - let (tx, rx) = flume::bounded(4); + let (mut tx, rx) = mpsc::channel(4); mixer.add_source(SOURCE_INFO, rx); - let mut mixer = mixer.build().unwrap(); + let mut mixer = mixer.build(output_tx).unwrap(); - tx.send(( + tx.send(AudioFrame::new( SOURCE_INFO.wrap_frame(&vec![0; SAMPLES_SECOND / 2]), Timestamp::Instant(start.instant()), )) + .await .unwrap(); mixer.buffer_sources(Timestamp::Instant(start.instant())); assert_eq!(mixer.sources[0].buffer.len(), 1); - assert!(mixer.sources[0].rx.is_empty()); + assert!(mixer.sources[0].rx.try_next().is_err()); } - #[test] - fn frame_gap() { - let (output_tx, _) = flume::bounded(4); - let mut mixer = AudioMixerBuilder::new(output_tx); + #[tokio::test] + async fn frame_gap() { + let (output_tx, _) = mpsc::channel(4); + let mut mixer = AudioMixerBuilder::new(); - let (tx, rx) = flume::bounded(4); + let (mut tx, rx) = mpsc::channel(4); mixer.add_source(SOURCE_INFO, rx); - let mut mixer = mixer.build().unwrap(); + let mut mixer = mixer.build(output_tx).unwrap(); - tx.send(( + tx.send(AudioFrame::new( SOURCE_INFO.wrap_frame(&vec![0; SAMPLES_SECOND / 2]), Timestamp::Instant(mixer.timestamps.instant()), )) + .await .unwrap(); - tx.send(( + tx.send(AudioFrame::new( SOURCE_INFO.wrap_frame(&vec![0; SAMPLES_SECOND / 2]), Timestamp::Instant(mixer.timestamps.instant() + ONE_SECOND), )) + .await .unwrap(); mixer.buffer_sources(Timestamp::Instant(mixer.timestamps.instant())); - let source = &mixer.sources[0]; + let source = &mut mixer.sources[0]; assert_eq!(source.buffer.len(), 3); - assert!(source.rx.is_empty()); + assert!(source.rx.try_next().is_err()); assert_eq!( - source.buffer[1].1.duration_since(mixer.timestamps), + source.buffer[1].timestamp.duration_since(mixer.timestamps), ONE_SECOND / 2 ); - assert_eq!( - source.buffer[1].0.samples(), - SOURCE_INFO.rate() as usize / 2 - ); + assert_eq!(source.buffer[1].samples(), SOURCE_INFO.rate() as usize / 2); } - #[test] - fn start_gap() { - let (output_tx, _) = flume::bounded(4); - let mut mixer = AudioMixerBuilder::new(output_tx); + #[tokio::test] + async fn start_gap() { + let (output_tx, _) = mpsc::channel(4); + let mut mixer = AudioMixerBuilder::new(); - let (tx, rx) = flume::bounded(4); + let (mut tx, rx) = mpsc::channel(4); mixer.add_source(SOURCE_INFO, rx); - let mut mixer = mixer.build().unwrap(); + let mut mixer = mixer.build(output_tx).unwrap(); let start = mixer.timestamps; - tx.send(( + tx.send(AudioFrame::new( SOURCE_INFO.wrap_frame(&vec![0; SAMPLES_SECOND / 2]), Timestamp::Instant(start.instant() + ONE_SECOND / 2), )) + .await .unwrap(); mixer.buffer_sources(Timestamp::Instant(start.instant())); - let source = &mixer.sources[0]; + let source = &mut mixer.sources[0]; assert_eq!(source.buffer.len(), 1); - assert!(source.rx.is_empty()); + assert!(source.rx.try_next().is_err()); - assert_eq!(source.buffer[0].1.duration_since(start), ONE_SECOND / 2); assert_eq!( - source.buffer[0].0.samples(), - SOURCE_INFO.rate() as usize / 2 + source.buffer[0].timestamp.duration_since(start), + ONE_SECOND / 2 ); + assert_eq!(source.buffer[0].samples(), SOURCE_INFO.rate() as usize / 2); } - #[test] - fn after_draining() { - let (output_tx, _) = flume::bounded(4); - let mut mixer = AudioMixerBuilder::new(output_tx); + #[tokio::test] + async fn after_draining() { + let (output_tx, _) = mpsc::channel(4); + let mut mixer = AudioMixerBuilder::new(); - let (tx, rx) = flume::bounded(4); + let (mut tx, rx) = mpsc::channel(4); mixer.add_source(SOURCE_INFO, rx); - let mut mixer = mixer.build().unwrap(); + let mut mixer = mixer.build(output_tx).unwrap(); let start = mixer.timestamps; - tx.send(( + tx.send(AudioFrame::new( SOURCE_INFO.wrap_frame(&vec![0; SAMPLES_SECOND / 2]), Timestamp::Instant(start.instant()), )) + .await .unwrap(); mixer.buffer_sources(Timestamp::Instant(start.instant())); mixer.sources[0].buffer.clear(); - tx.send(( + tx.send(AudioFrame::new( SOURCE_INFO.wrap_frame(&vec![0; SAMPLES_SECOND / 2]), Timestamp::Instant(start.instant() + ONE_SECOND), )) + .await .unwrap(); mixer.buffer_sources(Timestamp::Instant(start.instant() + ONE_SECOND)); - let source = &mixer.sources[0]; + let source = &mut mixer.sources[0]; assert_eq!(source.buffer.len(), 2); - assert!(source.rx.is_empty()); + assert!(source.rx.try_next().is_err()); let item = &source.buffer[0]; assert_eq!(item.timestamp.duration_since(start), ONE_SECOND / 2); diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs index cc6a135849..4cc228a41b 100644 --- a/crates/recording/src/sources/screen_capture/macos.rs +++ b/crates/recording/src/sources/screen_capture/macos.rs @@ -7,6 +7,7 @@ use crate::{ }, }; use anyhow::{Context, anyhow}; +use cap_timestamp::Timestamp; use cidre::*; use futures::{FutureExt as _, channel::mpsc, future::BoxFuture}; use std::{ diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index 9daf4c4541..4120b047b0 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -1,6 +1,5 @@ use cap_cursor_capture::CursorCropBounds; use cap_media_info::{AudioInfo, VideoInfo}; -use cap_timestamp::Timestamp; use scap_targets::{Display, DisplayId, Window, WindowId, bounds::*}; use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index d1fadede0d..a3b3afd5df 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -1,37 +1,30 @@ use crate::{ - AudioFrame, ChannelAudioSource, ChannelVideoSource, ChannelVideoSourceConfig, SetupCtx, - output_pipeline, + AudioFrame, SetupCtx, output_pipeline, screen_capture::{ScreenCaptureConfig, ScreenCaptureFormat}, }; use ::windows::Win32::Graphics::Direct3D11::{D3D11_BOX, ID3D11Device}; use anyhow::anyhow; -use cap_fail::fail_err; use cap_media_info::{AudioInfo, VideoInfo}; -use cap_timestamp::{PerformanceCounterTimestamp, Timestamp, Timestamps}; +use cap_timestamp::{PerformanceCounterTimestamp, Timestamp}; use cpal::traits::{DeviceTrait, HostTrait}; use futures::{ FutureExt, SinkExt, StreamExt, channel::{mpsc, oneshot}, }; -use scap_direct3d::StopCapturerError; use scap_ffmpeg::*; use scap_targets::{Display, DisplayId}; use std::{ - collections::VecDeque, sync::{ Arc, atomic::{self, AtomicU32}, }, - time::{Duration, Instant}, -}; -use tokio_util::{ - future::FutureExt as _, - sync::{CancellationToken, DropGuard}, + time::Duration, }; +use tokio_util::{future::FutureExt as _, sync::CancellationToken}; use tracing::*; -const WINDOW_DURATION: Duration = Duration::from_secs(3); -const LOG_INTERVAL: Duration = Duration::from_secs(5); +// const WINDOW_DURATION: Duration = Duration::from_secs(3); +// const LOG_INTERVAL: Duration = Duration::from_secs(5); const MAX_DROP_RATE_THRESHOLD: f64 = 0.25; #[derive(Debug)] @@ -61,24 +54,6 @@ impl ScreenCaptureFormat for Direct3DCapture { } } -#[derive(Clone, Debug, thiserror::Error)] -enum SourceError { - #[error("NoDisplay: Id '{0}'")] - NoDisplay(DisplayId), - #[error("AsCaptureItem: {0}")] - AsCaptureItem(::windows::core::Error), - #[error("CreateAudioCapture/{0}")] - CreateAudioCapture(scap_cpal::CapturerError), - #[error("StartCapturingAudio/{0}")] - StartCapturingAudio( - String, /* SendError */ - ), - #[error("Closed")] - Closed, -} - -struct CapturerHandle {} - pub struct VideoFrame { pub frame: scap_direct3d::Frame, pub timestamp: Timestamp, diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index 682acf1e1f..bb06f96cfd 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -17,7 +17,6 @@ use cap_timestamp::{Timestamp, Timestamps}; use futures::{FutureExt, StreamExt, future::OptionFuture, stream::FuturesUnordered}; use kameo::{Actor as _, prelude::*}; use relative_path::RelativePathBuf; -use scap_targets::WindowId; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -346,7 +345,7 @@ pub struct ActorBuilder { camera_feed: Option>, custom_cursor: bool, #[cfg(target_os = "macos")] - excluded_windows: Vec, + excluded_windows: Vec, } impl ActorBuilder { @@ -384,7 +383,7 @@ impl ActorBuilder { } #[cfg(target_os = "macos")] - pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self { + pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self { self.excluded_windows = excluded_windows; self } diff --git a/crates/rendering/src/zoom.rs b/crates/rendering/src/zoom.rs index a0291cf1b9..47f59a9fd9 100644 --- a/crates/rendering/src/zoom.rs +++ b/crates/rendering/src/zoom.rs @@ -240,7 +240,7 @@ mod test { }; } - fn c(time: f64, segments: &[ZoomSegment]) -> SegmentsCursor { + fn c<'a>(time: f64, segments: &'a [ZoomSegment]) -> SegmentsCursor<'a> { SegmentsCursor::new(time, segments) } diff --git a/crates/scap-ffmpeg/examples/cli.rs b/crates/scap-ffmpeg/examples/cli.rs index 274f5382c5..6b329ee638 100644 --- a/crates/scap-ffmpeg/examples/cli.rs +++ b/crates/scap-ffmpeg/examples/cli.rs @@ -72,9 +72,7 @@ pub async fn main() { ff_frame.height(); ff_frame.format(); }) - .with_stop_with_err_cb(|stream, error| { - (stream, error); - }) + .with_stop_with_err_cb(|_, _| {}) .build() .expect("Failed to build capturer"); diff --git a/crates/scap-screencapturekit/Cargo.toml b/crates/scap-screencapturekit/Cargo.toml index 7a0ee12a32..6a82597ba4 100644 --- a/crates/scap-screencapturekit/Cargo.toml +++ b/crates/scap-screencapturekit/Cargo.toml @@ -21,3 +21,4 @@ workspace = true clap = { version = "4.5.40", features = ["derive"] } inquire = "0.7.5" scap-targets = { path = "../scap-targets" } +tokio.workspace = true diff --git a/crates/scap-screencapturekit/examples/cli.rs b/crates/scap-screencapturekit/examples/cli.rs index 36d95dbc2a..fb5b98ae09 100644 --- a/crates/scap-screencapturekit/examples/cli.rs +++ b/crates/scap-screencapturekit/examples/cli.rs @@ -5,14 +5,15 @@ fn main() { #[cfg(target_os = "macos")] mod macos { - + use cidre::sc; use scap_targets::Display; use std::time::Duration; use futures::executor::block_on; use scap_screencapturekit::{Capturer, StreamCfgBuilder}; - fn main() { + #[tokio::main] + pub async fn main() { let display = Display::primary(); let display = display.raw_handle(); @@ -24,23 +25,21 @@ mod macos { let config = StreamCfgBuilder::default() .with_fps(60.0) - .with_width(display.physical_size().width() as usize) - .with_height(display.physical_size().height() as usize) + .with_width(display.physical_size().unwrap().width() as usize) + .with_height(display.physical_size().unwrap().height() as usize) .build(); let capturer = Capturer::builder( - block_on(display.as_content_filter()).expect("Failed to get display as content filter"), + block_on(display.as_content_filter(sc::ShareableContent::current().await.unwrap())) + .expect("Failed to get display as content filter"), config, ) - .with_output_sample_buf_cb(|frame| { - dbg!(frame.output_type()); + .with_output_sample_buf_cb(|_| { // if let Some(image_buf) = buf.image_buf() { // image_buf.show(); // } }) - .with_stop_with_err_cb(|stream, error| { - dbg!(stream, error); - }) + .with_stop_with_err_cb(|_, _| {}) .build() .expect("Failed to build capturer"); diff --git a/crates/timestamp/src/macos.rs b/crates/timestamp/src/macos.rs index fd1065ec44..a316d7533f 100644 --- a/crates/timestamp/src/macos.rs +++ b/crates/timestamp/src/macos.rs @@ -58,16 +58,3 @@ impl Sub for MachAbsoluteTimestamp { Self((self.0 as f64 - rhs.as_nanos() as f64 * freq) as u64) } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test() { - let a = MachAbsoluteTimestamp::new(0); - - dbg!(MachAbsoluteTimestamp::now()); - dbg!(a + Duration::from_secs(1)); - } -}