Skip to main content

egui_cameras/
lib.rs

1//! egui / eframe integration for the [`cameras`] crate.
2//!
3//! This crate owns only the thin glue between a running
4//! [`cameras::pump::Pump`] and an [`egui::TextureHandle`]. Every camera
5//! primitive (pause / resume, single-frame capture, hotplug, source
6//! abstraction) lives upstream in [`cameras`] itself and is re-exported here
7//! for convenience.
8//!
9//! # Wiring an eframe app
10//!
11//! ```ignore
12//! use egui_cameras::cameras::{self, PixelFormat, Resolution, StreamConfig};
13//! use eframe::egui;
14//!
15//! struct App {
16//!     stream: egui_cameras::Stream,
17//! }
18//!
19//! impl eframe::App for App {
20//!     fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
21//!         let ctx = ui.ctx().clone();
22//!         egui_cameras::update_texture(&mut self.stream, &ctx).ok();
23//!         egui::CentralPanel::default().show_inside(ui, |ui| {
24//!             egui_cameras::show(&self.stream, ui);
25//!         });
26//!         ctx.request_repaint();
27//!     }
28//! }
29//! ```
30//!
31//! The doctest is `ignore`d because it uses `eframe`, which `egui-cameras`
32//! does not depend on. The `apps/egui-demo` app in this repo is the
33//! runnable version.
34
35#![warn(missing_docs)]
36
37pub use cameras;
38
39#[cfg(all(feature = "discover", any(target_os = "macos", target_os = "windows")))]
40mod discover;
41
42#[cfg(all(feature = "discover", any(target_os = "macos", target_os = "windows")))]
43#[cfg_attr(
44    docsrs,
45    doc(cfg(all(feature = "discover", any(target_os = "macos", target_os = "windows"))))
46)]
47pub use discover::{
48    DiscoverySession, cancel_discovery, poll_discovery, show_discovery, show_discovery_results,
49    show_discovery_status, start_discovery,
50};
51
52use std::sync::{Arc, Mutex, PoisonError};
53
54use cameras::{Camera, Frame, pump};
55use egui::{ColorImage, Context, TextureHandle, TextureOptions, Ui};
56
57pub use cameras::pump::{Pump, capture_frame, set_active, stop_and_join};
58
59const DEFAULT_TEXTURE_NAME: &str = "cameras-frame";
60
61/// A running camera pump plus an [`egui::TextureHandle`] that is refreshed
62/// each time [`update_texture`] is called.
63///
64/// Obtained from [`spawn`]. All fields are public, data-oriented, no methods.
65pub struct Stream {
66    /// The underlying pump. Pass by reference to [`set_active`],
67    /// [`capture_frame`], or [`stop_and_join`] to drive the pump.
68    pub pump: Pump,
69    /// Shared slot the pump writes each frame into. Cleared by
70    /// [`update_texture`] once the frame is uploaded.
71    pub sink: Sink,
72    /// The egui texture the frame is uploaded to. `None` until the first
73    /// frame arrives.
74    pub texture: Option<TextureHandle>,
75    /// Name the texture is registered under in egui's texture cache.
76    pub name: String,
77}
78
79/// Shared slot that a [`Pump`] writes each frame into.
80///
81/// Cheap to clone: it holds a single `Arc`.
82#[derive(Clone, Default)]
83pub struct Sink {
84    frame: Arc<Mutex<Option<Frame>>>,
85}
86
87/// Spawn a pump that feeds a fresh [`Stream`] backed by a default-named
88/// texture. The returned [`Stream`] is in the active state.
89pub fn spawn(camera: Camera) -> Stream {
90    spawn_named(camera, DEFAULT_TEXTURE_NAME)
91}
92
93/// Like [`spawn`], but lets you name the egui texture. Useful when you have
94/// more than one concurrent camera stream in the same app.
95pub fn spawn_named(camera: Camera, name: impl Into<String>) -> Stream {
96    let sink = Sink::default();
97    let pump = spawn_pump(camera, sink.clone());
98    Stream {
99        pump,
100        sink,
101        texture: None,
102        name: name.into(),
103    }
104}
105
106/// Spawn a [`Pump`] that writes each incoming frame into `sink`. Use this
107/// when you want to manage the [`Sink`] and [`TextureHandle`] yourself
108/// instead of bundling them in a [`Stream`].
109pub fn spawn_pump(camera: Camera, sink: Sink) -> Pump {
110    pump::spawn(camera, move |frame| publish_frame(&sink, frame))
111}
112
113/// Publish `frame` to `sink`, replacing any previous frame.
114pub fn publish_frame(sink: &Sink, frame: Frame) {
115    let mut slot = sink.frame.lock().unwrap_or_else(PoisonError::into_inner);
116    *slot = Some(frame);
117}
118
119/// Take the latest frame out of `sink`, returning `None` if no frame has
120/// arrived since the last call.
121pub fn take_frame(sink: &Sink) -> Option<Frame> {
122    sink.frame
123        .lock()
124        .unwrap_or_else(PoisonError::into_inner)
125        .take()
126}
127
128/// Convert a cameras [`Frame`] into an egui [`ColorImage`] (RGBA8).
129pub fn frame_to_color_image(frame: &Frame) -> Result<ColorImage, cameras::Error> {
130    let rgba = cameras::to_rgba8(frame)?;
131    Ok(ColorImage::from_rgba_unmultiplied(
132        [frame.width as usize, frame.height as usize],
133        &rgba,
134    ))
135}
136
137/// Upload the latest frame on `stream`'s [`Sink`] to its [`TextureHandle`].
138///
139/// Returns `Ok(true)` if a new frame was uploaded this call, `Ok(false)` if
140/// no new frame was waiting, or an error if pixel conversion failed. Call
141/// once per frame (typically at the top of your eframe `update` method).
142pub fn update_texture(stream: &mut Stream, ctx: &Context) -> Result<bool, cameras::Error> {
143    let Some(frame) = take_frame(&stream.sink) else {
144        return Ok(false);
145    };
146    let image = frame_to_color_image(&frame)?;
147    match &mut stream.texture {
148        Some(texture) => texture.set(image, TextureOptions::LINEAR),
149        None => {
150            stream.texture = Some(ctx.load_texture(&stream.name, image, TextureOptions::LINEAR));
151        }
152    }
153    Ok(true)
154}
155
156/// Draw the stream's texture into `ui` as a sized [`egui::Image`] that
157/// fills the available width while preserving aspect ratio.
158///
159/// No-op if no frame has arrived yet (the texture is still `None`).
160pub fn show(stream: &Stream, ui: &mut Ui) {
161    let Some(texture) = &stream.texture else {
162        return;
163    };
164    let aspect = texture.aspect_ratio();
165    let available = ui.available_size();
166    let width = available.x.min(available.y * aspect);
167    let height = width / aspect;
168    ui.image((texture.id(), egui::vec2(width, height)));
169}