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
10 changes: 6 additions & 4 deletions crates/export/src/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ impl Mp4ExportSettings {

let mut encoded_frames = 0;
while let Ok(frame) = frame_rx.recv() {
encoder.queue_video_frame(
frame.video,
Duration::from_secs_f32(encoded_frames as f32 / fps as f32),
);
encoder
.queue_video_frame(
frame.video,
Duration::from_secs_f32(encoded_frames as f32 / fps as f32),
)
.map_err(|err| err.to_string())?;
encoded_frames += 1;
if let Some(audio) = frame.audio {
encoder.queue_audio_frame(audio);
Expand Down
32 changes: 32 additions & 0 deletions crates/recording/src/capture_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,38 @@ use anyhow::anyhow;
use cap_timestamp::Timestamps;
use std::{path::PathBuf, sync::Arc};

#[cfg(windows)]
use std::sync::atomic::{AtomicBool, Ordering};

#[cfg(windows)]
#[derive(Clone, Debug)]
pub struct EncoderPreferences {
force_software: Arc<AtomicBool>,
}

#[cfg(windows)]
impl EncoderPreferences {
pub fn new() -> Self {
Self {
force_software: Arc::new(AtomicBool::new(false)),
}
}

pub fn should_force_software(&self) -> bool {
self.force_software.load(Ordering::Relaxed)
}

pub fn force_software_only(&self) {
self.force_software.store(true, Ordering::Relaxed);
}
}

pub trait MakeCapturePipeline: ScreenCaptureFormat + std::fmt::Debug + 'static {
async fn make_studio_mode_pipeline(
screen_capture: screen_capture::VideoSourceConfig,
output_path: PathBuf,
start_time: Timestamps,
#[cfg(windows)] encoder_preferences: EncoderPreferences,
) -> anyhow::Result<OutputPipeline>
where
Self: Sized;
Expand All @@ -23,6 +50,7 @@ pub trait MakeCapturePipeline: ScreenCaptureFormat + std::fmt::Debug + 'static {
mic_feed: Option<Arc<MicrophoneFeedLock>>,
output_path: PathBuf,
output_resolution: (u32, u32),
#[cfg(windows)] encoder_preferences: EncoderPreferences,
) -> anyhow::Result<OutputPipeline>
where
Self: Sized;
Expand Down Expand Up @@ -76,6 +104,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
screen_capture: screen_capture::VideoSourceConfig,
output_path: PathBuf,
start_time: Timestamps,
encoder_preferences: EncoderPreferences,
) -> anyhow::Result<OutputPipeline> {
let d3d_device = screen_capture.d3d_device.clone();

Expand All @@ -88,6 +117,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
bitrate_multiplier: 0.15f32,
frame_rate: 30u32,
output_size: None,
encoder_preferences,
})
.await
}
Expand All @@ -98,6 +128,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
mic_feed: Option<Arc<MicrophoneFeedLock>>,
output_path: PathBuf,
output_resolution: (u32, u32),
encoder_preferences: EncoderPreferences,
) -> anyhow::Result<OutputPipeline> {
let d3d_device = screen_capture.d3d_device.clone();
let mut output_builder = OutputPipeline::builder(output_path.clone())
Expand All @@ -122,6 +153,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
Width: output_resolution.0 as i32,
Height: output_resolution.1 as i32,
}),
encoder_preferences,
})
.await
}
Expand Down
2 changes: 2 additions & 0 deletions crates/recording/src/instant_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ async fn create_pipeline(
mic_feed,
output_path.clone(),
output_resolution,
#[cfg(windows)]
crate::capture_pipeline::EncoderPreferences::new(),
)
.await?;

Expand Down
137 changes: 96 additions & 41 deletions crates/recording/src/output_pipeline/win.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct WindowsMuxerConfig {
pub frame_rate: u32,
pub bitrate_multiplier: f32,
pub output_size: Option<SizeInt32>,
pub encoder_preferences: crate::capture_pipeline::EncoderPreferences,
}

impl Muxer for WindowsMuxer {
Expand Down Expand Up @@ -71,54 +72,108 @@ impl Muxer for WindowsMuxer {
tasks.spawn_thread("windows-encoder", move || {
cap_mediafoundation_utils::thread_init();

let encoder_preferences = &config.encoder_preferences;

let encoder = (|| {
let mut output = output.lock().unwrap();

let native_encoder =
cap_enc_mediafoundation::H264Encoder::new_with_scaled_output(
&config.d3d_device,
config.pixel_format,
input_size,
output_size,
config.frame_rate,
config.bitrate_multiplier,
);

match native_encoder {
Ok(encoder) => cap_mediafoundation_ffmpeg::H264StreamMuxer::new(
&mut output,
cap_mediafoundation_ffmpeg::MuxerConfig {
width: output_size.Width as u32,
height: output_size.Height as u32,
fps: config.frame_rate,
bitrate: encoder.bitrate(),
},
)
.map(|muxer| either::Left((encoder, muxer)))
.map_err(|e| anyhow!("{e}")),
Err(e) => {
use tracing::{error, info};

error!("Failed to create native encoder: {e}");
let fallback = |reason: Option<String>| {
use tracing::{error, info};

encoder_preferences.force_software_only();
if let Some(reason) = reason.as_ref() {
error!("Falling back to software H264 encoder: {reason}");
} else {
info!("Falling back to software H264 encoder");
}

let fallback_width = if output_size.Width > 0 {
output_size.Width as u32
} else {
video_config.width
};
let fallback_height = if output_size.Height > 0 {
output_size.Height as u32
} else {
video_config.height
};

let mut output_guard = match output.lock() {
Ok(guard) => guard,
Err(poisoned) => {
return Err(anyhow!(
"ScreenSoftwareEncoder: failed to lock output mutex: {}",
poisoned
));
}
};

cap_enc_ffmpeg::H264Encoder::builder(video_config)
.with_output_size(fallback_width, fallback_height)
.and_then(|builder| builder.build(&mut *output_guard))
.map(either::Right)
.map_err(|e| anyhow!("ScreenSoftwareEncoder/{e}"))
};

if encoder_preferences.should_force_software() {
return fallback(None);
}

match cap_enc_mediafoundation::H264Encoder::new_with_scaled_output(
&config.d3d_device,
config.pixel_format,
input_size,
output_size,
config.frame_rate,
config.bitrate_multiplier,
) {
Ok(encoder) => {
let width = match u32::try_from(output_size.Width) {
Ok(width) if width > 0 => width,
_ => {
return fallback(Some(format!(
"Invalid output width: {}",
output_size.Width
)));
}
};

let fallback_width = if output_size.Width > 0 {
output_size.Width as u32
} else {
video_config.width
let height = match u32::try_from(output_size.Height) {
Ok(height) if height > 0 => height,
_ => {
return fallback(Some(format!(
"Invalid output height: {}",
output_size.Height
)));
}
};
let fallback_height = if output_size.Height > 0 {
output_size.Height as u32
} else {
video_config.height

let muxer = {
let mut output_guard = match output.lock() {
Ok(guard) => guard,
Err(poisoned) => {
return fallback(Some(format!(
"Failed to lock output mutex: {}",
poisoned
)));
}
};

cap_mediafoundation_ffmpeg::H264StreamMuxer::new(
&mut *output_guard,
cap_mediafoundation_ffmpeg::MuxerConfig {
width,
height,
fps: config.frame_rate,
bitrate: encoder.bitrate(),
},
)
};

cap_enc_ffmpeg::H264Encoder::builder(video_config)
.with_output_size(fallback_width, fallback_height)
.and_then(|builder| builder.build(&mut output))
.map(either::Right)
.map_err(|e| anyhow!("ScreenSoftwareEncoder/{e}"))
match muxer {
Ok(muxer) => Ok(either::Left((encoder, muxer))),
Err(err) => fallback(Some(err.to_string())),
}
}
Err(err) => fallback(Some(err.to_string())),
}
})();

Expand Down
39 changes: 36 additions & 3 deletions crates/recording/src/sources/screen_capture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ impl<TCaptureFormat: ScreenCaptureFormat> ScreenCaptureConfig<TCaptureFormat> {
) -> Result<Self, ScreenCaptureInitError> {
cap_fail::fail!("ScreenCaptureSource::init");

let fps = max_fps.min(display.refresh_rate() as u32);
let target_refresh = validated_refresh_rate(display.refresh_rate());
let fps = std::cmp::max(1, std::cmp::min(max_fps, target_refresh));

let output_size = crop_bounds
.clone()
Expand Down Expand Up @@ -359,15 +360,43 @@ impl<TCaptureFormat: ScreenCaptureFormat> ScreenCaptureConfig<TCaptureFormat> {
}
}

fn validated_refresh_rate<T>(reported_refresh_rate: T) -> u32
where
T: Into<f64>,
{
let reported_refresh_rate = reported_refresh_rate.into();
let fallback_refresh = 60;
let rounded_refresh = reported_refresh_rate.round();
let is_invalid_refresh = !rounded_refresh.is_finite() || rounded_refresh <= 0.0;
let capped_refresh = if is_invalid_refresh {
fallback_refresh as f64
} else {
rounded_refresh.min(500.0)
};

if is_invalid_refresh {
warn!(
?reported_refresh_rate,
fallback = fallback_refresh,
"Display reported invalid refresh rate; falling back to default"
);
fallback_refresh
} else {
capped_refresh as u32
}
}

pub fn list_displays() -> Vec<(CaptureDisplay, Display)> {
scap_targets::Display::list()
.into_iter()
.filter_map(|display| {
let refresh_rate = validated_refresh_rate(display.raw_handle().refresh_rate());

Some((
CaptureDisplay {
id: display.id(),
name: display.name()?,
refresh_rate: display.raw_handle().refresh_rate() as u32,
refresh_rate,
},
display,
))
Expand Down Expand Up @@ -409,13 +438,17 @@ pub fn list_windows() -> Vec<(CaptureWindow, Window)> {
#[cfg(not(target_os = "macos"))]
let bundle_identifier = None;

let refresh_rate = v
.display()
.map(|display| validated_refresh_rate(display.raw_handle().refresh_rate()))?;

Some((
CaptureWindow {
id: v.id(),
name,
owner_name,
bounds: v.display_relative_logical_bounds()?,
refresh_rate: v.display()?.raw_handle().refresh_rate() as u32,
refresh_rate,
bundle_identifier,
},
v,
Expand Down
3 changes: 1 addition & 2 deletions crates/recording/src/sources/screen_capture/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use futures::{
FutureExt, SinkExt, StreamExt,
channel::{mpsc, oneshot},
};
use kameo::prelude::*;
use scap_direct3d::StopCapturerError;
use scap_ffmpeg::*;
use scap_targets::{Display, DisplayId};
Expand Down Expand Up @@ -179,7 +178,7 @@ impl output_pipeline::VideoSource for VideoSource {
where
Self: Sized,
{
let (mut error_tx, mut error_rx) = mpsc::channel(1);
let (error_tx, mut error_rx) = mpsc::channel(1);
let (ctrl_tx, ctrl_rx) = std::sync::mpsc::sync_channel::<VideoControl>(1);

let tokio_rt = tokio::runtime::Handle::current();
Expand Down
9 changes: 9 additions & 0 deletions crates/recording/src/studio_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ struct SegmentPipelineFactory {
start_time: Timestamps,
index: u32,
completion_tx: watch::Sender<Option<Result<(), PipelineDoneError>>>,
#[cfg(windows)]
encoder_preferences: crate::capture_pipeline::EncoderPreferences,
}

impl SegmentPipelineFactory {
Expand All @@ -607,6 +609,8 @@ impl SegmentPipelineFactory {
start_time,
index: 0,
completion_tx,
#[cfg(windows)]
encoder_preferences: crate::capture_pipeline::EncoderPreferences::new(),
}
}

Expand All @@ -624,6 +628,8 @@ impl SegmentPipelineFactory {
next_cursors_id,
self.custom_cursor_capture,
self.start_time,
#[cfg(windows)]
self.encoder_preferences.clone(),
)
.await?;

Expand Down Expand Up @@ -682,6 +688,7 @@ async fn create_segment_pipeline(
next_cursors_id: u32,
custom_cursor_capture: bool,
start_time: Timestamps,
#[cfg(windows)] encoder_preferences: crate::capture_pipeline::EncoderPreferences,
) -> anyhow::Result<Pipeline> {
#[cfg(windows)]
let d3d_device = crate::capture_pipeline::create_d3d_device().unwrap();
Expand Down Expand Up @@ -718,6 +725,8 @@ async fn create_segment_pipeline(
capture_source,
screen_output_path.clone(),
start_time,
#[cfg(windows)]
encoder_preferences,
)
.instrument(error_span!("screen-out"))
.await
Expand Down
Loading