├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── graphic-offsets ├── Cargo.toml ├── bin │ ├── get-graphics-offsets32.exe │ └── get-graphics-offsets64.exe └── src │ └── lib.rs ├── inject-helper ├── Cargo.toml ├── bin │ ├── graphics-hook32.dll │ ├── graphics-hook64.dll │ ├── inject-helper32.exe │ └── inject-helper64.exe └── src │ └── lib.rs ├── obs-client-ffi ├── Cargo.toml ├── README.md ├── cbindgen.toml ├── obs-client.h ├── obs-client.hpp └── src │ └── lib.rs ├── obs-client ├── Cargo.toml ├── examples │ └── basic.rs └── src │ ├── error.rs │ ├── hook_info.rs │ ├── lib.rs │ └── utils │ ├── color.rs │ ├── d3d11.rs │ ├── event.rs │ ├── file_mapping.rs │ ├── mod.rs │ ├── mutex.rs │ ├── pipe.rs │ └── process.rs └── rustfmt.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | client/target/ 5 | server/target/ 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | client/Cargo.lock 11 | server/Cargo.lock 12 | 13 | # These are backup files generated by rustfmt 14 | **/*.rs.bk 15 | 16 | # ============================================================================= 17 | 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 19 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 20 | 21 | # User-specific stuff 22 | **/.idea/**/workspace.xml 23 | **/.idea/**/tasks.xml 24 | **/.idea/**/usage.statistics.xml 25 | **/.idea/**/dictionaries 26 | **/.idea/**/shelf 27 | 28 | # Generated files 29 | **/.idea/**/contentModel.xml 30 | 31 | # Sensitive or high-churn files 32 | **/.idea/**/dataSources/ 33 | **/.idea/**/dataSources.ids 34 | **/.idea/**/dataSources.local.xml 35 | **/.idea/**/sqlDataSources.xml 36 | **/.idea/**/dynamic.xml 37 | **/.idea/**/uiDesigner.xml 38 | **/.idea/**/dbnavigator.xml 39 | 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "graphic-offsets", 4 | "inject-helper", 5 | "obs-client", 6 | "obs-client-ffi", 7 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matthias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # obs-rs 2 | Capture frames of any game using OBS. 3 | 4 | ## Features 5 | 6 | This projects uses the [`graphics-hook`](https://github.com/obsproject/obs-studio/tree/master/plugins/win-capture/graphics-hook) implementation from the [obs-studio](https://github.com/obsproject/obs-studio) project, to capture frames of any game. 7 | 8 | - The graphics hook is signed and whitelisted by all anti-cheats as it's used by streamers and content creators as well. 9 | - Works for many graphics APIs (D3D9, D3D10, D3D11, Vulkan, ...) and thus also for many different games. 10 | - This implementation is **extremely fast**, because it only copies the pixels from the framebuffer. On my machine, this crate is almost **5 times faster** compared to an implementation using [`BitBlt`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-bitblt). 11 | 12 | ## Example 13 | 14 | ```rust 15 | use obs_client::Capture; 16 | 17 | fn main() { 18 | simple_logger::SimpleLogger::new() 19 | .with_level(log::LevelFilter::Warn) 20 | .init() 21 | .unwrap(); 22 | 23 | let mut capture = Capture::new("Rainbow Six"); 24 | if capture.try_launch().is_err() { 25 | println!("Failed to launch the capture"); 26 | return; 27 | } 28 | 29 | loop { 30 | let _ = capture.capture_frame::(); 31 | std::thread::sleep(std::time::Duration::from_secs(5)); 32 | } 33 | } 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /graphic-offsets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphic-offsets" 3 | version = "0.1.0" 4 | authors = ["not-matthias <26800596+not-matthias@users.noreply.github.com>"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | serde = { version = "1.0.125", features = ["derive"] } 11 | toml = "0.5.8" 12 | -------------------------------------------------------------------------------- /graphic-offsets/bin/get-graphics-offsets32.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/obs-rs/02ccb8866bc73c2064e4db2f512d9c8e23c79386/graphic-offsets/bin/get-graphics-offsets32.exe -------------------------------------------------------------------------------- /graphic-offsets/bin/get-graphics-offsets64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/obs-rs/02ccb8866bc73c2064e4db2f512d9c8e23c79386/graphic-offsets/bin/get-graphics-offsets64.exe -------------------------------------------------------------------------------- /graphic-offsets/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::{os::windows::process::CommandExt, path::Path, process::Command}; 3 | 4 | #[derive(Debug)] 5 | pub enum GraphicOffsetsError { 6 | WriteBinaryToFile(std::io::Error), 7 | ExecuteBinary(std::io::Error), 8 | ParseOutput(toml::de::Error), 9 | } 10 | 11 | #[doc(hidden)] 12 | #[repr(C)] 13 | #[derive(Deserialize)] 14 | pub struct ParsedGraphicOffsets { 15 | pub d3d8: D3D8, 16 | pub d3d9: D3D9, 17 | pub dxgi: DXGI, 18 | } 19 | 20 | #[repr(C)] 21 | #[derive(Deserialize, Default, Debug)] 22 | pub struct GraphicOffsets { 23 | pub d3d8: D3D8, 24 | pub d3d9: D3D9, 25 | pub dxgi: DXGI, 26 | pub ddraw: DDraw, 27 | } 28 | 29 | #[repr(C)] 30 | #[derive(Deserialize, Default, Debug)] 31 | pub struct D3D8 { 32 | pub present: u32, 33 | } 34 | 35 | #[repr(C)] 36 | #[derive(Deserialize, Default, Debug)] 37 | pub struct D3D9 { 38 | pub present: u32, 39 | pub present_ex: u32, 40 | pub present_swap: u32, 41 | pub d3d9_clsoff: u32, 42 | pub is_d3d9ex_clsoff: u32, 43 | } 44 | 45 | #[allow(clippy::upper_case_acronyms)] 46 | #[repr(C)] 47 | #[derive(Deserialize, Default, Debug)] 48 | pub struct DXGI { 49 | pub present: u32, 50 | pub present1: u32, 51 | pub resize: u32, 52 | } 53 | 54 | #[repr(C)] 55 | #[derive(Deserialize, Default, Debug)] 56 | pub struct DDraw { 57 | pub surface_create: u32, 58 | pub surface_restore: u32, 59 | pub surface_release: u32, 60 | pub surface_unlock: u32, 61 | pub surface_blt: u32, 62 | pub surface_flip: u32, 63 | pub surface_set_palette: u32, 64 | pub palette_set_entries: u32, 65 | } 66 | 67 | /// Loads the graphic offsets and returns them. 68 | /// 69 | /// # How this works. 70 | /// 71 | /// TODO: Explain how it's done. 72 | pub fn load_graphic_offsets() -> Result { 73 | // Write the binary to the file 74 | // 75 | if !Path::new("get-graphic-offsets.exe").exists() { 76 | std::fs::write( 77 | "get-graphic-offsets.exe", 78 | include_bytes!("../bin/get-graphics-offsets64.exe"), 79 | ) 80 | .map_err(GraphicOffsetsError::WriteBinaryToFile)?; 81 | } 82 | 83 | // Execute the binary 84 | // 85 | const CREATE_NO_WINDOW: u32 = 0x08000000; 86 | // const DETACHED_PROCESS: u32 = 0x00000008; 87 | 88 | let output = Command::new("./get-graphic-offsets.exe") 89 | .creation_flags(CREATE_NO_WINDOW) 90 | .output() 91 | .map_err(GraphicOffsetsError::ExecuteBinary)?; 92 | 93 | // Parse the output. We need to do this with a separate structure, because the 94 | // sizes need to match. Wrapping the `ddraw` with an Option, will add 4 more 95 | // bytes that we don't need. 96 | // 97 | let parsed = toml::from_str::(&*String::from_utf8_lossy(&*output.stdout)) 98 | .map_err(GraphicOffsetsError::ParseOutput)?; 99 | 100 | Ok(GraphicOffsets { 101 | d3d8: parsed.d3d8, 102 | d3d9: parsed.d3d9, 103 | dxgi: parsed.dxgi, 104 | ddraw: Default::default(), 105 | }) 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | use super::*; 111 | 112 | #[test] 113 | fn test_sizes() { 114 | assert_eq!(core::mem::size_of::(), 4); 115 | assert_eq!(core::mem::size_of::(), 20); 116 | assert_eq!(core::mem::size_of::(), 12); 117 | assert_eq!(core::mem::size_of::(), 32); 118 | assert_eq!(core::mem::size_of::(), 68); 119 | } 120 | 121 | #[test] 122 | fn test_load() { 123 | assert!(load_graphic_offsets().is_ok()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /inject-helper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inject-helper" 3 | version = "0.1.0" 4 | authors = ["not-matthias <26800596+not-matthias@users.noreply.github.com>"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /inject-helper/bin/graphics-hook32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/obs-rs/02ccb8866bc73c2064e4db2f512d9c8e23c79386/inject-helper/bin/graphics-hook32.dll -------------------------------------------------------------------------------- /inject-helper/bin/graphics-hook64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/obs-rs/02ccb8866bc73c2064e4db2f512d9c8e23c79386/inject-helper/bin/graphics-hook64.dll -------------------------------------------------------------------------------- /inject-helper/bin/inject-helper32.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/obs-rs/02ccb8866bc73c2064e4db2f512d9c8e23c79386/inject-helper/bin/inject-helper32.exe -------------------------------------------------------------------------------- /inject-helper/bin/inject-helper64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/obs-rs/02ccb8866bc73c2064e4db2f512d9c8e23c79386/inject-helper/bin/inject-helper64.exe -------------------------------------------------------------------------------- /inject-helper/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, os::windows::process::CommandExt, path::Path, process::Command}; 2 | 3 | #[derive(Debug)] 4 | pub enum InjectHelperError { 5 | WriteBinaryToFile(std::io::Error), 6 | ExecuteBinary(std::io::Error), 7 | InjectError(ExitStatus), 8 | } 9 | 10 | #[derive(Debug)] 11 | pub enum ExitStatus { 12 | InjectFailed, 13 | InvalidParams, 14 | OpenProcessFail, 15 | UnlikelyFail, 16 | Unknown(i32), 17 | } 18 | 19 | impl TryFrom for ExitStatus { 20 | type Error = (); 21 | 22 | fn try_from(value: i32) -> Result { 23 | match value { 24 | -1 => Ok(Self::InjectFailed), 25 | -2 => Ok(Self::InvalidParams), 26 | -3 => Ok(Self::OpenProcessFail), 27 | -4 => Ok(Self::UnlikelyFail), 28 | _ => Err(()), 29 | } 30 | } 31 | } 32 | 33 | /// Tries to inject the graphics hook into the specified process. 34 | pub fn inject_graphics_hook(pid: u32, anti_cheat_compatible: bool) -> Result<(), InjectHelperError> { 35 | // Write the binaries to disk 36 | // 37 | if !Path::new("inject-helper.exe").exists() { 38 | std::fs::write("inject-helper.exe", include_bytes!("../bin/inject-helper64.exe")) 39 | .map_err(InjectHelperError::WriteBinaryToFile)?; 40 | } 41 | 42 | if !Path::new("graphics-hook64.dll").exists() { 43 | std::fs::write("graphics-hook64.dll", include_bytes!("../bin/graphics-hook64.dll")) 44 | .map_err(InjectHelperError::WriteBinaryToFile)?; 45 | } 46 | 47 | // Run the injector 48 | // 49 | const CREATE_NO_WINDOW: u32 = 0x08000000; 50 | // const DETACHED_PROCESS: u32 = 0x00000008; 51 | 52 | let exit_status = Command::new("inject-helper.exe") 53 | .args(&[ 54 | "graphics-hook64.dll", 55 | (anti_cheat_compatible as u8).to_string().as_str(), 56 | pid.to_string().as_str(), 57 | ]) 58 | .creation_flags(CREATE_NO_WINDOW) 59 | .status() 60 | .map_err(InjectHelperError::ExecuteBinary)?; 61 | 62 | if exit_status.success() { 63 | Ok(()) 64 | } else { 65 | Err(InjectHelperError::InjectError( 66 | exit_status 67 | .code() 68 | .map(|code| ExitStatus::try_from(code).unwrap_or(ExitStatus::Unknown(code))) 69 | .unwrap_or(ExitStatus::Unknown(0)), 70 | )) 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | #[test] 79 | fn test_inject() { 80 | let result = inject_graphics_hook(std::process::id(), false); 81 | println!("{:?}", result); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /obs-client-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "obs-client-ffi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | name = "obs_client_ffi" 10 | crate-type = ["lib", "cdylib", "staticlib"] 11 | 12 | [dependencies] 13 | obs-client = { path = "../obs-client" } -------------------------------------------------------------------------------- /obs-client-ffi/README.md: -------------------------------------------------------------------------------- 1 | # obs-client-ffi 2 | 3 | ## Requirements 4 | 5 | ``` 6 | cargo +nightly install cbindgen 7 | ``` 8 | 9 | ## Headers 10 | 11 | Generate with: 12 | ``` 13 | cbindgen --config cbindgen.toml --crate obs-client-ffi --output obs-client.h -l C 14 | cbindgen --config cbindgen.toml --crate obs-client-ffi --output obs-client.hpp -l C++ 15 | ``` -------------------------------------------------------------------------------- /obs-client-ffi/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | 3 | include_guard = "OBS_CLIENT_H" 4 | tab_width = 4 5 | style = "both" 6 | cpp_compat = true 7 | 8 | after_includes = "typedef void *Capture;" 9 | 10 | [fn] 11 | sort_by = "None" 12 | 13 | [enum] 14 | prefix_with_name = true -------------------------------------------------------------------------------- /obs-client-ffi/obs-client.h: -------------------------------------------------------------------------------- 1 | #ifndef OBS_CLIENT_H 2 | #define OBS_CLIENT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | typedef void *Capture; 9 | 10 | typedef struct Frame { 11 | uintptr_t width; 12 | uintptr_t height; 13 | uint8_t *data; 14 | } Frame; 15 | 16 | #ifdef __cplusplus 17 | extern "C" { 18 | #endif // __cplusplus 19 | 20 | Capture *create_capture(const char *name_str); 21 | 22 | void free_capture(Capture *capture); 23 | 24 | bool try_launch_capture(Capture *capture); 25 | 26 | struct Frame *capture_frame(Capture *capture); 27 | 28 | #ifdef __cplusplus 29 | } // extern "C" 30 | #endif // __cplusplus 31 | 32 | #endif /* OBS_CLIENT_H */ 33 | -------------------------------------------------------------------------------- /obs-client-ffi/obs-client.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OBS_CLIENT_H 2 | #define OBS_CLIENT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | typedef void *Capture; 10 | 11 | struct Frame { 12 | uintptr_t width; 13 | uintptr_t height; 14 | uint8_t *data; 15 | }; 16 | 17 | extern "C" { 18 | 19 | Capture *create_capture(const char *name_str); 20 | 21 | void free_capture(Capture *capture); 22 | 23 | bool try_launch_capture(Capture *capture); 24 | 25 | Frame *capture_frame(Capture *capture); 26 | 27 | } // extern "C" 28 | 29 | #endif // OBS_CLIENT_H 30 | -------------------------------------------------------------------------------- /obs-client-ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use obs_client::Capture; 2 | use std::ffi::{c_char, CStr}; 3 | 4 | #[no_mangle] 5 | pub extern "C" fn create_capture(name_str: *const c_char) -> *mut Capture { 6 | let name = unsafe { CStr::from_ptr(name_str).to_string_lossy().into_owned() }; 7 | 8 | let capture = Capture::new(name.as_str()); 9 | Box::into_raw(Box::new(capture)) 10 | } 11 | 12 | #[no_mangle] 13 | pub extern "C" fn free_capture(capture: *mut Capture) { 14 | if capture.is_null() { 15 | return; 16 | } 17 | let capture = unsafe { Box::from_raw(capture) }; 18 | core::mem::drop(capture); 19 | } 20 | 21 | #[no_mangle] 22 | pub extern "C" fn try_launch_capture(capture: *mut Capture) -> bool { 23 | if capture.is_null() { 24 | return false; 25 | } 26 | 27 | if let Err(e) = unsafe { (*capture).try_launch() } { 28 | eprintln!("Failed to launch capture: {:?}", e); 29 | false 30 | } else { 31 | true 32 | } 33 | } 34 | 35 | #[repr(C)] 36 | pub struct Frame { 37 | width: usize, 38 | height: usize, 39 | data: *mut u8, 40 | } 41 | 42 | #[no_mangle] 43 | pub extern "C" fn capture_frame(capture: *mut Capture) -> *mut Frame { 44 | if capture.is_null() { 45 | return std::ptr::null_mut(); 46 | } 47 | 48 | let frame = unsafe { (*capture).capture_frame() }; 49 | let (data, (width, height)) = match frame { 50 | Ok(frame) => frame, 51 | Err(e) => { 52 | eprintln!("Failed to capture frame: {:?}", e); 53 | return std::ptr::null_mut(); 54 | } 55 | }; 56 | 57 | let frame = Frame { 58 | width, 59 | height, 60 | data: data.as_mut_ptr(), 61 | }; 62 | Box::into_raw(Box::new(frame)) 63 | } 64 | -------------------------------------------------------------------------------- /obs-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "obs-client" 3 | version = "0.1.0" 4 | authors = ["not-matthias <26800596+not-matthias@users.noreply.github.com>"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | graphic-offsets = { path = "../graphic-offsets" } 11 | inject-helper = { path = "../inject-helper" } 12 | 13 | winapi = { version = "0.3.9", features = ["winuser", "windef", "synchapi", "winnt", "winbase", "memoryapi", "errhandlingapi", "d3d11", "d3dcommon", "winerror", "dxgi", "securitybaseapi", "minwinbase", "namedpipeapi", "ioapiset"] } 14 | simple_logger = "1.11.0" 15 | log = "0.4.14" 16 | wio = "0.2.2" 17 | 18 | [dev-dependencies] 19 | fps_counter = "2.0.0" -------------------------------------------------------------------------------- /obs-client/examples/basic.rs: -------------------------------------------------------------------------------- 1 | use obs_client::Capture; 2 | 3 | fn main() { 4 | simple_logger::SimpleLogger::new() 5 | .with_level(log::LevelFilter::Warn) 6 | .init() 7 | .unwrap(); 8 | 9 | let mut capture = Capture::new("Rainbow Six"); 10 | if capture.try_launch().is_err() { 11 | println!("Failed to launch the capture"); 12 | return; 13 | } 14 | 15 | let mut fps = fps_counter::FPSCounter::new(); 16 | loop { 17 | let (buffer, (width, height)) = capture.capture_frame::().unwrap(); 18 | println!("{:?} | {:?}x{:?} | {:?}", fps.tick(), width, height, buffer.len()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /obs-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use graphic_offsets::GraphicOffsetsError; 2 | use inject_helper::InjectHelperError; 3 | 4 | #[derive(Debug)] 5 | pub enum ObsError { 6 | ProcessNotFound, 7 | Inject(InjectHelperError), 8 | LoadGraphicOffsets(GraphicOffsetsError), 9 | CreatePipe, 10 | CreateMutex, 11 | CreateEvent, 12 | CreateFileMapping(u32), 13 | CreateDevice, 14 | OpenSharedResource, 15 | CreateTexture, 16 | MapSurface, 17 | } 18 | -------------------------------------------------------------------------------- /obs-client/src/hook_info.rs: -------------------------------------------------------------------------------- 1 | use graphic_offsets::GraphicOffsets; 2 | use winapi::um::winnt::{EVENT_MODIFY_STATE, SYNCHRONIZE}; 3 | 4 | pub const EVENT_FLAGS: u32 = EVENT_MODIFY_STATE | SYNCHRONIZE; 5 | pub const MUTEX_FLAGS: u32 = SYNCHRONIZE; 6 | 7 | /// Signalled by the graphics-hook both when the hook has been setup (see 8 | /// [`init_hook`](https://github.com/obsproject/obs-studio/blob/d46e8b03c963ba15548cf3e62951a26223749c27/plugins/win-capture/graphics-hook/graphics-hook.c#L252)) or when the capture has been freed (see [`capture_free`](https://github.com/obsproject/obs-studio/blob/master/plugins/win-capture/graphics-hook/graphics-hook.c#L807)). 9 | /// 10 | /// It's also used by the game-capture to reuse an already existing hook. See [`attempt_existing_hook`](https://github.com/obsproject/obs-studio/blob/d46e8b03c963ba15548cf3e62951a26223749c27/plugins/win-capture/game-capture.c#L685). 11 | pub const EVENT_CAPTURE_RESTART: &str = "CaptureHook_Restart"; 12 | pub const EVENT_CAPTURE_STOP: &str = "CaptureHook_Stop"; 13 | 14 | /// Used by the graphics-hook to signalize that the hook has been set. 15 | /// 16 | /// An example can be found in `graphics-hook.c` in the function [capture_init_shtex](https://github.com/obsproject/obs-studio/blob/d46e8b03c963ba15548cf3e62951a26223749c27/plugins/win-capture/graphics-hook/graphics-hook.c#L538). 17 | pub const EVENT_HOOK_READY: &str = "CaptureHook_HookReady"; 18 | pub const EVENT_HOOK_EXIT: &str = "CaptureHook_Exit"; 19 | pub const EVENT_HOOK_INIT: &str = "CaptureHook_Initialize"; 20 | 21 | pub const WINDOW_HOOK_KEEPALIVE: &str = "CaptureHook_KeepAlive"; 22 | 23 | pub const MUTEX_TEXTURE1: &str = "CaptureHook_TextureMutex1"; 24 | pub const MUTEX_TEXTURE2: &str = "CaptureHook_TextureMutex2"; 25 | 26 | pub const SHMEM_HOOK_INFO: &str = "CaptureHook_HookInfo"; 27 | pub const SHMEM_TEXTURE: &str = "CaptureHook_Texture"; 28 | 29 | pub const PIPE_NAME: &str = "CaptureHook_Pipe"; 30 | 31 | #[derive(Debug)] 32 | #[repr(C)] 33 | pub struct SharedTextureData { 34 | pub tex_handle: u32, 35 | } 36 | 37 | #[derive(Debug)] 38 | #[repr(C)] 39 | pub enum CaptureType { 40 | Memory, 41 | Texture, 42 | } 43 | 44 | #[derive(Debug)] 45 | #[repr(C)] 46 | pub struct HookInfo { 47 | /* hook version */ 48 | pub hook_ver_major: u32, 49 | pub hook_ver_minor: u32, 50 | 51 | /* capture info */ 52 | pub capture_type: CaptureType, 53 | pub window: u32, 54 | pub format: u32, 55 | pub cx: u32, 56 | pub cy: u32, 57 | #[doc(hidden)] 58 | unused_base_cx: u32, 59 | #[doc(hidden)] 60 | unused_base_cy: u32, 61 | pub pitch: u32, 62 | pub map_id: u32, 63 | pub map_size: u32, 64 | pub flip: bool, 65 | 66 | /* additional options */ 67 | pub frame_interval: u64, 68 | #[doc(hidden)] 69 | pub unused_use_scale: bool, 70 | pub force_shmem: bool, 71 | pub capture_overlay: bool, 72 | 73 | /* hook addresses */ 74 | pub graphics_offsets: GraphicOffsets, 75 | 76 | #[doc(hidden)] 77 | reserved: [u32; 128], 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | 84 | #[test] 85 | fn test_sizes() { 86 | assert_eq!(core::mem::size_of::(), 4); 87 | assert_eq!(core::mem::size_of::(), 4); 88 | 89 | assert_eq!(core::mem::size_of::(), 648); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /obs-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ObsError, 3 | hook_info::{ 4 | HookInfo, SharedTextureData, EVENT_CAPTURE_RESTART, EVENT_CAPTURE_STOP, EVENT_HOOK_EXIT, EVENT_HOOK_INIT, 5 | EVENT_HOOK_READY, PIPE_NAME, SHMEM_HOOK_INFO, SHMEM_TEXTURE, WINDOW_HOOK_KEEPALIVE, 6 | }, 7 | utils::{color::BGRA8, d3d11, event::Event, file_mapping::FileMapping, mutex::Mutex, pipe::NamedPipe}, 8 | }; 9 | use std::{mem, ptr, slice}; 10 | use winapi::{ 11 | shared::{ 12 | dxgi::{IDXGISurface1, DXGI_MAPPED_RECT, DXGI_MAP_READ, DXGI_RESOURCE_PRIORITY_MAXIMUM}, 13 | winerror::FAILED, 14 | }, 15 | um::{ 16 | d3d11::{ 17 | ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D, D3D11_CPU_ACCESS_READ, 18 | D3D11_USAGE_STAGING, 19 | }, 20 | errhandlingapi::GetLastError, 21 | }, 22 | }; 23 | use wio::com::ComPtr; 24 | 25 | pub mod error; 26 | pub mod hook_info; 27 | pub mod utils; 28 | 29 | #[derive(Default)] 30 | pub struct CaptureConfig { 31 | window_name: String, 32 | #[allow(dead_code)] 33 | frames: u32, 34 | capture_overlays: bool, 35 | } 36 | 37 | /// Everything needed to run the game capture. 38 | #[allow(dead_code)] 39 | #[derive(Default)] 40 | pub struct Context { 41 | hwnd: usize, 42 | pid: u32, 43 | thread_id: u32, 44 | texture_handle: u32, 45 | 46 | keepalive_mutex: Option, 47 | pipe: Option, 48 | 49 | hook_restart: Option, 50 | hook_stop: Option, 51 | hook_init: Option, 52 | hook_ready: Option, 53 | hook_exit: Option, 54 | 55 | device: Option>, 56 | device_context: Option>, 57 | resource: Option>, 58 | 59 | // Temporary storage so that we don't leak memory / have UB. 60 | frame_surface: Option>, 61 | } 62 | 63 | pub struct Capture { 64 | config: CaptureConfig, 65 | context: Context, 66 | } 67 | 68 | unsafe impl Send for Capture {} 69 | unsafe impl Sync for Capture {} 70 | 71 | impl Capture { 72 | pub fn new(window_name: S) -> Self { 73 | Self { 74 | config: CaptureConfig { 75 | window_name: window_name.to_string(), 76 | ..Default::default() 77 | }, 78 | context: Context::default(), 79 | } 80 | } 81 | 82 | fn init_keepalive(&mut self) -> Result<(), ObsError> { 83 | log::info!("Initializing the keepalive mutex"); 84 | 85 | if self.context.keepalive_mutex.is_none() { 86 | let name = format!("{}{}", WINDOW_HOOK_KEEPALIVE, self.context.pid); 87 | self.context.keepalive_mutex = Some(Mutex::create(name).ok_or(ObsError::CreateMutex)?); 88 | } 89 | 90 | Ok(()) 91 | } 92 | 93 | fn init_pipe(&mut self) -> Result<(), ObsError> { 94 | if self.context.pipe.is_none() { 95 | let name = format!("{}{}", PIPE_NAME, self.context.pid); 96 | self.context.pipe = Some(NamedPipe::create(name).ok_or(ObsError::CreatePipe)?); 97 | } 98 | 99 | Ok(()) 100 | } 101 | 102 | fn attempt_existing_hook(&mut self) -> bool { 103 | log::info!("Attempting to reuse the existing hook"); 104 | 105 | // Create the event if not yet done 106 | // 107 | if let Some(event) = Event::open(format!("{}{}", EVENT_CAPTURE_RESTART, self.context.pid)) { 108 | log::info!("Found an existing hook. Signalling the event"); 109 | 110 | if event.signal().is_none() { 111 | log::warn!("Failed to signal the event"); 112 | }; 113 | 114 | true 115 | } else { 116 | log::info!("Found no existing hook."); 117 | false 118 | } 119 | } 120 | 121 | fn init_hook_info(&mut self) -> Result<(), ObsError> { 122 | log::info!("Initializing the hook information"); 123 | 124 | let mut hook_info = FileMapping::::open(format!("{}{}", SHMEM_HOOK_INFO, self.context.pid)) 125 | .ok_or_else(|| ObsError::CreateFileMapping(unsafe { GetLastError() as u32 }))?; 126 | 127 | let graphic_offsets = graphic_offsets::load_graphic_offsets().map_err(ObsError::LoadGraphicOffsets)?; 128 | unsafe { (**hook_info).graphics_offsets = graphic_offsets }; 129 | unsafe { (**hook_info).capture_overlay = self.config.capture_overlays }; 130 | unsafe { (**hook_info).force_shmem = false }; 131 | unsafe { (**hook_info).unused_use_scale = false }; 132 | 133 | log::info!("Hook info: {:?}", unsafe { &**hook_info }); 134 | 135 | Ok(()) 136 | } 137 | 138 | fn init_events(&mut self) -> Result<(), ObsError> { 139 | macro_rules! open_event { 140 | ($var_name:tt, $event_name:expr) => { 141 | // if self.context.$var_name.is_none() { 142 | if let Some(event) = Event::open(format!("{}{}", $event_name, self.context.pid)) { 143 | self.context.$var_name = Some(event) 144 | } else { 145 | log::warn!("Couldn't find {:?} ({:?}).", $event_name, unsafe { 146 | GetLastError() 147 | }); 148 | return Err(ObsError::CreateEvent); 149 | } 150 | // } 151 | }; 152 | } 153 | 154 | open_event!(hook_restart, EVENT_CAPTURE_RESTART); 155 | open_event!(hook_stop, EVENT_CAPTURE_STOP); 156 | open_event!(hook_init, EVENT_HOOK_INIT); 157 | open_event!(hook_ready, EVENT_HOOK_READY); 158 | open_event!(hook_exit, EVENT_HOOK_EXIT); 159 | 160 | Ok(()) 161 | } 162 | 163 | /// Tries to launch the capture. 164 | pub fn try_launch(&mut self) -> Result<(), ObsError> { 165 | let hwnd = utils::process::get_hwnd(&*self.config.window_name).ok_or(ObsError::ProcessNotFound)?; 166 | let (pid, thread_id) = utils::process::get_window_thread_pid(hwnd).ok_or(ObsError::ProcessNotFound)?; 167 | 168 | log::info!( 169 | "Found the process. pid = {}, thread id = {}, hwnd = {}", 170 | pid, 171 | thread_id, 172 | hwnd 173 | ); 174 | 175 | self.context.hwnd = hwnd; 176 | self.context.pid = pid; 177 | self.context.thread_id = thread_id; 178 | 179 | self.init_keepalive()?; 180 | self.init_pipe()?; 181 | 182 | if !self.attempt_existing_hook() { 183 | log::info!( 184 | "Trying to inject the graphics hook into the thread {}.", 185 | self.context.thread_id 186 | ); 187 | inject_helper::inject_graphics_hook(self.context.thread_id, true).map_err(ObsError::Inject)?; 188 | } 189 | 190 | self.init_hook_info()?; 191 | self.init_events()?; 192 | 193 | // Create and signal the hook init event 194 | // 195 | let event = Event::open(format!("{}{}", EVENT_HOOK_INIT, self.context.pid)).ok_or(ObsError::CreateEvent)?; 196 | if event.signal().is_none() { 197 | log::warn!("Failed to signal the hook init event"); 198 | }; 199 | 200 | // Extract the handle 201 | // 202 | let hook_info = FileMapping::::open(format!("{}{}", SHMEM_HOOK_INFO, self.context.pid)) 203 | .ok_or_else(|| ObsError::CreateFileMapping(unsafe { GetLastError() as u32 }))?; 204 | 205 | let texture_data = FileMapping::::open(format!( 206 | "{}_{}_{}", 207 | SHMEM_TEXTURE, 208 | unsafe { (**hook_info).window }, 209 | unsafe { (**hook_info).map_id } 210 | )) 211 | .ok_or_else(|| ObsError::CreateFileMapping(unsafe { GetLastError() as u32 }))?; 212 | 213 | let texture_handle = unsafe { (**texture_data).tex_handle }; 214 | self.context.texture_handle = texture_handle; 215 | 216 | // Initialize the d3d11 variables 217 | // 218 | 219 | let (device, device_context) = d3d11::create_device()?; 220 | let resource = d3d11::open_resource(&device, self.context.texture_handle)?; 221 | 222 | self.context.device = Some(device); 223 | self.context.device_context = Some(device_context); 224 | self.context.resource = Some(resource); 225 | 226 | Ok(()) 227 | } 228 | 229 | fn map_resource(&mut self) -> Result<(DXGI_MAPPED_RECT, (usize, usize)), ObsError> { 230 | // Cleanup resources from the previous run 231 | // 232 | if let Some(frame_surface) = &self.context.frame_surface { 233 | unsafe { frame_surface.Unmap() }; 234 | self.context.frame_surface = None; 235 | } 236 | 237 | // Copy the resource (https://github.com/bryal/dxgcap-rs/blob/master/src/lib.rs#L187) 238 | // 239 | let frame_texture = self 240 | .context 241 | .resource 242 | .as_ref() 243 | .unwrap() 244 | .cast::() 245 | .unwrap(); 246 | let mut texture_desc = unsafe { 247 | let mut texture_desc = mem::zeroed(); 248 | frame_texture.GetDesc(&mut texture_desc); 249 | texture_desc 250 | }; 251 | 252 | // Configure the description to make the texture readable 253 | texture_desc.Usage = D3D11_USAGE_STAGING; 254 | texture_desc.BindFlags = 0; 255 | texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; 256 | texture_desc.MiscFlags = 0; 257 | 258 | log::trace!("Creating a 2d texture"); 259 | let readable_texture = unsafe { 260 | let mut readable_texture = ptr::null_mut(); 261 | let hr = self.context.device.as_ref().unwrap().CreateTexture2D( 262 | &texture_desc, 263 | ptr::null(), 264 | &mut readable_texture, 265 | ); 266 | if FAILED(hr) { 267 | log::error!("Failed to create the 2d texture {:x}", hr); 268 | return Err(ObsError::CreateTexture); 269 | } 270 | ComPtr::from_raw(readable_texture) 271 | }; 272 | 273 | // Lower priorities causes stuff to be needlessly copied from gpu to ram, 274 | // causing huge ram usage on some systems. 275 | unsafe { readable_texture.SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM) }; 276 | let readable_surface = readable_texture.up::(); 277 | 278 | log::trace!("Copying the resources"); 279 | unsafe { 280 | self.context 281 | .device_context 282 | .as_ref() 283 | .unwrap() 284 | .CopyResource(readable_surface.as_raw(), frame_texture.up::().as_raw()); 285 | } 286 | let frame_surface: ComPtr = readable_surface.cast().unwrap(); 287 | log::trace!("Texture Size: {} x {}", texture_desc.Width, texture_desc.Height); 288 | 289 | // Resource to Surface (https://github.com/bryal/dxgcap-rs/blob/master/src/lib.rs#L229) 290 | // 291 | log::trace!("Mapping the surface"); 292 | let mapped_surface = unsafe { 293 | let mut mapped_surface = mem::zeroed(); 294 | let result = frame_surface.Map(&mut mapped_surface, DXGI_MAP_READ); 295 | if FAILED(result) { 296 | log::error!("Failed to map surface: {:x}", result); 297 | frame_surface.Release(); 298 | return Err(ObsError::MapSurface); 299 | } 300 | 301 | mapped_surface 302 | }; 303 | 304 | // Set the frame surface so that we can unmap it in the next run. We have to do 305 | // it this way so that we can don't have to copy the pixels to a new buffer. 306 | // 307 | self.context.frame_surface = Some(frame_surface); 308 | 309 | Ok(( 310 | mapped_surface, 311 | (texture_desc.Width as usize, texture_desc.Height as usize), 312 | )) 313 | } 314 | 315 | /// Captures the frame and returns it. 316 | /// 317 | /// # Returns 318 | /// 319 | /// Returns a tuple with the: 320 | /// - Frame 321 | /// - Width and Height 322 | pub fn capture_frame(&mut self) -> Result<(&mut [T], (usize, usize)), ObsError> { 323 | // Restart the capture if the game has been rehooked 324 | // 325 | if self 326 | .context 327 | .hook_restart 328 | .as_ref() 329 | .map(|e| e.signalled().is_some()) 330 | .unwrap_or(true) 331 | { 332 | log::warn!("The restart event has been signalled. Restarting the capture."); 333 | self.try_launch()?; 334 | } 335 | 336 | // Copy the texture 337 | // 338 | let (mapped_surface, (width, height)) = self.map_resource()?; 339 | 340 | let byte_size = |x| x * mem::size_of::() / mem::size_of::(); 341 | let stride = mapped_surface.Pitch as usize / mem::size_of::(); 342 | let mapped_pixels = 343 | unsafe { slice::from_raw_parts_mut(mapped_surface.pBits as *mut T, byte_size(stride) * height) }; 344 | 345 | Ok((mapped_pixels, (width, height))) 346 | } 347 | } 348 | 349 | impl Drop for Capture { 350 | fn drop(&mut self) { 351 | if let Some(frame_surface) = &self.context.frame_surface { 352 | unsafe { frame_surface.Unmap() }; 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /obs-client/src/utils/color.rs: -------------------------------------------------------------------------------- 1 | /// Color represented by additive channels: Blue (b), Green (g), Red (r), and 2 | /// Alpha (a). 3 | #[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq, Ord)] 4 | pub struct BGRA8 { 5 | pub b: u8, 6 | pub g: u8, 7 | pub r: u8, 8 | pub a: u8, 9 | } 10 | -------------------------------------------------------------------------------- /obs-client/src/utils/d3d11.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ObsError; 2 | use std::ptr; 3 | use winapi::{ 4 | shared::winerror::FAILED, 5 | um::{ 6 | d3d11::{D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, D3D11_SDK_VERSION}, 7 | d3dcommon::D3D_DRIVER_TYPE_HARDWARE, 8 | }, 9 | Interface, 10 | }; 11 | use wio::com::ComPtr; 12 | 13 | pub fn create_device() -> Result<(ComPtr, ComPtr), ObsError> { 14 | let mut device: *mut ID3D11Device = ptr::null_mut(); 15 | let mut device_context: *mut ID3D11DeviceContext = ptr::null_mut(); 16 | 17 | log::info!("Creating the device"); 18 | let result = unsafe { 19 | D3D11CreateDevice( 20 | ptr::null_mut(), 21 | D3D_DRIVER_TYPE_HARDWARE, 22 | ptr::null_mut(), 23 | 0, 24 | ptr::null_mut(), 25 | 0, 26 | D3D11_SDK_VERSION, 27 | &mut device, 28 | ptr::null_mut(), 29 | &mut device_context, 30 | ) 31 | }; 32 | if FAILED(result) { 33 | log::error!("Failed to create device"); 34 | return Err(ObsError::CreateDevice); 35 | } 36 | let device = unsafe { ComPtr::from_raw(device) }; 37 | let device_context = unsafe { ComPtr::from_raw(device_context) }; 38 | 39 | Ok((device, device_context)) 40 | } 41 | 42 | pub fn open_resource(device: &ComPtr, handle: u32) -> Result, ObsError> { 43 | log::info!("Opening the shared resource"); 44 | let mut resource: *mut ID3D11Resource = ptr::null_mut(); 45 | let result = unsafe { 46 | device.OpenSharedResource( 47 | handle as _, 48 | &ID3D11Resource::uuidof(), 49 | &mut resource as *mut *mut _ as _, 50 | ) 51 | }; 52 | if FAILED(result) { 53 | log::error!("Failed to open the shared resource"); 54 | return Err(ObsError::OpenSharedResource); 55 | } 56 | let resource = unsafe { ComPtr::from_raw(resource) }; 57 | 58 | Ok(resource) 59 | } 60 | -------------------------------------------------------------------------------- /obs-client/src/utils/event.rs: -------------------------------------------------------------------------------- 1 | use crate::hook_info::EVENT_FLAGS; 2 | use winapi::um::{ 3 | handleapi::CloseHandle, 4 | synchapi::{CreateEventA, OpenEventA, SetEvent, WaitForSingleObject}, 5 | winbase::WAIT_OBJECT_0, 6 | }; 7 | 8 | pub struct Event { 9 | handle: usize, 10 | } 11 | 12 | impl Event { 13 | pub fn create(name: Option<&str>) -> Option { 14 | let name = if let Some(name) = name { 15 | format!("{}\0", name).as_ptr() as _ 16 | } else { 17 | std::ptr::null_mut() 18 | }; 19 | 20 | let event = unsafe { CreateEventA(std::ptr::null_mut(), false as _, false as _, name) }; 21 | if event.is_null() { 22 | None 23 | } else { 24 | log::trace!("Created the event {:?} = 0x{:x}", name, event as usize); 25 | 26 | Some(Self { handle: event as usize }) 27 | } 28 | } 29 | 30 | pub fn open>(name: S) -> Option { 31 | let event = unsafe { OpenEventA(EVENT_FLAGS, false as _, format!("{}\0", name.as_ref()).as_ptr() as _) }; 32 | 33 | if event.is_null() { 34 | None 35 | } else { 36 | log::trace!("Created the event {:?} = 0x{:x}", name.as_ref(), event as usize); 37 | Some(Self { handle: event as usize }) 38 | } 39 | } 40 | 41 | /// Sets the event to the signalled state. 42 | pub fn signal(&self) -> Option<()> { 43 | if unsafe { SetEvent(self.handle as _) } == 0 { 44 | None 45 | } else { 46 | Some(()) 47 | } 48 | } 49 | 50 | /// Checks whether the event is signalled. 51 | pub fn signalled(&self) -> Option<()> { 52 | unsafe { WaitForSingleObject(self.handle as _, 0) == WAIT_OBJECT_0 }.then_some(()) 53 | } 54 | 55 | /// Returns the internal handle 56 | pub fn handle(&self) -> usize { self.handle } 57 | } 58 | 59 | impl Drop for Event { 60 | fn drop(&mut self) { 61 | log::trace!("Dropping the event"); 62 | unsafe { CloseHandle(self.handle as _) }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /obs-client/src/utils/file_mapping.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | use winapi::um::{ 3 | handleapi::CloseHandle, 4 | memoryapi::{MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS}, 5 | winbase::OpenFileMappingA, 6 | }; 7 | 8 | pub struct FileMapping { 9 | handle: usize, 10 | file_mapping: *mut T, 11 | } 12 | 13 | impl FileMapping { 14 | pub fn open>(name: S) -> Option { 15 | let handle = unsafe { 16 | OpenFileMappingA( 17 | FILE_MAP_ALL_ACCESS, 18 | false as _, 19 | format!("{}\0", name.as_ref()).as_ptr() as _, 20 | ) 21 | }; 22 | if handle.is_null() { 23 | log::warn!("Failed to open file mapping ({:?}).", name.as_ref()); 24 | return None; 25 | } 26 | 27 | let file_mapping = 28 | unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, std::mem::size_of::()) } as *mut T; 29 | if file_mapping.is_null() { 30 | log::warn!("Failed to map view of file ({:?}).", name.as_ref()); 31 | return None; 32 | } 33 | 34 | log::trace!( 35 | "Created the file mapping for {:?} = {:x}, {:x}", 36 | name.as_ref(), 37 | handle as usize, 38 | file_mapping as usize 39 | ); 40 | 41 | Some(Self { 42 | handle: handle as usize, 43 | file_mapping, 44 | }) 45 | } 46 | } 47 | 48 | impl Deref for FileMapping { 49 | type Target = *mut T; 50 | 51 | fn deref(&self) -> &Self::Target { &(self.file_mapping) } 52 | } 53 | 54 | impl DerefMut for FileMapping { 55 | fn deref_mut(&mut self) -> &mut Self::Target { &mut (self.file_mapping) } 56 | } 57 | 58 | impl Drop for FileMapping { 59 | fn drop(&mut self) { 60 | log::trace!("Dropping the file mapping"); 61 | unsafe { UnmapViewOfFile(self.file_mapping as _) }; 62 | unsafe { CloseHandle(self.handle as _) }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /obs-client/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod color; 2 | pub mod d3d11; 3 | pub mod event; 4 | pub mod file_mapping; 5 | pub mod mutex; 6 | pub mod pipe; 7 | pub mod process; 8 | -------------------------------------------------------------------------------- /obs-client/src/utils/mutex.rs: -------------------------------------------------------------------------------- 1 | use winapi::um::{errhandlingapi::GetLastError, handleapi::CloseHandle, synchapi::CreateMutexA}; 2 | 3 | pub struct Mutex { 4 | handle: usize, 5 | } 6 | 7 | impl Mutex { 8 | pub fn create>(name: S) -> Option { 9 | let handle = unsafe { 10 | CreateMutexA( 11 | std::ptr::null_mut(), 12 | false as _, 13 | format!("{}\0", name.as_ref()).as_ptr() as _, 14 | ) 15 | }; 16 | if handle.is_null() { 17 | log::warn!("Failed to create mutex ({:?}, {:?})", name.as_ref(), unsafe { 18 | GetLastError() 19 | }); 20 | None 21 | } else { 22 | log::trace!("Created the mutex {:?} = 0x{:x}", name.as_ref(), handle as usize); 23 | Some(Self { handle: handle as _ }) 24 | } 25 | } 26 | } 27 | 28 | impl Drop for Mutex { 29 | fn drop(&mut self) { 30 | log::trace!("Dropping the mutex"); 31 | unsafe { CloseHandle(self.handle as _) }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /obs-client/src/utils/pipe.rs: -------------------------------------------------------------------------------- 1 | use crate::Event; 2 | use std::{ 3 | mem::MaybeUninit, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering}, 6 | Arc, 7 | }, 8 | thread::JoinHandle, 9 | }; 10 | use winapi::{ 11 | shared::winerror::ERROR_IO_PENDING, 12 | um::{ 13 | errhandlingapi::GetLastError, 14 | fileapi::ReadFile, 15 | handleapi::{CloseHandle, INVALID_HANDLE_VALUE}, 16 | ioapiset::GetOverlappedResult, 17 | minwinbase::{OVERLAPPED, SECURITY_ATTRIBUTES}, 18 | namedpipeapi::ConnectNamedPipe, 19 | securitybaseapi::{InitializeSecurityDescriptor, SetSecurityDescriptorDacl}, 20 | synchapi::WaitForSingleObject, 21 | winbase::{ 22 | CreateNamedPipeA, FILE_FLAG_OVERLAPPED, INFINITE, PIPE_ACCESS_DUPLEX, PIPE_READMODE_MESSAGE, 23 | PIPE_TYPE_MESSAGE, PIPE_WAIT, WAIT_OBJECT_0, 24 | }, 25 | winnt::{SECURITY_DESCRIPTOR, SECURITY_DESCRIPTOR_REVISION}, 26 | }, 27 | }; 28 | 29 | pub const IPC_PIPE_BUFFER_SIZE: u32 = 1024; 30 | 31 | pub struct NamedPipe { 32 | handle: usize, 33 | 34 | _thread: JoinHandle<()>, 35 | thread_running: Arc, 36 | } 37 | 38 | impl NamedPipe { 39 | fn create_events() -> Option { Event::create(None) } 40 | 41 | fn create_full_access_security_descriptor() -> Option { 42 | let mut sd = MaybeUninit::::uninit(); 43 | 44 | if unsafe { InitializeSecurityDescriptor(sd.as_mut_ptr() as _, SECURITY_DESCRIPTOR_REVISION) == 0 } { 45 | return None; 46 | } 47 | 48 | if unsafe { SetSecurityDescriptorDacl(sd.as_mut_ptr() as _, true as _, std::ptr::null_mut(), false as _) == 0 } 49 | { 50 | return None; 51 | } 52 | 53 | Some(unsafe { sd.assume_init() }) 54 | } 55 | 56 | fn create_pipe>(name: S) -> Option { 57 | let mut sd = Self::create_full_access_security_descriptor()?; 58 | let mut sa: SECURITY_ATTRIBUTES = unsafe { core::mem::zeroed() }; 59 | 60 | sa.nLength = core::mem::size_of::() as _; 61 | sa.lpSecurityDescriptor = &mut sd as *mut _ as _; 62 | sa.bInheritHandle = false as _; 63 | 64 | let pipe_name = format!("\\\\.\\pipe\\{}\0", name.as_ref()); 65 | let handle = unsafe { 66 | CreateNamedPipeA( 67 | pipe_name.as_ptr() as _, 68 | PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 69 | PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 70 | 1, 71 | IPC_PIPE_BUFFER_SIZE, 72 | IPC_PIPE_BUFFER_SIZE, 73 | 0, 74 | &mut sa as *mut _, 75 | ) 76 | }; 77 | 78 | if handle != INVALID_HANDLE_VALUE { 79 | log::trace!("Created the named pipe {:?} = 0x{:x}", name.as_ref(), handle as usize); 80 | Some(handle as usize) 81 | } else { 82 | None 83 | } 84 | } 85 | 86 | fn wait_for_connection(pipe_handle: usize, event_handle: usize) -> Option { 87 | let mut overlap: OVERLAPPED = unsafe { std::mem::zeroed() }; 88 | overlap.hEvent = event_handle as _; 89 | 90 | let result = unsafe { ConnectNamedPipe(pipe_handle as _, &mut overlap as _) }; 91 | if result != 0 || Self::io_pending() { 92 | Some(overlap) 93 | } else { 94 | None 95 | } 96 | } 97 | 98 | fn io_pending() -> bool { unsafe { GetLastError() == ERROR_IO_PENDING } } 99 | 100 | pub fn create>(name: S) -> Option { 101 | let ready_event = Self::create_events()?; 102 | let pipe_handle = Self::create_pipe(name)? as usize; 103 | 104 | let thread_running = Arc::new(AtomicBool::new(true)); 105 | let thread_running_copy = thread_running.clone(); 106 | 107 | // Create the read thread 108 | // 109 | log::info!("Creating the thread"); 110 | let thread_handle = std::thread::spawn(move || { 111 | // Initialize the overlap struct 112 | // 113 | let mut overlap = if let Some(overlap) = Self::wait_for_connection(pipe_handle, ready_event.handle()) { 114 | overlap 115 | } else { 116 | log::warn!("Self::wait_for_connection failed"); 117 | return; 118 | }; 119 | let ready_event = ready_event; 120 | 121 | // Wait for connection 122 | // 123 | let wait = unsafe { WaitForSingleObject(ready_event.handle() as _, INFINITE) }; 124 | if wait != WAIT_OBJECT_0 { 125 | log::warn!("wait != WAIT_OBJECT_0"); 126 | return; 127 | } 128 | 129 | // 130 | // 131 | let mut buffer: Vec = Vec::with_capacity(IPC_PIPE_BUFFER_SIZE as _); 132 | let mut temp: [u8; IPC_PIPE_BUFFER_SIZE as _] = [0_u8; IPC_PIPE_BUFFER_SIZE as _]; 133 | while thread_running.load(Ordering::Relaxed) { 134 | // Read to the buffer 135 | // 136 | if unsafe { 137 | ReadFile( 138 | pipe_handle as _, 139 | temp.as_mut_ptr() as _, 140 | IPC_PIPE_BUFFER_SIZE, 141 | std::ptr::null_mut(), 142 | &mut overlap, 143 | ) 144 | } != 0 145 | && !Self::io_pending() 146 | { 147 | log::warn!("ReadFile failed ({:?})", unsafe { GetLastError() }); 148 | break; 149 | } 150 | 151 | if unsafe { WaitForSingleObject(ready_event.handle() as _, INFINITE) } != WAIT_OBJECT_0 { 152 | log::warn!("WaitForSingleObject failed"); 153 | break; 154 | } 155 | 156 | let mut bytes = 0; 157 | let success = 158 | unsafe { GetOverlappedResult(pipe_handle as _, &mut overlap, &mut bytes, true as _) } != 0; 159 | if !success || bytes == 0 { 160 | log::warn!("GetOverlappedResult failed"); 161 | break; 162 | } 163 | 164 | buffer.extend_from_slice(&temp); 165 | 166 | // Print the log 167 | // 168 | if success { 169 | match std::ffi::CString::from_vec_with_nul(buffer[..bytes as _].to_vec()) { 170 | Ok(data) => log::info!("[pipe] {:?}", data), 171 | Err(error) => log::error!("Failed to convert buffer to string ({:?})", error), 172 | } 173 | 174 | buffer.clear(); 175 | } 176 | } 177 | 178 | // CancelIoEx(pipe->handle, &pipe->overlap); 179 | // SetEvent(pipe->ready_event); 180 | // WaitForSingleObject(pipe->thread, INFINITE); 181 | // CloseHandle(pipe->thread); 182 | }); 183 | 184 | Some(Self { 185 | handle: pipe_handle, 186 | _thread: thread_handle, 187 | thread_running: thread_running_copy, 188 | }) 189 | } 190 | } 191 | 192 | impl Drop for NamedPipe { 193 | fn drop(&mut self) { 194 | self.thread_running.store(false, Ordering::Relaxed); 195 | 196 | if self.handle != 0 { 197 | log::trace!("Dropping the named pipe"); 198 | unsafe { CloseHandle(self.handle as _) }; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /obs-client/src/utils/process.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use winapi::um::winuser::{FindWindowA, GetWindowThreadProcessId}; 3 | 4 | pub fn get_hwnd(window_name: &str) -> Option { 5 | let window = unsafe { FindWindowA(std::ptr::null_mut(), format!("{}\0", window_name).as_ptr() as _) }; 6 | if window.is_null() { 7 | None 8 | } else { 9 | Some(window as usize) 10 | } 11 | } 12 | 13 | /// Finds the window thread and process id. 14 | /// 15 | /// # Returns 16 | /// 17 | /// Returns a tuple: `(process_id, thread_id)` 18 | pub fn get_window_thread_pid(hwnd: usize) -> Option<(u32, u32)> { 19 | let mut pid = MaybeUninit::uninit(); 20 | let thread_id = unsafe { GetWindowThreadProcessId(hwnd as _, pid.as_mut_ptr()) }; 21 | 22 | Some((unsafe { pid.assume_init() }, thread_id)) 23 | } 24 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | 3 | brace_style = "PreferSameLine" 4 | color = "Always" 5 | fn_args_layout = "Compressed" 6 | fn_single_line = true 7 | format_code_in_doc_comments = true 8 | format_macro_matchers = true 9 | format_macro_bodies = true 10 | format_strings = true 11 | inline_attribute_width = 80 12 | max_width = 120 13 | imports_granularity = "Crate" 14 | normalize_doc_attributes = true 15 | reorder_impl_items = true 16 | reorder_imports = true 17 | reorder_modules = true 18 | use_field_init_shorthand = true 19 | use_try_shorthand = true 20 | where_single_line = true 21 | wrap_comments = true --------------------------------------------------------------------------------