Skip to content
Open
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
439 changes: 364 additions & 75 deletions crates/enc-ffmpeg/src/mux/segmented_stream.rs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions crates/enc-ffmpeg/src/video/h264.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1992,6 +1992,13 @@ fn get_bitrate(width: u32, height: u32, frame_rate: f32, bpp: f32) -> usize {
((pixels_per_second * bpp as f64) as usize).min(MAX_BITRATE)
}

/// Bitrate this module's encoders target for `bpp`, so a hardware encoder fed straight from the
/// capture texture keeps the recording quality of the software path.
pub fn bitrate_for_bpp(width: u32, height: u32, frame_rate: ffmpeg::Rational, bpp: f32) -> u32 {
let fps = frame_rate.0 as f32 / frame_rate.1.max(1) as f32;
get_bitrate(width, height, fps, bpp) as u32
}

const MAX_BITRATE: usize = 1_000_000_000;

#[cfg(test)]
Expand Down
13 changes: 9 additions & 4 deletions crates/enc-mediafoundation/src/d3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use windows::Win32::Graphics::{
D3D_DRIVER_TYPE, D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN, D3D_DRIVER_TYPE_WARP,
},
Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_FLAG, D3D11_SDK_VERSION,
D3D11CreateDevice, ID3D11Device,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_FLAG,
D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device,
},
Dxgi::{DXGI_ERROR_UNSUPPORTED, IDXGIDevice},
};
Expand Down Expand Up @@ -70,7 +70,7 @@ fn create_d3d_device_on_pinned_adapter(
pub fn create_d3d_device() -> Result<ID3D11Device> {
let mut device = None;
let flags = {
let mut flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
let mut flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
if cfg!(feature = "d3ddebug") {
flags |= D3D11_CREATE_DEVICE_DEBUG;
}
Expand All @@ -97,7 +97,12 @@ pub fn create_d3d_device() -> Result<ID3D11Device> {
if let Err(error) = &result
&& error.code() == DXGI_ERROR_UNSUPPORTED
{
result = create_d3d_device_with_type(D3D_DRIVER_TYPE_WARP, flags, &mut device);
// WARP rejects VIDEO_SUPPORT; a WARP device cannot hardware-encode anyway.
result = create_d3d_device_with_type(
D3D_DRIVER_TYPE_WARP,
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
&mut device,
);
}
result?;
device.ok_or_else(windows::core::Error::from_win32)
Expand Down
552 changes: 399 additions & 153 deletions crates/enc-mediafoundation/src/video/h264.rs

Large diffs are not rendered by default.

28 changes: 27 additions & 1 deletion crates/enc-mediafoundation/src/video/video_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ use windows::{
ID3D11VideoContext, ID3D11VideoDevice, ID3D11VideoProcessor,
ID3D11VideoProcessorInputView, ID3D11VideoProcessorOutputView,
},
Dxgi::Common::{DXGI_FORMAT, DXGI_RATIONAL, DXGI_SAMPLE_DESC},
Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_RATIONAL,
DXGI_SAMPLE_DESC,
},
},
},
core::Interface,
Expand Down Expand Up @@ -218,6 +221,29 @@ impl VideoProcessor {
&self.video_output_texture
}

pub(super) fn set_bt709_color_space(&self, input_format: DXGI_FORMAT) {
let input_range = if matches!(
input_format,
DXGI_FORMAT_B8G8R8A8_UNORM | DXGI_FORMAT_R8G8B8A8_UNORM
) {
D3D11_VIDEO_PROCESSOR_NOMINAL_RANGE_0_255
} else {
D3D11_VIDEO_PROCESSOR_NOMINAL_RANGE_16_235
};
let output = D3D11_VIDEO_PROCESSOR_COLOR_SPACE {
_bitfield: 1 | (1 << 2) | ((D3D11_VIDEO_PROCESSOR_NOMINAL_RANGE_16_235.0 as u32) << 4),
};
let input = D3D11_VIDEO_PROCESSOR_COLOR_SPACE {
_bitfield: 1 | (1 << 2) | ((input_range.0 as u32) << 4),
};
unsafe {
self.video_context
.VideoProcessorSetOutputColorSpace(&self.video_processor, &output);
self.video_context
.VideoProcessorSetStreamColorSpace(&self.video_processor, 0, &input);
}
}

pub fn process_texture(
&mut self,
input_texture: &ID3D11Texture2D,
Expand Down
123 changes: 84 additions & 39 deletions crates/mediafoundation-ffmpeg/src/h264.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl H264StreamMuxer {
return Err("Muxer is already finished".into());
}

let mut packet = self.mf_sample_to_avpacket(sample)?;
let mut packet = sample_to_packet(sample, self.time_base, self.stream_index)?;

packet.rescale_ts(
self.time_base,
Expand Down Expand Up @@ -145,44 +145,9 @@ impl H264StreamMuxer {

packet.write_interleaved(output)?;

Ok(())
}

fn mf_sample_to_avpacket(&self, sample: &IMFSample) -> windows::core::Result<ffmpeg::Packet> {
let len = unsafe { sample.GetTotalLength()? };
let mut packet = ffmpeg::Packet::new(len as usize);

{
let buffer = unsafe { sample.ConvertToContiguousBuffer()? };
let data = buffer.lock()?;

packet
.data_mut()
.unwrap()
.copy_from_slice(&data[0..len as usize]);
}

let pts = unsafe { sample.GetSampleTime() }
.ok()
.map(|v| mf_from_mf_time(self.time_base, v));
packet.set_pts(pts);
packet.set_dts(pts);
self.frame_count += 1;

let duration = unsafe { sample.GetSampleDuration() }
.ok()
.map(|v| mf_from_mf_time(self.time_base, v))
.unwrap_or_default();
packet.set_duration(duration);

if let Ok(t) = unsafe { sample.GetUINT32(&MFSampleExtension_CleanPoint) }
&& t != 0
{
packet.set_flags(packet::Flags::KEY);
}

packet.set_stream(self.stream_index);

Ok(packet)
Ok(())
}

pub fn finish(&mut self) -> Result<(), ffmpeg::Error> {
Expand All @@ -206,7 +171,73 @@ impl H264StreamMuxer {
}
}

const MF_TIMEBASE: ffmpeg::Rational = ffmpeg::Rational(1, 10_000_000);
pub const MF_TIMEBASE: ffmpeg::Rational = ffmpeg::Rational(1, 10_000_000);

pub fn sample_to_packet(
sample: &IMFSample,
time_base: Rational,
stream_index: usize,
) -> windows::core::Result<ffmpeg::Packet> {
let len = unsafe { sample.GetTotalLength()? };
let mut packet = ffmpeg::Packet::new(len as usize);

{
let buffer = unsafe { sample.ConvertToContiguousBuffer()? };
let data = buffer.lock()?;

packet
.data_mut()
.unwrap()
.copy_from_slice(&data[0..len as usize]);
}

let pts = unsafe { sample.GetSampleTime() }
.ok()
.map(|v| mf_from_mf_time(time_base, v));
packet.set_pts(pts);
packet.set_dts(pts);

let duration = unsafe { sample.GetSampleDuration() }
.ok()
.map(|v| mf_from_mf_time(time_base, v))
.unwrap_or_default();
packet.set_duration(duration);

let clean_point =
unsafe { sample.GetUINT32(&MFSampleExtension_CleanPoint) }.is_ok_and(|value| value != 0);
if clean_point || contains_idr_nal(packet.data().unwrap_or_default()) {
packet.set_flags(packet::Flags::KEY);
}

packet.set_stream(stream_index);

Ok(packet)
}

// Some Media Foundation encoders leave `MFSampleExtension_CleanPoint` unset. The DASH muxer
// splits segments on keyframes, so fall back to the bitstream: an IDR NAL (type 5) after an
// Annex B start code.
fn contains_idr_nal(data: &[u8]) -> bool {
let mut index = 0;
while index + 3 < data.len() {
if data[index] == 0
&& data[index + 1] == 0
&& (data[index + 2] == 1
|| (data[index + 2] == 0 && index + 4 < data.len() && data[index + 3] == 1))
{
let nal = if data[index + 2] == 1 {
index + 3
} else {
index + 4
};
if data.get(nal).is_some_and(|byte| byte & 0x1F == 5) {
return true;
}
}
index += 1;
}
false
}

fn mf_from_mf_time(tb: Rational, stime: i64) -> i64 {
unsafe { av_rescale_q(stime, MF_TIMEBASE.into(), tb.into()) }
Expand Down Expand Up @@ -253,3 +284,17 @@ pub fn set_fragmented_mp4_options(

Ok(())
}

#[cfg(test)]
mod tests {
use super::contains_idr_nal;

#[test]
fn detects_idr_after_three_and_four_byte_start_codes() {
assert!(contains_idr_nal(&[0, 0, 1, 0x65]));
assert!(contains_idr_nal(&[0, 0, 0, 1, 0x65]));
assert!(!contains_idr_nal(&[0, 0, 1, 0x41]));
assert!(!contains_idr_nal(&[0, 0, 1]));
assert!(!contains_idr_nal(&[]));
}
}
4 changes: 3 additions & 1 deletion crates/mediafoundation-ffmpeg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ mod audio;
mod h264;

pub use audio::AudioExt;
pub use h264::{H264StreamMuxer, MuxerConfig, set_fragmented_mp4_options};
pub use h264::{
H264StreamMuxer, MF_TIMEBASE, MuxerConfig, sample_to_packet, set_fragmented_mp4_options,
};
12 changes: 11 additions & 1 deletion crates/recording/src/capture_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
.parent()
.map(|p| p.join("display"))
.unwrap_or_else(|| output_path.with_file_name("display"));
let d3d_device = screen_capture.d3d_device.clone();
let pixel_format = screen_capture::Direct3DCapture::PIXEL_FORMAT.as_dxgi();

let bpp = if ultra {
H264EncoderBuilder::ULTRA_BPP
Expand Down Expand Up @@ -301,7 +303,6 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
bpp,
output_size,
shared_pause_state,
disk_space_callback: None,
segment_tx: None,
..Default::default()
})
Expand All @@ -318,7 +319,11 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
bpp,
output_size,
shared_pause_state,
d3d_device: Some(d3d_device),
pixel_format: Some(pixel_format),
encoder_preferences: encoder_preferences.clone(),
disk_space_callback: None,
prefer_texture_input: false,
segment_tx: None,
})
.await
Expand Down Expand Up @@ -356,6 +361,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
start_gate: Option<RecordingStartGate>,
segment_tx: Option<std::sync::mpsc::Sender<SegmentCompletedEvent>>,
) -> anyhow::Result<OutputPipeline> {
let d3d_device = screen_capture.d3d_device.clone();
OutputPipeline::builder(segments_dir)
.with_video::<screen_capture::VideoSource>(screen_capture)
.with_timestamps(start_time)
Expand All @@ -367,7 +373,11 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
bpp: H264EncoderBuilder::INSTANT_MODE_BPP,
output_size: Some(output_size),
shared_pause_state: None,
d3d_device: Some(d3d_device),
pixel_format: Some(screen_capture::Direct3DCapture::PIXEL_FORMAT.as_dxgi()),
encoder_preferences: EncoderPreferences::new(),
disk_space_callback: None,
prefer_texture_input: mf_texture_input_enabled(),
segment_tx,
})
.await
Expand Down
10 changes: 0 additions & 10 deletions crates/recording/src/output_pipeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,6 @@ mod win;
#[cfg(windows)]
pub use win::*;

#[cfg(windows)]
mod win_segmented;
#[cfg(windows)]
pub use win_segmented::*;

#[cfg(windows)]
mod win_segmented_camera;
#[cfg(windows)]
pub use win_segmented_camera::*;

#[cfg(windows)]
mod win_fragmented_m4s;
#[cfg(windows)]
Expand Down
20 changes: 19 additions & 1 deletion crates/recording/src/output_pipeline/oop_fragmented_m4s_win.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ impl WindowsOOPFragmentedM4SMuxer {
let mut last_timestamp: Option<Duration> = None;
let mut first_timestamp: Option<Duration> = None;
let mut total_frames = 0u64;
let mut readback_misses = 0u64;
let mut duplicated_frames = 0u64;
let mut slow_convert_count = 0u32;
let mut slow_encode_count = 0u32;
Expand Down Expand Up @@ -471,7 +472,24 @@ impl WindowsOOPFragmentedM4SMuxer {
(Some(f), ts)
}
Err(e) => {
warn!("Windows OOP failed to convert D3D11 frame: {e:?}");
let readback_miss = matches!(
e,
scap_ffmpeg::AsFFmpegError::NotReady
| scap_ffmpeg::AsFFmpegError::StagingBusy
);
if !readback_miss {
warn!("Windows OOP failed to convert D3D11 frame: {e:?}");
} else {
readback_misses += 1;
if readback_misses == 1
|| readback_misses.is_multiple_of(90)
{
warn!(
readback_misses,
"Screen readback is missing deadlines; the encoder is duplicating the last frame"
);
}
}
duplicated_frames += 1;
(last_ffmpeg_frame.as_ref().map(reference_video_frame), ts)
}
Expand Down
22 changes: 20 additions & 2 deletions crates/recording/src/output_pipeline/win.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ impl Muxer for WindowsMuxer {
let mut pacer = FramePacer::new(config.frame_rate);
let mut last_ffmpeg_frame: Option<ffmpeg::frame::Video> = None;
let mut first_timestamp: Option<Duration> = None;
let mut readback_misses = 0u64;

loop {
let (ffmpeg_frame, ts) = match pacer.next(&video_rx, &pause_flag) {
Expand All @@ -427,7 +428,24 @@ impl Muxer for WindowsMuxer {
(Some(f), timestamp)
}
Err(e) => {
warn!("Failed to convert frame: {e:?}");
let readback_miss = matches!(
e,
scap_ffmpeg::AsFFmpegError::NotReady
| scap_ffmpeg::AsFFmpegError::StagingBusy
);
if !readback_miss {
warn!("Failed to convert frame: {e:?}");
} else {
readback_misses += 1;
if readback_misses == 1
|| readback_misses.is_multiple_of(90)
{
warn!(
readback_misses,
"Screen readback is missing deadlines; the encoder is duplicating the last frame"
);
}
}
(
last_ffmpeg_frame.as_ref().map(reference_video_frame),
timestamp,
Expand Down Expand Up @@ -587,7 +605,7 @@ impl AudioMuxer for WindowsMuxer {
}
}

fn duration_to_timespan(duration: Duration) -> TimeSpan {
pub(crate) fn duration_to_timespan(duration: Duration) -> TimeSpan {
const TICKS_PER_SEC: u64 = 10_000_000;
const NANOS_PER_TICK: u32 = 100;

Expand Down
Loading
Loading