use std::path::Path;
use windows::Win32::Media::MediaFoundation::{
IMFSourceReader, MF_API_VERSION, MF_MT_AVG_BITRATE, MF_MT_DEFAULT_STRIDE, MF_MT_FRAME_RATE,
MF_MT_FRAME_SIZE, MF_MT_MAJOR_TYPE, MF_MT_SUBTYPE, MF_MT_VIDEO_ROTATION, MF_PD_DURATION,
MF_SDK_VERSION, MF_SOURCE_READER_DISABLE_DXVA,
MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, MF_SOURCE_READER_FIRST_VIDEO_STREAM,
MF_SOURCE_READER_MEDIASOURCE, MF_SOURCE_READERF_ENDOFSTREAM, MFCreateAttributes,
MFCreateMediaType, MFCreateSourceReaderFromURL, MFMediaType_Video, MFStartup,
MFVideoFormat_RGB32,
};
use windows::Win32::System::Com::StructuredStorage::PROPVARIANT;
use windows::Win32::System::Com::{COINIT_MULTITHREADED, CoInitializeEx};
use windows::Win32::System::Variant::{VT_I8, VT_UI8};
use windows::core::{GUID, PCWSTR};
use crate::error::PlatformError;
use crate::traits::media_runtime::{ExtractVideoThumbnailRequest, VideoInfo, VideoThumbnail};
const MFSTARTUP_LITE: u32 = 0x1;
pub(super) fn ensure_media_foundation() {
use std::sync::OnceLock;
static STARTED: OnceLock<()> = OnceLock::new();
STARTED.get_or_init(|| unsafe {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
if let Err(err) = MFStartup((MF_SDK_VERSION << 16) | MF_API_VERSION, MFSTARTUP_LITE) {
log::warn!("MFStartup failed: {err}");
}
});
}
fn open_reader(path: &Path, with_processing: bool) -> Result<IMFSourceReader, PlatformError> {
ensure_media_foundation();
let wide: Vec<u16> = path
.to_string_lossy()
.trim_start_matches(r"\\?\")
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let attributes = if with_processing {
let mut attributes = None;
unsafe {
MFCreateAttributes(&mut attributes, 2)
.map_err(|err| PlatformError::Platform(format!("attributes: {err}")))?;
}
if let Some(attributes) = attributes.as_ref() {
unsafe {
let _ = attributes.SetUINT32(&MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, 1);
let _ = attributes.SetUINT32(&MF_SOURCE_READER_DISABLE_DXVA, 1);
}
}
attributes
} else {
None
};
unsafe { MFCreateSourceReaderFromURL(PCWSTR(wide.as_ptr()), attributes.as_ref()) }
.map_err(|err| PlatformError::Platform(format!("failed to open {}: {err}", path.display())))
}
fn hundred_ns(variant: &PROPVARIANT) -> u64 {
unsafe {
let inner = &*variant.Anonymous.Anonymous;
if inner.vt == VT_UI8 || inner.vt == VT_I8 {
inner.Anonymous.uhVal
} else {
0
}
}
}
fn mime_for(path: &Path) -> Option<String> {
let ext = path.extension()?.to_string_lossy().to_lowercase();
let mime = match ext.as_str() {
"mp4" | "m4v" => "video/mp4",
"mov" => "video/quicktime",
"avi" => "video/x-msvideo",
"mkv" => "video/x-matroska",
"webm" => "video/webm",
"wmv" => "video/x-ms-wmv",
_ => return None,
};
Some(mime.to_string())
}
pub(super) fn read_video_info(path: &Path) -> Result<VideoInfo, PlatformError> {
let reader = open_reader(path, false)?;
let duration_ms = unsafe {
reader.GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE.0 as u32, &MF_PD_DURATION)
}
.map(|value| hundred_ns(&value) / 10_000)
.unwrap_or(0);
let native =
unsafe { reader.GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM.0 as u32, 0) }
.map_err(|err| PlatformError::Platform(format!("no video stream: {err}")))?;
let frame = unsafe { native.GetUINT64(&MF_MT_FRAME_SIZE) }.unwrap_or(0);
let (width, height) = ((frame >> 32) as u32, frame as u32);
let fps = unsafe { native.GetUINT64(&MF_MT_FRAME_RATE) }
.ok()
.and_then(|rate| {
let (numerator, denominator) = ((rate >> 32) as u32, rate as u32);
(denominator > 0).then(|| numerator as f32 / denominator as f32)
});
let bitrate = unsafe { native.GetUINT32(&MF_MT_AVG_BITRATE) }
.ok()
.map(u64::from);
let rotation = unsafe { native.GetUINT32(&MF_MT_VIDEO_ROTATION) }
.ok()
.map(|rotation| rotation as u16);
Ok(VideoInfo {
width,
height,
duration_ms,
rotation,
bitrate,
fps,
mime_type: mime_for(path),
})
}
pub(super) fn extract_thumbnail(
request: &ExtractVideoThumbnailRequest,
source: &Path,
) -> Result<VideoThumbnail, PlatformError> {
let reader = open_reader(source, true)?;
let stream = MF_SOURCE_READER_FIRST_VIDEO_STREAM.0 as u32;
unsafe {
let rgb = MFCreateMediaType()
.map_err(|err| PlatformError::Platform(format!("media type: {err}")))?;
rgb.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Video)
.and_then(|_| rgb.SetGUID(&MF_MT_SUBTYPE, &MFVideoFormat_RGB32))
.and_then(|_| reader.SetCurrentMediaType(stream, None, &rgb))
.map_err(|err| {
PlatformError::Platform(format!("RGB32 conversion unavailable: {err}"))
})?;
}
let requested_100ns = request
.time_ms
.filter(|time| *time > 0)
.map(|time_ms| time_ms as i64 * 10_000);
if let Some(position_100ns) = requested_100ns {
let mut position = PROPVARIANT::default();
unsafe {
let inner = &mut *position.Anonymous.Anonymous;
inner.vt = VT_I8;
inner.Anonymous.hVal = position_100ns;
let _ = reader.SetCurrentPosition(&GUID::zeroed(), &position);
}
}
let sample = unsafe {
let mut sample = None;
for _ in 0..2048 {
let mut flags = 0u32;
let mut current = None;
reader
.ReadSample(stream, 0, None, Some(&mut flags), None, Some(&mut current))
.map_err(|err| PlatformError::Platform(format!("decode failed: {err}")))?;
if let Some(current) = current {
let reached = match requested_100ns {
Some(target) => {
let time = current.GetSampleTime().unwrap_or(i64::MAX);
let duration = current.GetSampleDuration().unwrap_or(0).max(0);
time + duration >= target
}
None => true,
};
sample = Some(current);
if reached {
break;
}
}
if flags & (MF_SOURCE_READERF_ENDOFSTREAM.0 as u32) != 0 {
break;
}
}
sample.ok_or_else(|| PlatformError::Platform("no decodable video frame".to_string()))?
};
let current_type = unsafe { reader.GetCurrentMediaType(stream) }
.map_err(|err| PlatformError::Platform(format!("media type: {err}")))?;
let frame = unsafe { current_type.GetUINT64(&MF_MT_FRAME_SIZE) }
.map_err(|err| PlatformError::Platform(format!("frame size: {err}")))?;
let (width, height) = ((frame >> 32) as u32, frame as u32);
if width == 0 || height == 0 {
return Err(PlatformError::Platform("empty video frame".to_string()));
}
let stride = unsafe { current_type.GetUINT32(&MF_MT_DEFAULT_STRIDE) }
.map(|stride| stride as i32)
.unwrap_or((width * 4) as i32);
let mut pixels = vec![0u8; (width * height * 4) as usize];
unsafe {
let buffer = sample
.ConvertToContiguousBuffer()
.map_err(|err| PlatformError::Platform(format!("frame buffer: {err}")))?;
let mut data: *mut u8 = std::ptr::null_mut();
let mut length = 0u32;
buffer
.Lock(&mut data, None, Some(&mut length))
.map_err(|err| PlatformError::Platform(format!("frame lock: {err}")))?;
let row_bytes = (width * 4) as usize;
let abs_stride = stride.unsigned_abs() as usize;
for row in 0..height as usize {
let source_row = if stride < 0 {
height as usize - 1 - row
} else {
row
};
let offset = source_row * abs_stride;
if offset + row_bytes <= length as usize {
std::ptr::copy_nonoverlapping(
data.add(offset),
pixels.as_mut_ptr().add(row * row_bytes),
row_bytes,
);
}
}
let _ = buffer.Unlock();
}
for pixel in pixels.chunks_exact_mut(4) {
pixel.swap(0, 2);
pixel[3] = 0xff;
}
let image = image::RgbaImage::from_raw(width, height, pixels)
.ok_or_else(|| PlatformError::Platform("frame conversion failed".to_string()))?;
let mut dynamic = image::DynamicImage::ImageRgba8(image);
let max_width = request.max_width.unwrap_or(width).max(1);
let max_height = request.max_height.unwrap_or(height).max(1);
if width > max_width || height > max_height {
dynamic = dynamic.resize(max_width, max_height, image::imageops::FilterType::Triangle);
}
if let Some(parent) = request.output_path.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
PlatformError::Platform(format!("failed to create {}: {err}", parent.display()))
})?;
}
let is_png = request
.output_path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("png"));
if is_png {
dynamic
.to_rgba8()
.save_with_format(&request.output_path, image::ImageFormat::Png)
.map_err(|err| PlatformError::Platform(format!("thumbnail encode: {err}")))?;
} else {
let file = std::fs::File::create(&request.output_path).map_err(|err| {
PlatformError::Platform(format!(
"failed to create {}: {err}",
request.output_path.display()
))
})?;
let mut writer = std::io::BufWriter::new(file);
let quality = request.quality.clamp(1, 100);
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut writer, quality);
dynamic
.to_rgb8()
.write_with_encoder(encoder)
.map_err(|err| PlatformError::Platform(format!("thumbnail encode: {err}")))?;
}
Ok(VideoThumbnail {
path: request.output_path.clone(),
width: dynamic.width(),
height: dynamic.height(),
mime_type: Some(if is_png { "image/png" } else { "image/jpeg" }.to_string()),
})
}