use std::ffi::c_void;
use std::ptr;
use std::slice;
use windows::Win32::Graphics::Direct3D11::{ID3D11Device, ID3D11Texture2D};
use windows::Win32::Media::MediaFoundation::{
IMF2DBuffer, IMFActivate, IMFAttributes, IMFDXGIBuffer, IMFDXGIDeviceManager, IMFMediaSource, IMFSample,
IMFSourceReader, MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID, MF_MT_FRAME_RATE, MF_MT_FRAME_SIZE, MF_MT_MAJOR_TYPE,
MF_MT_SUBTYPE, MF_SOURCE_READER_D3D_MANAGER, MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING,
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, MF_SOURCE_READER_FIRST_VIDEO_STREAM, MF_SOURCE_READERF_ENDOFSTREAM,
MFCreateAttributes, MFCreateMediaType, MFCreateSourceReaderFromMediaSource, MFEnumDeviceSources, MFMediaType_Video,
MFVideoFormat_NV12,
};
use windows::Win32::System::Com::CoTaskMemFree;
use windows::core::{Interface, PWSTR};
use super::channel::FrameChannel;
use super::pump::{self, Geometry};
use super::{Config, FrameStream};
use crate::Error;
use crate::frame::d3d11::Texture;
use crate::frame::{Frame, I420};
pub(super) async fn open(config: &Config, device: Option<&str>) -> Result<FrameStream, Error> {
let config = config.clone();
let device = device.map(str::to_string);
let chan = FrameChannel::new();
let (geo, guard) = pump::spawn(
chan.clone(),
move || {
let camera = Camera::open(&config, device.as_deref())?;
let geometry = Geometry {
width: camera.width,
height: camera.height,
framerate: camera.framerate,
device: camera.device_name.clone(),
};
Ok((camera, geometry))
},
Camera::read,
)
.await?;
Ok(FrameStream::new(
chan,
geo.width,
geo.height,
geo.framerate,
geo.device,
None,
Box::new(guard),
))
}
use crate::mf::{ComGuard, create_d3d_device, mf_err, pack_2x32, unpack_2x32};
struct Camera {
source: IMFMediaSource,
reader: IMFSourceReader,
device: Option<ID3D11Device>,
width: u32,
height: u32,
framerate: Option<u32>,
device_name: String,
_manager: Option<IMFDXGIDeviceManager>,
_com: ComGuard,
}
impl Camera {
fn open(config: &Config, selector: Option<&str>) -> Result<Self, Error> {
let com = ComGuard::new()?;
let (source, device_name) = open_source(selector)?;
let gpu = match create_d3d_device() {
Ok(gpu) => Some(gpu),
Err(e) => {
tracing::debug!(error = %e, "no D3D11 device; using CPU capture path");
None
}
};
let reader_attrs = create_attributes(2)?;
unsafe {
match &gpu {
Some((_, manager)) => {
reader_attrs
.SetUnknown(&MF_SOURCE_READER_D3D_MANAGER, manager)
.map_err(|e| mf_err("set D3D manager", e))?;
reader_attrs
.SetUINT32(&MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, 1)
.map_err(|e| mf_err("enable advanced video processing", e))?;
}
None => {
reader_attrs
.SetUINT32(&MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, 1)
.map_err(|e| mf_err("enable video processing", e))?;
}
}
}
let reader = unsafe {
MFCreateSourceReaderFromMediaSource(&source, &reader_attrs)
.map_err(|e| mf_err("create source reader", e))?
};
let want = unsafe { MFCreateMediaType().map_err(|e| mf_err("create media type", e))? };
unsafe {
want.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Video)
.map_err(|e| mf_err("set major type", e))?;
want.SetGUID(&MF_MT_SUBTYPE, &MFVideoFormat_NV12)
.map_err(|e| mf_err("set subtype", e))?;
if let (Some(w), Some(h)) = (config.width, config.height) {
want.SetUINT64(&MF_MT_FRAME_SIZE, pack_2x32(w, h))
.map_err(|e| mf_err("set frame size", e))?;
}
if let Some(fps) = config.framerate {
want.SetUINT64(&MF_MT_FRAME_RATE, pack_2x32(fps, 1))
.map_err(|e| mf_err("set frame rate", e))?;
}
reader
.SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM.0 as u32, None, &want)
.map_err(|e| mf_err("set NV12 output type", e))?;
}
let current = unsafe {
reader
.GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM.0 as u32)
.map_err(|e| mf_err("get current media type", e))?
};
let frame_size = unsafe {
current
.GetUINT64(&MF_MT_FRAME_SIZE)
.map_err(|e| mf_err("read frame size", e))?
};
let (width, height) = unpack_2x32(frame_size);
if width % 2 != 0 || height % 2 != 0 {
return Err(Error::Codec(anyhow::anyhow!(
"camera resolution {width}x{height} must be even for H.264 encoding"
)));
}
let framerate = unsafe { current.GetUINT64(&MF_MT_FRAME_RATE).ok() }.and_then(|packed| {
let (num, den) = unpack_2x32(packed);
(den != 0).then(|| (num / den).max(1))
});
let (device, manager) = match gpu {
Some((device, manager)) => (Some(device), Some(manager)),
None => (None, None),
};
tracing::info!(
device = %device_name,
width,
height,
framerate,
gpu = device.is_some(),
"opened Media Foundation capture"
);
Ok(Self {
source,
reader,
device,
width,
height,
framerate,
device_name,
_manager: manager,
_com: com,
})
}
fn sample_to_texture(&self, device: &ID3D11Device, sample: &IMFSample) -> Result<Frame, Error> {
let buffer = unsafe { sample.GetBufferByIndex(0).map_err(|e| mf_err("get buffer", e))? };
let dxgi = buffer
.cast::<IMFDXGIBuffer>()
.map_err(|e| mf_err("buffer is not a DXGI surface", e))?;
let mut raw: *mut c_void = ptr::null_mut();
unsafe {
dxgi.GetResource(&ID3D11Texture2D::IID, &mut raw)
.map_err(|e| mf_err("get DXGI resource", e))?;
}
let texture = unsafe { ID3D11Texture2D::from_raw(raw) };
let subresource = unsafe {
dxgi.GetSubresourceIndex()
.map_err(|e| mf_err("get subresource index", e))?
};
Ok(Frame::Texture(Texture::new(
device.clone(),
texture,
subresource,
self.width,
self.height,
)))
}
fn sample_to_i420(&self, sample: &IMFSample) -> Result<Frame, Error> {
let buffer = unsafe {
sample
.ConvertToContiguousBuffer()
.map_err(|e| mf_err("contiguous buffer", e))?
};
let nv12 = if let Ok(buf2d) = buffer.cast::<IMF2DBuffer>() {
let len = unsafe {
buf2d
.GetContiguousLength()
.map_err(|e| mf_err("contiguous length", e))?
};
let mut data = vec![0u8; len as usize];
unsafe {
buf2d
.ContiguousCopyTo(&mut data)
.map_err(|e| mf_err("contiguous copy", e))?;
}
data
} else {
let mut ptr_out: *mut u8 = ptr::null_mut();
let mut current_len: u32 = 0;
unsafe {
buffer
.Lock(&mut ptr_out, None, Some(&mut current_len))
.map_err(|e| mf_err("lock buffer", e))?;
}
let data = unsafe { slice::from_raw_parts(ptr_out, current_len as usize) }.to_vec();
unsafe {
let _ = buffer.Unlock();
}
data
};
Ok(Frame::I420(I420::from_nv12(&nv12, self.width, self.height)?))
}
fn read(&mut self) -> Result<Option<Frame>, Error> {
loop {
let mut flags: u32 = 0;
let mut sample: Option<IMFSample> = None;
unsafe {
self.reader
.ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM.0 as u32,
0,
None,
Some(&mut flags),
None,
Some(&mut sample),
)
.map_err(|e| mf_err("read sample", e))?;
}
if flags & MF_SOURCE_READERF_ENDOFSTREAM.0 as u32 != 0 {
return Ok(None);
}
let Some(sample) = sample else {
continue;
};
let frame = match &self.device {
Some(device) => self.sample_to_texture(device, &sample)?,
None => self.sample_to_i420(&sample)?,
};
return Ok(Some(frame));
}
}
}
impl Drop for Camera {
fn drop(&mut self) {
unsafe {
let _ = self.source.Shutdown();
}
}
}
fn create_attributes(capacity: u32) -> Result<IMFAttributes, Error> {
let mut attrs: Option<IMFAttributes> = None;
unsafe {
MFCreateAttributes(&mut attrs, capacity).map_err(|e| mf_err("create attributes", e))?;
}
attrs.ok_or_else(|| Error::Codec(anyhow::anyhow!("MFCreateAttributes returned null")))
}
enum Selector {
Index(usize),
Name(String),
}
fn open_source(selector: Option<&str>) -> Result<(IMFMediaSource, String), Error> {
let selector = match selector {
None => Selector::Index(0),
Some(spec) => match spec.parse::<usize>() {
Ok(i) => Selector::Index(i),
Err(_) => Selector::Name(spec.to_string()),
},
};
let attrs = create_attributes(1)?;
unsafe {
attrs
.SetGUID(
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID,
)
.map_err(|e| mf_err("set device source type", e))?;
}
let mut activates: *mut Option<IMFActivate> = ptr::null_mut();
let mut count: u32 = 0;
unsafe {
MFEnumDeviceSources(&attrs, &mut activates, &mut count).map_err(|e| mf_err("enumerate devices", e))?;
}
if count == 0 {
return Err(Error::Codec(anyhow::anyhow!("no video capture devices found")));
}
let entries = unsafe { slice::from_raw_parts_mut(activates, count as usize) };
let mut chosen: Option<(IMFActivate, String)> = None;
for (i, slot) in entries.iter_mut().enumerate() {
let Some(activate) = slot.take() else { continue };
let name = unsafe { friendly_name(&activate) }.unwrap_or_else(|_| format!("camera {i}"));
let matched = match &selector {
Selector::Index(idx) => i == *idx,
Selector::Name(want) => name.to_lowercase().contains(&want.to_lowercase()),
};
if matched && chosen.is_none() {
chosen = Some((activate, name));
}
}
unsafe {
CoTaskMemFree(Some(activates as *const c_void));
}
let (activate, name) = chosen.ok_or_else(|| match &selector {
Selector::Index(i) => Error::Codec(anyhow::anyhow!("camera index {i} out of range ({count} found)")),
Selector::Name(n) => Error::Codec(anyhow::anyhow!("no camera matching {n:?} ({count} found)")),
})?;
let source: IMFMediaSource = unsafe { activate.ActivateObject().map_err(|e| mf_err("activate device", e))? };
Ok((source, name))
}
unsafe fn friendly_name(activate: &IMFActivate) -> Result<String, Error> {
let mut value = PWSTR::null();
let mut len: u32 = 0;
unsafe {
activate
.GetAllocatedString(&MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &mut value, &mut len)
.map_err(|e| mf_err("friendly name", e))?;
}
let name = unsafe { value.to_string() }.unwrap_or_default();
unsafe {
CoTaskMemFree(Some(value.0 as *const c_void));
}
Ok(name)
}