pinray-core 0.1.0

Multi-platform screen capture library for Rust
Documentation
use std::time::Duration;

use tracing::{debug, instrument};

use crate::{
    audio::AudioFrame,
    backend::{AudioBackend, BackendBundle, BackendInfo, BackendResolver, VideoBackend},
    config::{AudioCapture, CursorMode, SessionConfig, VideoCaptureTarget},
    error::{PinrayError, Result},
    frame::{CaptureEvent, ColorSpace, PixelFormat, Rect},
};

pub struct CaptureSession {
    config: SessionConfig,
    backend_info: BackendInfo,
    video_backend: Option<Box<dyn VideoBackend>>,
    audio_backend: Option<Box<dyn AudioBackend>>,
    running: bool,
}

impl CaptureSession {
    pub fn new(config: SessionConfig, bundle: BackendBundle) -> Self {
        Self {
            config,
            backend_info: bundle.info,
            video_backend: bundle.video,
            audio_backend: bundle.audio,
            running: false,
        }
    }

    pub fn config(&self) -> &SessionConfig {
        &self.config
    }

    pub fn backend_info(&self) -> &BackendInfo {
        &self.backend_info
    }

    pub fn is_running(&self) -> bool {
        self.running
    }

    #[instrument(skip(self), fields(backend = ?self.backend_info.kind))]
    pub fn start(&mut self) -> Result<()> {
        if self.running {
            return Ok(());
        }

        if let Some(video) = self.video_backend.as_mut() {
            video.start()?;
        }

        if let Some(audio) = self.audio_backend.as_mut() {
            audio.start()?;
        }

        self.running = true;
        debug!("capture session started");
        Ok(())
    }

    #[instrument(skip(self), fields(backend = ?self.backend_info.kind))]
    pub fn stop(&mut self) -> Result<()> {
        if !self.running {
            return Ok(());
        }

        if let Some(audio) = self.audio_backend.as_mut() {
            audio.stop()?;
        }

        if let Some(video) = self.video_backend.as_mut() {
            video.stop()?;
        }

        self.running = false;
        debug!("capture session stopped");
        Ok(())
    }

    pub fn next_event(&mut self, timeout: Option<Duration>) -> Result<CaptureEvent> {
        match (self.video_backend.as_mut(), self.audio_backend.as_mut()) {
            (Some(video), None) => video.next_event(timeout),
            (None, Some(audio)) => audio.next_audio(timeout).map(CaptureEvent::Audio),
            (Some(video), Some(audio)) => match video.next_event(timeout) {
                Ok(event) => Ok(event),
                Err(PinrayError::Timeout(_)) => audio.next_audio(timeout).map(CaptureEvent::Audio),
                Err(error) => Err(error),
            },
            (None, None) => Err(PinrayError::BackendNotSelected),
        }
    }

    pub fn next_audio(&mut self, timeout: Option<Duration>) -> Result<AudioFrame> {
        let audio = self
            .audio_backend
            .as_mut()
            .ok_or(PinrayError::BackendNotSelected)?;
        audio.next_audio(timeout)
    }
}

#[derive(Debug, Clone, Default)]
pub struct SessionBuilder {
    config: SessionConfig,
}

impl SessionBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn config(&self) -> &SessionConfig {
        &self.config
    }

    pub fn backend_preference(
        mut self,
        backend_preference: crate::backend::BackendPreference,
    ) -> Self {
        self.config.backend_preference = backend_preference;
        self
    }

    pub fn video_target(mut self, target: VideoCaptureTarget) -> Self {
        self.config.video_target = Some(target);
        self
    }

    pub fn audio(mut self, audio_capture: AudioCapture) -> Self {
        self.config.audio_capture = Some(audio_capture);
        self
    }

    pub fn pixel_format(mut self, pixel_format: PixelFormat) -> Self {
        self.config.pixel_format = pixel_format;
        self
    }

    pub fn restore_token(mut self, restore_token: impl Into<String>) -> Self {
        self.config.restore_token = Some(restore_token.into());
        self
    }

    pub fn color_space(mut self, color_space: Option<ColorSpace>) -> Self {
        self.config.color_space = color_space;
        self
    }

    pub fn cursor_mode(mut self, cursor_mode: CursorMode) -> Self {
        self.config.cursor_mode = cursor_mode;
        self
    }

    pub fn crop_rect(mut self, crop_rect: Option<Rect>) -> Self {
        self.config.crop_rect = crop_rect;
        self
    }

    pub fn frame_rate(mut self, frame_rate: Option<u32>) -> Self {
        self.config.frame_rate = frame_rate;
        self
    }

    pub fn queue_depth(mut self, queue_depth: u32) -> Self {
        self.config.queue_depth = queue_depth;
        self
    }

    #[instrument(skip(self, resolver))]
    pub fn build_with_resolver<R>(self, resolver: &R) -> Result<CaptureSession>
    where
        R: BackendResolver,
    {
        self.config.validate()?;
        let bundle = resolver.resolve(&self.config)?;
        Ok(CaptureSession::new(self.config, bundle))
    }
}

#[cfg(test)]
mod tests {
    use super::SessionBuilder;

    #[test]
    fn builder_requires_video_or_audio() {
        let config = SessionBuilder::new().config().clone();
        assert!(config.validate().is_err());
    }

    #[test]
    fn queue_depth_must_be_positive() {
        let config = SessionBuilder::new().queue_depth(0).config().clone();
        assert!(config.validate().is_err());
    }
}