├── .github ├── FUNDING.yml ├── banner.gif └── workflows │ └── initiateRelease.yml ├── .gitignore ├── release-plz.toml ├── .vscode └── settings.json ├── src ├── frame │ ├── mod.rs │ ├── audio.rs │ └── video.rs ├── utils │ ├── linux │ │ └── mod.rs │ ├── win │ │ └── mod.rs │ ├── mac │ │ └── mod.rs │ └── mod.rs ├── targets │ ├── linux │ │ └── mod.rs │ ├── mod.rs │ ├── win │ │ └── mod.rs │ └── mac │ │ └── mod.rs ├── capturer │ ├── engine │ │ ├── mac │ │ │ ├── ext.rs │ │ │ ├── pixel_buffer.rs │ │ │ ├── pixelformat.rs │ │ │ └── mod.rs │ │ ├── linux │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ └── portal.rs │ │ ├── mod.rs │ │ └── win │ │ │ └── mod.rs │ └── mod.rs ├── lib.rs └── main.rs ├── LICENSE ├── Cargo.toml ├── .all-contributorsrc ├── CHANGELOG.md ├── README.md └── Cargo.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clearlysid 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | /test 4 | /.idea 5 | -------------------------------------------------------------------------------- /.github/banner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapSoftware/scap/HEAD/.github/banner.gif -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | release_always = false 3 | publish_no_verify = true 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[rust]": { 3 | "editor.formatOnSave": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/frame/mod.rs: -------------------------------------------------------------------------------- 1 | mod audio; 2 | mod video; 3 | 4 | pub use audio::*; 5 | pub use video::*; 6 | 7 | pub enum Frame { 8 | Audio(AudioFrame), 9 | Video(VideoFrame), 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/linux/mod.rs: -------------------------------------------------------------------------------- 1 | // TODO 2 | pub fn is_supported() -> bool { 3 | true 4 | // false 5 | } 6 | 7 | // TODO 8 | pub fn has_permission() -> bool { 9 | true 10 | // false 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/win/mod.rs: -------------------------------------------------------------------------------- 1 | use windows_capture::graphics_capture_api::GraphicsCaptureApi; 2 | 3 | pub fn is_supported() -> bool { 4 | GraphicsCaptureApi::is_supported().expect("Failed to check support") 5 | } 6 | -------------------------------------------------------------------------------- /src/targets/linux/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Target; 2 | 3 | // On Linux, the target is selected when a Recorder is instanciated because this 4 | // requires user interaction 5 | pub fn get_all_targets() -> Vec { 6 | Vec::new() 7 | } 8 | -------------------------------------------------------------------------------- /src/capturer/engine/mac/ext.rs: -------------------------------------------------------------------------------- 1 | use cidre::cg; 2 | use core_graphics_helmer_fork::display::{CGDisplay, CGDisplayMode}; 3 | 4 | pub trait DirectDisplayIdExt { 5 | fn display_mode(&self) -> Option; 6 | } 7 | 8 | impl DirectDisplayIdExt for cg::DirectDisplayId { 9 | #[inline] 10 | fn display_mode(&self) -> Option { 11 | CGDisplay::new(self.0).display_mode() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Cross Platform, Performant and High Quality screen recordings 2 | 3 | pub mod capturer; 4 | pub mod frame; 5 | mod targets; 6 | mod utils; 7 | 8 | // Helper Methods 9 | pub use targets::{get_all_targets, get_main_display}; 10 | pub use targets::{Display, Target}; 11 | pub use utils::has_permission; 12 | pub use utils::is_supported; 13 | pub use utils::request_permission; 14 | 15 | #[cfg(target_os = "macos")] 16 | pub mod engine { 17 | pub use crate::capturer::engine::mac; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/mac/mod.rs: -------------------------------------------------------------------------------- 1 | use core_graphics_helmer_fork::access::ScreenCaptureAccess; 2 | use sysinfo::System; 3 | 4 | pub fn has_permission() -> bool { 5 | ScreenCaptureAccess.preflight() 6 | } 7 | 8 | pub fn request_permission() -> bool { 9 | ScreenCaptureAccess.request() 10 | } 11 | 12 | pub fn is_supported() -> bool { 13 | let os_version = System::os_version() 14 | .expect("Failed to get macOS version") 15 | .as_bytes() 16 | .to_vec(); 17 | 18 | let min_version: Vec = "12.3\n".as_bytes().to_vec(); 19 | 20 | os_version >= min_version 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/initiateRelease.yml: -------------------------------------------------------------------------------- 1 | name: Initiate Release 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release(PR or Publish) 15 | runs-on: ubuntu-latest 16 | environment: production 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | - name: Run release-plz 25 | uses: MarcoIeni/release-plz-action@v0.5 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | -------------------------------------------------------------------------------- /src/capturer/engine/mac/pixel_buffer.rs: -------------------------------------------------------------------------------- 1 | use cidre::{arc, cm, sc}; 2 | use std::sync::mpsc; 3 | 4 | use crate::capturer::RawCapturer; 5 | 6 | impl RawCapturer<'_> { 7 | pub fn get_next_sample_buffer( 8 | &self, 9 | ) -> Result<(arc::R, sc::stream::OutputType), mpsc::RecvError> { 10 | use std::time::Duration; 11 | 12 | let capturer = &self.capturer; 13 | 14 | loop { 15 | let error_flag = capturer 16 | .engine 17 | .error_flag 18 | .load(std::sync::atomic::Ordering::Relaxed); 19 | if error_flag { 20 | return Err(mpsc::RecvError); 21 | } 22 | 23 | return match capturer.rx.recv_timeout(Duration::from_millis(10)) { 24 | Ok(v) => Ok(v), 25 | Err(mpsc::RecvTimeoutError::Timeout) => continue, 26 | Err(mpsc::RecvTimeoutError::Disconnected) => Err(mpsc::RecvError), 27 | }; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2025 Cap Software, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | mod mac; 3 | 4 | #[cfg(target_os = "windows")] 5 | mod win; 6 | 7 | #[cfg(target_os = "linux")] 8 | mod linux; 9 | 10 | /// Checks if process has permission to capture the screen 11 | pub fn has_permission() -> bool { 12 | #[cfg(target_os = "macos")] 13 | return mac::has_permission(); 14 | 15 | #[cfg(target_os = "windows")] 16 | return true; 17 | 18 | #[cfg(target_os = "linux")] 19 | return true; 20 | } 21 | 22 | /// Prompts user to grant screen capturing permission to current process 23 | pub fn request_permission() -> bool { 24 | #[cfg(target_os = "macos")] 25 | return mac::request_permission(); 26 | 27 | // assume windows to be true 28 | #[cfg(target_os = "windows")] 29 | return true; 30 | 31 | // TODO: check if linux has permission system 32 | #[cfg(target_os = "linux")] 33 | return true; 34 | } 35 | 36 | /// Checks if scap is supported on the current system 37 | pub fn is_supported() -> bool { 38 | #[cfg(target_os = "macos")] 39 | return mac::is_supported(); 40 | 41 | #[cfg(target_os = "windows")] 42 | return win::is_supported(); 43 | 44 | #[cfg(target_os = "linux")] 45 | return linux::is_supported(); 46 | } 47 | -------------------------------------------------------------------------------- /src/capturer/engine/linux/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::{self, Display, Formatter}, 4 | sync::PoisonError, 5 | }; 6 | 7 | use pipewire::spa::pod::serialize::GenError; 8 | 9 | #[derive(Debug)] 10 | pub struct LinCapError { 11 | msg: String, 12 | } 13 | 14 | impl Error for LinCapError {} 15 | 16 | impl LinCapError { 17 | pub fn new(msg: String) -> Self { 18 | Self { msg } 19 | } 20 | } 21 | 22 | impl Display for LinCapError { 23 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 24 | write!(f, "{}", self.msg) 25 | } 26 | } 27 | 28 | impl From for LinCapError { 29 | fn from(e: pipewire::Error) -> Self { 30 | Self::new(e.to_string()) 31 | } 32 | } 33 | 34 | impl From> for LinCapError { 35 | fn from(e: std::sync::mpsc::SendError) -> Self { 36 | Self::new(e.to_string()) 37 | } 38 | } 39 | 40 | impl From for LinCapError { 41 | fn from(e: GenError) -> Self { 42 | Self::new(e.to_string()) 43 | } 44 | } 45 | 46 | impl From for LinCapError { 47 | fn from(e: dbus::Error) -> Self { 48 | Self::new(e.to_string()) 49 | } 50 | } 51 | 52 | impl From> for LinCapError { 53 | fn from(e: PoisonError) -> Self { 54 | Self::new(e.to_string()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scap" 3 | description = "Modern, high-performance screen capture library for Rust. Cross-platform." 4 | version = "0.1.0-beta.1" 5 | edition = "2021" 6 | rust-version = "1.85" 7 | license = "MIT" 8 | authors = [ 9 | "Siddharth ", 10 | "Pranav ", 11 | ] 12 | readme = "README.md" 13 | repository = "https://github.com/helmerapp/scap" 14 | documentation = "https://docs.rs/scap" 15 | keywords = ["screen", "recording", "video", "capture", "media"] 16 | categories = ["graphics", "multimedia", "multimedia::video"] 17 | 18 | [dependencies] 19 | futures = "0.3.31" 20 | sysinfo = "0.30.0" 21 | thiserror = "2.0.12" 22 | 23 | [target.'cfg(target_os = "windows")'.dependencies] 24 | windows-capture = "1.5.0" 25 | windows = { version = "0.58", features = [ 26 | "Win32_Foundation", 27 | "Win32_Graphics_Gdi", 28 | "Win32_UI_HiDpi", 29 | "Win32_UI_WindowsAndMessaging", 30 | "Win32_System_Performance", 31 | ] } 32 | cpal = "0.15.3" 33 | 34 | [target.'cfg(target_os = "macos")'.dependencies] 35 | core-graphics-helmer-fork = "0.24.0" 36 | cocoa = "0.25.0" 37 | objc = "0.2.7" 38 | cidre = { version = "0.10.1", default-features = false, features = [ 39 | "async", 40 | "av", 41 | "sc", 42 | "dispatch", 43 | "macos_13_0", 44 | ] } 45 | 46 | [target.'cfg(target_os = "linux")'.dependencies] 47 | pipewire = "0.8.0" 48 | dbus = "0.9.7" 49 | rand = "0.8.5" 50 | -------------------------------------------------------------------------------- /src/targets/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | use futures::executor::block_on; 3 | 4 | #[cfg(target_os = "macos")] 5 | mod mac; 6 | 7 | #[cfg(target_os = "windows")] 8 | mod win; 9 | 10 | #[cfg(target_os = "linux")] 11 | mod linux; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct Window { 15 | pub id: u32, 16 | pub title: String, 17 | 18 | #[cfg(target_os = "windows")] 19 | pub raw_handle: windows::Win32::Foundation::HWND, 20 | 21 | #[cfg(target_os = "macos")] 22 | pub raw_handle: cidre::cg::WindowId, 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct Display { 27 | pub id: u32, 28 | pub title: String, 29 | 30 | #[cfg(target_os = "windows")] 31 | pub raw_handle: windows::Win32::Graphics::Gdi::HMONITOR, 32 | 33 | #[cfg(target_os = "macos")] 34 | pub raw_handle: cidre::cg::DirectDisplayId, 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub enum Target { 39 | Window(Window), 40 | Display(Display), 41 | } 42 | 43 | /// Returns a list of targets that can be captured 44 | pub fn get_all_targets() -> Vec { 45 | #[cfg(target_os = "macos")] 46 | return mac::get_all_targets(); 47 | 48 | #[cfg(target_os = "windows")] 49 | return win::get_all_targets(); 50 | 51 | #[cfg(target_os = "linux")] 52 | return linux::get_all_targets(); 53 | } 54 | 55 | pub fn get_scale_factor(target: &Target) -> f64 { 56 | #[cfg(target_os = "macos")] 57 | return mac::get_scale_factor(target); 58 | 59 | #[cfg(target_os = "windows")] 60 | return win::get_scale_factor(target); 61 | 62 | #[cfg(target_os = "linux")] 63 | return 1.0; 64 | } 65 | 66 | pub fn get_main_display() -> Display { 67 | #[cfg(target_os = "macos")] 68 | return mac::get_main_display(); 69 | 70 | #[cfg(target_os = "windows")] 71 | return win::get_main_display(); 72 | 73 | #[cfg(target_os = "linux")] 74 | unreachable!(); 75 | } 76 | 77 | pub fn get_target_dimensions(target: &Target) -> (u64, u64) { 78 | #[cfg(target_os = "macos")] 79 | return mac::get_target_dimensions(target); 80 | 81 | #[cfg(target_os = "windows")] 82 | return win::get_target_dimensions(target); 83 | 84 | #[cfg(target_os = "linux")] 85 | unreachable!(); 86 | } 87 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "scap", 3 | "projectOwner": "helmerapp", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributorsPerLine": 7, 10 | "contributors": [ 11 | { 12 | "login": "Pranav2612000", 13 | "name": "Pranav Joglekar", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/20909078?v=4", 15 | "profile": "https://github.com/Pranav2612000", 16 | "contributions": [ 17 | "code" 18 | ] 19 | }, 20 | { 21 | "login": "clearlysid", 22 | "name": "Siddharth", 23 | "avatar_url": "https://avatars.githubusercontent.com/u/30227512?v=4", 24 | "profile": "https://www.sid.me", 25 | "contributions": [ 26 | "code" 27 | ] 28 | }, 29 | { 30 | "login": "RohanPunjani", 31 | "name": "Rohan Punjani", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/48467821?v=4", 33 | "profile": "http://dev-rohan.in", 34 | "contributions": [ 35 | "code" 36 | ] 37 | }, 38 | { 39 | "login": "NiiightmareXD", 40 | "name": "NiiightmareXD", 41 | "avatar_url": "https://avatars.githubusercontent.com/u/90005793?v=4", 42 | "profile": "https://github.com/NiiightmareXD", 43 | "contributions": [ 44 | "code" 45 | ] 46 | }, 47 | { 48 | "login": "MAlba124", 49 | "name": "MAlba124", 50 | "avatar_url": "https://avatars.githubusercontent.com/u/83474682?v=4", 51 | "profile": "http://bringeber.dev", 52 | "contributions": [ 53 | "code" 54 | ] 55 | }, 56 | { 57 | "login": "anubhavitis", 58 | "name": "Anubhav Singhal", 59 | "avatar_url": "https://avatars.githubusercontent.com/u/26124625?v=4", 60 | "profile": "https://peerlist.io/anubhavitis", 61 | "contributions": [ 62 | "code" 63 | ] 64 | }, 65 | { 66 | "login": "vasusharma7", 67 | "name": "Vasu Sharma", 68 | "avatar_url": "https://avatars.githubusercontent.com/u/40715071?v=4", 69 | "profile": "http://linkedin.com/in/vasusharma7", 70 | "contributions": [ 71 | "code" 72 | ] 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.0.8](https://github.com/CapSoftware/scap/compare/v0.0.7...v0.0.8) - 2024-12-10 11 | 12 | ### Other 13 | 14 | - rewind version 15 | - Update mod.rs 16 | - Bump version (backwards compatible bug fix) 17 | - Change as_raw_nopadding_buffer to as_nopadding_buffer 18 | - Make GraphicsCaptureApiHandler's fn `new` implementation for Capturer have the correct signature 19 | - handle macos stream errors with error flag 20 | - expose CMSampleBuffer 21 | 22 | ## [0.0.7](https://github.com/CapSoftware/scap/compare/v0.0.6...v0.0.7) - 2024-11-07 23 | 24 | ### Features 25 | 26 | - Adds `RawCapturer::get_next_pixel_buffer`, a macOS-specific method to get the next frame as its raw pixel buffer, allowing allocations from pixel format conversion to be avoided. 27 | 28 | ### Fixed 29 | 30 | - `windows` crate versions updated (#120) 31 | 32 | ## [0.0.6](https://github.com/CapSoftware/scap/compare/v0.0.5...v0.0.6) - 2024-11-05 33 | 34 | ### Added 35 | 36 | - adds correct crop_area 37 | - get_crop_area for specific targets 38 | - adds scale_factor support for windows and displays on mac 39 | - get_main_display func improved 40 | - add unique identifier to unknown displays on mac 41 | - adds correct name of displays on macos 42 | - make scale_factor f64 43 | - exclude windows without title 44 | - adds windows as targets on mac 45 | - restructure util functions and add display name windows 46 | 47 | ### Fixed 48 | 49 | - Revert to DrawBorderSettings::Default on Windows 50 | - modified get_crop_area to include scale_factor for windows 51 | - minor change for scale factor 52 | - use cg types from sckit_sys 53 | - output frame size target 54 | - windows tweaks 55 | - macos imports after restructure 56 | 57 | ### Other 58 | 59 | - backwards compatability + enum error 60 | - vendor apple-sys bindings 61 | - Merge pull request [#95](https://github.com/CapSoftware/scap/pull/95) from MAlba124/main 62 | - Make STREAM_STATE_CHANGED_TO_ERROR reset on stop_capture 63 | - Fix restart on pipewire capturer 64 | - Merge pull request [#89](https://github.com/CapSoftware/scap/pull/89) from MAlba124/main 65 | - update .all-contributorsrc 66 | - update README.md 67 | - cleanup deps and remove cgtype in favor of area 68 | - update readme and add todo for windows 69 | - Merge branch 'feat/solo-target' into feat/use-targets-mac 70 | - Merge branch 'feat/solo-target' into feat/mac-targets-scale-factor 71 | - Merge pull request [#81](https://github.com/CapSoftware/scap/pull/81) from helmerapp/feat/windows-improvements 72 | - Merge branch 'main' into feat/windows-targets 73 | - Merge branch 'feat/windows-targets' of https://github.com/helmerapp/scap into feat/windows-targets 74 | - extract pixelformat conversions to different file 75 | - source rect simplifier 76 | - shorten width, height 77 | - windows engine 78 | - tweak example app 79 | - updates readme 80 | 81 | ## [0.0.5](https://github.com/helmerapp/scap/compare/v0.0.4...v0.0.5) - 2024-05-25 82 | 83 | ### Other 84 | - don't build before releasing 85 | - remove CHANGELOG 86 | -------------------------------------------------------------------------------- /src/targets/win/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{Display, Target}; 2 | use windows::Win32::UI::HiDpi::{GetDpiForMonitor, GetDpiForWindow, MDT_EFFECTIVE_DPI}; 3 | use windows::Win32::{ 4 | Foundation::{HWND, RECT}, 5 | Graphics::Gdi::HMONITOR, 6 | }; 7 | use windows_capture::{monitor::Monitor, window::Window}; 8 | 9 | pub fn get_all_targets() -> Vec { 10 | let mut targets: Vec = Vec::new(); 11 | 12 | // Add displays to targets 13 | let displays = Monitor::enumerate().expect("Failed to enumerate monitors"); 14 | for display in displays { 15 | let id = display.as_raw_hmonitor() as u32; 16 | let title = display.device_name().expect("Failed to get monitor name"); 17 | 18 | let target = Target::Display(super::Display { 19 | id, 20 | title, 21 | raw_handle: HMONITOR(display.as_raw_hmonitor()), 22 | }); 23 | targets.push(target); 24 | } 25 | 26 | // Add windows to targets 27 | let windows = Window::enumerate().expect("Failed to enumerate windows"); 28 | for window in windows { 29 | let id = window.as_raw_hwnd() as u32; 30 | let title = window.title().unwrap().to_string(); 31 | 32 | let target = Target::Window(super::Window { 33 | id, 34 | title, 35 | raw_handle: HWND(window.as_raw_hwnd()), 36 | }); 37 | targets.push(target); 38 | } 39 | 40 | targets 41 | } 42 | 43 | pub fn get_main_display() -> Display { 44 | let display = Monitor::primary().expect("Failed to get primary monitor"); 45 | let id = display.as_raw_hmonitor() as u32; 46 | 47 | Display { 48 | id, 49 | title: display.device_name().expect("Failed to get monitor name"), 50 | raw_handle: HMONITOR(display.as_raw_hmonitor()), 51 | } 52 | } 53 | 54 | // Referred to: https://github.com/tauri-apps/tao/blob/ab792dbd6c5f0a708c818b20eaff1d9a7534c7c1/src/platform_impl/windows/dpi.rs#L50 55 | pub fn get_scale_factor(target: &Target) -> f64 { 56 | const BASE_DPI: u32 = 96; 57 | 58 | let mut dpi_x = 0; 59 | let mut dpi_y = 0; 60 | 61 | let dpi = match target { 62 | Target::Window(window) => unsafe { GetDpiForWindow(window.raw_handle) }, 63 | Target::Display(display) => unsafe { 64 | if GetDpiForMonitor( 65 | display.raw_handle, 66 | MDT_EFFECTIVE_DPI, 67 | &mut dpi_x, 68 | &mut dpi_y, 69 | ) 70 | .is_ok() 71 | { 72 | dpi_x.into() 73 | } else { 74 | BASE_DPI 75 | } 76 | }, 77 | }; 78 | 79 | let scale_factor = dpi as f64 / BASE_DPI as f64; 80 | scale_factor as f64 81 | } 82 | 83 | pub fn get_target_dimensions(target: &Target) -> (u64, u64) { 84 | match target { 85 | Target::Window(window) => unsafe { 86 | let hwnd = window.raw_handle; 87 | 88 | // get width and height of the window 89 | let mut rect = RECT::default(); 90 | let _ = windows::Win32::UI::WindowsAndMessaging::GetWindowRect(hwnd, &mut rect); 91 | let width = rect.right - rect.left; 92 | let height = rect.bottom - rect.top; 93 | 94 | (width as u64, height as u64) 95 | }, 96 | Target::Display(display) => { 97 | let monitor = Monitor::from_raw_hmonitor(display.raw_handle.0); 98 | 99 | ( 100 | monitor.width().unwrap() as u64, 101 | monitor.height().unwrap() as u64, 102 | ) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/frame/audio.rs: -------------------------------------------------------------------------------- 1 | use std::{alloc::System, time::SystemTime}; 2 | 3 | pub struct AudioFrame { 4 | format: AudioFormat, 5 | channels: u16, 6 | is_planar: bool, 7 | data: Vec, 8 | sample_count: usize, 9 | rate: u32, 10 | timestamp: SystemTime, 11 | } 12 | 13 | impl AudioFrame { 14 | pub(crate) fn new( 15 | format: AudioFormat, 16 | channels: u16, 17 | is_planar: bool, 18 | data: Vec, 19 | sample_count: usize, 20 | rate: u32, 21 | timestamp: SystemTime, 22 | ) -> Self { 23 | assert!(data.len() >= sample_count * format.sample_size() as usize * channels as usize); 24 | 25 | Self { 26 | format, 27 | channels, 28 | is_planar, 29 | data, 30 | sample_count, 31 | rate, 32 | timestamp, 33 | } 34 | } 35 | 36 | pub fn format(&self) -> AudioFormat { 37 | self.format 38 | } 39 | 40 | pub fn planes(&self) -> u16 { 41 | if self.is_planar { 42 | self.channels 43 | } else { 44 | 1 45 | } 46 | } 47 | 48 | pub fn channels(&self) -> u16 { 49 | self.channels 50 | } 51 | 52 | pub fn rate(&self) -> u32 { 53 | self.rate 54 | } 55 | 56 | pub fn is_planar(&self) -> bool { 57 | self.is_planar 58 | } 59 | 60 | pub fn raw_data(&self) -> &[u8] { 61 | &self.data 62 | } 63 | 64 | pub fn sample_count(&self) -> usize { 65 | self.sample_count 66 | } 67 | 68 | pub fn time(&self) -> SystemTime { 69 | self.timestamp 70 | } 71 | 72 | pub fn plane_data(&self, plane: usize) -> &[u8] { 73 | if !self.is_planar { 74 | return &self.data; 75 | } else { 76 | let plane_size = self.sample_count * self.format.sample_size() as usize; 77 | let base = plane * plane_size; 78 | &self.data[base..base + plane_size] 79 | } 80 | } 81 | } 82 | 83 | #[non_exhaustive] 84 | #[derive(Debug, Clone, Copy)] 85 | pub enum AudioFormat { 86 | I8, 87 | I16, 88 | I32, 89 | I64, 90 | U8, 91 | U16, 92 | U32, 93 | U64, 94 | F32, 95 | F64, 96 | } 97 | 98 | impl AudioFormat { 99 | pub fn sample_size(&self) -> usize { 100 | match self { 101 | Self::I8 => std::mem::size_of::(), 102 | Self::I16 => std::mem::size_of::(), 103 | Self::I32 => std::mem::size_of::(), 104 | Self::I64 => std::mem::size_of::(), 105 | Self::U8 => std::mem::size_of::(), 106 | Self::U16 => std::mem::size_of::(), 107 | Self::U32 => std::mem::size_of::(), 108 | Self::U64 => std::mem::size_of::(), 109 | Self::F32 => std::mem::size_of::(), 110 | Self::F64 => std::mem::size_of::(), 111 | // v => panic!("Sample format {v:?} not supported"), 112 | } 113 | } 114 | } 115 | 116 | #[cfg(windows)] 117 | impl From for AudioFormat { 118 | fn from(value: cpal::SampleFormat) -> Self { 119 | match value { 120 | cpal::SampleFormat::F32 => Self::F32, 121 | cpal::SampleFormat::F64 => Self::F64, 122 | cpal::SampleFormat::I8 => Self::I8, 123 | cpal::SampleFormat::I16 => Self::I16, 124 | cpal::SampleFormat::I32 => Self::I32, 125 | cpal::SampleFormat::I64 => Self::I64, 126 | cpal::SampleFormat::U8 => Self::U8, 127 | cpal::SampleFormat::U16 => Self::U16, 128 | cpal::SampleFormat::U32 => Self::U32, 129 | cpal::SampleFormat::U64 => Self::U64, 130 | _ => panic!("sample format {value:?} not supported"), 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/capturer/engine/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | 3 | use super::Options; 4 | use crate::frame::Frame; 5 | 6 | #[cfg(target_os = "macos")] 7 | pub mod mac; 8 | 9 | #[cfg(target_os = "windows")] 10 | mod win; 11 | 12 | #[cfg(target_os = "linux")] 13 | mod linux; 14 | 15 | #[cfg(target_os = "macos")] 16 | pub type ChannelItem = ( 17 | cidre::arc::R, 18 | cidre::sc::stream::OutputType, 19 | ); 20 | #[cfg(not(target_os = "macos"))] 21 | pub type ChannelItem = Frame; 22 | 23 | pub fn get_output_frame_size(options: &Options) -> [u32; 2] { 24 | #[cfg(target_os = "macos")] 25 | { 26 | mac::get_output_frame_size(options) 27 | } 28 | 29 | #[cfg(target_os = "windows")] 30 | { 31 | win::get_output_frame_size(options) 32 | } 33 | 34 | #[cfg(target_os = "linux")] 35 | { 36 | // TODO: How to calculate this on Linux? 37 | return [0, 0]; 38 | } 39 | } 40 | 41 | pub struct Engine { 42 | options: Options, 43 | 44 | #[cfg(target_os = "macos")] 45 | mac: ( 46 | cidre::arc::R, 47 | cidre::arc::R, 48 | cidre::arc::R, 49 | ), 50 | #[cfg(target_os = "macos")] 51 | error_flag: std::sync::Arc, 52 | 53 | #[cfg(target_os = "windows")] 54 | win: win::WCStream, 55 | 56 | #[cfg(target_os = "linux")] 57 | linux: linux::LinuxCapturer, 58 | } 59 | 60 | impl Engine { 61 | pub fn new(options: &Options, tx: mpsc::Sender) -> Engine { 62 | #[cfg(target_os = "macos")] 63 | { 64 | let error_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); 65 | let mac = mac::create_capturer(options, tx, error_flag.clone()).unwrap(); 66 | 67 | Engine { 68 | mac, 69 | error_flag, 70 | options: (*options).clone(), 71 | } 72 | } 73 | 74 | #[cfg(target_os = "windows")] 75 | { 76 | let win = win::create_capturer(&options, tx).unwrap(); 77 | return Engine { 78 | win, 79 | options: (*options).clone(), 80 | }; 81 | } 82 | 83 | #[cfg(target_os = "linux")] 84 | { 85 | let linux = linux::create_capturer(&options, tx); 86 | return Engine { 87 | linux, 88 | options: (*options).clone(), 89 | }; 90 | } 91 | } 92 | 93 | pub fn start(&mut self) { 94 | #[cfg(target_os = "macos")] 95 | { 96 | use futures::executor::block_on; 97 | 98 | block_on(self.mac.2.start()).expect("Failed to start capture"); 99 | } 100 | 101 | #[cfg(target_os = "windows")] 102 | { 103 | self.win.start_capture(); 104 | } 105 | 106 | #[cfg(target_os = "linux")] 107 | { 108 | self.linux.start_capture(); 109 | } 110 | } 111 | 112 | pub fn stop(&mut self) { 113 | #[cfg(target_os = "macos")] 114 | { 115 | use futures::executor::block_on; 116 | 117 | block_on(self.mac.2.stop()).expect("Failed to stop capture"); 118 | } 119 | 120 | #[cfg(target_os = "windows")] 121 | { 122 | self.win.stop_capture(); 123 | } 124 | 125 | #[cfg(target_os = "linux")] 126 | { 127 | self.linux.stop_capture(); 128 | } 129 | } 130 | 131 | pub fn get_output_frame_size(&mut self) -> [u32; 2] { 132 | get_output_frame_size(&self.options) 133 | } 134 | 135 | pub fn process_channel_item(&self, data: ChannelItem) -> Option { 136 | #[cfg(target_os = "macos")] 137 | { 138 | mac::process_sample_buffer(data.0, data.1, self.options.output_type) 139 | } 140 | #[cfg(not(target_os = "macos"))] 141 | return Some(data); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // This program is just a testing application 2 | // Refer to `lib.rs` for the library source code 3 | 4 | use scap::{ 5 | capturer::{Area, Capturer, Options, Point, Size}, 6 | frame::{Frame, VideoFrame}, 7 | }; 8 | use std::process; 9 | 10 | fn main() { 11 | // Check if the platform is supported 12 | if !scap::is_supported() { 13 | println!("❌ Platform not supported"); 14 | return; 15 | } 16 | 17 | // Check if we have permission to capture screen 18 | // If we don't, request it. 19 | if !scap::has_permission() { 20 | println!("❌ Permission not granted. Requesting permission..."); 21 | if !scap::request_permission() { 22 | println!("❌ Permission denied"); 23 | return; 24 | } 25 | } 26 | 27 | // // Get recording targets 28 | // let targets = scap::get_all_targets(); 29 | 30 | // Create Options 31 | let options = Options { 32 | fps: 60, 33 | show_cursor: true, 34 | show_highlight: false, 35 | excluded_targets: None, 36 | output_type: scap::frame::FrameType::BGRAFrame, 37 | output_resolution: scap::capturer::Resolution::_720p, 38 | crop_area: Some(Area { 39 | origin: Point { x: 0.0, y: 0.0 }, 40 | size: Size { 41 | width: 500.0, 42 | height: 500.0, 43 | }, 44 | }), 45 | captures_audio: true, 46 | ..Default::default() 47 | }; 48 | 49 | // Create Recorder with options 50 | let mut recorder = Capturer::build(options).unwrap_or_else(|err| { 51 | println!("Problem with building Capturer: {err}"); 52 | process::exit(1); 53 | }); 54 | 55 | // Start Capture 56 | recorder.start_capture(); 57 | 58 | // Capture 100 frames 59 | for i in 0..100 { 60 | let frame = loop { 61 | match recorder.get_next_frame().expect("Error") { 62 | Frame::Video(frame) => { 63 | break frame; 64 | } 65 | Frame::Audio(_) => { 66 | continue; 67 | } 68 | } 69 | }; 70 | 71 | match frame { 72 | VideoFrame::YUVFrame(frame) => { 73 | println!( 74 | "Recieved YUV frame {} of width {} and height {} and pts {:?}", 75 | i, frame.width, frame.height, frame.display_time 76 | ); 77 | } 78 | VideoFrame::BGR0(frame) => { 79 | println!( 80 | "Received BGR0 frame of width {} and height {}", 81 | frame.width, frame.height 82 | ); 83 | } 84 | VideoFrame::RGB(frame) => { 85 | println!( 86 | "Recieved RGB frame {} of width {} and height {} and time {:?}", 87 | i, frame.width, frame.height, frame.display_time 88 | ); 89 | } 90 | VideoFrame::RGBx(frame) => { 91 | println!( 92 | "Recieved RGBx frame of width {} and height {}", 93 | frame.width, frame.height 94 | ); 95 | } 96 | VideoFrame::XBGR(frame) => { 97 | println!( 98 | "Recieved xRGB frame of width {} and height {}", 99 | frame.width, frame.height 100 | ); 101 | } 102 | VideoFrame::BGRx(frame) => { 103 | println!( 104 | "Recieved BGRx frame of width {} and height {}", 105 | frame.width, frame.height 106 | ); 107 | } 108 | VideoFrame::BGRA(frame) => { 109 | println!( 110 | "Recieved BGRA frame {} of width {} and height {} and time {:?}", 111 | i, frame.width, frame.height, frame.display_time 112 | ); 113 | } 114 | } 115 | } 116 | 117 | // Stop Capture 118 | recorder.stop_capture(); 119 | } 120 | -------------------------------------------------------------------------------- /src/targets/mac/mod.rs: -------------------------------------------------------------------------------- 1 | use cidre::{cg, ns, sc}; 2 | use cocoa::appkit::{NSApp, NSScreen}; 3 | use cocoa::base::{id, nil}; 4 | use cocoa::foundation::{NSRect, NSString, NSUInteger}; 5 | use futures::executor::block_on; 6 | use objc::{msg_send, sel, sel_impl}; 7 | 8 | use crate::engine::mac::ext::DirectDisplayIdExt; 9 | 10 | use super::{Display, Target}; 11 | 12 | fn get_display_name(display_id: cg::DirectDisplayId) -> String { 13 | unsafe { 14 | // Get all screens 15 | let screens: id = NSScreen::screens(nil); 16 | let count: u64 = msg_send![screens, count]; 17 | 18 | for i in 0..count { 19 | let screen: id = msg_send![screens, objectAtIndex: i]; 20 | let device_description: id = msg_send![screen, deviceDescription]; 21 | let display_id_number: id = msg_send![device_description, objectForKey: NSString::alloc(nil).init_str("NSScreenNumber")]; 22 | let display_id_number: u32 = msg_send![display_id_number, unsignedIntValue]; 23 | 24 | if display_id_number == display_id.0 { 25 | let localized_name: id = msg_send![screen, localizedName]; 26 | let name: *const i8 = msg_send![localized_name, UTF8String]; 27 | return std::ffi::CStr::from_ptr(name) 28 | .to_string_lossy() 29 | .into_owned(); 30 | } 31 | } 32 | 33 | format!("Unknown Display {}", display_id.0) 34 | } 35 | } 36 | 37 | pub fn get_all_targets() -> Vec { 38 | let mut targets: Vec = Vec::new(); 39 | 40 | let content = block_on(sc::ShareableContent::current()).unwrap(); 41 | 42 | // Add displays to targets 43 | for display in content.displays().iter() { 44 | let id = display.display_id(); 45 | 46 | let title = get_display_name(id); 47 | 48 | let target = Target::Display(super::Display { 49 | id: id.0, 50 | title, 51 | raw_handle: id, 52 | }); 53 | 54 | targets.push(target); 55 | } 56 | 57 | // Add windows to targets 58 | for window in content.windows().iter() { 59 | let id = window.id(); 60 | let title = window 61 | .title() 62 | // on intel chips we can have Some but also a null pointer for some reason 63 | .filter(|v| !unsafe { v.utf8_chars_ar().is_null() }); 64 | 65 | let target = Target::Window(super::Window { 66 | id, 67 | title: title.map(|v| v.to_string()).unwrap_or_default(), 68 | raw_handle: id, 69 | }); 70 | targets.push(target); 71 | } 72 | 73 | targets 74 | } 75 | 76 | pub fn get_main_display() -> Display { 77 | let id = cg::direct_display::Id::main(); 78 | let title = get_display_name(id); 79 | 80 | Display { 81 | id: id.0, 82 | title, 83 | raw_handle: id, 84 | } 85 | } 86 | 87 | pub fn get_scale_factor(target: &Target) -> f64 { 88 | match target { 89 | Target::Window(window) => unsafe { 90 | let cg_win_id = window.raw_handle; 91 | let ns_app: id = NSApp(); 92 | let ns_window: id = msg_send![ns_app, windowWithWindowNumber: cg_win_id as NSUInteger]; 93 | let scale_factor: f64 = msg_send![ns_window, backingScaleFactor]; 94 | scale_factor 95 | }, 96 | Target::Display(display) => { 97 | let mode = display.raw_handle.display_mode().unwrap(); 98 | (mode.pixel_width() / mode.width()) as f64 99 | } 100 | } 101 | } 102 | 103 | pub fn get_target_dimensions(target: &Target) -> (u64, u64) { 104 | match target { 105 | Target::Window(window) => unsafe { 106 | let cg_win_id = window.raw_handle; 107 | let ns_app: id = NSApp(); 108 | let ns_window: id = msg_send![ns_app, windowWithWindowNumber: cg_win_id as NSUInteger]; 109 | let frame: NSRect = msg_send![ns_window, frame]; 110 | (frame.size.width as u64, frame.size.height as u64) 111 | }, 112 | Target::Display(display) => { 113 | let mode = display.raw_handle.display_mode().unwrap(); 114 | (mode.width(), mode.height()) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/capturer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod engine; 2 | 3 | use std::{error::Error, sync::mpsc}; 4 | 5 | use engine::ChannelItem; 6 | 7 | use crate::{ 8 | frame::{Frame, FrameType, VideoFrame}, 9 | has_permission, is_supported, 10 | targets::Target, 11 | }; 12 | 13 | pub use engine::get_output_frame_size; 14 | 15 | #[derive(Debug, Clone, Copy, Default)] 16 | pub enum Resolution { 17 | _480p, 18 | _720p, 19 | _1080p, 20 | _1440p, 21 | _2160p, 22 | _4320p, 23 | 24 | #[default] 25 | Captured, 26 | } 27 | 28 | impl Resolution { 29 | fn value(&self, aspect_ratio: f32) -> [u32; 2] { 30 | match *self { 31 | Resolution::_480p => [640, (640_f32 / aspect_ratio).floor() as u32], 32 | Resolution::_720p => [1280, (1280_f32 / aspect_ratio).floor() as u32], 33 | Resolution::_1080p => [1920, (1920_f32 / aspect_ratio).floor() as u32], 34 | Resolution::_1440p => [2560, (2560_f32 / aspect_ratio).floor() as u32], 35 | Resolution::_2160p => [3840, (3840_f32 / aspect_ratio).floor() as u32], 36 | Resolution::_4320p => [7680, (7680_f32 / aspect_ratio).floor() as u32], 37 | Resolution::Captured => { 38 | panic!(".value should not be called when Resolution type is Captured") 39 | } 40 | } 41 | } 42 | } 43 | 44 | #[derive(Debug, Default, Clone)] 45 | pub struct Point { 46 | pub x: f64, 47 | pub y: f64, 48 | } 49 | 50 | #[derive(Debug, Default, Clone)] 51 | pub struct Size { 52 | pub width: f64, 53 | pub height: f64, 54 | } 55 | #[derive(Debug, Default, Clone)] 56 | pub struct Area { 57 | pub origin: Point, 58 | pub size: Size, 59 | } 60 | 61 | /// Options passed to the screen capturer 62 | #[derive(Debug, Default, Clone)] 63 | pub struct Options { 64 | pub fps: u32, 65 | pub show_cursor: bool, 66 | pub show_highlight: bool, 67 | pub target: Option, 68 | pub crop_area: Option, 69 | pub output_type: FrameType, 70 | pub output_resolution: Resolution, 71 | // excluded targets will only work on macOS 72 | pub excluded_targets: Option>, 73 | /// Only implemented for Windows and macOS currently 74 | pub captures_audio: bool, 75 | pub exclude_current_process_audio: bool, 76 | } 77 | 78 | /// Screen capturer class 79 | pub struct Capturer { 80 | engine: engine::Engine, 81 | rx: mpsc::Receiver, 82 | } 83 | 84 | #[derive(Debug)] 85 | pub enum CapturerBuildError { 86 | NotSupported, 87 | PermissionNotGranted, 88 | } 89 | 90 | impl std::fmt::Display for CapturerBuildError { 91 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 92 | match self { 93 | CapturerBuildError::NotSupported => write!(f, "Screen capturing is not supported"), 94 | CapturerBuildError::PermissionNotGranted => { 95 | write!(f, "Permission to capture the screen is not granted") 96 | } 97 | } 98 | } 99 | } 100 | 101 | impl Error for CapturerBuildError {} 102 | 103 | impl Capturer { 104 | /// Build a new [Capturer] instance with the provided options 105 | pub fn build(options: Options) -> Result { 106 | if !is_supported() { 107 | return Err(CapturerBuildError::NotSupported); 108 | } 109 | 110 | if !has_permission() { 111 | return Err(CapturerBuildError::PermissionNotGranted); 112 | } 113 | 114 | let (tx, rx) = mpsc::channel(); 115 | let engine = engine::Engine::new(&options, tx); 116 | 117 | Ok(Capturer { engine, rx }) 118 | } 119 | 120 | // TODO 121 | // Prevent starting capture if already started 122 | /// Start capturing the frames 123 | pub fn start_capture(&mut self) { 124 | self.engine.start(); 125 | } 126 | 127 | /// Stop the capturer 128 | pub fn stop_capture(&mut self) { 129 | self.engine.stop(); 130 | } 131 | 132 | /// Get the next captured frame 133 | pub fn get_next_frame(&self) -> Result { 134 | loop { 135 | let res = self.rx.recv()?; 136 | 137 | if let Some(frame) = self.engine.process_channel_item(res) { 138 | return Ok(frame); 139 | } 140 | } 141 | } 142 | 143 | /// Get the dimensions the frames will be captured in 144 | pub fn get_output_frame_size(&mut self) -> [u32; 2] { 145 | self.engine.get_output_frame_size() 146 | } 147 | 148 | pub fn raw(&self) -> RawCapturer { 149 | RawCapturer { capturer: self } 150 | } 151 | } 152 | 153 | pub struct RawCapturer<'a> { 154 | capturer: &'a Capturer, 155 | } 156 | -------------------------------------------------------------------------------- /src/frame/video.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct YUVFrame { 5 | pub display_time: SystemTime, 6 | pub width: i32, 7 | pub height: i32, 8 | pub luminance_bytes: Vec, 9 | pub luminance_stride: i32, 10 | pub chrominance_bytes: Vec, 11 | pub chrominance_stride: i32, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct RGBFrame { 16 | pub display_time: SystemTime, 17 | pub width: i32, 18 | pub height: i32, 19 | pub data: Vec, 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct RGB8Frame { 24 | pub display_time: SystemTime, 25 | pub width: i32, 26 | pub height: i32, 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct RGBxFrame { 31 | pub display_time: SystemTime, 32 | pub width: i32, 33 | pub height: i32, 34 | pub data: Vec, 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct XBGRFrame { 39 | pub display_time: SystemTime, 40 | pub width: i32, 41 | pub height: i32, 42 | pub data: Vec, 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub struct BGRxFrame { 47 | pub display_time: SystemTime, 48 | pub width: i32, 49 | pub height: i32, 50 | pub data: Vec, 51 | } 52 | 53 | #[derive(Debug, Clone)] 54 | pub struct BGRFrame { 55 | pub display_time: SystemTime, 56 | pub width: i32, 57 | pub height: i32, 58 | pub data: Vec, 59 | } 60 | 61 | #[derive(Debug, Clone)] 62 | pub struct BGRAFrame { 63 | pub display_time: SystemTime, 64 | pub width: i32, 65 | pub height: i32, 66 | pub data: Vec, 67 | } 68 | 69 | #[derive(Debug, Clone, Copy, Default)] 70 | pub enum FrameType { 71 | #[default] 72 | YUVFrame, 73 | BGR0, 74 | RGB, // Prefer BGR0 because RGB is slower 75 | BGRAFrame, 76 | } 77 | 78 | #[derive(Debug, Clone)] 79 | pub enum VideoFrame { 80 | YUVFrame(YUVFrame), 81 | RGB(RGBFrame), 82 | RGBx(RGBxFrame), 83 | XBGR(XBGRFrame), 84 | BGRx(BGRxFrame), 85 | BGR0(BGRFrame), 86 | BGRA(BGRAFrame), 87 | } 88 | 89 | pub enum FrameData<'a> { 90 | NV12(&'a YUVFrame), 91 | BGR0(&'a [u8]), 92 | } 93 | 94 | pub fn remove_alpha_channel(frame_data: Vec) -> Vec { 95 | let width = frame_data.len(); 96 | let width_without_alpha = (width / 4) * 3; 97 | 98 | let mut data: Vec = vec![0; width_without_alpha]; 99 | 100 | for (src, dst) in frame_data.chunks_exact(4).zip(data.chunks_exact_mut(3)) { 101 | dst[0] = src[0]; 102 | dst[1] = src[1]; 103 | dst[2] = src[2]; 104 | } 105 | 106 | data 107 | } 108 | 109 | pub fn convert_bgra_to_rgb(frame_data: Vec) -> Vec { 110 | let width = frame_data.len(); 111 | let width_without_alpha = (width / 4) * 3; 112 | 113 | let mut data: Vec = vec![0; width_without_alpha]; 114 | 115 | for (src, dst) in frame_data.chunks_exact(4).zip(data.chunks_exact_mut(3)) { 116 | dst[0] = src[2]; 117 | dst[1] = src[1]; 118 | dst[2] = src[0]; 119 | } 120 | 121 | data 122 | } 123 | 124 | pub fn get_cropped_data(data: Vec, cur_width: i32, height: i32, width: i32) -> Vec { 125 | if data.len() as i32 != height * cur_width * 4 { 126 | data 127 | } else { 128 | let mut cropped_data: Vec = vec![0; (4 * height * width).try_into().unwrap()]; 129 | let mut cropped_data_index = 0; 130 | 131 | for (i, item) in data.iter().enumerate() { 132 | let x = i as i32 % (cur_width * 4); 133 | if x < (width * 4) { 134 | cropped_data[cropped_data_index] = *item; 135 | cropped_data_index += 1; 136 | } 137 | } 138 | cropped_data 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use super::*; 145 | 146 | #[test] 147 | fn test_remove_alpha_channel() { 148 | assert_eq!(remove_alpha_channel(vec![1, 2, 3, 0]), vec![1, 2, 3]); 149 | assert_eq!( 150 | remove_alpha_channel(vec![1, 2, 3, 4, 5, 6, 7, 8]), 151 | vec![1, 2, 3, 5, 6, 7] 152 | ); 153 | } 154 | 155 | #[test] 156 | fn test_convert_bgra_to_rgb() { 157 | assert_eq!(convert_bgra_to_rgb(vec![1, 2, 3, 0]), vec![3, 2, 1]); 158 | assert_eq!( 159 | convert_bgra_to_rgb(vec![1, 2, 3, 4, 5, 6, 7, 8]), 160 | vec![3, 2, 1, 7, 6, 5] 161 | ); 162 | } 163 | 164 | macro_rules! rgba { 165 | ($n:expr) => { 166 | &mut vec![$n, $n, $n, $n] 167 | }; 168 | } 169 | 170 | #[test] 171 | pub fn test_get_cropped_data() { 172 | let mut data: Vec = Vec::new(); 173 | for i in 1..=9 { 174 | data.append(rgba!(i)); 175 | } 176 | let mut expected: Vec = Vec::new(); 177 | expected.append(rgba!(1)); 178 | expected.append(rgba!(2)); 179 | expected.append(rgba!(4)); 180 | expected.append(rgba!(5)); 181 | expected.append(rgba!(7)); 182 | expected.append(rgba!(8)); 183 | assert_eq!(get_cropped_data(data, 3, 3, 2), expected) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/capturer/engine/mac/pixelformat.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use cidre::{cm, cv}; 4 | 5 | use crate::frame::{ 6 | convert_bgra_to_rgb, get_cropped_data, remove_alpha_channel, BGRAFrame, BGRFrame, RGBFrame, 7 | YUVFrame, 8 | }; 9 | 10 | pub unsafe fn create_yuv_frame( 11 | sample_buffer: &mut cm::SampleBuf, 12 | display_time: SystemTime, 13 | ) -> Option { 14 | let image_buffer = sample_buffer.image_buf_mut().unwrap(); 15 | 16 | unsafe { 17 | image_buffer 18 | .lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT) 19 | .result() 20 | .unwrap() 21 | }; 22 | 23 | let width = image_buffer.width(); 24 | let height = image_buffer.height(); 25 | 26 | if width == 0 || height == 0 { 27 | return None; 28 | } 29 | 30 | let luminance_stride = image_buffer.plane_bytes_per_row(0); 31 | let luminance_bytes = unsafe { 32 | std::slice::from_raw_parts( 33 | image_buffer.plane_base_address(0), 34 | luminance_stride * image_buffer.plane_height(0), 35 | ) 36 | } 37 | .to_vec(); 38 | 39 | let chrominance_stride = image_buffer.plane_bytes_per_row(0); 40 | let chrominance_bytes = unsafe { 41 | std::slice::from_raw_parts( 42 | image_buffer.plane_base_address(0), 43 | luminance_stride * image_buffer.plane_height(0), 44 | ) 45 | } 46 | .to_vec(); 47 | 48 | unsafe { 49 | image_buffer 50 | .unlock_lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT) 51 | .result() 52 | .unwrap() 53 | }; 54 | 55 | Some(YUVFrame { 56 | display_time, 57 | width: width as i32, 58 | height: height as i32, 59 | luminance_bytes, 60 | luminance_stride: luminance_stride as i32, 61 | chrominance_bytes, 62 | chrominance_stride: chrominance_stride as i32, 63 | }) 64 | } 65 | 66 | pub unsafe fn create_bgr_frame( 67 | sample_buffer: &mut cm::SampleBuf, 68 | display_time: SystemTime, 69 | ) -> Option { 70 | let image_buffer = sample_buffer.image_buf_mut().unwrap(); 71 | 72 | unsafe { 73 | image_buffer 74 | .lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT) 75 | .result() 76 | .unwrap() 77 | }; 78 | 79 | let width = image_buffer.width(); 80 | let height = image_buffer.height(); 81 | 82 | if width == 0 || height == 0 { 83 | return None; 84 | } 85 | 86 | let stride = image_buffer.plane_bytes_per_row(0); 87 | let bytes = unsafe { 88 | std::slice::from_raw_parts( 89 | image_buffer.plane_base_address(0), 90 | stride * image_buffer.plane_height(0), 91 | ) 92 | } 93 | .to_vec(); 94 | 95 | let cropped_data = get_cropped_data(bytes, (stride / 4) as i32, height as i32, width as i32); 96 | 97 | Some(BGRFrame { 98 | display_time, 99 | width: width as i32, // width does not give accurate results - https://stackoverflow.com/questions/19587185/cvpixelbuffergetbytesperrow-for-cvimagebufferref-returns-unexpected-wrong-valu 100 | height: height as i32, 101 | data: remove_alpha_channel(cropped_data), 102 | }) 103 | } 104 | 105 | pub unsafe fn create_bgra_frame( 106 | sample_buffer: &mut cm::SampleBuf, 107 | display_time: SystemTime, 108 | ) -> Option { 109 | let image_buffer = sample_buffer.image_buf_mut().unwrap(); 110 | 111 | unsafe { 112 | image_buffer 113 | .lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT) 114 | .result() 115 | .unwrap() 116 | }; 117 | 118 | let width = image_buffer.width(); 119 | let height = image_buffer.height(); 120 | 121 | if width == 0 || height == 0 { 122 | return None; 123 | } 124 | 125 | let stride = image_buffer.plane_bytes_per_row(0); 126 | 127 | let mut data: Vec = vec![]; 128 | 129 | let bytes = unsafe { 130 | std::slice::from_raw_parts( 131 | image_buffer.plane_base_address(0), 132 | stride * image_buffer.plane_height(0), 133 | ) 134 | }; 135 | 136 | for i in 0..height { 137 | let base = i * stride; 138 | data.extend_from_slice(&bytes[base as usize..(base + 4 * width) as usize]); 139 | } 140 | 141 | Some(BGRAFrame { 142 | display_time, 143 | width: width as i32, // width does not give accurate results - https://stackoverflow.com/questions/19587185/cvpixelbuffergetbytesperrow-for-cvimagebufferref-returns-unexpected-wrong-valu 144 | height: height as i32, 145 | data, 146 | }) 147 | } 148 | 149 | pub unsafe fn create_rgb_frame( 150 | sample_buffer: &mut cm::SampleBuf, 151 | display_time: SystemTime, 152 | ) -> Option { 153 | let image_buffer = sample_buffer.image_buf_mut().unwrap(); 154 | 155 | unsafe { 156 | image_buffer 157 | .lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT) 158 | .result() 159 | .unwrap() 160 | }; 161 | 162 | let width = image_buffer.width(); 163 | let height = image_buffer.height(); 164 | 165 | if width == 0 || height == 0 { 166 | return None; 167 | } 168 | 169 | let stride = image_buffer.plane_bytes_per_row(0); 170 | 171 | let bytes = unsafe { 172 | std::slice::from_raw_parts( 173 | image_buffer.plane_base_address(0), 174 | stride * image_buffer.plane_height(0), 175 | ) 176 | } 177 | .to_vec(); 178 | 179 | let cropped_data = get_cropped_data(bytes, (stride / 4) as i32, height as i32, width as i32); 180 | 181 | Some(RGBFrame { 182 | display_time, 183 | width: width as i32, // width does not give accurate results - https://stackoverflow.com/questions/19587185/cvpixelbuffergetbytesperrow-for-cvimagebufferref-returns-unexpected-wrong-valu 184 | height: height as i32, 185 | data: convert_bgra_to_rgb(cropped_data), 186 | }) 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Github banner](./.github/banner.gif) 2 | 3 | [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://cap.link/discord) 4 | [![Twitter](https://img.shields.io/badge/twitter-blue?style=for-the-badge&logo=twitter&logoColor=white&labelColor=%231DA1F2&color=%231DA1F2)](https://www.x.com/cap) 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/capsoftware/scap?style=for-the-badge&logo=github&label=Github%20Stars&labelColor=black) 6 | ![docs.rs](https://img.shields.io/docsrs/scap?style=for-the-badge&logo=rust&logoColor=white&labelColor=black) 7 | ![Crates.io MSRV](https://img.shields.io/crates/msrv/scap?style=for-the-badge&logo=rust&logoColor=white&labelColor=black) 8 | 9 | A Rust library for high-quality screen capture that leverages native OS APIs for optimal performance! 10 | 11 | 1. macOS: [ScreenCaptureKit](https://developer.apple.com/documentation/screencapturekit) 12 | 2. Windows: [Windows.Graphics.Capture](https://learn.microsoft.com/en-us/uwp/api/windows.graphics.capture?view=winrt-22621) 13 | 3. Linux: [Pipewire](https://pipewire.org) 14 | 15 | --- 16 | 17 | ## Features 18 | 19 | 1. Cross-platform across Windows, Mac and Linux! 20 | 2. Checks for support and recording permissions. 21 | 3. Query list of captureable targets (displays and windows). 22 | 4. Exclude certain targets from being captured. 23 | 24 | ## Contributing 25 | 26 | We found most of Rust's tooling around screen capture either very outdated, non-performant or platform-specific. This project is our attempt to change that. Contributions, PRs and Issues are most welcome! 27 | 28 | If you want to contribute code, here's a quick primer: 29 | 30 | 1. Clone the repo and run it with `cargo run`. 31 | 2. Explore the API and library code in [lib.rs](./src/lib.rs). 32 | 3. Platform-specific code lives in the `win`, `mac` and `linux` modules. 33 | 4. The [main.rs](./src/main.rs) is a small program that "consumes" the library, for easy testing. 34 | 35 | ## Usage 36 | 37 | ```rust 38 | use scap::{ 39 | capturer::{Point, Area, Size, Capturer, Options}, 40 | frame::Frame, 41 | }; 42 | 43 | fn main() { 44 | // Check if the platform is supported 45 | if !scap::is_supported() { 46 | println!("❌ Platform not supported"); 47 | return; 48 | } 49 | 50 | // Check if we have permission to capture screen 51 | // If we don't, request it. 52 | if !scap::has_permission() { 53 | println!("❌ Permission not granted. Requesting permission..."); 54 | if !scap::request_permission() { 55 | println!("❌ Permission denied"); 56 | return; 57 | } 58 | } 59 | 60 | // Get recording targets 61 | let targets = scap::get_all_targets(); 62 | println!("Targets: {:?}", targets); 63 | 64 | // All your displays and windows are targets 65 | // You can filter this and capture the one you need. 66 | 67 | // Create Options 68 | let options = Options { 69 | fps: 60, 70 | target: None, // None captures the primary display 71 | show_cursor: true, 72 | show_highlight: true, 73 | excluded_targets: None, 74 | output_type: scap::frame::FrameType::BGRAFrame, 75 | output_resolution: scap::capturer::Resolution::_720p, 76 | crop_area: Some(Area { 77 | origin: Point { x: 0.0, y: 0.0 }, 78 | size: Size { 79 | width: 2000.0, 80 | height: 1000.0, 81 | }, 82 | }), 83 | ..Default::default() 84 | }; 85 | 86 | // Create Capturer 87 | let mut capturer = Capturer::build(options).unwrap(); 88 | 89 | // Start Capture 90 | capturer.start_capture(); 91 | 92 | let mut input = String::new(); 93 | std::io::stdin().read_line(&mut input).unwrap(); 94 | 95 | // Stop Capture 96 | capturer.stop_capture(); 97 | } 98 | ``` 99 | 100 | ## License 101 | 102 | The code in this repository is open-sourced under the MIT license, though it may be relying on dependencies that are licensed differently. Please consult their documentation for exact terms. 103 | 104 | ## Contributors 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
Pranav Joglekar
Pranav Joglekar

💻
Siddharth
Siddharth

💻
Rohan Punjani
Rohan Punjani

💻
NiiightmareXD
NiiightmareXD

💻
MAlba124
MAlba124

💻
Anubhav Singhal
Anubhav Singhal

💻
Vasu Sharma
Vasu Sharma

💻
122 | 123 | 124 | 125 | 126 | 127 | 128 | ## Credits 129 | 130 | This project builds on top of the fabulous work done by: 131 | 132 | - [@MAlba124](https://github.com/MAlba124) for Linux support via Pipewire 133 | - [@svtlabs](https://github.com/svtlabs) for [screencapturekit-rs](https://github.com/svtlabs/screencapturekit-rs) 134 | - [@NiiightmareXD](https://github.com/NiiightmareXD) for [windows-capture](https://github.com/NiiightmareXD/windows-capture) 135 | -------------------------------------------------------------------------------- /src/capturer/engine/linux/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem::size_of, 3 | sync::{ 4 | atomic::{AtomicBool, AtomicU8}, 5 | mpsc::{self, sync_channel, SyncSender}, 6 | }, 7 | thread::JoinHandle, 8 | time::Duration, 9 | }; 10 | 11 | use pipewire as pw; 12 | use pw::{ 13 | context::Context, 14 | main_loop::MainLoop, 15 | properties::properties, 16 | spa::{ 17 | self, 18 | param::{ 19 | format::{FormatProperties, MediaSubtype, MediaType}, 20 | video::VideoFormat, 21 | ParamType, 22 | }, 23 | pod::{Pod, Property}, 24 | sys::{ 25 | spa_buffer, spa_meta_header, SPA_META_Header, SPA_PARAM_META_size, SPA_PARAM_META_type, 26 | }, 27 | utils::{Direction, SpaTypes}, 28 | }, 29 | stream::{StreamRef, StreamState}, 30 | }; 31 | 32 | use crate::{ 33 | capturer::Options, 34 | frame::{BGRxFrame, Frame, RGBFrame, RGBxFrame, XBGRFrame}, 35 | }; 36 | 37 | use self::{error::LinCapError, portal::ScreenCastPortal}; 38 | 39 | mod error; 40 | mod portal; 41 | 42 | static CAPTURER_STATE: AtomicU8 = AtomicU8::new(0); 43 | static STREAM_STATE_CHANGED_TO_ERROR: AtomicBool = AtomicBool::new(false); 44 | 45 | #[derive(Clone)] 46 | struct ListenerUserData { 47 | pub tx: mpsc::Sender, 48 | pub format: spa::param::video::VideoInfoRaw, 49 | } 50 | 51 | fn param_changed_callback( 52 | _stream: &StreamRef, 53 | user_data: &mut ListenerUserData, 54 | id: u32, 55 | param: Option<&Pod>, 56 | ) { 57 | let Some(param) = param else { 58 | return; 59 | }; 60 | if id != pw::spa::param::ParamType::Format.as_raw() { 61 | return; 62 | } 63 | let (media_type, media_subtype) = match pw::spa::param::format_utils::parse_format(param) { 64 | Ok(v) => v, 65 | Err(_) => return, 66 | }; 67 | 68 | if media_type != MediaType::Video || media_subtype != MediaSubtype::Raw { 69 | return; 70 | } 71 | 72 | user_data 73 | .format 74 | .parse(param) 75 | // TODO: Tell library user of the error 76 | .expect("Failed to parse format parameter"); 77 | } 78 | 79 | fn state_changed_callback( 80 | _stream: &StreamRef, 81 | _user_data: &mut ListenerUserData, 82 | _old: StreamState, 83 | new: StreamState, 84 | ) { 85 | match new { 86 | StreamState::Error(e) => { 87 | eprintln!("pipewire: State changed to error({e})"); 88 | STREAM_STATE_CHANGED_TO_ERROR.store(true, std::sync::atomic::Ordering::Relaxed); 89 | } 90 | _ => {} 91 | } 92 | } 93 | 94 | unsafe fn get_timestamp(buffer: *mut spa_buffer) -> i64 { 95 | let n_metas = (*buffer).n_metas; 96 | if n_metas > 0 { 97 | let mut meta_ptr = (*buffer).metas; 98 | let metas_end = (*buffer).metas.wrapping_add(n_metas as usize); 99 | while meta_ptr != metas_end { 100 | if (*meta_ptr).type_ == SPA_META_Header { 101 | let meta_header: &mut spa_meta_header = 102 | &mut *((*meta_ptr).data as *mut spa_meta_header); 103 | return meta_header.pts; 104 | } 105 | meta_ptr = meta_ptr.wrapping_add(1); 106 | } 107 | 0 108 | } else { 109 | 0 110 | } 111 | } 112 | 113 | fn process_callback(stream: &StreamRef, user_data: &mut ListenerUserData) { 114 | let buffer = unsafe { stream.dequeue_raw_buffer() }; 115 | if !buffer.is_null() { 116 | 'outside: { 117 | let buffer = unsafe { (*buffer).buffer }; 118 | if buffer.is_null() { 119 | break 'outside; 120 | } 121 | let timestamp = unsafe { get_timestamp(buffer) }; 122 | 123 | let n_datas = unsafe { (*buffer).n_datas }; 124 | if n_datas < 1 { 125 | return; 126 | } 127 | let frame_size = user_data.format.size(); 128 | let frame_data: Vec = unsafe { 129 | std::slice::from_raw_parts( 130 | (*(*buffer).datas).data as *mut u8, 131 | (*(*buffer).datas).maxsize as usize, 132 | ) 133 | .to_vec() 134 | }; 135 | 136 | if let Err(e) = match user_data.format.format() { 137 | VideoFormat::RGBx => user_data.tx.send(Frame::RGBx(RGBxFrame { 138 | display_time: timestamp as u64, 139 | width: frame_size.width as i32, 140 | height: frame_size.height as i32, 141 | data: frame_data, 142 | })), 143 | VideoFormat::RGB => user_data.tx.send(Frame::RGB(RGBFrame { 144 | display_time: timestamp as u64, 145 | width: frame_size.width as i32, 146 | height: frame_size.height as i32, 147 | data: frame_data, 148 | })), 149 | VideoFormat::xBGR => user_data.tx.send(Frame::XBGR(XBGRFrame { 150 | display_time: timestamp as u64, 151 | width: frame_size.width as i32, 152 | height: frame_size.height as i32, 153 | data: frame_data, 154 | })), 155 | VideoFormat::BGRx => user_data.tx.send(Frame::BGRx(BGRxFrame { 156 | display_time: timestamp as u64, 157 | width: frame_size.width as i32, 158 | height: frame_size.height as i32, 159 | data: frame_data, 160 | })), 161 | _ => panic!("Unsupported frame format received"), 162 | } { 163 | eprintln!("{e}"); 164 | } 165 | } 166 | } else { 167 | eprintln!("Out of buffers"); 168 | } 169 | 170 | unsafe { stream.queue_raw_buffer(buffer) }; 171 | } 172 | 173 | // TODO: Format negotiation 174 | fn pipewire_capturer( 175 | options: Options, 176 | tx: mpsc::Sender, 177 | ready_sender: &SyncSender, 178 | stream_id: u32, 179 | ) -> Result<(), LinCapError> { 180 | pw::init(); 181 | 182 | let mainloop = MainLoop::new(None)?; 183 | let context = Context::new(&mainloop)?; 184 | let core = context.connect(None)?; 185 | 186 | let user_data = ListenerUserData { 187 | tx, 188 | format: Default::default(), 189 | }; 190 | 191 | let stream = pw::stream::Stream::new( 192 | &core, 193 | "scap", 194 | properties! { 195 | *pw::keys::MEDIA_TYPE => "Video", 196 | *pw::keys::MEDIA_CATEGORY => "Capture", 197 | *pw::keys::MEDIA_ROLE => "Screen", 198 | }, 199 | )?; 200 | 201 | let _listener = stream 202 | .add_local_listener_with_user_data(user_data.clone()) 203 | .state_changed(state_changed_callback) 204 | .param_changed(param_changed_callback) 205 | .process(process_callback) 206 | .register()?; 207 | 208 | let obj = pw::spa::pod::object!( 209 | pw::spa::utils::SpaTypes::ObjectParamFormat, 210 | pw::spa::param::ParamType::EnumFormat, 211 | pw::spa::pod::property!(FormatProperties::MediaType, Id, MediaType::Video), 212 | pw::spa::pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw), 213 | pw::spa::pod::property!( 214 | FormatProperties::VideoFormat, 215 | Choice, 216 | Enum, 217 | Id, 218 | pw::spa::param::video::VideoFormat::RGB, 219 | pw::spa::param::video::VideoFormat::RGBA, 220 | pw::spa::param::video::VideoFormat::RGBx, 221 | pw::spa::param::video::VideoFormat::BGRx, 222 | ), 223 | pw::spa::pod::property!( 224 | FormatProperties::VideoSize, 225 | Choice, 226 | Range, 227 | Rectangle, 228 | pw::spa::utils::Rectangle { 229 | // Default 230 | width: 128, 231 | height: 128, 232 | }, 233 | pw::spa::utils::Rectangle { 234 | // Min 235 | width: 1, 236 | height: 1, 237 | }, 238 | pw::spa::utils::Rectangle { 239 | // Max 240 | width: 4096, 241 | height: 4096, 242 | } 243 | ), 244 | pw::spa::pod::property!( 245 | FormatProperties::VideoMaxFramerate, 246 | Fraction, 247 | pw::spa::utils::Fraction { 248 | num: options.fps, 249 | denom: 1 250 | } 251 | ), 252 | ); 253 | 254 | let metas_obj = pw::spa::pod::object!( 255 | SpaTypes::ObjectParamMeta, 256 | ParamType::Meta, 257 | Property::new( 258 | SPA_PARAM_META_type, 259 | pw::spa::pod::Value::Id(pw::spa::utils::Id(SPA_META_Header)) 260 | ), 261 | Property::new( 262 | SPA_PARAM_META_size, 263 | pw::spa::pod::Value::Int(size_of::() as i32) 264 | ), 265 | ); 266 | 267 | let values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( 268 | std::io::Cursor::new(Vec::new()), 269 | &pw::spa::pod::Value::Object(obj), 270 | )? 271 | .0 272 | .into_inner(); 273 | let metas_values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( 274 | std::io::Cursor::new(Vec::new()), 275 | &pw::spa::pod::Value::Object(metas_obj), 276 | )? 277 | .0 278 | .into_inner(); 279 | 280 | let mut params = [ 281 | pw::spa::pod::Pod::from_bytes(&values).unwrap(), 282 | pw::spa::pod::Pod::from_bytes(&metas_values).unwrap(), 283 | ]; 284 | 285 | stream.connect( 286 | Direction::Input, 287 | Some(stream_id), 288 | pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS, 289 | &mut params, 290 | )?; 291 | 292 | ready_sender.send(true)?; 293 | 294 | while CAPTURER_STATE.load(std::sync::atomic::Ordering::Relaxed) == 0 { 295 | std::thread::sleep(Duration::from_millis(10)); 296 | } 297 | 298 | let pw_loop = mainloop.loop_(); 299 | 300 | // User has called Capturer::start() and we start the main loop 301 | while CAPTURER_STATE.load(std::sync::atomic::Ordering::Relaxed) == 1 302 | && /* If the stream state got changed to `Error`, we exit. TODO: tell user that we exited */ 303 | !STREAM_STATE_CHANGED_TO_ERROR.load(std::sync::atomic::Ordering::Relaxed) 304 | { 305 | pw_loop.iterate(Duration::from_millis(100)); 306 | } 307 | 308 | Ok(()) 309 | } 310 | 311 | pub struct LinuxCapturer { 312 | capturer_join_handle: Option>>, 313 | // The pipewire stream is deleted when the connection is dropped. 314 | // That's why we keep it alive 315 | _connection: dbus::blocking::Connection, 316 | } 317 | 318 | impl LinuxCapturer { 319 | // TODO: Error handling 320 | pub fn new(options: &Options, tx: mpsc::Sender) -> Self { 321 | let connection = 322 | dbus::blocking::Connection::new_session().expect("Failed to create dbus connection"); 323 | let stream_id = ScreenCastPortal::new(&connection) 324 | .show_cursor(options.show_cursor) 325 | .expect("Unsupported cursor mode") 326 | .create_stream() 327 | .expect("Failed to get screencast stream") 328 | .pw_node_id(); 329 | 330 | // TODO: Fix this hack 331 | let options = options.clone(); 332 | let (ready_sender, ready_recv) = sync_channel(1); 333 | let capturer_join_handle = std::thread::spawn(move || { 334 | let res = pipewire_capturer(options, tx, &ready_sender, stream_id); 335 | if res.is_err() { 336 | ready_sender.send(false)?; 337 | } 338 | res 339 | }); 340 | 341 | if !ready_recv.recv().expect("Failed to receive") { 342 | panic!("Failed to setup capturer"); 343 | } 344 | 345 | Self { 346 | capturer_join_handle: Some(capturer_join_handle), 347 | _connection: connection, 348 | } 349 | } 350 | 351 | pub fn start_capture(&self) { 352 | CAPTURER_STATE.store(1, std::sync::atomic::Ordering::Relaxed); 353 | } 354 | 355 | pub fn stop_capture(&mut self) { 356 | CAPTURER_STATE.store(2, std::sync::atomic::Ordering::Relaxed); 357 | if let Some(handle) = self.capturer_join_handle.take() { 358 | if let Err(e) = handle.join().expect("Failed to join capturer thread") { 359 | eprintln!("Error occured capturing: {e}"); 360 | } 361 | } 362 | CAPTURER_STATE.store(0, std::sync::atomic::Ordering::Relaxed); 363 | STREAM_STATE_CHANGED_TO_ERROR.store(false, std::sync::atomic::Ordering::Relaxed); 364 | } 365 | } 366 | 367 | pub fn create_capturer(options: &Options, tx: mpsc::Sender) -> LinuxCapturer { 368 | LinuxCapturer::new(options, tx) 369 | } 370 | -------------------------------------------------------------------------------- /src/capturer/engine/mac/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicBool; 2 | use std::sync::mpsc; 3 | use std::{cmp, sync::Arc}; 4 | 5 | use cidre::mach; 6 | use cidre::sc::StreamDelegateImpl; 7 | use cidre::{ 8 | arc, cg, cm, cv, define_obj_type, dispatch, ns, objc, 9 | sc::{self, StreamDelegate, StreamOutput, StreamOutputImpl}, 10 | }; 11 | use futures::executor::block_on; 12 | 13 | use crate::frame::{AudioFormat, AudioFrame, Frame, FrameType, VideoFrame}; 14 | use crate::targets::Target; 15 | use crate::{ 16 | capturer::{Area, Options, Point, Resolution, Size}, 17 | frame::BGRAFrame, 18 | targets, 19 | }; 20 | 21 | use super::ChannelItem; 22 | 23 | pub(crate) mod ext; 24 | mod pixel_buffer; 25 | mod pixelformat; 26 | 27 | struct ErrorHandlerInner { 28 | error_flag: Arc, 29 | } 30 | 31 | define_obj_type!( 32 | pub ErrorHandler + StreamDelegateImpl, 33 | ErrorHandlerInner, 34 | ERROR_HANDLER 35 | ); 36 | 37 | impl sc::stream::Delegate for ErrorHandler {} 38 | 39 | #[objc::add_methods] 40 | impl sc::stream::DelegateImpl for ErrorHandler { 41 | extern "C" fn impl_stream_did_stop_with_err( 42 | &mut self, 43 | _cmd: Option<&objc::Sel>, 44 | stream: &sc::Stream, 45 | error: &ns::Error, 46 | ) { 47 | eprintln!("Screen capture error occurred."); 48 | self.inner_mut() 49 | .error_flag 50 | .store(true, std::sync::atomic::Ordering::Relaxed); 51 | } 52 | } 53 | 54 | #[repr(C)] 55 | pub struct CapturerInner { 56 | pub tx: mpsc::Sender, 57 | } 58 | 59 | define_obj_type!(pub Capturer + StreamOutputImpl, CapturerInner, CAPTURER); 60 | 61 | impl sc::stream::Output for Capturer {} 62 | 63 | #[objc::add_methods] 64 | impl sc::stream::OutputImpl for Capturer { 65 | extern "C" fn impl_stream_did_output_sample_buf( 66 | &mut self, 67 | _cmd: Option<&objc::Sel>, 68 | _stream: &sc::Stream, 69 | sample_buf: &mut cm::SampleBuf, 70 | kind: sc::OutputType, 71 | ) { 72 | let _ = self.inner_mut().tx.send((sample_buf.retained(), kind)); 73 | } 74 | } 75 | 76 | #[derive(thiserror::Error, Debug)] 77 | pub(crate) enum CreateCapturerError { 78 | #[error("{0}")] 79 | OtherNative(#[from] arc::R), 80 | #[error("Window with title '{0}' not found")] 81 | WindowNotFound(String), 82 | #[error("Display with title '{0}' not found")] 83 | DisplayNotFound(String), 84 | } 85 | 86 | pub(crate) fn create_capturer( 87 | options: &Options, 88 | tx: mpsc::Sender, 89 | error_flag: Arc, 90 | ) -> Result<(arc::R, arc::R, arc::R), CreateCapturerError> { 91 | // If no target is specified, capture the main display 92 | let target = options 93 | .target 94 | .clone() 95 | .unwrap_or_else(|| Target::Display(targets::get_main_display())); 96 | 97 | let shareable_content = block_on(sc::ShareableContent::current())?; 98 | 99 | let filter = match target { 100 | Target::Window(window) => { 101 | let windows = shareable_content.windows(); 102 | 103 | // Get SCWindow from window id 104 | let sc_window = windows 105 | .iter() 106 | .find(|sc_win| sc_win.id() == window.id) 107 | .ok_or_else(|| CreateCapturerError::WindowNotFound(window.title))?; 108 | 109 | // Return a DesktopIndependentWindow 110 | // https://developer.apple.com/documentation/screencapturekit/sccontentfilter/3919804-init 111 | sc::ContentFilter::with_desktop_independent_window(sc_window) 112 | } 113 | Target::Display(display) => { 114 | let displays = shareable_content.displays(); 115 | // Get SCDisplay from display id 116 | let sc_display = displays 117 | .iter() 118 | .find(|sc_dis| sc_dis.display_id() == display.raw_handle) 119 | .ok_or_else(|| CreateCapturerError::DisplayNotFound(display.title))?; 120 | 121 | match &options.excluded_targets { 122 | None => sc::ContentFilter::with_display_excluding_windows( 123 | &sc_display, 124 | &ns::Array::new(), 125 | ), 126 | Some(excluded_targets) => { 127 | let windows = shareable_content.windows(); 128 | let excluded_windows = windows 129 | .iter() 130 | .filter(|window| { 131 | excluded_targets 132 | .iter() 133 | .any(|excluded_target| match excluded_target { 134 | Target::Window(excluded_window) => { 135 | excluded_window.id == window.id() 136 | } 137 | _ => false, 138 | }) 139 | }) 140 | .collect::>(); 141 | 142 | sc::ContentFilter::with_display_excluding_windows( 143 | &sc_display, 144 | &ns::Array::from_slice(&excluded_windows), 145 | ) 146 | } 147 | } 148 | } 149 | }; 150 | 151 | let crop_area = get_crop_area(options); 152 | 153 | let source_rect = cg::Rect { 154 | origin: cg::Point { 155 | x: crop_area.origin.x, 156 | y: crop_area.origin.y, 157 | }, 158 | size: cg::Size { 159 | width: crop_area.size.width, 160 | height: crop_area.size.height, 161 | }, 162 | }; 163 | 164 | let pixel_format = match options.output_type { 165 | FrameType::YUVFrame => cv::PixelFormat::_420V, 166 | FrameType::BGR0 => cv::PixelFormat::_32_BGRA, 167 | FrameType::RGB => cv::PixelFormat::_32_BGRA, 168 | FrameType::BGRAFrame => cv::PixelFormat::_32_BGRA, 169 | }; 170 | 171 | let [width, height] = get_output_frame_size(options); 172 | 173 | let mut stream_config = sc::StreamCfg::new(); 174 | stream_config.set_width(width as usize); 175 | stream_config.set_height(height as usize); 176 | stream_config.set_src_rect(source_rect); 177 | stream_config.set_pixel_format(pixel_format); 178 | stream_config.set_shows_cursor(options.show_cursor); 179 | stream_config.set_minimum_frame_interval(cm::Time { 180 | value: 1, 181 | scale: options.fps as i32, 182 | epoch: 0, 183 | flags: cm::TimeFlags::VALID, 184 | }); 185 | stream_config.set_captures_audio(options.captures_audio); 186 | 187 | let error_handler = ErrorHandler::with(ErrorHandlerInner { error_flag }); 188 | let stream = sc::Stream::with_delegate(&filter, &stream_config, error_handler.as_ref()); 189 | 190 | let capturer = CapturerInner { tx }; 191 | 192 | let queue = dispatch::Queue::serial_with_ar_pool(); 193 | 194 | let capturer = Capturer::with(capturer); 195 | 196 | if options.captures_audio { 197 | stream 198 | .add_stream_output(capturer.as_ref(), sc::OutputType::Audio, Some(&queue)) 199 | .unwrap(); 200 | } 201 | 202 | stream 203 | .add_stream_output(capturer.as_ref(), sc::OutputType::Screen, Some(&queue)) 204 | .unwrap(); 205 | 206 | Ok((capturer, error_handler, stream)) 207 | } 208 | 209 | pub fn get_output_frame_size(options: &Options) -> [u32; 2] { 210 | let target = options 211 | .target 212 | .clone() 213 | .unwrap_or_else(|| Target::Display(targets::get_main_display())); 214 | 215 | let scale_factor = targets::get_scale_factor(&target); 216 | let source_rect = get_crop_area(options); 217 | 218 | // Calculate the output height & width based on the required resolution 219 | // Output width and height need to be multiplied by scale (or dpi) 220 | let mut output_width = (source_rect.size.width as u32) * (scale_factor as u32); 221 | let mut output_height = (source_rect.size.height as u32) * (scale_factor as u32); 222 | // 1200x800 223 | match options.output_resolution { 224 | Resolution::Captured => {} 225 | _ => { 226 | let [resolved_width, resolved_height] = options 227 | .output_resolution 228 | .value((source_rect.size.width as f32) / (source_rect.size.height as f32)); 229 | // 1280 x 853 230 | output_width = cmp::min(output_width, resolved_width); 231 | output_height = cmp::min(output_height, resolved_height); 232 | } 233 | } 234 | 235 | output_width -= output_width % 2; 236 | output_height -= output_height % 2; 237 | 238 | [output_width, output_height] 239 | } 240 | 241 | pub fn get_crop_area(options: &Options) -> Area { 242 | let target = options 243 | .target 244 | .clone() 245 | .unwrap_or_else(|| Target::Display(targets::get_main_display())); 246 | 247 | let (width, height) = targets::get_target_dimensions(&target); 248 | 249 | options 250 | .crop_area 251 | .as_ref() 252 | .map(|val| { 253 | let input_width = val.size.width + (val.size.width % 2.0); 254 | let input_height = val.size.height + (val.size.height % 2.0); 255 | 256 | Area { 257 | origin: Point { 258 | x: val.origin.x, 259 | y: val.origin.y, 260 | }, 261 | size: Size { 262 | width: input_width, 263 | height: input_height, 264 | }, 265 | } 266 | }) 267 | .unwrap_or_else(|| Area { 268 | origin: Point { x: 0.0, y: 0.0 }, 269 | size: Size { 270 | width: width as f64, 271 | height: height as f64, 272 | }, 273 | }) 274 | } 275 | 276 | pub fn process_sample_buffer( 277 | mut sample: arc::R, 278 | of_type: sc::stream::OutputType, 279 | output_type: FrameType, 280 | ) -> Option { 281 | let system_time = std::time::SystemTime::now(); 282 | let system_mach_time = mach::abs_time(); 283 | 284 | let frame_cm_time = sample.pts(); 285 | let frame_mach_time = cm::Clock::convert_host_time_to_sys_units(frame_cm_time); 286 | 287 | let mach_time_diff = if frame_mach_time > system_mach_time { 288 | (frame_mach_time - system_mach_time) as i64 289 | } else { 290 | -((system_mach_time - frame_mach_time) as i64) 291 | }; 292 | 293 | // Convert mach time difference to nanoseconds 294 | let mach_timebase = mach::TimeBaseInfo::new(); 295 | let nanos_diff = (mach_time_diff * mach_timebase.numer as i64) / mach_timebase.denom as i64; 296 | 297 | // Calculate frame SystemTime 298 | let frame_system_time = if nanos_diff >= 0 { 299 | system_time + std::time::Duration::from_nanos(nanos_diff as u64) 300 | } else { 301 | system_time - std::time::Duration::from_nanos((-nanos_diff) as u64) 302 | }; 303 | 304 | match of_type { 305 | sc::stream::OutputType::Screen => { 306 | let attaches = sample.attaches(false).and_then(|a| { 307 | let mut iter = a.iter(); 308 | iter.next() 309 | })?; 310 | 311 | match attaches 312 | .get(sc::FrameInfo::status().as_cf())? 313 | .as_number() 314 | .to_i32() 315 | .unwrap() 316 | { 317 | 0 => unsafe { 318 | return Some(Frame::Video(match output_type { 319 | FrameType::YUVFrame => { 320 | let yuvframe = 321 | pixelformat::create_yuv_frame(sample.as_mut(), frame_system_time) 322 | .unwrap(); 323 | VideoFrame::YUVFrame(yuvframe) 324 | } 325 | FrameType::RGB => { 326 | let rgbframe = 327 | pixelformat::create_rgb_frame(sample.as_mut(), frame_system_time) 328 | .unwrap(); 329 | VideoFrame::RGB(rgbframe) 330 | } 331 | FrameType::BGR0 => { 332 | let bgrframe = 333 | pixelformat::create_bgr_frame(sample.as_mut(), frame_system_time) 334 | .unwrap(); 335 | VideoFrame::BGR0(bgrframe) 336 | } 337 | FrameType::BGRAFrame => { 338 | let bgraframe = 339 | pixelformat::create_bgra_frame(sample.as_mut(), frame_system_time) 340 | .unwrap(); 341 | VideoFrame::BGRA(bgraframe) 342 | } 343 | })); 344 | }, 345 | 1 => { 346 | // Quick hack - just send an empty frame, and the caller can figure out how to handle it 347 | if let FrameType::BGRAFrame = output_type { 348 | return Some(Frame::Video(VideoFrame::BGRA(BGRAFrame { 349 | display_time: frame_system_time, 350 | width: 0, 351 | height: 0, 352 | data: vec![], 353 | }))); 354 | } 355 | } 356 | _ => {} 357 | }; 358 | 359 | None 360 | } 361 | sc::stream::OutputType::Audio => { 362 | let list = sample.audio_buf_list::<2>().ok()?; 363 | let mut bytes = Vec::::new(); 364 | 365 | for buffer in list.list().buffers { 366 | bytes.extend(unsafe { 367 | std::slice::from_raw_parts(buffer.data, buffer.data_bytes_size as usize) 368 | }); 369 | } 370 | 371 | return Some(Frame::Audio(AudioFrame::new( 372 | AudioFormat::F32, 373 | 2, 374 | false, 375 | bytes, 376 | sample.num_samples() as usize, 377 | 48_000, 378 | frame_system_time, 379 | ))); 380 | } 381 | _ => None, 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/capturer/engine/linux/portal.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{atomic::AtomicBool, Arc, Mutex}, 3 | time::Duration, 4 | }; 5 | 6 | use dbus::{ 7 | arg::{self, PropMap, RefArg, Variant}, 8 | blocking::{Connection, Proxy}, 9 | message::MatchRule, 10 | strings::{BusName, Interface}, 11 | }; 12 | 13 | use super::error::LinCapError; 14 | 15 | // This code was autogenerated with `dbus-codegen-rust -d org.freedesktop.portal.Desktop -p /org/freedesktop/portal/desktop -f org.freedesktop.portal.ScreenCast`, see https://github.com/diwic/dbus-rs 16 | // { 17 | use dbus::blocking; 18 | 19 | trait OrgFreedesktopPortalScreenCast { 20 | fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; 21 | fn select_sources( 22 | &self, 23 | session_handle: dbus::Path, 24 | options: arg::PropMap, 25 | ) -> Result, dbus::Error>; 26 | fn start( 27 | &self, 28 | session_handle: dbus::Path, 29 | parent_window: &str, 30 | options: arg::PropMap, 31 | ) -> Result, dbus::Error>; 32 | fn open_pipe_wire_remote( 33 | &self, 34 | session_handle: dbus::Path, 35 | options: arg::PropMap, 36 | ) -> Result; 37 | fn available_source_types(&self) -> Result; 38 | fn available_cursor_modes(&self) -> Result; 39 | fn version(&self) -> Result; 40 | } 41 | 42 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> 43 | OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C> 44 | { 45 | fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { 46 | self.method_call( 47 | "org.freedesktop.portal.ScreenCast", 48 | "CreateSession", 49 | (options,), 50 | ) 51 | .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) 52 | } 53 | 54 | fn select_sources( 55 | &self, 56 | session_handle: dbus::Path, 57 | options: arg::PropMap, 58 | ) -> Result, dbus::Error> { 59 | self.method_call( 60 | "org.freedesktop.portal.ScreenCast", 61 | "SelectSources", 62 | (session_handle, options), 63 | ) 64 | .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) 65 | } 66 | 67 | fn start( 68 | &self, 69 | session_handle: dbus::Path, 70 | parent_window: &str, 71 | options: arg::PropMap, 72 | ) -> Result, dbus::Error> { 73 | self.method_call( 74 | "org.freedesktop.portal.ScreenCast", 75 | "Start", 76 | (session_handle, parent_window, options), 77 | ) 78 | .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) 79 | } 80 | 81 | fn open_pipe_wire_remote( 82 | &self, 83 | session_handle: dbus::Path, 84 | options: arg::PropMap, 85 | ) -> Result { 86 | self.method_call( 87 | "org.freedesktop.portal.ScreenCast", 88 | "OpenPipeWireRemote", 89 | (session_handle, options), 90 | ) 91 | .and_then(|r: (arg::OwnedFd,)| Ok(r.0)) 92 | } 93 | 94 | fn available_source_types(&self) -> Result { 95 | ::get( 96 | &self, 97 | "org.freedesktop.portal.ScreenCast", 98 | "AvailableSourceTypes", 99 | ) 100 | } 101 | 102 | fn available_cursor_modes(&self) -> Result { 103 | ::get( 104 | &self, 105 | "org.freedesktop.portal.ScreenCast", 106 | "AvailableCursorModes", 107 | ) 108 | } 109 | 110 | fn version(&self) -> Result { 111 | ::get( 112 | &self, 113 | "org.freedesktop.portal.ScreenCast", 114 | "version", 115 | ) 116 | } 117 | } 118 | // } 119 | 120 | // This code was autogenerated with `dbus-codegen-rust --file org.freedesktop.portal.Request.xml`, see https://github.com/diwic/dbus-rs 121 | // { 122 | trait OrgFreedesktopPortalRequest { 123 | fn close(&self) -> Result<(), dbus::Error>; 124 | } 125 | 126 | #[derive(Debug)] 127 | pub struct OrgFreedesktopPortalRequestResponse { 128 | pub response: u32, 129 | pub results: arg::PropMap, 130 | } 131 | 132 | impl arg::AppendAll for OrgFreedesktopPortalRequestResponse { 133 | fn append(&self, i: &mut arg::IterAppend) { 134 | arg::RefArg::append(&self.response, i); 135 | arg::RefArg::append(&self.results, i); 136 | } 137 | } 138 | 139 | impl arg::ReadAll for OrgFreedesktopPortalRequestResponse { 140 | fn read(i: &mut arg::Iter) -> Result { 141 | Ok(OrgFreedesktopPortalRequestResponse { 142 | response: i.read()?, 143 | results: i.read()?, 144 | }) 145 | } 146 | } 147 | 148 | impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse { 149 | const NAME: &'static str = "Response"; 150 | const INTERFACE: &'static str = "org.freedesktop.portal.Request"; 151 | } 152 | 153 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgFreedesktopPortalRequest 154 | for blocking::Proxy<'a, C> 155 | { 156 | fn close(&self) -> Result<(), dbus::Error> { 157 | self.method_call("org.freedesktop.portal.Request", "Close", ()) 158 | } 159 | } 160 | // } 161 | 162 | type Response = Option; 163 | 164 | #[derive(Debug)] 165 | #[allow(dead_code)] 166 | pub struct StreamVardict { 167 | id: Option, 168 | position: Option<(i32, i32)>, 169 | size: Option<(i32, i32)>, 170 | source_type: Option, 171 | mapping_id: Option, 172 | } 173 | 174 | #[derive(Debug)] 175 | pub struct Stream(u32, StreamVardict); 176 | 177 | impl Stream { 178 | pub fn pw_node_id(&self) -> u32 { 179 | self.0 180 | } 181 | 182 | pub fn from_dbus(stream: &Variant>) -> Option { 183 | let mut stream = stream.as_iter()?.next()?.as_iter()?; 184 | let pipewire_node_id = stream.next()?.as_iter()?.next()?.as_u64()?; 185 | 186 | // TODO: Get the rest of the properties 187 | 188 | Some(Self( 189 | pipewire_node_id as u32, 190 | StreamVardict { 191 | id: None, 192 | position: None, 193 | size: None, 194 | source_type: None, 195 | mapping_id: None, 196 | }, 197 | )) 198 | } 199 | } 200 | 201 | macro_rules! match_response { 202 | ( $code:expr ) => { 203 | match $code { 204 | 0 => {} 205 | 1 => { 206 | return Err(LinCapError::new(String::from( 207 | "User cancelled the interaction", 208 | ))); 209 | } 210 | 2 => { 211 | return Err(LinCapError::new(String::from( 212 | "The user interaction was ended in some other way", 213 | ))); 214 | } 215 | _ => unreachable!(), 216 | } 217 | }; 218 | } 219 | 220 | pub struct ScreenCastPortal<'a> { 221 | proxy: Proxy<'a, &'a Connection>, 222 | token: String, 223 | cursor_mode: u32, 224 | } 225 | 226 | impl<'a> ScreenCastPortal<'a> { 227 | pub fn new(connection: &'a Connection) -> Self { 228 | let proxy = connection.with_proxy( 229 | "org.freedesktop.portal.Desktop", 230 | "/org/freedesktop/portal/desktop", 231 | Duration::from_secs(4), 232 | ); 233 | 234 | let token = format!("scap_{}", rand::random::()); 235 | 236 | Self { 237 | proxy, 238 | token, 239 | cursor_mode: 1, 240 | } 241 | } 242 | 243 | fn create_session_args(&self) -> arg::PropMap { 244 | let mut map = arg::PropMap::new(); 245 | map.insert( 246 | String::from("handle_token"), 247 | Variant(Box::new(self.token.clone())), 248 | ); 249 | map.insert( 250 | String::from("session_handle_token"), 251 | Variant(Box::new(self.token.clone())), 252 | ); 253 | map 254 | } 255 | 256 | fn select_sources_args(&self) -> Result { 257 | let mut map = arg::PropMap::new(); 258 | map.insert( 259 | String::from("handle_token"), 260 | Variant(Box::new(self.token.clone())), 261 | ); 262 | map.insert( 263 | String::from("types"), 264 | Variant(Box::new(self.proxy.available_source_types()?)), 265 | ); 266 | map.insert(String::from("multiple"), Variant(Box::new(false))); 267 | map.insert( 268 | String::from("cursor_mode"), 269 | Variant(Box::new(self.cursor_mode)), 270 | ); 271 | Ok(map) 272 | } 273 | 274 | fn handle_req_response( 275 | connection: &Connection, 276 | path: dbus::Path<'static>, 277 | iterations: usize, 278 | timeout: Duration, 279 | response: Arc>, 280 | ) -> Result<(), dbus::Error> { 281 | let got_response = Arc::new(AtomicBool::new(false)); 282 | let got_response_clone = Arc::clone(&got_response); 283 | 284 | let mut rule = MatchRule::new(); 285 | rule.path = Some(dbus::Path::from(path)); 286 | rule.msg_type = Some(dbus::MessageType::Signal); 287 | rule.sender = Some(BusName::from("org.freedesktop.portal.Desktop")); 288 | rule.interface = Some(Interface::from("org.freedesktop.portal.Request")); 289 | connection.add_match( 290 | rule, 291 | move |res: OrgFreedesktopPortalRequestResponse, _chuh, _msg| { 292 | let mut response = response.lock().expect("Failed to lock response mutex"); 293 | *response = Some(res); 294 | got_response_clone.store(true, std::sync::atomic::Ordering::Relaxed); 295 | false 296 | }, 297 | )?; 298 | 299 | for _ in 0..iterations { 300 | connection.process(timeout)?; 301 | 302 | if got_response.load(std::sync::atomic::Ordering::Relaxed) { 303 | break; 304 | } 305 | } 306 | 307 | Ok(()) 308 | } 309 | 310 | fn create_session(&self) -> Result { 311 | let request_handle = self.proxy.create_session(self.create_session_args())?; 312 | 313 | let response = Arc::new(Mutex::new(None)); 314 | let response_clone = Arc::clone(&response); 315 | Self::handle_req_response( 316 | self.proxy.connection, 317 | request_handle, 318 | 100, 319 | Duration::from_millis(100), 320 | response_clone, 321 | )?; 322 | 323 | if let Some(res) = response.lock()?.take() { 324 | match_response!(res.response); 325 | match res 326 | .results 327 | .get("session_handle") 328 | .map(|h| h.0.as_str().map(String::from)) 329 | { 330 | Some(h) => { 331 | let p = dbus::Path::from(match h { 332 | Some(p) => p, 333 | None => { 334 | return Err(LinCapError::new(String::from( 335 | "Invalid session_handle received", 336 | ))) 337 | } 338 | }); 339 | 340 | return Ok(p); 341 | } 342 | None => return Err(LinCapError::new(String::from("Did not get session handle"))), 343 | } 344 | } 345 | 346 | Err(LinCapError::new(String::from("Did not get response"))) 347 | } 348 | 349 | fn select_sources(&self, session_handle: dbus::Path) -> Result<(), LinCapError> { 350 | let request_handle = self 351 | .proxy 352 | .select_sources(session_handle, self.select_sources_args()?)?; 353 | 354 | let response = Arc::new(Mutex::new(None)); 355 | let response_clone = Arc::clone(&response); 356 | Self::handle_req_response( 357 | self.proxy.connection, 358 | request_handle, 359 | 1200, // Wait 2 min 360 | Duration::from_millis(100), 361 | response_clone, 362 | )?; 363 | 364 | if let Some(res) = response.lock()?.take() { 365 | match_response!(res.response); 366 | return Ok(()); 367 | } 368 | 369 | Err(LinCapError::new(String::from("Did not get response"))) 370 | } 371 | 372 | fn start(&self, session_handle: dbus::Path) -> Result { 373 | let request_handle = self.proxy.start(session_handle, "", PropMap::new())?; 374 | 375 | let response = Arc::new(Mutex::new(None)); 376 | let response_clone = Arc::clone(&response); 377 | Self::handle_req_response( 378 | self.proxy.connection, 379 | request_handle, 380 | 100, // Wait 10 s 381 | Duration::from_millis(100), 382 | response_clone, 383 | )?; 384 | 385 | if let Some(res) = response.lock()?.take() { 386 | match_response!(res.response); 387 | match res.results.get("streams") { 388 | Some(s) => match Stream::from_dbus(s) { 389 | Some(s) => return Ok(s), 390 | None => { 391 | return Err(LinCapError::new(String::from( 392 | "Failed to extract stream properties", 393 | ))) 394 | } 395 | }, 396 | None => return Err(LinCapError::new(String::from("Did not get any streams"))), 397 | } 398 | } 399 | 400 | Err(LinCapError::new(String::from("Did not get response"))) 401 | } 402 | 403 | pub fn create_stream(&self) -> Result { 404 | let session_handle = self.create_session()?; 405 | self.select_sources(session_handle.clone())?; 406 | self.start(session_handle) 407 | } 408 | 409 | pub fn show_cursor(mut self, mode: bool) -> Result { 410 | let available_modes = self.proxy.available_cursor_modes()?; 411 | if mode && available_modes & 2 == 2 { 412 | self.cursor_mode = 2; 413 | return Ok(self); 414 | } 415 | if !mode && available_modes & 1 == 1 { 416 | self.cursor_mode = 1; 417 | return Ok(self); 418 | } 419 | 420 | Err(LinCapError::new("Unsupported cursor mode".to_string())) 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/capturer/engine/win/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | capturer::{Area, Options, Point, Resolution, Size}, 3 | frame::{AudioFormat, AudioFrame, BGRAFrame, Frame, FrameType, VideoFrame}, 4 | targets::{self, get_scale_factor, Target}, 5 | }; 6 | use ::windows::Win32::System::Performance::{QueryPerformanceCounter, QueryPerformanceFrequency}; 7 | use cpal::{ 8 | traits::{DeviceTrait, HostTrait, StreamTrait}, 9 | StreamInstant, 10 | }; 11 | use std::time::{SystemTime, UNIX_EPOCH}; 12 | use std::{cmp, time::Duration}; 13 | use std::{ 14 | os::windows, 15 | ptr::null_mut, 16 | sync::mpsc::{self, Receiver, RecvTimeoutError, Sender}, 17 | }; 18 | use windows_capture::{ 19 | capture::{CaptureControl, Context, GraphicsCaptureApiHandler}, 20 | frame::Frame as WCFrame, 21 | graphics_capture_api::{GraphicsCaptureApi, InternalCaptureControl}, 22 | monitor::Monitor as WCMonitor, 23 | settings::{ 24 | ColorFormat, CursorCaptureSettings, DirtyRegionSettings, DrawBorderSettings, 25 | MinimumUpdateIntervalSettings, SecondaryWindowSettings, Settings as WCSettings, 26 | }, 27 | window::Window as WCWindow, 28 | }; 29 | 30 | #[derive(Debug)] 31 | struct Capturer { 32 | pub tx: mpsc::Sender, 33 | pub crop: Option, 34 | pub start_time: (i64, SystemTime), 35 | pub perf_freq: i64, 36 | } 37 | 38 | #[derive(Clone)] 39 | enum Settings { 40 | Window(WCSettings), 41 | Display(WCSettings), 42 | } 43 | 44 | pub struct WCStream { 45 | settings: Settings, 46 | capture_control: Option>>, 47 | audio_stream: Option, 48 | } 49 | 50 | impl GraphicsCaptureApiHandler for Capturer { 51 | type Flags = FlagStruct; 52 | type Error = Box; 53 | 54 | fn new(context: Context) -> Result { 55 | Ok(Self { 56 | tx: context.flags.tx, 57 | crop: context.flags.crop, 58 | start_time: ( 59 | unsafe { 60 | let mut time = 0; 61 | QueryPerformanceCounter(&mut time); 62 | time 63 | }, 64 | SystemTime::now(), 65 | ), 66 | perf_freq: unsafe { 67 | let mut freq = 0; 68 | QueryPerformanceFrequency(&mut freq); 69 | freq 70 | }, 71 | }) 72 | } 73 | 74 | fn on_frame_arrived( 75 | &mut self, 76 | frame: &mut WCFrame, 77 | _: InternalCaptureControl, 78 | ) -> Result<(), Self::Error> { 79 | let elapsed = frame.timespan().Duration - self.start_time.0; 80 | let display_time = self 81 | .start_time 82 | .1 83 | .checked_add(Duration::from_secs_f64( 84 | elapsed as f64 / self.perf_freq as f64, 85 | )) 86 | .unwrap(); 87 | 88 | match &self.crop { 89 | Some(cropped_area) => { 90 | // get the cropped area 91 | let start_x = cropped_area.origin.x as u32; 92 | let start_y = cropped_area.origin.y as u32; 93 | let end_x = (cropped_area.origin.x + cropped_area.size.width) as u32; 94 | let end_y = (cropped_area.origin.y + cropped_area.size.height) as u32; 95 | 96 | // crop the frame 97 | let mut cropped_buffer = frame 98 | .buffer_crop(start_x, start_y, end_x, end_y) 99 | .expect("Failed to crop buffer"); 100 | 101 | // get raw frame buffer 102 | let raw_frame_buffer = match cropped_buffer.as_nopadding_buffer() { 103 | Ok(buffer) => buffer, 104 | Err(_) => return Err(("Failed to get raw buffer").into()), 105 | }; 106 | 107 | let current_time = SystemTime::now() 108 | .duration_since(UNIX_EPOCH) 109 | .expect("Failed to get current time") 110 | .as_nanos() as u64; 111 | 112 | let bgr_frame = BGRAFrame { 113 | display_time, 114 | width: cropped_area.size.width as i32, 115 | height: cropped_area.size.height as i32, 116 | data: raw_frame_buffer.to_vec(), 117 | }; 118 | 119 | let _ = self.tx.send(Frame::Video(VideoFrame::BGRA(bgr_frame))); 120 | } 121 | None => { 122 | // get raw frame buffer 123 | let mut frame_buffer = frame.buffer().unwrap(); 124 | let raw_frame_buffer = frame_buffer.as_raw_buffer(); 125 | let frame_data = raw_frame_buffer.to_vec(); 126 | let current_time = SystemTime::now() 127 | .duration_since(UNIX_EPOCH) 128 | .expect("Failed to get current time") 129 | .as_nanos() as u64; 130 | let bgr_frame = BGRAFrame { 131 | display_time, 132 | width: frame.width() as i32, 133 | height: frame.height() as i32, 134 | data: frame_data, 135 | }; 136 | 137 | let _ = self.tx.send(Frame::Video(VideoFrame::BGRA(bgr_frame))); 138 | } 139 | } 140 | Ok(()) 141 | } 142 | 143 | fn on_closed(&mut self) -> Result<(), Self::Error> { 144 | println!("Closed"); 145 | Ok(()) 146 | } 147 | } 148 | 149 | impl WCStream { 150 | pub fn start_capture(&mut self) { 151 | let cc = match &self.settings { 152 | Settings::Display(st) => Capturer::start_free_threaded(st.to_owned()).unwrap(), 153 | Settings::Window(st) => Capturer::start_free_threaded(st.to_owned()).unwrap(), 154 | }; 155 | 156 | if let Some(audio_stream) = &self.audio_stream { 157 | let _ = audio_stream.ctrl_tx.send(AudioStreamControl::Start); 158 | } 159 | 160 | self.capture_control = Some(cc) 161 | } 162 | 163 | pub fn stop_capture(&mut self) { 164 | let capture_control = self.capture_control.take().unwrap(); 165 | let _ = capture_control.stop(); 166 | 167 | if let Some(audio_stream) = &self.audio_stream { 168 | let _ = audio_stream.ctrl_tx.send(AudioStreamControl::Stop); 169 | } 170 | } 171 | } 172 | 173 | #[derive(Clone, Debug)] 174 | struct FlagStruct { 175 | pub tx: mpsc::Sender, 176 | pub crop: Option, 177 | } 178 | 179 | #[derive(Debug)] 180 | pub enum CreateCapturerError { 181 | AudioStreamConfig(cpal::DefaultStreamConfigError), 182 | BuildAudioStream(cpal::BuildStreamError), 183 | } 184 | 185 | pub fn create_capturer( 186 | options: &Options, 187 | tx: mpsc::Sender, 188 | ) -> Result { 189 | let target = options 190 | .target 191 | .clone() 192 | .unwrap_or_else(|| Target::Display(targets::get_main_display())); 193 | 194 | let color_format = match options.output_type { 195 | FrameType::BGRAFrame => ColorFormat::Bgra8, 196 | _ => ColorFormat::Rgba8, 197 | }; 198 | 199 | let show_cursor = match options.show_cursor { 200 | true => CursorCaptureSettings::WithCursor, 201 | false => CursorCaptureSettings::WithoutCursor, 202 | }; 203 | 204 | let draw_border = if GraphicsCaptureApi::is_border_settings_supported().unwrap_or(false) { 205 | options 206 | .show_highlight 207 | .then_some(DrawBorderSettings::WithBorder) 208 | .unwrap_or(DrawBorderSettings::WithoutBorder) 209 | } else { 210 | DrawBorderSettings::Default 211 | }; 212 | 213 | let settings = match target { 214 | Target::Display(display) => Settings::Display(WCSettings::new( 215 | WCMonitor::from_raw_hmonitor(display.raw_handle.0), 216 | show_cursor, 217 | draw_border, 218 | SecondaryWindowSettings::Default, 219 | MinimumUpdateIntervalSettings::Default, 220 | DirtyRegionSettings::Default, 221 | color_format, 222 | FlagStruct { 223 | tx: tx.clone(), 224 | crop: Some(get_crop_area(options)), 225 | }, 226 | )), 227 | Target::Window(window) => Settings::Window(WCSettings::new( 228 | WCWindow::from_raw_hwnd(window.raw_handle.0), 229 | show_cursor, 230 | draw_border, 231 | SecondaryWindowSettings::Default, 232 | MinimumUpdateIntervalSettings::Default, 233 | DirtyRegionSettings::Default, 234 | color_format, 235 | FlagStruct { 236 | tx: tx.clone(), 237 | crop: Some(get_crop_area(options)), 238 | }, 239 | )), 240 | }; 241 | 242 | let audio_stream = if options.captures_audio { 243 | let (ctrl_tx, ctrl_rx) = mpsc::channel(); 244 | let (ready_tx, ready_rx) = mpsc::channel(); 245 | 246 | spawn_audio_stream(tx.clone(), ready_tx, ctrl_rx); 247 | 248 | match ready_rx.recv() { 249 | Ok(Ok(())) => {} 250 | Ok(Err(e)) => return Err(e), 251 | Err(_) => panic!("Audio spawn panicked"), 252 | } 253 | 254 | Some(AudioStreamHandle { ctrl_tx }) 255 | } else { 256 | None 257 | }; 258 | 259 | Ok(WCStream { 260 | settings, 261 | capture_control: None, 262 | audio_stream, 263 | }) 264 | } 265 | 266 | pub fn get_output_frame_size(options: &Options) -> [u32; 2] { 267 | let target = options 268 | .target 269 | .clone() 270 | .unwrap_or_else(|| Target::Display(targets::get_main_display())); 271 | 272 | let crop_area = get_crop_area(options); 273 | 274 | let mut output_width = (crop_area.size.width) as u32; 275 | let mut output_height = (crop_area.size.height) as u32; 276 | 277 | match options.output_resolution { 278 | Resolution::Captured => {} 279 | _ => { 280 | let [resolved_width, resolved_height] = options 281 | .output_resolution 282 | .value((crop_area.size.width as f32) / (crop_area.size.height as f32)); 283 | // 1280 x 853 284 | output_width = cmp::min(output_width, resolved_width); 285 | output_height = cmp::min(output_height, resolved_height); 286 | } 287 | } 288 | 289 | output_width -= output_width % 2; 290 | output_height -= output_height % 2; 291 | 292 | [output_width, output_height] 293 | } 294 | 295 | fn get_absolute_value(value: f64, scale_factor: f64) -> f64 { 296 | let value = (value).floor(); 297 | value + value % 2.0 298 | } 299 | 300 | pub fn get_crop_area(options: &Options) -> Area { 301 | let target = options 302 | .target 303 | .clone() 304 | .unwrap_or_else(|| Target::Display(targets::get_main_display())); 305 | 306 | let (width, height) = targets::get_target_dimensions(&target); 307 | 308 | let scale_factor = targets::get_scale_factor(&target); 309 | options 310 | .crop_area 311 | .as_ref() 312 | .map(|val| { 313 | // WINDOWS: limit values [input-width, input-height] = [146, 50] 314 | Area { 315 | origin: Point { 316 | x: get_absolute_value(val.origin.x, scale_factor), 317 | y: get_absolute_value(val.origin.y, scale_factor), 318 | }, 319 | size: Size { 320 | width: get_absolute_value(val.size.width, scale_factor), 321 | height: get_absolute_value(val.size.height, scale_factor), 322 | }, 323 | } 324 | }) 325 | .unwrap_or_else(|| Area { 326 | origin: Point { x: 0.0, y: 0.0 }, 327 | size: Size { 328 | width: width as f64, 329 | height: height as f64, 330 | }, 331 | }) 332 | } 333 | 334 | struct AudioStreamHandle { 335 | ctrl_tx: mpsc::Sender, 336 | } 337 | 338 | #[derive(Debug)] 339 | enum AudioStreamControl { 340 | Start, 341 | Stop, 342 | } 343 | 344 | fn build_audio_stream( 345 | sample_tx: mpsc::Sender< 346 | Result<(Vec, cpal::InputCallbackInfo, SystemTime), cpal::StreamError>, 347 | >, 348 | ) -> Result<(cpal::Stream, cpal::SupportedStreamConfig), CreateCapturerError> { 349 | let host = cpal::default_host(); 350 | let output_device = 351 | host.default_output_device() 352 | .ok_or(CreateCapturerError::AudioStreamConfig( 353 | cpal::DefaultStreamConfigError::DeviceNotAvailable, 354 | ))?; 355 | let supported_config = output_device 356 | .default_output_config() 357 | .map_err(CreateCapturerError::AudioStreamConfig)?; 358 | let config = supported_config.clone().into(); 359 | 360 | let stream = output_device 361 | .build_input_stream_raw( 362 | &config, 363 | supported_config.sample_format(), 364 | { 365 | let sample_tx = sample_tx.clone(); 366 | move |data, info: &cpal::InputCallbackInfo| { 367 | sample_tx 368 | .send(Ok((data.bytes().to_vec(), info.clone(), SystemTime::now()))) 369 | .unwrap(); 370 | } 371 | }, 372 | move |e| { 373 | let _ = sample_tx.send(Err(e)); 374 | }, 375 | None, 376 | ) 377 | .map_err(CreateCapturerError::BuildAudioStream)?; 378 | 379 | Ok((stream, supported_config)) 380 | } 381 | 382 | fn spawn_audio_stream( 383 | tx: Sender, 384 | ready_tx: Sender>, 385 | ctrl_rx: Receiver, 386 | ) { 387 | std::thread::spawn(move || { 388 | let (sample_tx, sample_rx) = mpsc::channel(); 389 | 390 | let res = build_audio_stream(sample_tx); 391 | 392 | let (stream, config) = match res { 393 | Ok(stream) => { 394 | let _ = ready_tx.send(Ok(())); 395 | stream 396 | } 397 | Err(e) => { 398 | let _ = ready_tx.send(Err(e)); 399 | return; 400 | } 401 | }; 402 | 403 | let Ok(ctrl) = ctrl_rx.recv() else { 404 | return; 405 | }; 406 | 407 | match ctrl { 408 | AudioStreamControl::Start => { 409 | stream.play().unwrap(); 410 | } 411 | AudioStreamControl::Stop => { 412 | return; 413 | } 414 | } 415 | 416 | let audio_format = AudioFormat::from(config.sample_format()); 417 | 418 | loop { 419 | match ctrl_rx.try_recv() { 420 | Ok(AudioStreamControl::Stop) => return, 421 | Ok(_) | Err(mpsc::TryRecvError::Empty) => {} 422 | Err(_) => return, 423 | }; 424 | 425 | let (data, info, timestamp) = match sample_rx.recv_timeout(Duration::from_millis(100)) { 426 | Ok(Ok(data)) => data, 427 | Err(RecvTimeoutError::Timeout) => { 428 | continue; 429 | } 430 | _ => { 431 | // let _ = tx.send(Err(e)); 432 | return; 433 | } 434 | }; 435 | 436 | let sample_count = 437 | data.len() / (audio_format.sample_size() * config.channels() as usize); 438 | let frame = AudioFrame::new( 439 | audio_format, 440 | config.channels(), 441 | false, 442 | data, 443 | sample_count, 444 | config.sample_rate().0, 445 | timestamp, 446 | ); 447 | 448 | if let Err(_) = tx.send(Frame::Audio(frame)) { 449 | return; 450 | }; 451 | } 452 | }); 453 | } 454 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "alsa" 16 | version = "0.9.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" 19 | dependencies = [ 20 | "alsa-sys", 21 | "bitflags 2.6.0", 22 | "cfg-if", 23 | "libc", 24 | ] 25 | 26 | [[package]] 27 | name = "alsa-sys" 28 | version = "0.3.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 31 | dependencies = [ 32 | "libc", 33 | "pkg-config", 34 | ] 35 | 36 | [[package]] 37 | name = "annotate-snippets" 38 | version = "0.9.2" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" 41 | dependencies = [ 42 | "unicode-width", 43 | "yansi-term", 44 | ] 45 | 46 | [[package]] 47 | name = "anyhow" 48 | version = "1.0.89" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" 51 | 52 | [[package]] 53 | name = "autocfg" 54 | version = "1.3.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 57 | 58 | [[package]] 59 | name = "bindgen" 60 | version = "0.69.4" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" 63 | dependencies = [ 64 | "annotate-snippets", 65 | "bitflags 2.6.0", 66 | "cexpr", 67 | "clang-sys", 68 | "itertools", 69 | "lazy_static", 70 | "lazycell", 71 | "proc-macro2", 72 | "quote", 73 | "regex", 74 | "rustc-hash", 75 | "shlex", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "bindgen" 81 | version = "0.70.1" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" 84 | dependencies = [ 85 | "bitflags 2.6.0", 86 | "cexpr", 87 | "clang-sys", 88 | "itertools", 89 | "proc-macro2", 90 | "quote", 91 | "regex", 92 | "rustc-hash", 93 | "shlex", 94 | "syn", 95 | ] 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "1.3.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 102 | 103 | [[package]] 104 | name = "bitflags" 105 | version = "2.6.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 108 | 109 | [[package]] 110 | name = "block" 111 | version = "0.1.6" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 114 | 115 | [[package]] 116 | name = "bumpalo" 117 | version = "3.17.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 120 | 121 | [[package]] 122 | name = "byteorder" 123 | version = "1.5.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 126 | 127 | [[package]] 128 | name = "bytes" 129 | version = "1.10.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 132 | 133 | [[package]] 134 | name = "cc" 135 | version = "1.1.21" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" 138 | dependencies = [ 139 | "jobserver", 140 | "libc", 141 | "shlex", 142 | ] 143 | 144 | [[package]] 145 | name = "cesu8" 146 | version = "1.1.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 149 | 150 | [[package]] 151 | name = "cexpr" 152 | version = "0.6.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 155 | dependencies = [ 156 | "nom", 157 | ] 158 | 159 | [[package]] 160 | name = "cfg-expr" 161 | version = "0.15.8" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" 164 | dependencies = [ 165 | "smallvec", 166 | "target-lexicon", 167 | ] 168 | 169 | [[package]] 170 | name = "cfg-if" 171 | version = "1.0.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 174 | 175 | [[package]] 176 | name = "cidre" 177 | version = "0.10.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "c7a105a9a8dd9e625e1e5938aa8442f63551990bfb2ab4bd606b1b30ccbfd8cf" 180 | dependencies = [ 181 | "cidre-macros", 182 | "parking_lot", 183 | ] 184 | 185 | [[package]] 186 | name = "cidre-macros" 187 | version = "0.2.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "8f07a383a232853874a9b0b3e80e6eefe9bea73495b8ab4bdd960bc7c1db4c6d" 190 | 191 | [[package]] 192 | name = "clang-sys" 193 | version = "1.8.1" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 196 | dependencies = [ 197 | "glob", 198 | "libc", 199 | "libloading", 200 | ] 201 | 202 | [[package]] 203 | name = "cocoa" 204 | version = "0.25.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" 207 | dependencies = [ 208 | "bitflags 1.3.2", 209 | "block", 210 | "cocoa-foundation", 211 | "core-foundation", 212 | "core-graphics", 213 | "foreign-types", 214 | "libc", 215 | "objc", 216 | ] 217 | 218 | [[package]] 219 | name = "cocoa-foundation" 220 | version = "0.1.2" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" 223 | dependencies = [ 224 | "bitflags 1.3.2", 225 | "block", 226 | "core-foundation", 227 | "core-graphics-types", 228 | "libc", 229 | "objc", 230 | ] 231 | 232 | [[package]] 233 | name = "combine" 234 | version = "4.6.7" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 237 | dependencies = [ 238 | "bytes", 239 | "memchr", 240 | ] 241 | 242 | [[package]] 243 | name = "convert_case" 244 | version = "0.6.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 247 | dependencies = [ 248 | "unicode-segmentation", 249 | ] 250 | 251 | [[package]] 252 | name = "cookie-factory" 253 | version = "0.3.3" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" 256 | dependencies = [ 257 | "futures", 258 | ] 259 | 260 | [[package]] 261 | name = "core-foundation" 262 | version = "0.9.4" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 265 | dependencies = [ 266 | "core-foundation-sys", 267 | "libc", 268 | ] 269 | 270 | [[package]] 271 | name = "core-foundation-sys" 272 | version = "0.8.7" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 275 | 276 | [[package]] 277 | name = "core-graphics" 278 | version = "0.23.2" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" 281 | dependencies = [ 282 | "bitflags 1.3.2", 283 | "core-foundation", 284 | "core-graphics-types", 285 | "foreign-types", 286 | "libc", 287 | ] 288 | 289 | [[package]] 290 | name = "core-graphics-helmer-fork" 291 | version = "0.24.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" 294 | dependencies = [ 295 | "bitflags 2.6.0", 296 | "core-foundation", 297 | "core-graphics-types", 298 | "foreign-types", 299 | "libc", 300 | ] 301 | 302 | [[package]] 303 | name = "core-graphics-types" 304 | version = "0.1.3" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" 307 | dependencies = [ 308 | "bitflags 1.3.2", 309 | "core-foundation", 310 | "libc", 311 | ] 312 | 313 | [[package]] 314 | name = "coreaudio-rs" 315 | version = "0.11.3" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" 318 | dependencies = [ 319 | "bitflags 1.3.2", 320 | "core-foundation-sys", 321 | "coreaudio-sys", 322 | ] 323 | 324 | [[package]] 325 | name = "coreaudio-sys" 326 | version = "0.2.16" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" 329 | dependencies = [ 330 | "bindgen 0.70.1", 331 | ] 332 | 333 | [[package]] 334 | name = "cpal" 335 | version = "0.15.3" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" 338 | dependencies = [ 339 | "alsa", 340 | "core-foundation-sys", 341 | "coreaudio-rs", 342 | "dasp_sample", 343 | "jni", 344 | "js-sys", 345 | "libc", 346 | "mach2", 347 | "ndk", 348 | "ndk-context", 349 | "oboe", 350 | "wasm-bindgen", 351 | "wasm-bindgen-futures", 352 | "web-sys", 353 | "windows 0.54.0", 354 | ] 355 | 356 | [[package]] 357 | name = "crossbeam-deque" 358 | version = "0.8.5" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 361 | dependencies = [ 362 | "crossbeam-epoch", 363 | "crossbeam-utils", 364 | ] 365 | 366 | [[package]] 367 | name = "crossbeam-epoch" 368 | version = "0.9.18" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 371 | dependencies = [ 372 | "crossbeam-utils", 373 | ] 374 | 375 | [[package]] 376 | name = "crossbeam-utils" 377 | version = "0.8.20" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 380 | 381 | [[package]] 382 | name = "dasp_sample" 383 | version = "0.11.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 386 | 387 | [[package]] 388 | name = "dbus" 389 | version = "0.9.7" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" 392 | dependencies = [ 393 | "libc", 394 | "libdbus-sys", 395 | "winapi", 396 | ] 397 | 398 | [[package]] 399 | name = "either" 400 | version = "1.13.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 403 | 404 | [[package]] 405 | name = "equivalent" 406 | version = "1.0.1" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 409 | 410 | [[package]] 411 | name = "foreign-types" 412 | version = "0.5.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" 415 | dependencies = [ 416 | "foreign-types-macros", 417 | "foreign-types-shared", 418 | ] 419 | 420 | [[package]] 421 | name = "foreign-types-macros" 422 | version = "0.2.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" 425 | dependencies = [ 426 | "proc-macro2", 427 | "quote", 428 | "syn", 429 | ] 430 | 431 | [[package]] 432 | name = "foreign-types-shared" 433 | version = "0.3.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 436 | 437 | [[package]] 438 | name = "futures" 439 | version = "0.3.31" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 442 | dependencies = [ 443 | "futures-channel", 444 | "futures-core", 445 | "futures-executor", 446 | "futures-io", 447 | "futures-sink", 448 | "futures-task", 449 | "futures-util", 450 | ] 451 | 452 | [[package]] 453 | name = "futures-channel" 454 | version = "0.3.31" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 457 | dependencies = [ 458 | "futures-core", 459 | "futures-sink", 460 | ] 461 | 462 | [[package]] 463 | name = "futures-core" 464 | version = "0.3.31" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 467 | 468 | [[package]] 469 | name = "futures-executor" 470 | version = "0.3.31" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 473 | dependencies = [ 474 | "futures-core", 475 | "futures-task", 476 | "futures-util", 477 | ] 478 | 479 | [[package]] 480 | name = "futures-io" 481 | version = "0.3.31" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 484 | 485 | [[package]] 486 | name = "futures-macro" 487 | version = "0.3.31" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 490 | dependencies = [ 491 | "proc-macro2", 492 | "quote", 493 | "syn", 494 | ] 495 | 496 | [[package]] 497 | name = "futures-sink" 498 | version = "0.3.31" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 501 | 502 | [[package]] 503 | name = "futures-task" 504 | version = "0.3.31" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 507 | 508 | [[package]] 509 | name = "futures-util" 510 | version = "0.3.31" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 513 | dependencies = [ 514 | "futures-channel", 515 | "futures-core", 516 | "futures-io", 517 | "futures-macro", 518 | "futures-sink", 519 | "futures-task", 520 | "memchr", 521 | "pin-project-lite", 522 | "pin-utils", 523 | "slab", 524 | ] 525 | 526 | [[package]] 527 | name = "getrandom" 528 | version = "0.2.15" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 531 | dependencies = [ 532 | "cfg-if", 533 | "libc", 534 | "wasi", 535 | ] 536 | 537 | [[package]] 538 | name = "glob" 539 | version = "0.3.1" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 542 | 543 | [[package]] 544 | name = "hashbrown" 545 | version = "0.14.5" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 548 | 549 | [[package]] 550 | name = "heck" 551 | version = "0.5.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 554 | 555 | [[package]] 556 | name = "indexmap" 557 | version = "2.5.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" 560 | dependencies = [ 561 | "equivalent", 562 | "hashbrown", 563 | ] 564 | 565 | [[package]] 566 | name = "itertools" 567 | version = "0.12.1" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 570 | dependencies = [ 571 | "either", 572 | ] 573 | 574 | [[package]] 575 | name = "jni" 576 | version = "0.21.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 579 | dependencies = [ 580 | "cesu8", 581 | "cfg-if", 582 | "combine", 583 | "jni-sys", 584 | "log", 585 | "thiserror 1.0.69", 586 | "walkdir", 587 | "windows-sys 0.45.0", 588 | ] 589 | 590 | [[package]] 591 | name = "jni-sys" 592 | version = "0.3.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 595 | 596 | [[package]] 597 | name = "jobserver" 598 | version = "0.1.32" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 601 | dependencies = [ 602 | "libc", 603 | ] 604 | 605 | [[package]] 606 | name = "js-sys" 607 | version = "0.3.77" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 610 | dependencies = [ 611 | "once_cell", 612 | "wasm-bindgen", 613 | ] 614 | 615 | [[package]] 616 | name = "lazy_static" 617 | version = "1.5.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 620 | 621 | [[package]] 622 | name = "lazycell" 623 | version = "1.3.0" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 626 | 627 | [[package]] 628 | name = "libc" 629 | version = "0.2.174" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 632 | 633 | [[package]] 634 | name = "libdbus-sys" 635 | version = "0.2.5" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" 638 | dependencies = [ 639 | "pkg-config", 640 | ] 641 | 642 | [[package]] 643 | name = "libloading" 644 | version = "0.8.5" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" 647 | dependencies = [ 648 | "cfg-if", 649 | "windows-targets 0.52.6", 650 | ] 651 | 652 | [[package]] 653 | name = "libspa" 654 | version = "0.8.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" 657 | dependencies = [ 658 | "bitflags 2.6.0", 659 | "cc", 660 | "convert_case", 661 | "cookie-factory", 662 | "libc", 663 | "libspa-sys", 664 | "nix", 665 | "nom", 666 | "system-deps", 667 | ] 668 | 669 | [[package]] 670 | name = "libspa-sys" 671 | version = "0.8.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" 674 | dependencies = [ 675 | "bindgen 0.69.4", 676 | "cc", 677 | "system-deps", 678 | ] 679 | 680 | [[package]] 681 | name = "lock_api" 682 | version = "0.4.13" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 685 | dependencies = [ 686 | "autocfg", 687 | "scopeguard", 688 | ] 689 | 690 | [[package]] 691 | name = "log" 692 | version = "0.4.27" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 695 | 696 | [[package]] 697 | name = "mach2" 698 | version = "0.4.2" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 701 | dependencies = [ 702 | "libc", 703 | ] 704 | 705 | [[package]] 706 | name = "malloc_buf" 707 | version = "0.0.6" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 710 | dependencies = [ 711 | "libc", 712 | ] 713 | 714 | [[package]] 715 | name = "memchr" 716 | version = "2.7.4" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 719 | 720 | [[package]] 721 | name = "minimal-lexical" 722 | version = "0.2.1" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 725 | 726 | [[package]] 727 | name = "ndk" 728 | version = "0.8.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" 731 | dependencies = [ 732 | "bitflags 2.6.0", 733 | "jni-sys", 734 | "log", 735 | "ndk-sys", 736 | "num_enum", 737 | "thiserror 1.0.69", 738 | ] 739 | 740 | [[package]] 741 | name = "ndk-context" 742 | version = "0.1.1" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 745 | 746 | [[package]] 747 | name = "ndk-sys" 748 | version = "0.5.0+25.2.9519653" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" 751 | dependencies = [ 752 | "jni-sys", 753 | ] 754 | 755 | [[package]] 756 | name = "nix" 757 | version = "0.27.1" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 760 | dependencies = [ 761 | "bitflags 2.6.0", 762 | "cfg-if", 763 | "libc", 764 | ] 765 | 766 | [[package]] 767 | name = "nom" 768 | version = "7.1.3" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 771 | dependencies = [ 772 | "memchr", 773 | "minimal-lexical", 774 | ] 775 | 776 | [[package]] 777 | name = "ntapi" 778 | version = "0.4.1" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 781 | dependencies = [ 782 | "winapi", 783 | ] 784 | 785 | [[package]] 786 | name = "num-derive" 787 | version = "0.4.2" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 790 | dependencies = [ 791 | "proc-macro2", 792 | "quote", 793 | "syn", 794 | ] 795 | 796 | [[package]] 797 | name = "num-traits" 798 | version = "0.2.19" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 801 | dependencies = [ 802 | "autocfg", 803 | ] 804 | 805 | [[package]] 806 | name = "num_enum" 807 | version = "0.7.3" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" 810 | dependencies = [ 811 | "num_enum_derive", 812 | ] 813 | 814 | [[package]] 815 | name = "num_enum_derive" 816 | version = "0.7.3" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" 819 | dependencies = [ 820 | "proc-macro-crate", 821 | "proc-macro2", 822 | "quote", 823 | "syn", 824 | ] 825 | 826 | [[package]] 827 | name = "objc" 828 | version = "0.2.7" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 831 | dependencies = [ 832 | "malloc_buf", 833 | ] 834 | 835 | [[package]] 836 | name = "oboe" 837 | version = "0.6.1" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" 840 | dependencies = [ 841 | "jni", 842 | "ndk", 843 | "ndk-context", 844 | "num-derive", 845 | "num-traits", 846 | "oboe-sys", 847 | ] 848 | 849 | [[package]] 850 | name = "oboe-sys" 851 | version = "0.6.1" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" 854 | dependencies = [ 855 | "cc", 856 | ] 857 | 858 | [[package]] 859 | name = "once_cell" 860 | version = "1.19.0" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 863 | 864 | [[package]] 865 | name = "parking_lot" 866 | version = "0.12.4" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 869 | dependencies = [ 870 | "lock_api", 871 | "parking_lot_core", 872 | ] 873 | 874 | [[package]] 875 | name = "parking_lot_core" 876 | version = "0.9.11" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 879 | dependencies = [ 880 | "cfg-if", 881 | "libc", 882 | "redox_syscall", 883 | "smallvec", 884 | "windows-targets 0.52.6", 885 | ] 886 | 887 | [[package]] 888 | name = "pin-project-lite" 889 | version = "0.2.14" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 892 | 893 | [[package]] 894 | name = "pin-utils" 895 | version = "0.1.0" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 898 | 899 | [[package]] 900 | name = "pipewire" 901 | version = "0.8.0" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" 904 | dependencies = [ 905 | "anyhow", 906 | "bitflags 2.6.0", 907 | "libc", 908 | "libspa", 909 | "libspa-sys", 910 | "nix", 911 | "once_cell", 912 | "pipewire-sys", 913 | "thiserror 1.0.69", 914 | ] 915 | 916 | [[package]] 917 | name = "pipewire-sys" 918 | version = "0.8.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" 921 | dependencies = [ 922 | "bindgen 0.69.4", 923 | "libspa-sys", 924 | "system-deps", 925 | ] 926 | 927 | [[package]] 928 | name = "pkg-config" 929 | version = "0.3.31" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 932 | 933 | [[package]] 934 | name = "ppv-lite86" 935 | version = "0.2.20" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 938 | dependencies = [ 939 | "zerocopy", 940 | ] 941 | 942 | [[package]] 943 | name = "proc-macro-crate" 944 | version = "3.3.0" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 947 | dependencies = [ 948 | "toml_edit", 949 | ] 950 | 951 | [[package]] 952 | name = "proc-macro2" 953 | version = "1.0.94" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 956 | dependencies = [ 957 | "unicode-ident", 958 | ] 959 | 960 | [[package]] 961 | name = "quote" 962 | version = "1.0.37" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 965 | dependencies = [ 966 | "proc-macro2", 967 | ] 968 | 969 | [[package]] 970 | name = "rand" 971 | version = "0.8.5" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 974 | dependencies = [ 975 | "libc", 976 | "rand_chacha", 977 | "rand_core", 978 | ] 979 | 980 | [[package]] 981 | name = "rand_chacha" 982 | version = "0.3.1" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 985 | dependencies = [ 986 | "ppv-lite86", 987 | "rand_core", 988 | ] 989 | 990 | [[package]] 991 | name = "rand_core" 992 | version = "0.6.4" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 995 | dependencies = [ 996 | "getrandom", 997 | ] 998 | 999 | [[package]] 1000 | name = "rayon" 1001 | version = "1.10.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1004 | dependencies = [ 1005 | "either", 1006 | "rayon-core", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "rayon-core" 1011 | version = "1.12.1" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1014 | dependencies = [ 1015 | "crossbeam-deque", 1016 | "crossbeam-utils", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "redox_syscall" 1021 | version = "0.5.4" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" 1024 | dependencies = [ 1025 | "bitflags 2.6.0", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "regex" 1030 | version = "1.10.6" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 1033 | dependencies = [ 1034 | "aho-corasick", 1035 | "memchr", 1036 | "regex-automata", 1037 | "regex-syntax", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "regex-automata" 1042 | version = "0.4.7" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 1045 | dependencies = [ 1046 | "aho-corasick", 1047 | "memchr", 1048 | "regex-syntax", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "regex-syntax" 1053 | version = "0.8.4" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 1056 | 1057 | [[package]] 1058 | name = "rustc-hash" 1059 | version = "1.1.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1062 | 1063 | [[package]] 1064 | name = "rustversion" 1065 | version = "1.0.20" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1068 | 1069 | [[package]] 1070 | name = "same-file" 1071 | version = "1.0.6" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1074 | dependencies = [ 1075 | "winapi-util", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "scap" 1080 | version = "0.1.0-beta.1" 1081 | dependencies = [ 1082 | "cidre", 1083 | "cocoa", 1084 | "core-graphics-helmer-fork", 1085 | "cpal", 1086 | "dbus", 1087 | "futures", 1088 | "objc", 1089 | "pipewire", 1090 | "rand", 1091 | "sysinfo", 1092 | "thiserror 2.0.12", 1093 | "windows 0.58.0", 1094 | "windows-capture", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "scopeguard" 1099 | version = "1.2.0" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1102 | 1103 | [[package]] 1104 | name = "serde" 1105 | version = "1.0.210" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 1108 | dependencies = [ 1109 | "serde_derive", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "serde_derive" 1114 | version = "1.0.210" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 1117 | dependencies = [ 1118 | "proc-macro2", 1119 | "quote", 1120 | "syn", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "serde_spanned" 1125 | version = "0.6.8" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1128 | dependencies = [ 1129 | "serde", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "shlex" 1134 | version = "1.3.0" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1137 | 1138 | [[package]] 1139 | name = "slab" 1140 | version = "0.4.9" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1143 | dependencies = [ 1144 | "autocfg", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "smallvec" 1149 | version = "1.13.2" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1152 | 1153 | [[package]] 1154 | name = "syn" 1155 | version = "2.0.100" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1158 | dependencies = [ 1159 | "proc-macro2", 1160 | "quote", 1161 | "unicode-ident", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "sysinfo" 1166 | version = "0.30.13" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" 1169 | dependencies = [ 1170 | "cfg-if", 1171 | "core-foundation-sys", 1172 | "libc", 1173 | "ntapi", 1174 | "once_cell", 1175 | "rayon", 1176 | "windows 0.52.0", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "system-deps" 1181 | version = "6.2.2" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" 1184 | dependencies = [ 1185 | "cfg-expr", 1186 | "heck", 1187 | "pkg-config", 1188 | "toml", 1189 | "version-compare", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "target-lexicon" 1194 | version = "0.12.16" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 1197 | 1198 | [[package]] 1199 | name = "thiserror" 1200 | version = "1.0.69" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1203 | dependencies = [ 1204 | "thiserror-impl 1.0.69", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "thiserror" 1209 | version = "2.0.12" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1212 | dependencies = [ 1213 | "thiserror-impl 2.0.12", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "thiserror-impl" 1218 | version = "1.0.69" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1221 | dependencies = [ 1222 | "proc-macro2", 1223 | "quote", 1224 | "syn", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "thiserror-impl" 1229 | version = "2.0.12" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1232 | dependencies = [ 1233 | "proc-macro2", 1234 | "quote", 1235 | "syn", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "toml" 1240 | version = "0.8.19" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1243 | dependencies = [ 1244 | "serde", 1245 | "serde_spanned", 1246 | "toml_datetime", 1247 | "toml_edit", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "toml_datetime" 1252 | version = "0.6.8" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1255 | dependencies = [ 1256 | "serde", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "toml_edit" 1261 | version = "0.22.24" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1264 | dependencies = [ 1265 | "indexmap", 1266 | "serde", 1267 | "serde_spanned", 1268 | "toml_datetime", 1269 | "winnow", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "unicode-ident" 1274 | version = "1.0.13" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1277 | 1278 | [[package]] 1279 | name = "unicode-segmentation" 1280 | version = "1.12.0" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1283 | 1284 | [[package]] 1285 | name = "unicode-width" 1286 | version = "0.1.14" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1289 | 1290 | [[package]] 1291 | name = "version-compare" 1292 | version = "0.2.0" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 1295 | 1296 | [[package]] 1297 | name = "walkdir" 1298 | version = "2.5.0" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1301 | dependencies = [ 1302 | "same-file", 1303 | "winapi-util", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "wasi" 1308 | version = "0.11.0+wasi-snapshot-preview1" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1311 | 1312 | [[package]] 1313 | name = "wasm-bindgen" 1314 | version = "0.2.100" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1317 | dependencies = [ 1318 | "cfg-if", 1319 | "once_cell", 1320 | "rustversion", 1321 | "wasm-bindgen-macro", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "wasm-bindgen-backend" 1326 | version = "0.2.100" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1329 | dependencies = [ 1330 | "bumpalo", 1331 | "log", 1332 | "proc-macro2", 1333 | "quote", 1334 | "syn", 1335 | "wasm-bindgen-shared", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "wasm-bindgen-futures" 1340 | version = "0.4.50" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1343 | dependencies = [ 1344 | "cfg-if", 1345 | "js-sys", 1346 | "once_cell", 1347 | "wasm-bindgen", 1348 | "web-sys", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "wasm-bindgen-macro" 1353 | version = "0.2.100" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1356 | dependencies = [ 1357 | "quote", 1358 | "wasm-bindgen-macro-support", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "wasm-bindgen-macro-support" 1363 | version = "0.2.100" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1366 | dependencies = [ 1367 | "proc-macro2", 1368 | "quote", 1369 | "syn", 1370 | "wasm-bindgen-backend", 1371 | "wasm-bindgen-shared", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "wasm-bindgen-shared" 1376 | version = "0.2.100" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1379 | dependencies = [ 1380 | "unicode-ident", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "web-sys" 1385 | version = "0.3.77" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1388 | dependencies = [ 1389 | "js-sys", 1390 | "wasm-bindgen", 1391 | ] 1392 | 1393 | [[package]] 1394 | name = "winapi" 1395 | version = "0.3.9" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1398 | dependencies = [ 1399 | "winapi-i686-pc-windows-gnu", 1400 | "winapi-x86_64-pc-windows-gnu", 1401 | ] 1402 | 1403 | [[package]] 1404 | name = "winapi-i686-pc-windows-gnu" 1405 | version = "0.4.0" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1408 | 1409 | [[package]] 1410 | name = "winapi-util" 1411 | version = "0.1.9" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1414 | dependencies = [ 1415 | "windows-sys 0.59.0", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "winapi-x86_64-pc-windows-gnu" 1420 | version = "0.4.0" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1423 | 1424 | [[package]] 1425 | name = "windows" 1426 | version = "0.52.0" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" 1429 | dependencies = [ 1430 | "windows-core 0.52.0", 1431 | "windows-targets 0.52.6", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "windows" 1436 | version = "0.54.0" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" 1439 | dependencies = [ 1440 | "windows-core 0.54.0", 1441 | "windows-targets 0.52.6", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "windows" 1446 | version = "0.58.0" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" 1449 | dependencies = [ 1450 | "windows-core 0.58.0", 1451 | "windows-targets 0.52.6", 1452 | ] 1453 | 1454 | [[package]] 1455 | name = "windows" 1456 | version = "0.61.3" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 1459 | dependencies = [ 1460 | "windows-collections", 1461 | "windows-core 0.61.2", 1462 | "windows-future", 1463 | "windows-link", 1464 | "windows-numerics", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "windows-capture" 1469 | version = "1.5.0" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" 1472 | dependencies = [ 1473 | "parking_lot", 1474 | "rayon", 1475 | "thiserror 2.0.12", 1476 | "windows 0.61.3", 1477 | "windows-future", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "windows-collections" 1482 | version = "0.2.0" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 1485 | dependencies = [ 1486 | "windows-core 0.61.2", 1487 | ] 1488 | 1489 | [[package]] 1490 | name = "windows-core" 1491 | version = "0.52.0" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1494 | dependencies = [ 1495 | "windows-targets 0.52.6", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "windows-core" 1500 | version = "0.54.0" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" 1503 | dependencies = [ 1504 | "windows-result 0.1.2", 1505 | "windows-targets 0.52.6", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "windows-core" 1510 | version = "0.58.0" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" 1513 | dependencies = [ 1514 | "windows-implement 0.58.0", 1515 | "windows-interface 0.58.0", 1516 | "windows-result 0.2.0", 1517 | "windows-strings 0.1.0", 1518 | "windows-targets 0.52.6", 1519 | ] 1520 | 1521 | [[package]] 1522 | name = "windows-core" 1523 | version = "0.61.2" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 1526 | dependencies = [ 1527 | "windows-implement 0.60.0", 1528 | "windows-interface 0.59.1", 1529 | "windows-link", 1530 | "windows-result 0.3.4", 1531 | "windows-strings 0.4.2", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "windows-future" 1536 | version = "0.2.1" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 1539 | dependencies = [ 1540 | "windows-core 0.61.2", 1541 | "windows-link", 1542 | "windows-threading", 1543 | ] 1544 | 1545 | [[package]] 1546 | name = "windows-implement" 1547 | version = "0.58.0" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" 1550 | dependencies = [ 1551 | "proc-macro2", 1552 | "quote", 1553 | "syn", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "windows-implement" 1558 | version = "0.60.0" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 1561 | dependencies = [ 1562 | "proc-macro2", 1563 | "quote", 1564 | "syn", 1565 | ] 1566 | 1567 | [[package]] 1568 | name = "windows-interface" 1569 | version = "0.58.0" 1570 | source = "registry+https://github.com/rust-lang/crates.io-index" 1571 | checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" 1572 | dependencies = [ 1573 | "proc-macro2", 1574 | "quote", 1575 | "syn", 1576 | ] 1577 | 1578 | [[package]] 1579 | name = "windows-interface" 1580 | version = "0.59.1" 1581 | source = "registry+https://github.com/rust-lang/crates.io-index" 1582 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1583 | dependencies = [ 1584 | "proc-macro2", 1585 | "quote", 1586 | "syn", 1587 | ] 1588 | 1589 | [[package]] 1590 | name = "windows-link" 1591 | version = "0.1.3" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1594 | 1595 | [[package]] 1596 | name = "windows-numerics" 1597 | version = "0.2.0" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 1600 | dependencies = [ 1601 | "windows-core 0.61.2", 1602 | "windows-link", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "windows-result" 1607 | version = "0.1.2" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 1610 | dependencies = [ 1611 | "windows-targets 0.52.6", 1612 | ] 1613 | 1614 | [[package]] 1615 | name = "windows-result" 1616 | version = "0.2.0" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 1619 | dependencies = [ 1620 | "windows-targets 0.52.6", 1621 | ] 1622 | 1623 | [[package]] 1624 | name = "windows-result" 1625 | version = "0.3.4" 1626 | source = "registry+https://github.com/rust-lang/crates.io-index" 1627 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 1628 | dependencies = [ 1629 | "windows-link", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "windows-strings" 1634 | version = "0.1.0" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 1637 | dependencies = [ 1638 | "windows-result 0.2.0", 1639 | "windows-targets 0.52.6", 1640 | ] 1641 | 1642 | [[package]] 1643 | name = "windows-strings" 1644 | version = "0.4.2" 1645 | source = "registry+https://github.com/rust-lang/crates.io-index" 1646 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 1647 | dependencies = [ 1648 | "windows-link", 1649 | ] 1650 | 1651 | [[package]] 1652 | name = "windows-sys" 1653 | version = "0.45.0" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1656 | dependencies = [ 1657 | "windows-targets 0.42.2", 1658 | ] 1659 | 1660 | [[package]] 1661 | name = "windows-sys" 1662 | version = "0.59.0" 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" 1664 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1665 | dependencies = [ 1666 | "windows-targets 0.52.6", 1667 | ] 1668 | 1669 | [[package]] 1670 | name = "windows-targets" 1671 | version = "0.42.2" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1674 | dependencies = [ 1675 | "windows_aarch64_gnullvm 0.42.2", 1676 | "windows_aarch64_msvc 0.42.2", 1677 | "windows_i686_gnu 0.42.2", 1678 | "windows_i686_msvc 0.42.2", 1679 | "windows_x86_64_gnu 0.42.2", 1680 | "windows_x86_64_gnullvm 0.42.2", 1681 | "windows_x86_64_msvc 0.42.2", 1682 | ] 1683 | 1684 | [[package]] 1685 | name = "windows-targets" 1686 | version = "0.52.6" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1689 | dependencies = [ 1690 | "windows_aarch64_gnullvm 0.52.6", 1691 | "windows_aarch64_msvc 0.52.6", 1692 | "windows_i686_gnu 0.52.6", 1693 | "windows_i686_gnullvm", 1694 | "windows_i686_msvc 0.52.6", 1695 | "windows_x86_64_gnu 0.52.6", 1696 | "windows_x86_64_gnullvm 0.52.6", 1697 | "windows_x86_64_msvc 0.52.6", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "windows-threading" 1702 | version = "0.1.0" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 1705 | dependencies = [ 1706 | "windows-link", 1707 | ] 1708 | 1709 | [[package]] 1710 | name = "windows_aarch64_gnullvm" 1711 | version = "0.42.2" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1714 | 1715 | [[package]] 1716 | name = "windows_aarch64_gnullvm" 1717 | version = "0.52.6" 1718 | source = "registry+https://github.com/rust-lang/crates.io-index" 1719 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1720 | 1721 | [[package]] 1722 | name = "windows_aarch64_msvc" 1723 | version = "0.42.2" 1724 | source = "registry+https://github.com/rust-lang/crates.io-index" 1725 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1726 | 1727 | [[package]] 1728 | name = "windows_aarch64_msvc" 1729 | version = "0.52.6" 1730 | source = "registry+https://github.com/rust-lang/crates.io-index" 1731 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1732 | 1733 | [[package]] 1734 | name = "windows_i686_gnu" 1735 | version = "0.42.2" 1736 | source = "registry+https://github.com/rust-lang/crates.io-index" 1737 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1738 | 1739 | [[package]] 1740 | name = "windows_i686_gnu" 1741 | version = "0.52.6" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1744 | 1745 | [[package]] 1746 | name = "windows_i686_gnullvm" 1747 | version = "0.52.6" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1750 | 1751 | [[package]] 1752 | name = "windows_i686_msvc" 1753 | version = "0.42.2" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1756 | 1757 | [[package]] 1758 | name = "windows_i686_msvc" 1759 | version = "0.52.6" 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" 1761 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1762 | 1763 | [[package]] 1764 | name = "windows_x86_64_gnu" 1765 | version = "0.42.2" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1768 | 1769 | [[package]] 1770 | name = "windows_x86_64_gnu" 1771 | version = "0.52.6" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1774 | 1775 | [[package]] 1776 | name = "windows_x86_64_gnullvm" 1777 | version = "0.42.2" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1780 | 1781 | [[package]] 1782 | name = "windows_x86_64_gnullvm" 1783 | version = "0.52.6" 1784 | source = "registry+https://github.com/rust-lang/crates.io-index" 1785 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1786 | 1787 | [[package]] 1788 | name = "windows_x86_64_msvc" 1789 | version = "0.42.2" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1792 | 1793 | [[package]] 1794 | name = "windows_x86_64_msvc" 1795 | version = "0.52.6" 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" 1797 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1798 | 1799 | [[package]] 1800 | name = "winnow" 1801 | version = "0.7.6" 1802 | source = "registry+https://github.com/rust-lang/crates.io-index" 1803 | checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" 1804 | dependencies = [ 1805 | "memchr", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "yansi-term" 1810 | version = "0.1.2" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" 1813 | dependencies = [ 1814 | "winapi", 1815 | ] 1816 | 1817 | [[package]] 1818 | name = "zerocopy" 1819 | version = "0.7.35" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1822 | dependencies = [ 1823 | "byteorder", 1824 | "zerocopy-derive", 1825 | ] 1826 | 1827 | [[package]] 1828 | name = "zerocopy-derive" 1829 | version = "0.7.35" 1830 | source = "registry+https://github.com/rust-lang/crates.io-index" 1831 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1832 | dependencies = [ 1833 | "proc-macro2", 1834 | "quote", 1835 | "syn", 1836 | ] 1837 | --------------------------------------------------------------------------------