#![cfg(test)]
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::print_stderr,
reason = "unit tests"
)]
use windows::Win32::Media::MediaFoundation::{
IMFActivate, MFMediaType_Video, MFT_CATEGORY_VIDEO_ENCODER, MFT_ENUM_FLAG,
MFT_ENUM_FLAG_HARDWARE, MFT_ENUM_FLAG_SORTANDFILTER, MFT_FRIENDLY_NAME_Attribute,
MFT_REGISTER_TYPE_INFO, MFTEnumEx, MFVideoFormat_AV1, MFVideoFormat_HEVC, MFVideoFormat_VP90,
};
use windows::Win32::System::Com::CoTaskMemFree;
use windows::core::PWSTR;
#[test]
fn list_encoder_mfts_for_each_codec() {
super::super::runtime::ensure_mf().expect("MF runtime init");
for (name, subtype) in [
("HEVC", MFVideoFormat_HEVC),
("AV1", MFVideoFormat_AV1),
("VP9", MFVideoFormat_VP90),
] {
let unfiltered = enum_encoder_mft_names(subtype, false);
let hw_only = enum_encoder_mft_names(subtype, true);
eprintln!("{name}: any-flag encoder MFTs = {unfiltered:?}");
eprintln!("{name}: MFT_ENUM_FLAG_HARDWARE encoder MFTs = {hw_only:?}");
}
}
fn enum_encoder_mft_names(subtype: windows::core::GUID, hardware_only: bool) -> Vec<String> {
let output = MFT_REGISTER_TYPE_INFO {
guidMajorType: MFMediaType_Video,
guidSubtype: subtype,
};
let flags = if hardware_only {
MFT_ENUM_FLAG(MFT_ENUM_FLAG_HARDWARE.0 | MFT_ENUM_FLAG_SORTANDFILTER.0)
} else {
MFT_ENUM_FLAG_SORTANDFILTER
};
let mut activates: *mut Option<IMFActivate> = std::ptr::null_mut();
let mut count = 0u32;
let hr = unsafe {
MFTEnumEx(
MFT_CATEGORY_VIDEO_ENCODER,
flags,
None,
Some(std::ptr::from_ref(&output)),
&raw mut activates,
&raw mut count,
)
};
if hr.is_err() || activates.is_null() {
return Vec::new();
}
let mut names = Vec::new();
for i in 0..count as usize {
let activate = unsafe { (*activates.add(i)).take() };
if let Some(activate) = activate {
names.push(friendly_name(&activate).unwrap_or_else(|| "<unnamed>".to_owned()));
}
}
unsafe {
CoTaskMemFree(Some(activates.cast_const().cast()));
}
names
}
fn friendly_name(activate: &IMFActivate) -> Option<String> {
let mut raw = PWSTR::null();
let mut len = 0u32;
unsafe {
activate.GetAllocatedString(&MFT_FRIENDLY_NAME_Attribute, &raw mut raw, &raw mut len)
}
.ok()?;
if raw.is_null() {
return None;
}
let name = unsafe { raw.to_string() }.ok();
unsafe {
CoTaskMemFree(Some(raw.0.cast()));
}
name
}
#[cfg(test)]
mod zero_copy {
use crate::{VideoEncoder, VideoEncoderConfig, VideoInputPreference};
use mediaway_common::{
CodecKind, ColorRange, GpuBufferHandle, GpuDeviceHandle, NativeHandle, PixelFormat,
Rational, VideoFrame, VideoFrameStorage,
};
use windows::Win32::Foundation::HMODULE;
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11CreateDevice,
ID3D11Device, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC,
};
use windows::core::Interface;
const W: u32 = 1920;
const H: u32 = 1080;
const FRAMES: i64 = 5;
fn hardware_device() -> Option<ID3D11Device> {
let mut device: Option<ID3D11Device> = None;
unsafe {
let _ = D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&raw mut device),
None,
None,
);
}
device
}
fn texture(device: &ID3D11Device, format: DXGI_FORMAT, bgra: bool) -> Option<ID3D11Texture2D> {
let mut bind = D3D11_BIND_SHADER_RESOURCE.0 as u32;
if bgra {
bind |= D3D11_BIND_RENDER_TARGET.0 as u32;
}
let desc = D3D11_TEXTURE2D_DESC {
Width: W,
Height: H,
MipLevels: 1,
ArraySize: 1,
Format: format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: bind,
CPUAccessFlags: 0,
MiscFlags: 0,
};
let mut out: Option<ID3D11Texture2D> = None;
unsafe {
let _ = device.CreateTexture2D(&raw const desc, None, Some(&raw mut out));
}
out
}
fn encode_case(
device: &ID3D11Device,
codec: CodecKind,
pixel_format: PixelFormat,
format: DXGI_FORMAT,
) -> Option<usize> {
let texture = texture(device, format, pixel_format == PixelFormat::Bgra8)?;
let texture_handle = NativeHandle::new(Interface::as_raw(&texture) as usize)?;
let device_handle = NativeHandle::new(Interface::as_raw(device) as usize)?;
let config = VideoEncoderConfig {
codec,
width: W,
height: H,
time_base: Rational::new(1, 60),
bitrate_bps: 20_000_000,
pixel_format,
color_range: ColorRange::Video,
input: VideoInputPreference::ZeroCopyGpu,
gpu_device: Some(GpuDeviceHandle::DirectX11(device_handle)),
gop_size: 120,
rate_control: None,
intra_refresh_period: None,
};
let mut encoder = super::super::WmfVideoEncoder::open(&config).ok()?;
for pts in 0..FRAMES {
let frame = VideoFrame {
pts,
duration: 1,
width: W,
height: H,
format: pixel_format,
storage: VideoFrameStorage::Gpu(GpuBufferHandle::DirectX11 {
texture: texture_handle,
subresource: 0,
}),
};
encoder
.push_frame(&frame)
.expect("async MFT must accept every frame, not just the first");
}
let packets = VideoEncoder::finish(encoder).expect("finish").len();
drop(texture);
Some(packets)
}
#[test]
fn dx11_zero_copy_encodes_multiple_frames_or_skip() {
let Some(device) = hardware_device() else {
eprintln!("skip: no D3D11 hardware device");
return;
};
for (codec, pixel_format, format) in [
(CodecKind::H264, PixelFormat::Nv12, DXGI_FORMAT_NV12),
(
CodecKind::H264,
PixelFormat::Bgra8,
DXGI_FORMAT_B8G8R8A8_UNORM,
),
(CodecKind::Hevc, PixelFormat::Nv12, DXGI_FORMAT_NV12),
(
CodecKind::Hevc,
PixelFormat::Bgra8,
DXGI_FORMAT_B8G8R8A8_UNORM,
),
] {
match encode_case(&device, codec, pixel_format, format) {
Some(packets) => assert_eq!(
packets,
usize::try_from(FRAMES).expect("frames"),
"{codec:?} {pixel_format:?}: encoded {FRAMES} frames, got {packets} packets",
),
None => eprintln!("skip: {codec:?} {pixel_format:?} unavailable on this machine"),
}
}
}
#[test]
#[ignore = "slow stress test; run explicitly"]
fn dropping_encoders_back_to_back_does_not_crash() {
let Some(device) = hardware_device() else {
eprintln!("skip: no D3D11 hardware device");
return;
};
let Some(texture) = texture(&device, DXGI_FORMAT_NV12, false) else {
return;
};
let Some(texture_handle) = NativeHandle::new(Interface::as_raw(&texture) as usize) else {
return;
};
let Some(device_handle) = NativeHandle::new(Interface::as_raw(&device) as usize) else {
return;
};
let config = VideoEncoderConfig {
codec: CodecKind::H264,
width: W,
height: H,
time_base: Rational::new(1, 60),
bitrate_bps: 20_000_000,
pixel_format: PixelFormat::Nv12,
color_range: ColorRange::Video,
input: VideoInputPreference::ZeroCopyGpu,
gpu_device: Some(GpuDeviceHandle::DirectX11(device_handle)),
gop_size: 120,
rate_control: None,
intra_refresh_period: None,
};
for i in 0..200 {
let Ok(mut encoder) = super::super::WmfVideoEncoder::open(&config) else {
eprintln!("skip: no hardware H.264 encoder (at iteration {i})");
return;
};
for pts in 0..FRAMES {
let frame = VideoFrame {
pts,
duration: 1,
width: W,
height: H,
format: PixelFormat::Nv12,
storage: VideoFrameStorage::Gpu(GpuBufferHandle::DirectX11 {
texture: texture_handle,
subresource: 0,
}),
};
encoder.push_frame(&frame).expect("push");
}
drop(encoder);
}
}
}