Skip to content

Commit b7d2946

Browse files
committed
feat(async): add AsyncSCStream with SampleBufferStream for async frame iteration
- Add SampleBufferStream for async frame iteration - AsyncSCStream::new() returns (stream, frames) tuple - Use frames.next().await for async frame processing - Separate sync (SCStream) and async (AsyncSCStream) APIs - Update 06_async example to demonstrate new pattern
1 parent 11d09de commit b7d2946

2 files changed

Lines changed: 205 additions & 89 deletions

File tree

examples/06_async.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Demonstrates async/await API (requires "async" feature).
44
//! This example shows:
55
//! - Async content retrieval
6-
//! - Async screenshot capture
6+
//! - Async stream with frame iteration
77
//! - Works with any async runtime (Tokio shown here)
88
99
#[cfg(not(feature = "async"))]
@@ -15,7 +15,7 @@ fn main() {
1515
#[cfg(feature = "async")]
1616
#[tokio::main]
1717
async fn main() -> Result<(), Box<dyn std::error::Error>> {
18-
use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCScreenshotManager};
18+
use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
1919
use screencapturekit::prelude::*;
2020

2121
println!("⚡ Async API Demo\n");
@@ -35,9 +35,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
3535
);
3636
}
3737

38-
// 2. Capture screenshot asynchronously
38+
// 2. Capture frames asynchronously
3939
if let Some(display) = displays.first() {
40-
println!("\nCapturing screenshot...");
40+
println!("\nStarting async capture...");
4141

4242
let filter = SCContentFilter::build()
4343
.display(display)
@@ -48,13 +48,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
4848
.set_width(1920)?
4949
.set_height(1080)?;
5050

51-
let image = AsyncSCScreenshotManager::capture_image(&filter, &config).await?;
51+
// Create async stream with 30-frame buffer
52+
let (stream, frames) = AsyncSCStream::new(
53+
&filter,
54+
&config,
55+
30,
56+
SCStreamOutputType::Screen
57+
);
58+
59+
stream.start_capture().await?;
5260

53-
println!("Captured: {}x{}", image.width(), image.height());
61+
// Capture 10 frames
62+
let mut count = 0;
63+
while count < 10 {
64+
if let Some(_frame) = frames.next().await {
65+
count += 1;
66+
println!(" Frame {}", count);
67+
}
68+
}
5469

55-
// Save screenshot
56-
image.save_to_png("async_screenshot.png")?;
57-
println!("✅ Saved to async_screenshot.png");
70+
stream.stop_capture().await?;
71+
println!("✅ Captured {} frames", count);
5872
}
5973

6074
Ok(())

src/async_api.rs

Lines changed: 182 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -325,86 +325,206 @@ impl AsyncSCContentSharingPicker {
325325
}
326326
}
327327

328-
/// Async wrapper for `SCStream`
328+
/// Async wrapper for `SCStream` with async frame iteration
329329
///
330-
/// Provides async methods for stream lifecycle operations.
330+
/// Provides async methods for stream lifecycle and frame iteration.
331331
/// **Executor-agnostic** - works with any async runtime.
332332
///
333-
/// Note: Stream handlers still use callbacks and are not awaitable.
334-
/// This wrapper provides async start/stop/update operations.
333+
/// # Examples
334+
///
335+
/// ```rust,no_run
336+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
337+
/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
338+
/// use screencapturekit::stream::configuration::SCStreamConfiguration;
339+
/// use screencapturekit::stream::content_filter::SCContentFilter;
340+
/// use screencapturekit::stream::output_type::SCStreamOutputType;
341+
///
342+
/// let content = AsyncSCShareableContent::get().await?;
343+
/// let display = &content.displays()[0];
344+
/// let filter = SCContentFilter::build().display(display).exclude_windows(&[]).build();
345+
/// let config = SCStreamConfiguration::build();
346+
///
347+
/// let (mut stream, frames) = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
348+
/// stream.start_capture().await?;
349+
///
350+
/// // Process frames asynchronously
351+
/// while let Some(frame) = frames.next().await {
352+
/// println!("Got frame!");
353+
/// }
354+
/// # Ok(())
355+
/// # }
356+
/// ```
335357
pub struct AsyncSCStream {
336358
stream: crate::stream::SCStream,
337359
}
338360

339-
impl AsyncSCStream {
340-
/// Create a new async stream
341-
///
342-
/// This is synchronous - the stream is created immediately.
343-
#[must_use]
344-
pub fn new(filter: &SCContentFilter, config: &SCStreamConfiguration) -> Self {
345-
Self {
346-
stream: crate::stream::SCStream::new(filter, config),
361+
/// Async stream of sample buffers
362+
///
363+
/// Provides async iteration over captured frames.
364+
/// Created by [`AsyncSCStream::new`].
365+
pub struct SampleBufferStream {
366+
inner: Arc<Mutex<SampleBufferStreamState>>,
367+
}
368+
369+
struct SampleBufferStreamState {
370+
buffer: std::collections::VecDeque<crate::cm::CMSampleBuffer>,
371+
waker: Option<Waker>,
372+
closed: bool,
373+
capacity: usize,
374+
}
375+
376+
/// Internal sender for sample buffer stream
377+
struct SampleBufferSender {
378+
inner: Arc<Mutex<SampleBufferStreamState>>,
379+
}
380+
381+
impl crate::stream::output_trait::SCStreamOutputTrait for SampleBufferSender {
382+
fn did_output_sample_buffer(
383+
&self,
384+
sample_buffer: crate::cm::CMSampleBuffer,
385+
_of_type: crate::stream::output_type::SCStreamOutputType,
386+
) {
387+
let Ok(mut state) = self.inner.lock() else {
388+
return;
389+
};
390+
391+
// Drop oldest if at capacity
392+
if state.buffer.len() >= state.capacity {
393+
state.buffer.pop_front();
394+
}
395+
396+
state.buffer.push_back(sample_buffer);
397+
398+
if let Some(waker) = state.waker.take() {
399+
waker.wake();
400+
}
401+
}
402+
}
403+
404+
impl Drop for SampleBufferSender {
405+
fn drop(&mut self) {
406+
if let Ok(mut state) = self.inner.lock() {
407+
state.closed = true;
408+
if let Some(waker) = state.waker.take() {
409+
waker.wake();
410+
}
347411
}
348412
}
413+
}
349414

350-
/// Add an output handler (synchronous)
415+
impl SampleBufferStream {
416+
/// Get the next sample buffer asynchronously
351417
///
352-
/// Handlers use callbacks and cannot be made async.
353-
pub fn add_output_handler(
354-
&mut self,
355-
handler: impl crate::stream::output_trait::SCStreamOutputTrait + 'static,
356-
of_type: crate::stream::output_type::SCStreamOutputType,
357-
) -> Option<usize> {
358-
self.stream.add_output_handler(handler, of_type)
418+
/// Returns `None` when the stream is closed.
419+
pub fn next(&self) -> SampleBufferNext<'_> {
420+
SampleBufferNext { stream: self }
359421
}
360422

361-
/// Add an output handler with a specific dispatch queue (synchronous)
362-
pub fn add_output_handler_with_queue(
363-
&mut self,
364-
handler: impl crate::stream::output_trait::SCStreamOutputTrait + 'static,
365-
of_type: crate::stream::output_type::SCStreamOutputType,
366-
queue: Option<&crate::dispatch_queue::DispatchQueue>,
367-
) -> Option<usize> {
368-
self.stream
369-
.add_output_handler_with_queue(handler, of_type, queue)
423+
/// Try to get a sample without waiting
424+
#[must_use]
425+
pub fn try_next(&self) -> Option<crate::cm::CMSampleBuffer> {
426+
self.inner.lock().ok()?.buffer.pop_front()
370427
}
371428

372-
/// Remove an output handler (synchronous)
373-
pub fn remove_output_handler(
374-
&mut self,
375-
id: usize,
376-
of_type: crate::stream::output_type::SCStreamOutputType,
377-
) -> bool {
378-
self.stream.remove_output_handler(id, of_type)
429+
/// Check if the stream has been closed
430+
#[must_use]
431+
pub fn is_closed(&self) -> bool {
432+
self.inner.lock().map(|s| s.closed).unwrap_or(true)
379433
}
380434

381-
/// Asynchronously start capture
382-
///
383-
/// Runs the start operation on a separate thread to avoid blocking the async runtime.
384-
/// **Executor-agnostic** - works with any async runtime.
385-
///
386-
/// # Errors
435+
/// Get the number of buffered samples
436+
#[must_use]
437+
pub fn len(&self) -> usize {
438+
self.inner.lock().map(|s| s.buffer.len()).unwrap_or(0)
439+
}
440+
441+
/// Check if the buffer is empty
442+
#[must_use]
443+
pub fn is_empty(&self) -> bool {
444+
self.len() == 0
445+
}
446+
447+
/// Clear all buffered samples
448+
pub fn clear(&self) {
449+
if let Ok(mut state) = self.inner.lock() {
450+
state.buffer.clear();
451+
}
452+
}
453+
}
454+
455+
/// Future for getting the next sample buffer
456+
pub struct SampleBufferNext<'a> {
457+
stream: &'a SampleBufferStream,
458+
}
459+
460+
impl Future for SampleBufferNext<'_> {
461+
type Output = Option<crate::cm::CMSampleBuffer>;
462+
463+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
464+
let Ok(mut state) = self.stream.inner.lock() else {
465+
return Poll::Ready(None);
466+
};
467+
468+
if let Some(sample) = state.buffer.pop_front() {
469+
return Poll::Ready(Some(sample));
470+
}
471+
472+
if state.closed {
473+
Poll::Ready(None)
474+
} else {
475+
state.waker = Some(cx.waker().clone());
476+
Poll::Pending
477+
}
478+
}
479+
}
480+
481+
unsafe impl Send for SampleBufferStream {}
482+
unsafe impl Sync for SampleBufferStream {}
483+
unsafe impl Send for SampleBufferSender {}
484+
unsafe impl Sync for SampleBufferSender {}
485+
486+
impl AsyncSCStream {
487+
/// Create a new async stream with frame iteration
387488
///
388-
/// Returns an error if the capture fails to start.
489+
/// Returns both the stream and an async iterator for frames.
389490
///
390-
/// # Examples
491+
/// # Arguments
391492
///
392-
/// ```rust,no_run
393-
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
394-
/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
395-
/// use screencapturekit::stream::configuration::SCStreamConfiguration;
396-
/// use screencapturekit::stream::content_filter::SCContentFilter;
493+
/// * `filter` - Content filter specifying what to capture
494+
/// * `config` - Stream configuration
495+
/// * `buffer_capacity` - Max frames to buffer (oldest dropped when full)
496+
/// * `output_type` - Type of output (Screen, Audio, Microphone)
497+
#[must_use]
498+
pub fn new(
499+
filter: &SCContentFilter,
500+
config: &SCStreamConfiguration,
501+
buffer_capacity: usize,
502+
output_type: crate::stream::output_type::SCStreamOutputType,
503+
) -> (Self, SampleBufferStream) {
504+
let state = Arc::new(Mutex::new(SampleBufferStreamState {
505+
buffer: std::collections::VecDeque::with_capacity(buffer_capacity),
506+
waker: None,
507+
closed: false,
508+
capacity: buffer_capacity,
509+
}));
510+
511+
let sender = SampleBufferSender {
512+
inner: Arc::clone(&state),
513+
};
514+
515+
let mut stream = crate::stream::SCStream::new(filter, config);
516+
stream.add_output_handler(sender, output_type);
517+
518+
let receiver = SampleBufferStream { inner: state };
519+
520+
(Self { stream }, receiver)
521+
}
522+
523+
/// Start capture asynchronously
397524
///
398-
/// let content = AsyncSCShareableContent::get().await?;
399-
/// let display = &content.displays()[0];
400-
/// let filter = SCContentFilter::build().display(display).build();
401-
/// let config = SCStreamConfiguration::build();
525+
/// # Errors
402526
///
403-
/// let mut stream = AsyncSCStream::new(&filter, &config);
404-
/// stream.start_capture().await?;
405-
/// # Ok(())
406-
/// # }
407-
/// ```
527+
/// Returns an error if capture fails to start.
408528
pub async fn start_capture(&self) -> Result<(), SCError> {
409529
let stream_ptr = std::ptr::addr_of!(self.stream) as usize;
410530
BlockingFuture::new(move || {
@@ -413,14 +533,11 @@ impl AsyncSCStream {
413533
}).await
414534
}
415535

416-
/// Asynchronously stop capture
417-
///
418-
/// Runs the stop operation on a separate thread to avoid blocking the async runtime.
419-
/// **Executor-agnostic** - works with any async runtime.
536+
/// Stop capture asynchronously
420537
///
421538
/// # Errors
422539
///
423-
/// Returns an error if the capture fails to stop.
540+
/// Returns an error if capture fails to stop.
424541
pub async fn stop_capture(&self) -> Result<(), SCError> {
425542
let stream_ptr = std::ptr::addr_of!(self.stream) as usize;
426543
BlockingFuture::new(move || {
@@ -429,9 +546,7 @@ impl AsyncSCStream {
429546
}).await
430547
}
431548

432-
/// Asynchronously update the stream configuration
433-
///
434-
/// **Executor-agnostic** - works with any async runtime.
549+
/// Update stream configuration asynchronously
435550
///
436551
/// # Errors
437552
///
@@ -445,9 +560,7 @@ impl AsyncSCStream {
445560
}).await
446561
}
447562

448-
/// Asynchronously update the content filter
449-
///
450-
/// **Executor-agnostic** - works with any async runtime.
563+
/// Update content filter asynchronously
451564
///
452565
/// # Errors
453566
///
@@ -466,17 +579,6 @@ impl AsyncSCStream {
466579
pub fn inner(&self) -> &crate::stream::SCStream {
467580
&self.stream
468581
}
469-
470-
/// Get a mutable reference to the underlying stream
471-
pub fn inner_mut(&mut self) -> &mut crate::stream::SCStream {
472-
&mut self.stream
473-
}
474-
475-
/// Consume this wrapper and return the underlying stream
476-
#[must_use]
477-
pub fn into_inner(self) -> crate::stream::SCStream {
478-
self.stream
479-
}
480582
}
481583

482584
/// Async wrapper for `SCRecordingOutput` (macOS 15.0+)

0 commit comments

Comments
 (0)