A Tauri v2 plugin that provides a unified JavaScript API for camera access across Android, iOS and desktop. It supports live preview, photo capture, video recording (with audio), camera controls (front/back, flash, zoom), saving to the photo gallery, and camera permission management.
| Platform | Supported |
|---|---|
| Android | ✓ |
| iOS | ✓ |
| macOS | ✓ (via getUserMedia) |
| Windows | ✓ (via getUserMedia) |
| Linux | ✓ (via getUserMedia) |
On desktop the camera is driven by the webview through
navigator.mediaDevices.getUserMediaplus<video>/<canvas>. On mobile it uses a native preview (CameraX on Android, AVFoundation on iOS) for full control and quality.
Add the Rust crate to src-tauri/Cargo.toml:
[dependencies]
tauri-plugin-camera-kessdev = "0.1.0"
# or from a git/path source:
# tauri-plugin-camera-kessdev = { path = "../tauri-plugin-camera" }Add the JavaScript bindings:
npm add tauri-plugin-camera-kessdev
# pnpm add tauri-plugin-camera-kessdev
# yarn add tauri-plugin-camera-kessdevIn src-tauri/src/lib.rs:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_camera_kessdev::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Add the default permission set to src-tauri/capabilities/default.json:
{
"permissions": [
"core:default",
"camera:default"
]
}To allow only specific commands, use the granular permissions instead of
camera:default (e.g. camera:allow-capture, camera:allow-start-recording,
camera:allow-check-permissions, …). The full list is generated under
permissions/autogenerated/commands/ as allow-<command> / deny-<command>.
The plugin's AndroidManifest.xml declares android.permission.CAMERA and
android.permission.RECORD_AUDIO (for video audio). The Kotlin plugin registers them
through the @TauriPlugin annotation, so checkPermissions / requestPermissions work
out of the box. No further manifest edits are required.
Add the following usage descriptions to the app Info.plist
(src-tauri/Info.plist):
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to capture photos and videos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone to record audio in videos.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app saves captured photos to your photo library.</string>NSMicrophoneUsageDescription is only required if you use video recording,
NSPhotoLibraryAddUsageDescription only if you use saveToGallery.
Desktop uses the browser's getUserMedia. The plugin installs a webview permission
handler on Windows (WebView2 PermissionRequested) and Linux (webkitgtk
permission-request) that allows camera and microphone, so startPreview() /
requestPermissions() work without extra configuration. The webview asks the plugin,
and the real gate is the operating system:
- Windows: Settings → Privacy & security → Camera → turn on camera access and "Let desktop apps access your camera".
- Linux: the desktop portal (e.g.
xdg-desktop-portal) governs camera access.
On macOS, WKWebView additionally requires NSCameraUsageDescription and
NSMicrophoneUsageDescription in the Info.plist (the same keys above).
import {
startPreview,
stopPreview,
capture,
startRecording,
stopRecording,
flipCamera,
setFlash,
setZoom,
saveToGallery,
checkPermissions,
requestPermissions,
getPreviewStream,
convertFileSrc
} from 'tauri-plugin-camera-kessdev'// windowed: true keeps the webview transparent so your UI renders over the camera.
await startPreview({ camera: 'back', windowed: true })
// Desktop: attach the stream to a <video> element.
const stream = await getPreviewStream()
if (stream) videoEl.srcObject = stream
await stopPreview()const photo = await capture({ flash: 'auto' })
// photo: { path, width, height, orientation, format, thumbnail? } (mobile)
// { dataUrl, width, height, orientation, format } (desktop)
// Mobile: display the captured file.
imageEl.src = convertFileSrc(photo.path)await startRecording()
// ... user records ...
const video = await stopRecording()
// video: { path, duration } (mobile) | { duration, blob, url } (desktop)await flipCamera()
await setFlash('on') // 'on' | 'off' | 'auto'
await setZoom(2.0)await saveToGallery(photo.path) // mobile onlyconst state = await checkPermissions() // 'granted' | 'denied' | 'prompt'
await requestPermissions()- Mobile writes the capture to a temporary file in native code and returns its path.
Use
convertFileSrc(path)to display it, read it with thefsplugin, or upload it withfetch/thehttpplugin. - Desktop returns a base64 data URL and a
Blob(no native file system write).
This keeps large image payloads out of the JSON IPC bridge (mobile) while staying idiomatic on the web (desktop).
The plugin ships unit tests for every layer. They are pure logic tests — no camera hardware, emulator or device is required.
| Layer | Test file | Command | Run from | Requires |
|---|---|---|---|---|
| Rust | src/models.rs (inline tests) |
cargo test |
plugin root | Rust + Cargo |
| JS | guest-js/index.test.ts |
npm test |
plugin root | Node.js 18+ + npm |
| Android | android/src/test/java/CameraPluginTest.kt |
gradle test |
android/ (or generated app) |
JDK 17+, Android SDK, Gradle |
| iOS | ios/Tests/PluginTests/PluginTests.swift |
swift test |
ios/ |
macOS + Xcode |
Rust
- Rust (stable toolchain).
- Optional — to verify the crate compiles for mobile:
rustup target add aarch64-linux-android
JavaScript
- Node.js 18+ and npm (or pnpm/yarn).
Android (Kotlin)
- JDK 17 or newer.
- Android SDK with
platforms/android-36andbuild-tools. - Gradle (or the generated project's wrapper).
iOS (Swift)
- macOS with Xcode. iOS tests cannot run on Linux or Windows.
cd tauri-plugin-camera
cargo testCovers model (de)serialization and the error types. To also verify the crate compiles against the Android target:
cargo check --target aarch64-linux-androidcd tauri-plugin-camera
npm install
npm testnpm test runs Vitest; the tests cover the API signatures and the mobile/desktop
dispatch. To validate types and generate the bindings:
npm run buildThe JUnit tests cover flash-mode mapping, zoom clamping and camera direction.
Running them requires the full Tauri Android pipeline. The plugin's android/
module is a Gradle library built as part of the generated app project, and the
Gradle config (tauri.settings.gradle) is only generated during a Tauri Android
build — not by tauri android init (that only lays down the base project). The
sequence is:
cd tauri-plugin-camera/examples/tauri-app
npm install
npm run tauri android init
npm run tauri android build # generates tauri.settings.gradle and compiles
cd src-tauri/gen/android
./gradlew testRequires JDK 17+, Android SDK + NDK, and platforms/android-36 — or set
compileSdk = 35 in android/build.gradle.kts if your SDK only goes up to 35.
tauri android build builds a release APK (needs a signing key); tauri android dev
builds a debug APK but requires a connected device/emulator. Either one generates
tauri.settings.gradle, after which ./gradlew test works.
The XCTest tests cover decoding of the command options. Testing requires full Xcode — Command Line Tools is not enough, because the plugin imports UIKit, AVFoundation and Photos, which need the iOS SDK and the Simulator.
The plugin is iOS-only, so it cannot be built with swift test (that targets
macOS, where UIKit is unavailable, and the Tauri package referenced by
ios/Package.swift is only created by the Tauri iOS setup). Validate the Swift
code by building the iOS app:
cd tauri-plugin-camera/examples/tauri-app
npm install
npm run tauri ios init
npm run tauri ios build # compiles the Swift plugin for iOSTo run the XCTest suite itself, use xcodebuild test against an iOS Simulator
destination from the generated Xcode project (src-tauri/gen/apple).
| Function | Mobile | Desktop |
|---|---|---|
startPreview(options?) |
✓ native preview | ✓ getUserMedia |
stopPreview() |
✓ | ✓ |
capture(options?) |
✓ ImageCapture / AVCapturePhotoOutput |
✓ canvas |
flipCamera() |
✓ | ✗ (throws) |
setFlash(mode) |
✓ | ✗ (throws) |
setZoom(factor) |
✓ | ✗ (throws) |
startRecording() |
✓ VideoCapture / AVCaptureMovieFileOutput |
✓ MediaRecorder |
stopRecording() |
✓ | ✓ |
saveToGallery(path) |
✓ | ✗ (throws) |
checkPermissions() |
✓ | ✓ |
requestPermissions() |
✓ | ✓ |
openAppSettings() |
✓ | ✗ (throws) |
getPreviewStream() |
null |
MediaStream |