diff --git a/.all-contributorsrc b/.all-contributorsrc index 292a480..5de3337 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -44,6 +44,15 @@ "contributions": [ "code" ] + }, + { + "login": "krzykro2", + "name": "Kris Krolak", + "avatar_url": "https://avatars.githubusercontent.com/u/6817875?v=4", + "profile": "https://github.com/krzykro2", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 87cc9bb..2bdf462 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: macos-13 + runs-on: macos-14 steps: - uses: actions/checkout@v3 - name: Update rust diff --git a/CHANGELOG.md b/CHANGELOG.md index e114cee..c751107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.6] - 2024-02-06 + +### Fixed + +- [#37](https://github.com/svtlabs/screencapturekit-rs/issues/37) Fix error handling +- [#35](https://github.com/svtlabs/screencapturekit-rs/issues/35) Make safe StreamConfiguration defaults equal to unsafe + ## [0.2.5] - 2024-01-25 ### Fixed + - 0.2.4 had an issue with the configuration. This is fixed now. ## [0.2.4] - 2024-01-23 ### Fixed + - [#34](https://github.com/svtlabs/screencapturekit-rs/issues/34) minimum_frame_interval not working as expected - [#33](https://github.com/svtlabs/screencapturekit-rs/issues/33) Can no longer import SCFrameStatus @@ -50,13 +60,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2023-08-21 - - ### Added - Initial commit with prototype version -[unreleased]: https://github.com/svtlabs/screencapturekit-rs/compare/v0.2.5...HEAD +[unreleased]: https://github.com/svtlabs/screencapturekit-rs/compare/v0.2.6...HEAD +[0.2.6]: https://github.com/svtlabs/screencapturekit-rs/compare/v0.2.5...v0.2.6 [0.2.5]: https://github.com/svtlabs/screencapturekit-rs/compare/v0.2.4...v0.2.5 [0.2.4]: https://github.com/svtlabs/screencapturekit-rs/compare/v0.2.3...v0.2.4 [0.2.3]: https://github.com/svtlabs/screencapturekit-rs/compare/v0.2.2...v0.2.3 diff --git a/Cargo.toml b/Cargo.toml index 5b89525..e034e0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [package] -name = "screencapturekit" + +keywords = ["screencapture", "screencapturekit", "macos"] description = "Rust wrapper for apple's ScreenCaptureKit" +name = "screencapturekit" categories = [ "external-ffi-bindings", "multimedia", @@ -14,7 +16,6 @@ homepage = "https://github.com/svtlabs" edition = "2021" rust-version = "1.72" version = "0.3.0" -keywords = ["screencapture", "screencapturekit", "macos"] license = "MIT OR Apache-2.0" [lib] @@ -30,6 +31,3 @@ dispatch = "0.2" once_cell = "1" core-foundation = { version = "0.9", features = ["mac_os_10_8_features"] } core-graphics = { version = "0.23" } - -[[example]] -name = "test_fps" diff --git a/README.md b/README.md index 26a10a1..aca9804 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # screencapturekit-rs -[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) ## Introduction @@ -87,6 +87,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Tokuhiro Matsuno
Tokuhiro Matsuno

💻 bigduu
bigduu

💻 Pranav Joglekar
Pranav Joglekar

💻 + Kris Krolak
Kris Krolak

💻 diff --git a/screencapturekit-sys/src/as_ptr.rs b/screencapturekit-sys/src/as_ptr.rs new file mode 100644 index 0000000..0799429 --- /dev/null +++ b/screencapturekit-sys/src/as_ptr.rs @@ -0,0 +1,13 @@ +pub trait AsPtr { + fn as_ptr(&self) -> *const Self { + self as *const Self + } +} +pub trait AsMutPtr { + fn as_mut_ptr(&self) -> *mut Self { + self as *const _ as *mut Self + } +} + +impl AsPtr for T {} +impl AsMutPtr for T {} diff --git a/screencapturekit-sys/src/lib.rs b/screencapturekit-sys/src/lib.rs new file mode 100644 index 0000000..ec1d285 --- /dev/null +++ b/screencapturekit-sys/src/lib.rs @@ -0,0 +1,16 @@ +pub mod as_ptr; +pub mod audio_buffer; +pub mod cm_block_buffer_ref; +pub mod cm_format_description_ref; +pub mod cm_sample_buffer_ref; +pub mod content_filter; +pub mod cv_image_buffer_ref; +pub mod cv_pixel_buffer_ref; +pub mod macros; +pub mod os_types; +pub mod sc_stream_frame_info; +pub mod shareable_content; +pub mod stream; +pub mod stream_configuration; +pub mod stream_error_handler; +pub mod stream_output_handler; diff --git a/screencapturekit-sys/src/os_types.rs b/screencapturekit-sys/src/os_types.rs new file mode 100644 index 0000000..e8c6437 --- /dev/null +++ b/screencapturekit-sys/src/os_types.rs @@ -0,0 +1,5 @@ +pub mod base; +pub mod four_char_code; +pub mod geometry; +pub mod graphics; +pub mod rc; diff --git a/screencapturekit-sys/src/os_types/four_char_code.rs b/screencapturekit-sys/src/os_types/four_char_code.rs new file mode 100644 index 0000000..62ee96b --- /dev/null +++ b/screencapturekit-sys/src/os_types/four_char_code.rs @@ -0,0 +1,78 @@ +use std::{ + ascii, + fmt::{self}, +}; + +#[repr(transparent)] +#[derive(Copy, Clone, Default, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct FourCharCode(u32); + +impl fmt::Display for FourCharCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.format()) + } +} + +impl fmt::Debug for FourCharCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "\"")?; + f.write_str(&self.format())?; + write!(f, "\"") + } +} + +impl FourCharCode { + #[inline] + fn format(&self) -> String { + // Format as escaped ASCII string. + + let raw = self + .into_chars() + .into_iter() + .flat_map(ascii::escape_default) + .collect::>(); + + String::from_utf8(raw).unwrap() + } + /// Returns an instance from the integer value. + #[inline] + pub const fn from_int(int: u32) -> Self { + Self(int) + } + + /// Returns an instance from the 4-character code. + #[inline] + pub const fn from_chars(chars: [u8; 4]) -> Self { + Self(u32::from_be_bytes(chars)) + } + + /// Returns this descriptor's integer value. + #[inline] + pub const fn into_int(self) -> u32 { + self.0 + } + + /// Returns this descriptor's 4-character code. + #[inline] + pub const fn into_chars(self) -> [u8; 4] { + self.0.to_be_bytes() + } + + /// Returns `true` if all of the characters in `self` are ASCII. + #[inline] + pub const fn is_ascii(&self) -> bool { + const NON_ASCII: u32 = u32::from_be_bytes([128; 4]); + + self.0 & NON_ASCII == 0 + } + + /// Returns `true` if all of the characters in `self` are ASCII graphic + /// characters: U+0021 '!' ..= U+007E '~'. + #[inline] + pub const fn is_ascii_graphic(&self) -> bool { + matches!( + self.into_chars(), + [b'!'..=b'~', b'!'..=b'~', b'!'..=b'~', b'!'..=b'~'], + ) + } +} diff --git a/screencapturekit-sys/src/stream_error_handler.rs b/screencapturekit-sys/src/stream_error_handler.rs new file mode 100644 index 0000000..b7bc091 --- /dev/null +++ b/screencapturekit-sys/src/stream_error_handler.rs @@ -0,0 +1,112 @@ +use std::sync::Once; + +use objc::{ + class, + declare::ClassDecl, + runtime::{Class, Object, Sel}, + Message, *, +}; +use objc_foundation::INSObject; +use objc_id::Id; + +pub trait UnsafeSCStreamError: Send + Sync + 'static { + fn handle_error(&self); +} + +#[repr(C)] +pub(crate) struct UnsafeSCStreamErrorHandler {} + +unsafe impl Message for UnsafeSCStreamErrorHandler {} + +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::RwLock; +static ERROR_HANDLERS: Lazy>>> = + Lazy::new(|| RwLock::new(HashMap::new())); + +impl INSObject for UnsafeSCStreamErrorHandler { + fn class() -> &'static Class { + static REGISTER_UNSAFE_SC_ERROR_HANDLER: Once = Once::new(); + REGISTER_UNSAFE_SC_ERROR_HANDLER.call_once(|| { + let mut decl = ClassDecl::new("SCStreamErrorHandler", class!(NSObject)).unwrap(); + decl.add_ivar::("_hash"); + + extern "C" fn stream_error( + this: &mut Object, + _cmd: Sel, + _stream: *mut Object, + _error: *mut Object, + ) { + unsafe { + let hash = this.get_ivar::("_hash"); + let lookup = ERROR_HANDLERS.read().unwrap(); + let error_handler = lookup.get(hash).unwrap(); + error_handler.handle_error(); + }; + } + unsafe { + let stream_error_method: extern "C" fn(&mut Object, Sel, *mut Object, *mut Object) = + stream_error; + + decl.add_method(sel!(stream:didStopWithError:), stream_error_method); + } + + decl.register(); + }); + class!(SCStreamErrorHandler) + } +} + +impl UnsafeSCStreamErrorHandler { + fn store_error_handler(&mut self, error_handler: impl UnsafeSCStreamError) { + unsafe { + let obj = &mut *(self as *mut _ as *mut Object); + let hash = self.hash_code(); + ERROR_HANDLERS + .write() + .unwrap() + .insert(hash, Box::new(error_handler)); + obj.set_ivar("_hash", hash); + } + } + // Error handlers passed into here will currently live forever inside the statically + // allocated map. + // TODO: Remove the handler from the HashMap whenever the associated stream is dropped. + pub fn init(error_handler: impl UnsafeSCStreamError) -> Id { + let mut handle = Self::new(); + handle.store_error_handler(error_handler); + handle + } +} + +#[cfg(test)] +mod tests { + use std::ptr; + use std::sync::mpsc::{sync_channel, SyncSender}; + + use super::*; + + struct TestHandler { + error_tx: SyncSender<()>, + } + impl UnsafeSCStreamError for TestHandler { + fn handle_error(&self) { + eprintln!("ERROR!"); + if let Err(e) = self.error_tx.send(()) { + panic!("can't send error message back on the channel: {:?}", e); + } + } + } + + #[test] + fn test_sc_stream_error_handler() { + let (error_tx, error_rx) = sync_channel(1); + let handle = UnsafeSCStreamErrorHandler::init(TestHandler { error_tx }); + unsafe { + msg_send![handle, stream: ptr::null_mut::() didStopWithError: ptr::null_mut::()] + } + if let Err(e) = error_rx.recv_timeout(std::time::Duration::from_millis(250)) { + panic!("failed to hear back from the error channel: {:?}", e); + } + } +} diff --git a/screencapturekit/Cargo.toml b/screencapturekit/Cargo.toml new file mode 100644 index 0000000..632bb85 --- /dev/null +++ b/screencapturekit/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "screencapturekit" +description = "Safe wrapper for Apple's ScreenCaptureKit" +categories = [ + "api-bindings", + "multimedia", + "multimedia::video", + "os::macos-apis", +] +readme = "README.md" +repository = "https://github.com/svtlabs/screencapturekit-rs/tree/main/screencapturekit" + +edition.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true +keywords.workspace = true +homepage.workspace = true +rust-version.workspace = true + +[features] +ci = [] + +[lib] +path = "./src/lib.rs" + +[dependencies] +screencapturekit-sys = { version = "0.2.6", path = "../screencapturekit-sys" } diff --git a/screencapturekit/src/sc_error_handler.rs b/screencapturekit/src/sc_error_handler.rs new file mode 100644 index 0000000..2ecf7a8 --- /dev/null +++ b/screencapturekit/src/sc_error_handler.rs @@ -0,0 +1,24 @@ +use screencapturekit_sys::stream_error_handler::UnsafeSCStreamError; + +// TODO: It might make sense to be a little more precise with lifetimes, than 'static. +// The lifetime could be potentially only as long as the relevant Stream, if the +// handler was dropped correctly together with the stream. For now the handler is never +// dropped and lives forever inside a statically allocated HashMap. See the relevant +// code in screencapturekit-sys crate. +pub trait StreamErrorHandler: Send + Sync + 'static { + fn on_error(&self); +} + +pub(crate) struct StreamErrorHandlerWrapper(T); + +impl StreamErrorHandlerWrapper { + pub fn new(error_handler: T) -> Self { + StreamErrorHandlerWrapper(error_handler) + } +} + +impl UnsafeSCStreamError for StreamErrorHandlerWrapper { + fn handle_error(&self) { + self.0.on_error(); + } +} diff --git a/screencapturekit/src/sc_output_handler.rs b/screencapturekit/src/sc_output_handler.rs new file mode 100644 index 0000000..c11fc04 --- /dev/null +++ b/screencapturekit/src/sc_output_handler.rs @@ -0,0 +1,36 @@ +use screencapturekit_sys::{ + cm_sample_buffer_ref::CMSampleBufferRef, os_types::rc::Id, + stream_output_handler::UnsafeSCStreamOutput, +}; + +use crate::cm_sample_buffer::CMSampleBuffer; + +#[derive(Clone, Copy, Debug)] +pub enum SCStreamOutputType { + Screen, + Audio, +} +pub trait StreamOutput: Sync + Send + 'static { + fn did_output_sample_buffer(&self, sample_buffer: CMSampleBuffer, of_type: SCStreamOutputType); +} + +pub(crate) struct StreamOutputWrapper(T); + +impl StreamOutputWrapper { + pub fn new(output: T) -> Self { + Self(output) + } +} + +impl UnsafeSCStreamOutput for StreamOutputWrapper { + fn did_output_sample_buffer(&self, sample_buffer_ref: Id, of_type: u8) { + self.0.did_output_sample_buffer( + CMSampleBuffer::new(sample_buffer_ref), + match of_type { + 0 => SCStreamOutputType::Screen, + 1 => SCStreamOutputType::Audio, + _ => unreachable!(), + }, + ); + } +} diff --git a/screencapturekit/src/sc_running_application.rs b/screencapturekit/src/sc_running_application.rs new file mode 100644 index 0000000..f7762b6 --- /dev/null +++ b/screencapturekit/src/sc_running_application.rs @@ -0,0 +1,20 @@ +use screencapturekit_sys::{os_types::rc::ShareId, shareable_content::UnsafeSCRunningApplication}; + +#[derive(Debug)] +pub struct SCRunningApplication { + pub(crate) _unsafe_ref: ShareId, + pub process_id: i32, + pub bundle_identifier: Option, + pub application_name: Option, +} + +impl From> for SCRunningApplication { + fn from(unsafe_ref: ShareId) -> Self { + SCRunningApplication { + process_id: unsafe_ref.get_process_id(), + bundle_identifier: unsafe_ref.get_bundle_identifier(), + application_name: unsafe_ref.get_application_name(), + _unsafe_ref: unsafe_ref, + } + } +} diff --git a/screencapturekit/src/sc_stream_configuration.rs b/screencapturekit/src/sc_stream_configuration.rs new file mode 100644 index 0000000..297914a --- /dev/null +++ b/screencapturekit/src/sc_stream_configuration.rs @@ -0,0 +1,176 @@ +use crate::sc_types::base::CMTime; +use crate::sc_types::four_char_code::FourCharCode; +use crate::sc_types::geometry::CGRect; +use crate::sc_types::graphics::CGColor; +use screencapturekit_sys::{ + os_types::rc::Id, + stream_configuration::{UnsafeStreamConfiguration, UnsafeStreamConfigurationRef}, +}; + +pub static PIXEL_FORMATS: [PixelFormat; 4] = [ + PixelFormat::ARGB8888, + PixelFormat::ARGB2101010, + PixelFormat::YCbCr420f, + PixelFormat::YCbCr420v, +]; + +#[derive(Copy, Clone, Debug, Default)] +pub enum PixelFormat { + ARGB8888, + ARGB2101010, + #[default] + YCbCr420v, + YCbCr420f, +} + +impl From for PixelFormat { + fn from(val: FourCharCode) -> Self { + let code_str = val.to_string(); + match code_str.as_str() { + "BGRA" => PixelFormat::ARGB8888, + "l10r" => PixelFormat::ARGB2101010, + "420v" => PixelFormat::YCbCr420v, + "420f" => PixelFormat::YCbCr420f, + _ => unreachable!(), + } + } +} +impl From for FourCharCode { + fn from(val: PixelFormat) -> Self { + match val { + PixelFormat::ARGB8888 => FourCharCode::from_chars(*b"BGRA"), + PixelFormat::ARGB2101010 => FourCharCode::from_chars(*b"l10r"), + PixelFormat::YCbCr420v => FourCharCode::from_chars(*b"420v"), + PixelFormat::YCbCr420f => FourCharCode::from_chars(*b"420f"), + } + } +} + +pub struct Size { + // The width of the output. + pub width: u32, + // The height of the output. + pub height: u32, + // A boolean value that indicates whether to scale the output to fit the configured width and height. + pub scales_to_fit: bool, +} + +#[derive(Debug)] +pub struct SCStreamConfiguration { + // The width of the output. + pub width: u32, + // The height of the output. + pub height: u32, + // A boolean value that indicates whether to scale the output to fit the configured width and height. + pub scales_to_fit: bool, + // A rectangle that specifies the source area to capture. + pub source_rect: CGRect, + // A rectangle that specifies a destination into which to write the output. + pub destination_rect: CGRect, + // A boolean value that determines whether the cursor is visible in the stream. + pub shows_cursor: bool, + + // A Boolean value that determines if the stream preserves aspect ratio. + pub preserves_aspect_ratio: bool, + // Optimizing Performance + // The maximum number of frames for the queue to store. + pub queue_depth: u32, + // The desired minimum time between frame updates, in seconds. + pub minimum_frame_interval: CMTime, + // Configuring Audi + // A boolean value that indicates whether to capture audio. + pub captures_audio: bool, + // The sample rate for audio capture. + pub sample_rate: u32, + // The number of audio channels to capture. + pub channel_count: u32, + // A boolean value that indicates whether to exclude a + pub excludes_current_process_audio: bool, + // Configuring Colors + // A pixel format for sample buffers that a stream outputs. + pub pixel_format: PixelFormat, + // A color matrix to apply to the output surface. + pub color_matrix: &'static str, + // A color space to use for the output buffer. + pub color_space_name: &'static str, + // A background color for the output. + // Controlling Visibility + pub background_color: CGColor, +} + +impl Default for SCStreamConfiguration { + fn default() -> Self { + Self { + width: Default::default(), + height: Default::default(), + scales_to_fit: Default::default(), + source_rect: Default::default(), + destination_rect: Default::default(), + // Apple docs set this to true by default, but we are consistent with UnsafeStreamConfiguration. + shows_cursor: Default::default(), + preserves_aspect_ratio: true, + queue_depth: Default::default(), + minimum_frame_interval: Default::default(), + captures_audio: Default::default(), + sample_rate: Default::default(), + channel_count: Default::default(), + excludes_current_process_audio: Default::default(), + pixel_format: PixelFormat::ARGB8888, + color_matrix: Default::default(), + color_space_name: Default::default(), + background_color: Default::default(), + } + } +} + +impl SCStreamConfiguration { + pub fn from_size(width: u32, height: u32, scales_to_fit: bool) -> Self { + Self { + width, + height, + scales_to_fit, + ..Default::default() + } + } +} + +impl From for UnsafeStreamConfiguration { + fn from(value: SCStreamConfiguration) -> Self { + UnsafeStreamConfiguration { + width: value.width, + height: value.height, + scales_to_fit: value.scales_to_fit as i8, + source_rect: value.source_rect, + destination_rect: value.destination_rect, + preserves_aspect_ratio: value.preserves_aspect_ratio as i8, + pixel_format: value.pixel_format.into(), + color_matrix: value.color_matrix.into(), + color_space_name: value.color_space_name.into(), + background_color: value.background_color, + shows_cursor: value.shows_cursor as i8, + queue_depth: value.queue_depth, + minimum_frame_interval: value.minimum_frame_interval, + captures_audio: value.captures_audio as i8, + sample_rate: value.sample_rate, + channel_count: value.channel_count, + excludes_current_process_audio: value.excludes_current_process_audio as i8, + } + } +} + +impl From for Id { + fn from(value: SCStreamConfiguration) -> Self { + let unsafe_config: UnsafeStreamConfiguration = value.into(); + unsafe_config.into() + } +} + +#[cfg(test)] +mod get_configuration { + + use super::*; + #[test] + fn test_configuration() { + SCStreamConfiguration::from_size(100, 100, false); + } +} diff --git a/src/audio/audio_buffer.rs b/src/audio/audio_buffer.rs index 25c3370..efbbd1d 100644 --- a/src/audio/audio_buffer.rs +++ b/src/audio/audio_buffer.rs @@ -21,4 +21,4 @@ pub struct CopiedAudioBuffer { } #[allow(non_upper_case_globals)] -pub const kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment: u32 = 1<<0; +pub const kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment: u32 = 1 << 0; diff --git a/src/core_media/cm_format_description_ref.rs b/src/core_media/cm_format_description_ref.rs index 4c26c0b..96c2482 100644 --- a/src/core_media/cm_format_description_ref.rs +++ b/src/core_media/cm_format_description_ref.rs @@ -4,7 +4,9 @@ use crate::macros::declare_ref_type; declare_ref_type!(CMFormatDescriptionRef); impl CMFormatDescriptionRef { - pub fn audio_format_description_get_stream_basic_description(&self) -> Option<&AudioStreamBasicDescription> { + pub fn audio_format_description_get_stream_basic_description( + &self, + ) -> Option<&AudioStreamBasicDescription> { unsafe { let ptr = CMAudioFormatDescriptionGetStreamBasicDescription(self); if ptr.is_null() { @@ -176,5 +178,7 @@ pub const kAudioFormatFLAC: ::std::os::raw::c_uint = 1718378851; pub const kAudioFormatOpus: ::std::os::raw::c_uint = 1869641075; extern "C" { - pub fn CMAudioFormatDescriptionGetStreamBasicDescription(desc: *const CMFormatDescriptionRef) -> *const AudioStreamBasicDescription; + pub fn CMAudioFormatDescriptionGetStreamBasicDescription( + desc: *const CMFormatDescriptionRef, + ) -> *const AudioStreamBasicDescription; }