Skip to content

Commit 61b4ca7

Browse files
authored
feat(camera): Bring back freya-camera (#1835)
1 parent 3db781f commit 61b4ca7

15 files changed

Lines changed: 3354 additions & 7 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ Freya is split in various crates, each with it's own meaning and purpose, here i
101101
- `freya-material-design`: Material Design Components for Freya apps.
102102
- `freya-plotters-backend`: Freya's skia-safe backend for plotters.
103103
- `freya-code-editor`: Set of APIs to create text Code Editors in Freya.
104+
- `freya-camera`: Camera capture support for Freya.
104105

105106
## Examples
106107
All important examples are located in the `./examples` folder although you might also find some in the form of docs comments in the code itself.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ members = [
4444
"./crates/freya-webview",
4545
"./crates/freya-terminal",
4646
"./crates/freya-code-editor",
47+
"./crates/freya-camera",
4748
"./crates/freya-android",
4849
"./examples/ai-chat",
4950
"./examples/state_query_sqlite",
@@ -83,6 +84,7 @@ freya-terminal = { path = "./crates/freya-terminal", version = "0.4.0-rc.19" }
8384
freya-code-editor = { path = "./crates/freya-code-editor", version = "0.4.0-rc.19", features = [
8485
"rust",
8586
] }
87+
freya-camera = { path = "./crates/freya-camera", version = "0.4.0-rc.19" }
8688
freya-android = { path = "./crates/freya-android", version = "0.4.0-rc.19" }
8789
mundy = { version = "0.2.3", default-features = false, features = [
8890
"async-io",
@@ -100,6 +102,7 @@ futures-util = "0.3.31"
100102
futures-channel = "0.3.31"
101103
futures-lite = "2.6.1"
102104
async-io = "2.5.0"
105+
async-channel = "2.5.0"
103106
blocking = "1.6.2"
104107
ureq = "3.1.4"
105108
serde = { version = "1", features = ["derive"] }

crates/freya-camera/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "freya-camera"
3+
version = "0.4.0-rc.19"
4+
description = "Camera capture support for Freya."
5+
edition = "2024"
6+
license = "MIT"
7+
authors = ["Marc Espín <mespinsanz@gmail.com>"]
8+
readme = "../../README.md"
9+
homepage = "https://freyaui.dev/"
10+
repository = "https://github.com/marc2332/freya"
11+
keywords = ["freya", "camera", "webcam", "video"]
12+
categories = ["gui", "multimedia::video"]
13+
14+
[lints]
15+
workspace = true
16+
17+
[dependencies]
18+
freya-core = { workspace = true }
19+
freya-engine = { workspace = true }
20+
nokhwa = { version = "0.10", features = ["input-native"] }
21+
blocking = { workspace = true }
22+
bytes = { workspace = true }
23+
tracing = { workspace = true }
24+
25+
[dev-dependencies]
26+
freya = { path = "../freya" }

crates/freya-camera/src/camera.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Camera configuration types.
2+
3+
pub use nokhwa::{
4+
NokhwaError as CameraError,
5+
utils::{
6+
CameraIndex,
7+
CameraInfo,
8+
},
9+
};
10+
use nokhwa::{
11+
query as nokhwa_query,
12+
utils::ApiBackend,
13+
};
14+
15+
/// Requested capture format. The negotiated values are reported via [`StreamInfo`].
16+
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
17+
pub enum CameraFormat {
18+
/// Highest framerate available, any resolution.
19+
#[default]
20+
HighestFrameRate,
21+
/// Highest resolution available, any framerate.
22+
HighestResolution,
23+
/// Highest framerate at the given resolution.
24+
Resolution { width: u32, height: u32 },
25+
/// Closest match to the given resolution and framerate.
26+
Exact {
27+
width: u32,
28+
height: u32,
29+
frame_rate: u32,
30+
},
31+
}
32+
33+
/// Configuration used to open a camera.
34+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
35+
pub struct CameraConfig {
36+
pub device: CameraIndex,
37+
pub format: CameraFormat,
38+
}
39+
40+
impl Default for CameraConfig {
41+
fn default() -> Self {
42+
Self {
43+
device: CameraIndex::Index(0),
44+
format: CameraFormat::default(),
45+
}
46+
}
47+
}
48+
49+
impl CameraConfig {
50+
pub fn new() -> Self {
51+
Self::default()
52+
}
53+
54+
pub fn device(mut self, device: CameraIndex) -> Self {
55+
self.device = device;
56+
self
57+
}
58+
59+
pub fn format(mut self, format: CameraFormat) -> Self {
60+
self.format = format;
61+
self
62+
}
63+
}
64+
65+
/// Negotiated information about a running camera.
66+
#[derive(Clone, Debug, PartialEq, Eq)]
67+
pub struct StreamInfo {
68+
pub width: u32,
69+
pub height: u32,
70+
pub frame_rate: u32,
71+
}
72+
73+
/// Enumerate the cameras available on the system.
74+
///
75+
/// # Example
76+
///
77+
/// ```rust, no_run
78+
/// use freya::camera::*;
79+
///
80+
/// for device in query().unwrap_or_default() {
81+
/// println!("{}: {}", device.human_name(), device.description());
82+
/// }
83+
/// ```
84+
pub fn query() -> Result<Vec<CameraInfo>, CameraError> {
85+
nokhwa_query(ApiBackend::Auto)
86+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! [`CameraViewer`] component.
2+
3+
use freya_core::{
4+
elements::image::*,
5+
prelude::*,
6+
};
7+
8+
use crate::{
9+
camera::CameraError,
10+
use_camera::Camera,
11+
};
12+
13+
/// Renders the latest frame produced by a [`Camera`].
14+
///
15+
/// # Example
16+
///
17+
/// ```rust, no_run
18+
/// use freya::{
19+
/// camera::*,
20+
/// prelude::*,
21+
/// };
22+
///
23+
/// fn app() -> impl IntoElement {
24+
/// let camera = use_camera(CameraConfig::default);
25+
/// CameraViewer::new(camera)
26+
/// }
27+
/// ```
28+
#[derive(PartialEq)]
29+
pub struct CameraViewer {
30+
camera: Camera,
31+
32+
layout: LayoutData,
33+
image_data: ImageData,
34+
accessibility: AccessibilityData,
35+
effect: EffectData,
36+
corner_radius: Option<CornerRadius>,
37+
38+
children: Vec<Element>,
39+
loading_placeholder: Option<Element>,
40+
error_renderer: Option<Callback<CameraError, Element>>,
41+
42+
key: DiffKey,
43+
}
44+
45+
impl CameraViewer {
46+
pub fn new(camera: Camera) -> Self {
47+
Self {
48+
camera,
49+
layout: LayoutData::default(),
50+
image_data: ImageData::default(),
51+
accessibility: AccessibilityData::default(),
52+
effect: EffectData::default(),
53+
corner_radius: None,
54+
children: Vec::new(),
55+
loading_placeholder: None,
56+
error_renderer: None,
57+
key: DiffKey::None,
58+
}
59+
}
60+
61+
pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
62+
self.corner_radius = Some(corner_radius.into());
63+
self
64+
}
65+
66+
/// Custom element rendered while the camera has not yet produced a frame.
67+
pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
68+
self.loading_placeholder = Some(placeholder.into());
69+
self
70+
}
71+
72+
/// Custom element rendered when the camera fails before producing any frame.
73+
pub fn error_renderer(mut self, renderer: impl Into<Callback<CameraError, Element>>) -> Self {
74+
self.error_renderer = Some(renderer.into());
75+
self
76+
}
77+
}
78+
79+
impl KeyExt for CameraViewer {
80+
fn write_key(&mut self) -> &mut DiffKey {
81+
&mut self.key
82+
}
83+
}
84+
85+
impl LayoutExt for CameraViewer {
86+
fn get_layout(&mut self) -> &mut LayoutData {
87+
&mut self.layout
88+
}
89+
}
90+
91+
impl ContainerExt for CameraViewer {}
92+
impl ContainerWithContentExt for CameraViewer {}
93+
94+
impl ImageExt for CameraViewer {
95+
fn get_image_data(&mut self) -> &mut ImageData {
96+
&mut self.image_data
97+
}
98+
}
99+
100+
impl AccessibilityExt for CameraViewer {
101+
fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
102+
&mut self.accessibility
103+
}
104+
}
105+
106+
impl ChildrenExt for CameraViewer {
107+
fn get_children(&mut self) -> &mut Vec<Element> {
108+
&mut self.children
109+
}
110+
}
111+
112+
impl EffectExt for CameraViewer {
113+
fn get_effect(&mut self) -> &mut EffectData {
114+
&mut self.effect
115+
}
116+
}
117+
118+
impl Component for CameraViewer {
119+
fn render(&self) -> impl IntoElement {
120+
if let Some(holder) = self.camera.frame.read().clone() {
121+
return image(holder)
122+
.accessibility(self.accessibility.clone())
123+
.a11y_role(AccessibilityRole::Image)
124+
.a11y_focusable(true)
125+
.layout(self.layout.clone())
126+
.image_data(self.image_data.clone())
127+
.effect(self.effect.clone())
128+
.children(self.children.clone())
129+
.map(self.corner_radius, |img, corner_radius| {
130+
img.corner_radius(corner_radius)
131+
})
132+
.into_element();
133+
}
134+
135+
if let Some(renderer) = &self.error_renderer
136+
&& let Some(err) = self.camera.error.read().clone()
137+
{
138+
return renderer.call(err);
139+
}
140+
141+
rect()
142+
.layout(self.layout.clone())
143+
.center()
144+
.map(self.loading_placeholder.clone(), |r, placeholder| {
145+
r.child(placeholder)
146+
})
147+
.into_element()
148+
}
149+
150+
fn render_key(&self) -> DiffKey {
151+
self.key.clone().or(self.default_key())
152+
}
153+
}

0 commit comments

Comments
 (0)