diff --git a/src/capturable_content.rs b/src/capturable_content.rs index 7339335b..4ae1f1be 100644 --- a/src/capturable_content.rs +++ b/src/capturable_content.rs @@ -187,6 +187,33 @@ impl ExactSizeIterator for CapturableDisplayIterator<'_> { } } +/// An iterator over capturable excluding_windows +pub struct CapturableExcludingWindowIterator<'content> { + content: &'content CapturableContent, + i: usize +} + +impl Iterator for CapturableExcludingWindowIterator<'_> { + type Item = CapturableWindow; + + fn next(&mut self) -> Option { + if self.i < self.content.impl_capturable_content.excluding_windows.len() { + let i = self.i; + self.i += 1; + Some(CapturableWindow { impl_capturable_window: ImplCapturableWindow::from_impl(self.content.impl_capturable_content.excluding_windows[i].clone()) }) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.i, Some(self.content.impl_capturable_content.excluding_windows.len())) + } +} + +impl ExactSizeIterator for CapturableExcludingWindowIterator<'_> { +} + impl CapturableContent { /// Requests capturable content from the OS /// @@ -203,6 +230,11 @@ impl CapturableContent { CapturableWindowIterator { content: self, i: 0 } } + /// Get an iterator over the capturable excluding windows + pub fn excluding_windows<'a>(&'a self) -> CapturableExcludingWindowIterator<'a> { + CapturableExcludingWindowIterator { content: self, i: 0 } + } + /// Get an iterator over the capturable displays pub fn displays<'a>(&'a self) -> CapturableDisplayIterator<'a> { CapturableDisplayIterator { content: self, i: 0 } @@ -225,6 +257,11 @@ unsafe impl Send for CapturableWindow {} unsafe impl Sync for CapturableWindow {} impl CapturableWindow { + /// Gets the id of the window + pub fn id(&self) -> u32 { + self.impl_capturable_window.id() + } + /// Gets the title of the window pub fn title(&self) -> String { self.impl_capturable_window.title() diff --git a/src/capture_stream.rs b/src/capture_stream.rs index ab1ed215..23816cc0 100644 --- a/src/capture_stream.rs +++ b/src/capture_stream.rs @@ -171,6 +171,7 @@ pub struct CaptureConfig { pub(crate) capture_audio: Option, pub(crate) impl_capture_config: ImplCaptureConfig, pub(crate) buffer_count: usize, + pub(crate) excluded_windows: Option> } /// Represents an error creating the capture config @@ -221,14 +222,16 @@ impl CaptureConfig { impl_capture_config: ImplCaptureConfig::new(), capture_audio: None, buffer_count: 3, + excluded_windows: None }) } /// Create a capture configuration for a given capturable display - pub fn with_display(display: CapturableDisplay, pixel_format: CapturePixelFormat) -> CaptureConfig { + pub fn with_display(display: CapturableDisplay, pixel_format: CapturePixelFormat, excluded_windows: Option>) -> CaptureConfig { let rect = display.rect(); CaptureConfig { target: Capturable::Display(display), + excluded_windows: excluded_windows, pixel_format, output_size: rect.size, show_cursor: false, diff --git a/src/platform/macos/capturable_content.rs b/src/platform/macos/capturable_content.rs index 994fc194..32d05a33 100644 --- a/src/platform/macos/capturable_content.rs +++ b/src/platform/macos/capturable_content.rs @@ -10,6 +10,7 @@ use super::objc_wrap::{get_window_description, get_window_levels, CGMainDisplayI pub struct MacosCapturableContent { pub windows: Vec, + pub excluding_windows: Vec, pub displays: Vec, } @@ -32,6 +33,10 @@ impl MacosCapturableContent { .into_iter() .filter(|window| filter.impl_capturable_content_filter.filter_scwindow(window)) .collect(); + let excluding_windows = content.windows() + .into_iter() + .filter(|window| !filter.impl_capturable_content_filter.filter_scwindow(window)) + .collect(); let displays = content.displays() .into_iter() .filter(|display| filter.impl_capturable_content_filter.filter_scdisplay(display)) @@ -39,6 +44,7 @@ impl MacosCapturableContent { Ok(Self { windows, displays, + excluding_windows, }) }, Ok(Err(error)) => { @@ -61,6 +67,10 @@ impl MacosCapturableWindow { } } + pub fn id(&self) -> u32 { + self.window.id().0 + } + pub fn title(&self) -> String { self.window.title() } diff --git a/src/platform/macos/capture_stream.rs b/src/platform/macos/capture_stream.rs index 47a2fac4..33387c4b 100644 --- a/src/platform/macos/capture_stream.rs +++ b/src/platform/macos/capture_stream.rs @@ -387,95 +387,177 @@ impl MacosCaptureStream { }) }, Capturable::Display(display) => { - let options_dict = NSDictionary::new_mutable(); - - #[cfg(feature = "metal")] - let callback_metal_device = metal_device.clone(); - - let display_id = display.impl_capturable_display.display.raw_id(); - - let size = (capture_config.output_size.width.ceil() as usize, capture_config.output_size.height.ceil() as usize); - + let mut config = SCStreamConfiguration::new(); let (pixel_format, set_color_matrix) = match capture_config.pixel_format { CapturePixelFormat::Bgra8888 => (SCStreamPixelFormat::BGRA8888, false), CapturePixelFormat::Argb2101010 => (SCStreamPixelFormat::L10R, false), CapturePixelFormat::V420 => (SCStreamPixelFormat::V420, true), CapturePixelFormat::F420 => (SCStreamPixelFormat::F420, true), }; + if set_color_matrix { + config.set_color_matrix(SCStreamColorMatrix::ItuR709_2); + } + config.set_pixel_format(pixel_format); + config.set_minimum_time_interval(CMTime::new_with_seconds(capture_config.impl_capture_config.maximum_fps.map(|x| 1.0 / x).unwrap_or(1.0 / 120.0) as f64, 240)); + /*config.set_source_rect(CGRect { + origin: CGPoint { + x: capture_config.source_rect.origin.x, + y: capture_config.source_rect.origin.y, + }, + size: CGSize { + x: capture_config.source_rect.size.width, + y: capture_config.source_rect.size.height + } + });*/ + let resolution_type = match capture_config.impl_capture_config.resolution_type { + MacosCaptureResolutionType::Automatic => SCCaptureResolutionType::SCCaptureResolutionAutomatic, + MacosCaptureResolutionType::Best => SCCaptureResolutionType::SCCaptureResolutionBest, + MacosCaptureResolutionType::Nominal => SCCaptureResolutionType::SCCaptureResolutionNominal, + }; + _ = config.set_resolution_type(resolution_type); + config.set_size(CGSize { + x: capture_config.output_size.width, + y: capture_config.output_size.height, + }); + config.set_scales_to_fit(capture_config.impl_capture_config.scale_to_fit); + config.set_queue_depth(capture_config.buffer_count as isize); + config.set_show_cursor(capture_config.show_cursor); + match capture_config.capture_audio { + Some(audio_config) => { + config.set_capture_audio(true); + let channel_count = match audio_config.channel_count { + crate::prelude::AudioChannelCount::Mono => 1, + crate::prelude::AudioChannelCount::Stereo => 2, + }; + config.set_channel_count(channel_count); + config.set_exclude_current_process_audio(audio_config.impl_capture_audio_config.exclude_current_process_audio); + let sample_rate = match audio_config.sample_rate { + crate::prelude::AudioSampleRate::Hz8000 => SCStreamSampleRate::R8000, + crate::prelude::AudioSampleRate::Hz16000 => SCStreamSampleRate::R16000, + crate::prelude::AudioSampleRate::Hz24000 => SCStreamSampleRate::R24000, + crate::prelude::AudioSampleRate::Hz48000 => SCStreamSampleRate::R48000, + }; + config.set_sample_rate(sample_rate); + }, + None => { + config.set_capture_audio(false); + } + } + let mut filter; + let windows = capture_config.excluded_windows; + let mut sc_windows = Vec::new(); + if let Some(windows) = windows { + for w in windows { + sc_windows.push(w.impl_capturable_window.window); + } + filter = SCContentFilter::new_with_display_excluding_window(display.impl_capturable_display.display, Some(sc_windows)); + } else { + filter = SCContentFilter::new_with_display_excluding_window(display.impl_capturable_display.display, None); + } + + + let handler_queue = DispatchQueue::make_concurrent("com.augmend.crabgrab.window_capture".into()); - let dispatch_queue = DispatchQueue::make_concurrent("crabgrab.capture".into()); - let mut audio_frame_id_counter = AtomicU64::new(0); let mut video_frame_id_counter = AtomicU64::new(0); let stopped_flag = Arc::new(AtomicBool::new(false)); let callback_stopped_flag = stopped_flag.clone(); - - let capture_time = Instant::now(); - - let stream_callback = move |status, duration, io_surface: IOSurface| { - let now = Instant::now(); - match status { - CGDisplayStreamFrameStatus::Complete => { - let frame_id = video_frame_id_counter.fetch_add(1, atomic::Ordering::AcqRel); - let rect = display.impl_capturable_display.display.frame(); - let w = io_surface.get_width(); - let h = io_surface.get_height(); - let video_frame = VideoFrame{ - impl_video_frame: MacosVideoFrame::CGDisplayStream ( - MacosCGDisplayStreamVideoFrame { - io_surface, - duration, - capture_timestamp: now, - capture_time: now - capture_time, - frame_id, - source_rect: Rect { - origin: Point { x: rect.origin.x, y: rect.origin.y }, - size: Size { width: rect.size.x, height: rect.size.y }, + + let handler = SCStreamHandler::new(Box::new(move |stream_result: Result<(CMSampleBuffer, SCStreamOutputType), SCStreamCallbackError>| { + let mut callback = stream_shared_callback.lock(); + let capture_time = Instant::now(); + match stream_result { + Ok((sample_buffer, output_type)) => { + match output_type { + SCStreamOutputType::Audio => { + let frame_id = audio_frame_id_counter.fetch_add(1, atomic::Ordering::AcqRel); + // TODO... + }, + SCStreamOutputType::Screen => { + let attachments = sample_buffer.get_sample_attachment_array(); + if attachments.len() == 0 { + return; + } + let status_nsnumber_ptr = unsafe { attachments[0].get_value(SCStreamFrameInfoStatus) }; + if status_nsnumber_ptr.is_null() { + return; + } + let status_i32 = unsafe { NSNumber::from_id_unretained(status_nsnumber_ptr as *mut AnyObject).as_i32() }; + let status_opt = SCFrameStatus::from_i32(status_i32); + if status_opt.is_none() { + return; + } + match status_opt.unwrap() { + SCFrameStatus::Complete => { + if callback_stopped_flag.load(atomic::Ordering::Acquire) { + return; + } + let frame_id = video_frame_id_counter.fetch_add(1, atomic::Ordering::AcqRel); + let video_frame = VideoFrame { + impl_video_frame: MacosVideoFrame::SCStream(MacosSCStreamVideoFrame { + sample_buffer, + capture_time, + dictionary: RefCell::new(None), + frame_id, + #[cfg(feature = "metal")] + metal_device: Some(callback_metal_device.clone()), + #[cfg(feature = "wgpu")] + wgpu_device: callback_wgpu_device.clone(), + }) + }; + (callback)(Ok(StreamEvent::Video(video_frame))); }, - dest_size: Size { width: w as f64, height: h as f64 }, - #[cfg(feature = "metal")] - metal_device: callback_metal_device.clone(), - #[cfg(feature = "wgpu")] - wgpu_device: callback_wgpu_device.clone(), + SCFrameStatus::Suspended | + SCFrameStatus::Idle => { + if callback_stopped_flag.load(atomic::Ordering::Acquire) { + return; + } + (callback)(Ok(StreamEvent::Idle)); + }, + SCFrameStatus::Stopped => { + if callback_stopped_flag.fetch_or(true, atomic::Ordering::AcqRel) { + return; + } + (callback)(Ok(StreamEvent::End)); + } + _ => {} } - ) - }; - - let mut callback = stream_shared_callback.lock(); - if !callback_stopped_flag.load(atomic::Ordering::Acquire) { - (callback)(Ok(StreamEvent::Video(video_frame))); - } - }, - CGDisplayStreamFrameStatus::Idle => { - let mut callback = stream_shared_callback.lock(); - if !callback_stopped_flag.load(atomic::Ordering::Acquire) { - (callback)(Ok(StreamEvent::Idle)); - } - }, - CGDisplayStreamFrameStatus::Stopped => { - let mut callback = stream_shared_callback.lock(); - if !callback_stopped_flag.fetch_or(true, atomic::Ordering::AcqRel) { - (callback)(Ok(StreamEvent::End)); + + + }, } }, - _ => {} + Err(err) => { + let event = match err { + SCStreamCallbackError::StreamStopped => { + if callback_stopped_flag.fetch_or(true, atomic::Ordering::AcqRel) { + return; + } + Ok(StreamEvent::End) + }, + SCStreamCallbackError::SampleBufferCopyFailed => Err(StreamError::Other("Failed to copy sample buffer".into())), + SCStreamCallbackError::Other(e) => Err(StreamError::Other(format!("Internal stream failure: [description: {}, reason: {}, code: {}, domain: {}]", e.description(), e.reason(), e.code(), e.domain()))), + }; + (callback)(event); + } } - }; + })); - let display_stream = CGDisplayStream::new(stream_callback, display_id, size, pixel_format, options_dict, dispatch_queue); + let mut sc_stream = SCStream::new(filter, config, handler_queue, handler) + .map_err(|error| StreamCreateError::Other(error))?; - display_stream.start().map_err(|_| StreamCreateError::Other("Stream failed to start".into()))?; + sc_stream.start(); Ok(MacosCaptureStream { - stream: MacosCaptureStreamInternal::Display(display_stream), stopped_flag, shared_callback, + stream: MacosCaptureStreamInternal::Window(sc_stream), #[cfg(feature = "metal")] metal_device, #[cfg(feature = "wgpu")] wgpu_device - }) + }) } } diff --git a/src/platform/macos/objc_wrap.rs b/src/platform/macos/objc_wrap.rs index 268d377c..ba8ca2c9 100644 --- a/src/platform/macos/objc_wrap.rs +++ b/src/platform/macos/objc_wrap.rs @@ -398,7 +398,7 @@ impl NSArray { pub(crate) fn add_object(&mut self, object: T) { unsafe { - let _: () = msg_send![self.0, addAnyObject: object]; + let _: () = msg_send![self.0, addObject: object]; } } @@ -1219,6 +1219,22 @@ impl SCContentFilter { } } + pub(crate) fn new_with_display_excluding_window(display: SCDisplay, windows: Option>) -> Self { + unsafe { + + let mut windows_nsarray = NSArray::new_mutable(); + if let Some(windows) = windows { + for w in windows { + windows_nsarray.add_object(w.0); + } + } + + let id: *mut AnyObject = msg_send![class!(SCContentFilter), alloc]; + let _: *mut AnyObject = msg_send![id, initWithDisplay: display.0 excludingWindows: windows_nsarray.0]; + Self(id) + } + } + pub(crate) fn new_with_display_excluding_apps_excepting_windows(display: SCDisplay, excluded_applications: NSArray, excepting_windows: NSArray) -> Self { unsafe { let id: *mut AnyObject = msg_send![class!(SCContentFilter), alloc]; diff --git a/src/platform/windows/capturable_content.rs b/src/platform/windows/capturable_content.rs index 96818a5d..47829984 100644 --- a/src/platform/windows/capturable_content.rs +++ b/src/platform/windows/capturable_content.rs @@ -24,6 +24,11 @@ impl WindowsCapturableWindow { Self(hwnd) } + pub fn id(&self) -> u32 { + todo!("Getting ID not yet implemented for windows"); + return 0; + } + pub fn title(&self) -> String { unsafe { let text_length = GetWindowTextLengthW(self.0);