Skip to content

Commit 52d90b7

Browse files
1313Copilot
andcommitted
feat(async): multi-output AsyncSCStream (capture A/V from one stream)
AsyncSCStream could previously carry only a single output type, so async audio+video capture was impossible (the sync API supports multiple handlers on one stream). Add it non-breakingly: - buffered samples are now tagged with their SCStreamOutputType - add_output_type() registers an additional output type (e.g. add Audio to a Screen stream) - next_typed() / try_next_typed() (and the NextSampleTyped future) yield the sample together with its output type so A/V can be told apart - next()/try_next() keep returning just the CMSampleBuffer (unchanged) Tests: typed Debug assertion + a real-capture A/V test asserting correct tagging. 823 tests pass; clippy clean. CHANGELOG updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c3f47b4 commit 52d90b7

3 files changed

Lines changed: 153 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4141
`AsyncSCStream` lifecycle methods.
4242
- `AsyncSCStream::take_error` — returns the `SCError` that stopped the stream,
4343
if any, after `next()` reports the iterator closed.
44+
- Multi-output async capture: `AsyncSCStream::add_output_type` registers an
45+
additional output type (e.g. add audio to a screen stream), and
46+
`AsyncSCStream::next_typed` / `try_next_typed` (plus the `NextSampleTyped`
47+
future) yield each sample together with its `SCStreamOutputType` so audio and
48+
video can be captured from one stream and told apart.
4449

4550
### Deprecated
4651

src/async_api.rs

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ use crate::error::SCError;
7070
use crate::shareable_content::SCShareableContent;
7171
use crate::stream::configuration::SCStreamConfiguration;
7272
use crate::stream::content_filter::SCContentFilter;
73+
use crate::stream::output_type::SCStreamOutputType;
7374
use crate::utils::completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
7475
use std::ffi::c_void;
7576
use std::future::Future;
@@ -304,7 +305,7 @@ impl AsyncSCShareableContent {
304305
/// stale ones, but means consumers that fall behind will miss intermediate
305306
/// frames rather than blocking the capture callback.
306307
struct AsyncSampleIteratorState {
307-
buffer: std::collections::VecDeque<crate::cm::CMSampleBuffer>,
308+
buffer: std::collections::VecDeque<(crate::cm::CMSampleBuffer, SCStreamOutputType)>,
308309
waker: Option<Waker>,
309310
closed: bool,
310311
capacity: usize,
@@ -320,7 +321,7 @@ impl crate::stream::output_trait::SCStreamOutputTrait for AsyncSampleSender {
320321
fn did_output_sample_buffer(
321322
&self,
322323
sample_buffer: crate::cm::CMSampleBuffer,
323-
_of_type: crate::stream::output_type::SCStreamOutputType,
324+
of_type: SCStreamOutputType,
324325
) {
325326
let Ok(mut state) = self.inner.lock() else {
326327
return;
@@ -331,7 +332,7 @@ impl crate::stream::output_trait::SCStreamOutputTrait for AsyncSampleSender {
331332
state.buffer.pop_front();
332333
}
333334

334-
state.buffer.push_back(sample_buffer);
335+
state.buffer.push_back((sample_buffer, of_type));
335336

336337
if let Some(waker) = state.waker.take() {
337338
waker.wake();
@@ -369,7 +370,7 @@ impl Future for NextSample<'_> {
369370
return Poll::Ready(None);
370371
};
371372

372-
if let Some(sample) = state.buffer.pop_front() {
373+
if let Some((sample, _of_type)) = state.buffer.pop_front() {
373374
return Poll::Ready(Some(sample));
374375
}
375376

@@ -392,11 +393,54 @@ impl Future for NextSample<'_> {
392393
}
393394
}
394395

396+
/// Future for getting the next sample buffer together with its output type.
397+
///
398+
/// Like [`NextSample`], but yields the [`SCStreamOutputType`] alongside the
399+
/// buffer so consumers of a multi-output stream (e.g. screen + audio via
400+
/// [`AsyncSCStream::add_output_type`]) can tell frames apart. Returned by
401+
/// [`AsyncSCStream::next_typed`].
402+
pub struct NextSampleTyped<'a> {
403+
state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
404+
}
405+
406+
impl std::fmt::Debug for NextSampleTyped<'_> {
407+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408+
f.debug_struct("NextSampleTyped").finish_non_exhaustive()
409+
}
410+
}
411+
412+
impl Future for NextSampleTyped<'_> {
413+
type Output = Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)>;
414+
415+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
416+
let Ok(mut state) = self.state.lock() else {
417+
return Poll::Ready(None);
418+
};
419+
420+
if let Some(sample) = state.buffer.pop_front() {
421+
return Poll::Ready(Some(sample));
422+
}
423+
424+
if state.closed {
425+
Poll::Ready(None)
426+
} else {
427+
// See `NextSample::poll` for the lost-wakeup rationale.
428+
let waker = cx.waker();
429+
match state.waker {
430+
Some(ref existing) if existing.will_wake(waker) => {}
431+
_ => state.waker = Some(waker.clone()),
432+
}
433+
Poll::Pending
434+
}
435+
}
436+
}
437+
395438
// SAFETY: `AsyncSampleSender` holds `Arc<Mutex<AsyncSampleIteratorState>>`.
396-
// `AsyncSampleIteratorState` contains `VecDeque<CMSampleBuffer>` and `Option<Waker>`;
397-
// `CMSampleBuffer` has its own `unsafe impl Send` (it is an Apple-owned handle
398-
// safe to transfer across threads) and `Waker` is `Send + Sync`, so the whole
399-
// `Arc<Mutex<...>>` is safe to send and share across threads.
439+
// `AsyncSampleIteratorState` buffers `(CMSampleBuffer, SCStreamOutputType)`
440+
// pairs plus an `Option<Waker>` and `Option<SCError>`; `CMSampleBuffer` has its
441+
// own `unsafe impl Send` (it is an Apple-owned handle safe to transfer across
442+
// threads) and the rest are `Send + Sync`, so the whole `Arc<Mutex<...>>` is
443+
// safe to send and share across threads.
400444
unsafe impl Send for AsyncSampleSender {}
401445
unsafe impl Sync for AsyncSampleSender {}
402446

@@ -602,16 +646,61 @@ impl AsyncSCStream {
602646

603647
/// Get the next sample buffer asynchronously
604648
///
605-
/// Returns `None` when the stream is closed.
649+
/// Returns `None` when the stream is closed. For a multi-output stream
650+
/// (see [`add_output_type`](Self::add_output_type)) use
651+
/// [`next_typed`](Self::next_typed) to also learn each sample's
652+
/// [`SCStreamOutputType`].
606653
pub fn next(&self) -> NextSample<'_> {
607654
NextSample {
608655
state: &self.iterator_state,
609656
}
610657
}
611658

659+
/// Get the next sample buffer together with its output type.
660+
///
661+
/// Use this when the stream carries more than one output type (e.g. screen
662+
/// and audio) and you need to tell the samples apart. Returns `None` when
663+
/// the stream is closed.
664+
pub fn next_typed(&self) -> NextSampleTyped<'_> {
665+
NextSampleTyped {
666+
state: &self.iterator_state,
667+
}
668+
}
669+
670+
/// Also deliver samples of an additional output type.
671+
///
672+
/// By default an [`AsyncSCStream`] carries the single output type passed to
673+
/// [`new`](Self::new). Call this to capture more than one type from one
674+
/// stream — for example add [`SCStreamOutputType::Audio`] to a stream
675+
/// created for [`SCStreamOutputType::Screen`] to capture audio and video
676+
/// together. Samples from every registered type share the same lossy
677+
/// buffer; use [`next_typed`](Self::next_typed) /
678+
/// [`try_next_typed`](Self::try_next_typed) to distinguish them.
679+
///
680+
/// Returns `true` if the output type was registered. Registration can fail
681+
/// if the stream configuration does not enable that type (e.g. audio
682+
/// capture was not configured).
683+
pub fn add_output_type(&mut self, output_type: SCStreamOutputType) -> bool {
684+
let sender = AsyncSampleSender {
685+
inner: Arc::clone(&self.iterator_state),
686+
};
687+
self.stream.add_output_handler(sender, output_type).is_some()
688+
}
689+
612690
/// Try to get a sample without waiting
613691
#[must_use]
614692
pub fn try_next(&self) -> Option<crate::cm::CMSampleBuffer> {
693+
self.iterator_state
694+
.lock()
695+
.ok()?
696+
.buffer
697+
.pop_front()
698+
.map(|(buffer, _of_type)| buffer)
699+
}
700+
701+
/// Try to get a sample together with its output type, without waiting.
702+
#[must_use]
703+
pub fn try_next_typed(&self) -> Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)> {
615704
self.iterator_state.lock().ok()?.buffer.pop_front()
616705
}
617706

tests/async_api_tests.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ fn test_next_sample_debug() {
310310
assert_debug::<NextSample<'_>>();
311311
}
312312

313+
#[test]
314+
fn test_next_sample_typed_debug() {
315+
fn assert_debug<T: std::fmt::Debug>() {}
316+
assert_debug::<NextSampleTyped<'_>>();
317+
}
318+
313319
#[test]
314320
fn test_stream_control_future_is_send_and_debug() {
315321
// The lifecycle control futures must be `Send` so they can be driven on
@@ -358,6 +364,50 @@ fn test_async_stream_take_error_initially_none() {
358364
}
359365
}
360366

367+
#[test]
368+
fn test_async_stream_multi_output_typed() {
369+
use screencapturekit::shareable_content::SCShareableContent;
370+
use screencapturekit::stream::configuration::SCStreamConfiguration;
371+
use screencapturekit::stream::content_filter::SCContentFilter;
372+
373+
if let Ok(content) = SCShareableContent::get() {
374+
if let Some(display) = content.displays().first() {
375+
let filter = SCContentFilter::create()
376+
.with_display(display)
377+
.with_excluding_windows(&[])
378+
.build();
379+
let config = SCStreamConfiguration::new()
380+
.with_width(160)
381+
.with_height(120)
382+
.with_captures_audio(true);
383+
384+
let mut stream = AsyncSCStream::new(&filter, &config, 16, SCStreamOutputType::Screen);
385+
386+
// Nothing is buffered before capture starts.
387+
assert!(stream.try_next_typed().is_none());
388+
389+
// Add audio as a second output type so one stream carries A/V.
390+
let _registered = stream.add_output_type(SCStreamOutputType::Audio);
391+
392+
if stream.inner().start_capture().is_ok() {
393+
std::thread::sleep(std::time::Duration::from_millis(300));
394+
395+
// Every delivered sample must be correctly tagged with its type.
396+
while let Some((_buf, ty)) = stream.try_next_typed() {
397+
assert!(matches!(
398+
ty,
399+
SCStreamOutputType::Screen
400+
| SCStreamOutputType::Audio
401+
| SCStreamOutputType::Microphone
402+
));
403+
}
404+
405+
let _ = stream.inner().stop_capture();
406+
}
407+
}
408+
}
409+
}
410+
361411
#[cfg(feature = "macos_14_0")]
362412
mod macos_14_tests {
363413
use super::*;

0 commit comments

Comments
 (0)