Discord: https://discord.gg/CYNQ25XP8B
# Pryty RustBrowser
[](https://crates.io/crates/pryty-rustbrowser)
[](https://docs.rs/pryty-rustbrowser)
[](LICENSE)
**Production-ready browser API bindings for Dioxus.**
One-line hooks for camera, microphone recording, clipboard, and localStorage β with full TypeScript-level safety and complete error handling.
## β¨ Features
- ποΈ **Audio Recording** β Multi-quality presets (low to lossless), pause/resume, time slices, auto-stop timer
- πΉ **Camera** β Resolution/frame rate presets (480p to 1080p), track lifecycle management
- π **Clipboard** β Async read/write with proper error propagation
- πΎ **Storage** β Type-safe localStorage with automatic JSON serialization (powered by `serde`)
- πͺ **Dioxus Hooks** β `use_recording`, `use_camera`, `use_clipboard`, `use_storage` β integrate in one line
- π§ͺ **Error Handling** β Rich error types for every failure mode
- π **Production Ready** β Proper cleanup (tracks, timers, closures), no leaks
## π¦ Installation
```bash
cargo add pryty-rustbrowserAlso add serde for storage (derive features as needed):
cargo add serde --features deriveuse dioxus::prelude::*;
use pryty_rustbrowser::{use_recording, use_camera, use_clipboard, use_storage, AudioQualityConfig};
#[component]
fn App() -> Element {
// One line β fully typed reactive state
let recording = use_recording();
let camera = use_camera();
let clipboard = use_clipboard();
let (username, set_username) = use_storage("username".to_string(), "Anonymous".to_string());
rsx! {
div { class: "p-4 space-y-4",
// ---------- Audio Recording ----------
div {
h3 { "ποΈ Audio Recording" }
p { "State: {recording.state:?}" }
div { class: "flex gap-2",
button {
onclick: move |_| recording.start(),
disabled: recording.is_busy(),
"Start"
}
button {
onclick: move |_| recording.start_with_quality(AudioQualityConfig::high()),
disabled: recording.is_busy(),
"Start (High Quality)"
}
button {
onclick: move |_| recording.pause(),
disabled: !recording.is_active(),
"Pause"
}
button {
onclick: move |_| recording.resume(),
disabled: !recording.is_paused(),
"Resume"
}
button {
onclick: move |_| recording.stop(),
disabled: !recording.is_active() && !recording.is_paused(),
"Stop"
}
}
if let Some(data) = recording.data() {
p { "Recorded: {} bytes", data.len() }
}
}
// ---------- Camera ----------
div {
h3 { "π· Camera" }
p { "State: {camera.state:?}" }
div { class: "flex gap-2",
button {
onclick: move |_| camera.start(),
disabled: camera.is_active(),
"Start Camera"
}
button {
onclick: move |_| camera.start_with_quality(CameraQualityConfig::hd()),
disabled: camera.is_active(),
"Start (HD)"
}
button {
onclick: move |_| camera.stop(),
disabled: !camera.is_active(),
"Stop Camera"
}
}
// Display camera preview
if let Some(stream) = camera.stream() {
video {
src_object: Some(stream),
autoplay: true,
muted: true,
class: "mt-2 w-96 rounded-lg border"
}
}
}
// ---------- Clipboard & Storage ----------
div {
h3 { "π Clipboard + Storage" }
input {
value: "{username}",
oninput: move |evt| set_username(evt.value()),
class: "border px-2 py-1"
}
button {
onclick: move |_| clipboard.write(&username)(),
"Copy to Clipboard"
}
button {
onclick: move |_| spawn(async move {
if let Ok(Some(text)) = clipboard.read().await {
println!("Read from clipboard: {text}");
}
}),
"Read from Clipboard"
}
}
}
}
}
fn main() {
dioxus::launch(App);
}| Field/Method | Type | Description |
|---|---|---|
start() |
Callback<()> |
Start recording with default quality |
start_with_quality(Quality) |
Callback<Quality> |
Start with preset (low/normal/high/studio/lossless) |
pause() |
Callback<()> |
Pause recording |
resume() |
Callback<()> |
Resume recording |
stop() |
Callback<()> |
Stop and finalize audio data |
data() |
Signal<Option<Vec<u8>>> |
Recorded audio bytes (WebM/Opus or PCM) |
state() |
Signal<RecordingState> |
Idle / Starting / Recording / Paused / Stopping / Error |
last_error() |
Signal<Option<String>> |
Last error message |
is_active() |
bool |
Returns true if currently recording |
is_paused() |
bool |
Returns true if paused |
is_busy() |
bool |
Returns true during starting/stopping |
Quality Presets:
| Method | Sample Rate | Channels | Bitrate |
|---|---|---|---|
AudioQualityConfig::low() |
22.05 kHz | 1 (mono) | 64 kbps |
normal() |
44.1 kHz | 1 | 128 kbps |
high() |
48 kHz | 2 (stereo) | 192 kbps |
studio() |
96 kHz | 2 | 320 kbps |
lossless() |
96 kHz | 2 | uncompressed PCM |
| Field/Method | Type | Description |
|---|---|---|
start() |
Callback<()> |
Start camera with default constraints |
start_with_quality(Quality) |
Callback<Quality> |
Start with resolution/frame rate preset |
stop() |
Callback<()> |
Stop camera and release tracks |
stream() |
Signal<Option<MediaStream>> |
Raw WebRTC stream (for <video> preview) |
state() |
Signal<CameraState> |
Idle / Starting / Active / Stopping / Error |
is_active() |
bool |
Returns true if camera streaming |
is_busy() |
bool |
Returns true during start/stop |
| Method | Description |
|---|---|
write(text)(&self) |
Fire-and-forget write (async inside) |
write_async(text).await |
Async write with Result |
read().await |
Async read β Result<Option<String>> |
Requirements: T: Serialize + DeserializeOwned + Clone
let (count, set_count) = use_storage("counter".to_string(), 0);
set_count(count() + 1);Also available: read_storage::<T>(key) and write_storage(key, &value) for one-off operations.
Every operation returns rich errors. Example:
match camera.start().await {
Ok(_) => println!("Camera ready"),
Err(CameraError::GetUserMediaFailed(msg)) => {
eprintln!("User denied permission or no camera: {msg}")
}
Err(e) => eprintln!("Other error: {e}"),
}All error types implement std::fmt::Display and Debug.
| API | Requires |
|---|---|
| Audio Recording | MediaRecorder, getUserMedia |
| Camera | getUserMedia, <video> element |
| Clipboard | navigator.clipboard (HTTPS or localhost) |
| Storage | localStorage |
All gracefully return errors when APIs unavailable.
Clone and run the demo:
git clone https://github.com/pryty26/pryty-rustbrowser
cd pryty-rustbrowser
cargo run --example demoOr use dx serve for web development.
PRs welcome! Please ensure:
- All public APIs have doc comments
- Error types cover all failure paths
- No
unwrap()/expect()in library code (use?or proper handling)
Dual-licensed under MIT or Apache-2.0 at your option.
Built for Dioxus 0.7+ β works with WebAssembly, desktop, and mobile backends.