Last active
September 2, 2021 19:25
-
-
Save Plecra/775ea0c779d49e4de6f0090bd0bb32d4 to your computer and use it in GitHub Desktop.
A (broken) Rust implementation of https://docs.microsoft.com/en-us/windows/win32/medfound/how-to-play-unprotected-media-files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![allow(non_snake_case)] | |
use std::path::Path; | |
use std::cell::Cell; | |
use std::ffi::c_void; | |
use std::sync::atomic::{AtomicU32, Ordering}; | |
use bindings::Windows::Win32::{ | |
Foundation::{HWND, LPARAM, PWSTR, WPARAM, E_UNEXPECTED, E_NOTIMPL}, | |
Media::{Audio::CoreAudio::GUID_NULL, MediaFoundation::*}, | |
System::LibraryLoader::GetModuleHandleW, | |
UI::WindowsAndMessaging::*, | |
}; | |
use bindings::bridge::{Result, Error, HRESULT, Interface, Guid}; | |
/// A COM object | |
struct VideoPlayer(*const VideoPlayerInner); | |
#[derive(Debug)] | |
struct VideoPlayerInner { | |
vtable: *const IMFAsyncCallback_abi, | |
reference_count: AtomicU32, | |
window_handle: HWND, | |
media_session: IMFMediaSession, | |
} | |
impl Drop for VideoPlayerInner { | |
fn drop(&mut self) { | |
unsafe { | |
DestroyWindow(self.window_handle); | |
} | |
} | |
} | |
unsafe extern "system" fn QueryInterface(_this: *mut c_void, _iid: &Guid, _interface: *mut *mut c_void) -> HRESULT { | |
todo!() | |
} | |
unsafe extern "system" fn AddRef(this: *mut c_void) -> u32 { | |
let inner = &*(this as *mut VideoPlayerInner); | |
println!("Cloning {:?}", inner); | |
let refs = inner.reference_count.fetch_add(1, Ordering::Relaxed); | |
// We want to abort on overflow instead of dropping the value. | |
// The reference count will never be zero when this is called; | |
// nevertheless, we insert an abort here to hint LLVM at | |
// an otherwise missed optimization. | |
if refs == 0 || refs == u32::MAX { | |
std::process::abort(); | |
} | |
refs + 1 | |
} | |
unsafe extern "system" fn Release(this: *mut c_void) -> u32 { | |
let inner = &*(this as *mut VideoPlayerInner); | |
println!("Releasing {:?}", inner); | |
let refs = inner.reference_count.fetch_sub(1, Ordering::Acquire); | |
if refs == 1 { | |
dbg!{{"deallocated it";}} | |
Box::from_raw(inner as *const VideoPlayerInner as *mut VideoPlayerInner); | |
} | |
refs - 1 | |
} | |
unsafe extern "system" fn GetParameters( | |
_this: *mut std::ffi::c_void, | |
_pdwflags: *mut u32, | |
_pdwqueue: *mut u32, | |
) -> HRESULT { | |
E_NOTIMPL | |
} | |
unsafe extern "system" fn Invoke( | |
this: *mut std::ffi::c_void, | |
pasyncresult: *mut std::ffi::c_void, | |
) -> HRESULT { | |
let inner = &*(this as *mut VideoPlayerInner); | |
let result = core::mem::transmute::<_, IMFAsyncResult>(pasyncresult); | |
println!("downcasting event"); | |
// This line manages to `Release` `this`, | |
// presumably because it's stored in `media_session` as a callback. | |
// However, there is no corresponding `AddRef` | |
// so the callback is freed prematurely | |
if let Ok(ev) = inner.media_session.EndGetEvent(result) { | |
println!("got event"); | |
if let Ok(ty) = ev.GetType() { | |
println!("got event {}", ty); | |
if ty != MESessionClosed.0 as u32 { | |
inner.media_session.BeginGetEvent(core::mem::transmute::<_, IMFAsyncCallback>(this), None).unwrap(); | |
} | |
} | |
} | |
HRESULT(0) | |
} | |
const VTABLE: IMFAsyncCallback_abi = IMFAsyncCallback_abi( | |
QueryInterface, | |
AddRef, | |
Release, | |
GetParameters, | |
Invoke | |
); | |
// impl VideoPlayerInner { | |
// unsafe fn add_ref | |
// } | |
const VIDEO_PLAYER_WINDOW_CLASS_NAME: &[u16] = &utf16_lit::utf16_null!("VIDEOPLAYER"); | |
impl Drop for VideoPlayer { | |
fn drop(&mut self) { | |
unsafe { | |
Release(self.0 as *mut VideoPlayerInner as *mut std::ffi::c_void); | |
} | |
} | |
} | |
impl VideoPlayer { | |
pub fn init() -> Result<()> { | |
unsafe extern "system" fn window_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> isize { | |
#[link(name = "USER32")] | |
extern "system" { | |
fn DefWindowProcW( | |
hwnd: HWND, | |
msg: u32, | |
wparam: WPARAM, | |
lparam: LPARAM, | |
) -> isize; | |
} | |
match msg { | |
WM_DESTROY => { | |
PostQuitMessage(0); | |
0 | |
}, | |
msg => DefWindowProcW(hwnd, msg, wparam, lparam), | |
} | |
} | |
// FIXME: `windows`' `LRESULT` definition is incorrect so we have to cast our function pointer | |
const VIDEOPLAYERPROC: WNDPROC = unsafe { std::mem::transmute::<unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> isize, _>(window_proc) }; | |
let instance = unsafe { GetModuleHandleW(None) }; | |
let atom = unsafe { | |
RegisterClassExW(&WNDCLASSEXW { | |
cbSize: core::mem::size_of::<WNDCLASSEXW>() as u32, | |
style: Default::default(), | |
lpfnWndProc: Some(VIDEOPLAYERPROC), | |
cbClsExtra: 0, | |
cbWndExtra: 0, | |
hInstance: instance, | |
hIcon: Default::default(), | |
hCursor: Default::default(), | |
hbrBackground: Default::default(), | |
// hbrBackground: COLOR_BACKGROUND, | |
lpszMenuName: Default::default(), | |
lpszClassName: PWSTR(VIDEO_PLAYER_WINDOW_CLASS_NAME.as_ptr() as *mut _), | |
hIconSm: Default::default(), | |
}) | |
}; | |
if atom == 0 { | |
Err(HRESULT::from_thread().into()) | |
} else { | |
Ok(()) | |
} | |
} | |
pub fn new() -> Result<Self> { | |
let window_handle = unsafe { | |
CreateWindowExW( | |
WS_EX_OVERLAPPEDWINDOW, | |
PWSTR(VIDEO_PLAYER_WINDOW_CLASS_NAME.as_ptr() as *mut u16), | |
PWSTR([0].as_ptr() as *mut u16), | |
WS_OVERLAPPEDWINDOW, | |
CW_USEDEFAULT, | |
CW_USEDEFAULT, | |
CW_USEDEFAULT, | |
CW_USEDEFAULT, | |
None, | |
None, | |
GetModuleHandleW(None), | |
core::ptr::null_mut(), | |
) | |
}; | |
if window_handle.is_null() { | |
return Err(HRESULT::from_thread().into()); | |
} | |
let media_session = unsafe { MFCreateMediaSession(None)? }; | |
let this = Box::into_raw(Box::new(VideoPlayerInner { | |
vtable: &VTABLE, | |
reference_count: AtomicU32::new(1), | |
window_handle, | |
media_session | |
})); | |
unsafe { | |
println!("Correct: {:?}", *this); | |
(*this).media_session.BeginGetEvent(core::mem::transmute::<_, IMFAsyncCallback>(this), None)?; | |
} | |
Ok(Self(this)) | |
} | |
fn inner(&self) -> &VideoPlayerInner { | |
unsafe { | |
&*self.0 | |
} | |
} | |
pub fn handle(&self) -> HWND { | |
self.inner().window_handle | |
} | |
fn open_media_source(path: &Path) -> Result<IMFMediaSource> { | |
use std::os::windows::ffi::OsStrExt; | |
let mut wide_chars = path.as_os_str().encode_wide().collect::<Vec<_>>(); | |
// Add null terminator | |
wide_chars.push(0); | |
let file = unsafe { | |
// File is released at end of scope | |
MFCreateFile( | |
MF_ACCESSMODE_READ, | |
MF_OPENMODE_FAIL_IF_NOT_EXIST, | |
MF_FILEFLAGS_NONE, | |
PWSTR(wide_chars.as_mut_ptr()) | |
)? | |
}; | |
let source_resolver = unsafe { | |
MFCreateSourceResolver()? | |
}; | |
let mut object_type = Default::default(); | |
let mut source = None; | |
unsafe { | |
source_resolver.CreateObjectFromByteStream( | |
&file, | |
PWSTR(wide_chars.as_mut_ptr()), | |
MF_RESOLUTION_MEDIASOURCE.0, | |
None, | |
&mut object_type, | |
&mut source | |
)?; | |
} | |
source | |
.ok_or_else(|| Error::new(E_UNEXPECTED, "`IMFSourceResolver::CreateObjectFromByteStream` did not return a a media source"))? | |
.cast() | |
} | |
pub fn play_file(&self, path: &Path) -> Result<()> { | |
let topology = unsafe { MFCreateTopology()? }; | |
let source = Self::open_media_source(path)?; | |
let descriptor = unsafe { | |
source.CreatePresentationDescriptor()? | |
}; | |
for i in 0..unsafe { descriptor.GetStreamDescriptorCount()? } { | |
let mut selected = false.into(); | |
let mut stream = None; | |
unsafe { | |
descriptor.GetStreamDescriptorByIndex(i, &mut selected, &mut stream)?; | |
} | |
let stream = stream | |
.ok_or_else(|| Error::new(E_UNEXPECTED, "`IMFPresentationDescriptor::GetStreamDescriptorByIndex` did not return a stream"))?; | |
if selected == true { | |
let handler = unsafe { stream.GetMediaTypeHandler()? }; | |
let major_type = unsafe { handler.GetMajorType()? }; | |
let sink_activate = if major_type == MFMediaType_Audio { | |
unsafe { MFCreateAudioRendererActivate()? } | |
} else if major_type == MFMediaType_Video { | |
unsafe { MFCreateVideoRendererActivate(self.inner().window_handle)? } | |
} else { | |
unimplemented!("weird mime type {:?}", major_type) | |
}; | |
let source_node = unsafe { MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE)? }; | |
unsafe { | |
source_node.SetUnknown(&MF_TOPONODE_SOURCE, &source)?; | |
source_node.SetUnknown(&MF_TOPONODE_PRESENTATION_DESCRIPTOR, &descriptor)?; | |
source_node.SetUnknown(&MF_TOPONODE_STREAM_DESCRIPTOR, &stream)?; | |
} | |
unsafe { | |
topology.AddNode(&source_node)?; | |
} | |
let output_node = unsafe { MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE)? }; | |
unsafe { | |
output_node.SetObject(sink_activate)?; | |
output_node.SetUINT32(&MF_TOPONODE_STREAMID, 0)?; | |
output_node.SetUINT32(&MF_TOPONODE_NOSHUTDOWN_ON_REMOVE, 0)?; | |
} | |
unsafe { | |
topology.AddNode(&output_node)?; | |
} | |
unsafe { | |
source_node.ConnectOutput(0, &output_node, 0)?; | |
} | |
} | |
} | |
let media_session = &self.inner().media_session; | |
unsafe { | |
media_session.SetTopology(MFSESSION_SETTOPOLOGY_IMMEDIATE.0 as _, topology)?; | |
} | |
let prop_variant = unsafe { core::mem::zeroed() }; | |
unsafe { | |
media_session.Start(&GUID_NULL, &prop_variant) | |
} | |
} | |
} | |
fn main() -> Result<()> { | |
let path = std::env::args_os().nth(1).unwrap(); | |
unsafe { | |
// TODO: Version must align with `windows` | |
MFStartup(0x0270, MFSTARTUP_NOSOCKET)?; | |
} | |
VideoPlayer::init()?; | |
let player = VideoPlayer::new()?; | |
player.play_file(path.as_ref())?; | |
unsafe { | |
ShowWindow(player.handle(), SW_SHOWNORMAL); | |
} | |
let mut message = core::mem::MaybeUninit::uninit(); | |
loop { | |
match unsafe { GetMessageW(message.as_mut_ptr(), None, 0, 0).0 } { | |
-1 => return Err(HRESULT::from_thread().into()), | |
0 => return Ok(()), | |
_ => { | |
unsafe { | |
TranslateMessage(message.as_ptr()); | |
DispatchMessageW(message.as_ptr()); | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment