tauri-plugin-camera-kessdev 0.1.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,314 @@
1
+ # Tauri Plugin Camera
2
+
3
+ A [Tauri v2](https://v2.tauri.app) plugin that provides a **unified JavaScript API** for
4
+ camera access across Android, iOS and desktop. It supports live preview, photo capture,
5
+ video recording (with audio), camera controls (front/back, flash, zoom), saving to the
6
+ photo gallery, and camera permission management.
7
+
8
+ | Platform | Supported |
9
+ |----------|-----------|
10
+ | Android | ✓ |
11
+ | iOS | ✓ |
12
+ | macOS | ✓ (via `getUserMedia`) |
13
+ | Windows | ✓ (via `getUserMedia`) |
14
+ | Linux | ✓ (via `getUserMedia`) |
15
+
16
+ > On desktop the camera is driven by the webview through `navigator.mediaDevices.getUserMedia`
17
+ > plus `<video>`/`<canvas>`. On mobile it uses a native preview (CameraX on Android,
18
+ > AVFoundation on iOS) for full control and quality.
19
+
20
+ ## Install
21
+
22
+ Add the Rust crate to `src-tauri/Cargo.toml`:
23
+
24
+ ```toml
25
+ [dependencies]
26
+ tauri-plugin-camera-kessdev = "0.1.0"
27
+ # or from a git/path source:
28
+ # tauri-plugin-camera-kessdev = { path = "../tauri-plugin-camera" }
29
+ ```
30
+
31
+ Add the JavaScript bindings:
32
+
33
+ ```bash
34
+ npm add tauri-plugin-camera-kessdev
35
+ # pnpm add tauri-plugin-camera-kessdev
36
+ # yarn add tauri-plugin-camera-kessdev
37
+ ```
38
+
39
+ ## Register
40
+
41
+ In `src-tauri/src/lib.rs`:
42
+
43
+ ```rust
44
+ pub fn run() {
45
+ tauri::Builder::default()
46
+ .plugin(tauri_plugin_camera_kessdev::init())
47
+ .run(tauri::generate_context!())
48
+ .expect("error while running tauri application");
49
+ }
50
+ ```
51
+
52
+ ## Permissions
53
+
54
+ ### Capabilities (Tauri v2)
55
+
56
+ Add the default permission set to `src-tauri/capabilities/default.json`:
57
+
58
+ ```json
59
+ {
60
+ "permissions": [
61
+ "core:default",
62
+ "camera:default"
63
+ ]
64
+ }
65
+ ```
66
+
67
+ To allow only specific commands, use the granular permissions instead of
68
+ `camera:default` (e.g. `camera:allow-capture`, `camera:allow-start-recording`,
69
+ `camera:allow-check-permissions`, …). The full list is generated under
70
+ `permissions/autogenerated/commands/` as `allow-<command>` / `deny-<command>`.
71
+
72
+ ### Android
73
+
74
+ The plugin's `AndroidManifest.xml` declares `android.permission.CAMERA` and
75
+ `android.permission.RECORD_AUDIO` (for video audio). The Kotlin plugin registers them
76
+ through the `@TauriPlugin` annotation, so `checkPermissions` / `requestPermissions` work
77
+ out of the box. No further manifest edits are required.
78
+
79
+ ### iOS
80
+
81
+ Add the following usage descriptions to the app `Info.plist`
82
+ (`src-tauri/Info.plist`):
83
+
84
+ ```xml
85
+ <key>NSCameraUsageDescription</key>
86
+ <string>This app uses the camera to capture photos and videos.</string>
87
+ <key>NSMicrophoneUsageDescription</key>
88
+ <string>This app uses the microphone to record audio in videos.</string>
89
+ <key>NSPhotoLibraryAddUsageDescription</key>
90
+ <string>This app saves captured photos to your photo library.</string>
91
+ ```
92
+
93
+ `NSMicrophoneUsageDescription` is only required if you use video recording,
94
+ `NSPhotoLibraryAddUsageDescription` only if you use `saveToGallery`.
95
+
96
+ ### Desktop
97
+
98
+ Desktop uses the browser's `getUserMedia`. On macOS, `WKWebView` additionally requires
99
+ `NSCameraUsageDescription` and `NSMicrophoneUsageDescription` in the `Info.plist` (the
100
+ same keys above).
101
+
102
+ ## Usage
103
+
104
+ ```ts
105
+ import {
106
+ startPreview,
107
+ stopPreview,
108
+ capture,
109
+ startRecording,
110
+ stopRecording,
111
+ flipCamera,
112
+ setFlash,
113
+ setZoom,
114
+ saveToGallery,
115
+ checkPermissions,
116
+ requestPermissions,
117
+ getPreviewStream,
118
+ convertFileSrc
119
+ } from 'tauri-plugin-camera-kessdev'
120
+ ```
121
+
122
+ ### Preview
123
+
124
+ ```ts
125
+ // windowed: true keeps the webview transparent so your UI renders over the camera.
126
+ await startPreview({ camera: 'back', windowed: true })
127
+
128
+ // Desktop: attach the stream to a <video> element.
129
+ const stream = await getPreviewStream()
130
+ if (stream) videoEl.srcObject = stream
131
+
132
+ await stopPreview()
133
+ ```
134
+
135
+ ### Photo capture
136
+
137
+ ```ts
138
+ const photo = await capture({ flash: 'auto' })
139
+ // photo: { path, width, height, orientation, format, thumbnail? } (mobile)
140
+ // { dataUrl, width, height, orientation, format } (desktop)
141
+
142
+ // Mobile: display the captured file.
143
+ imageEl.src = convertFileSrc(photo.path)
144
+ ```
145
+
146
+ ### Video recording (with audio)
147
+
148
+ ```ts
149
+ await startRecording()
150
+ // ... user records ...
151
+ const video = await stopRecording()
152
+ // video: { path, duration } (mobile) | { duration, blob, url } (desktop)
153
+ ```
154
+
155
+ ### Camera controls
156
+
157
+ ```ts
158
+ await flipCamera()
159
+ await setFlash('on') // 'on' | 'off' | 'auto'
160
+ await setZoom(2.0)
161
+ ```
162
+
163
+ ### Gallery
164
+
165
+ ```ts
166
+ await saveToGallery(photo.path) // mobile only
167
+ ```
168
+
169
+ ### Permissions
170
+
171
+ ```ts
172
+ const state = await checkPermissions() // 'granted' | 'denied' | 'prompt'
173
+ await requestPermissions()
174
+ ```
175
+
176
+ ## Image return pattern
177
+
178
+ - **Mobile** writes the capture to a temporary file in native code and returns its path.
179
+ Use `convertFileSrc(path)` to display it, read it with the
180
+ [`fs`](https://v2.tauri.app/plugin/file-system/) plugin, or upload it with
181
+ `fetch`/the [`http`](https://v2.tauri.app/plugin/http-client/) plugin.
182
+ - **Desktop** returns a base64 data URL and a `Blob` (no native file system write).
183
+
184
+ This keeps large image payloads out of the JSON IPC bridge (mobile) while staying
185
+ idiomatic on the web (desktop).
186
+
187
+ ## Testing
188
+
189
+ The plugin ships unit tests for every layer. They are **pure logic** tests — no camera
190
+ hardware, emulator or device is required.
191
+
192
+ | Layer | Test file | Command | Run from | Requires |
193
+ |---------|--------------------------------------------|----------------|---------------------|-----------------------------------|
194
+ | Rust | `src/models.rs` (inline tests) | `cargo test` | plugin root | Rust + Cargo |
195
+ | JS | `guest-js/index.test.ts` | `npm test` | plugin root | Node.js 18+ + npm |
196
+ | Android | `android/src/test/java/CameraPluginTest.kt`| `gradle test` | `android/` (or generated app) | JDK 17+, Android SDK, Gradle |
197
+ | iOS | `ios/Tests/PluginTests/PluginTests.swift` | `swift test` | `ios/` | macOS + Xcode |
198
+
199
+ ### Prerequisites
200
+
201
+ **Rust**
202
+
203
+ - [Rust](https://rustup.rs) (stable toolchain).
204
+ - Optional — to verify the crate compiles for mobile:
205
+ ```bash
206
+ rustup target add aarch64-linux-android
207
+ ```
208
+
209
+ **JavaScript**
210
+
211
+ - [Node.js](https://nodejs.org) 18+ and npm (or pnpm/yarn).
212
+
213
+ **Android (Kotlin)**
214
+
215
+ - JDK 17 or newer.
216
+ - Android SDK with `platforms/android-36` and `build-tools`.
217
+ - Gradle (or the generated project's wrapper).
218
+
219
+ **iOS (Swift)**
220
+
221
+ - macOS with Xcode. iOS tests cannot run on Linux or Windows.
222
+
223
+ ### Rust
224
+
225
+ ```bash
226
+ cd tauri-plugin-camera
227
+ cargo test
228
+ ```
229
+
230
+ Covers model (de)serialization and the error types. To also verify the crate
231
+ compiles against the Android target:
232
+
233
+ ```bash
234
+ cargo check --target aarch64-linux-android
235
+ ```
236
+
237
+ ### JavaScript
238
+
239
+ ```bash
240
+ cd tauri-plugin-camera
241
+ npm install
242
+ npm test
243
+ ```
244
+
245
+ `npm test` runs Vitest; the tests cover the API signatures and the mobile/desktop
246
+ dispatch. To validate types and generate the bindings:
247
+
248
+ ```bash
249
+ npm run build
250
+ ```
251
+
252
+ ### Android (Kotlin)
253
+
254
+ The JUnit tests cover flash-mode mapping, zoom clamping and camera direction.
255
+
256
+ Running them requires the full Tauri Android pipeline. The plugin's `android/`
257
+ module is a Gradle library built as part of the generated app project, and the
258
+ Gradle config (`tauri.settings.gradle`) is only generated during a Tauri Android
259
+ build — not by `tauri android init` (that only lays down the base project). The
260
+ sequence is:
261
+
262
+ ```bash
263
+ cd tauri-plugin-camera/examples/tauri-app
264
+ npm install
265
+ npm run tauri android init
266
+ npm run tauri android build # generates tauri.settings.gradle and compiles
267
+ cd src-tauri/gen/android
268
+ ./gradlew test
269
+ ```
270
+
271
+ Requires JDK 17+, Android SDK + NDK, and `platforms/android-36` — or set
272
+ `compileSdk = 35` in `android/build.gradle.kts` if your SDK only goes up to 35.
273
+ `tauri android build` builds a release APK (needs a signing key); `tauri android dev`
274
+ builds a debug APK but requires a connected device/emulator. Either one generates
275
+ `tauri.settings.gradle`, after which `./gradlew test` works.
276
+
277
+ ### iOS (Swift)
278
+
279
+ The XCTest tests cover decoding of the command options. Testing requires **full
280
+ Xcode** — Command Line Tools is not enough, because the plugin imports UIKit,
281
+ AVFoundation and Photos, which need the iOS SDK and the Simulator.
282
+
283
+ The plugin is iOS-only, so it cannot be built with `swift test` (that targets
284
+ macOS, where UIKit is unavailable, and the `Tauri` package referenced by
285
+ `ios/Package.swift` is only created by the Tauri iOS setup). Validate the Swift
286
+ code by building the iOS app:
287
+
288
+ ```bash
289
+ cd tauri-plugin-camera/examples/tauri-app
290
+ npm install
291
+ npm run tauri ios init
292
+ npm run tauri ios build # compiles the Swift plugin for iOS
293
+ ```
294
+
295
+ To run the XCTest suite itself, use `xcodebuild test` against an iOS Simulator
296
+ destination from the generated Xcode project (`src-tauri/gen/apple`).
297
+
298
+ ## Full API
299
+
300
+ | Function | Mobile | Desktop |
301
+ |----------|--------|---------|
302
+ | `startPreview(options?)` | ✓ native preview | ✓ `getUserMedia` |
303
+ | `stopPreview()` | ✓ | ✓ |
304
+ | `capture(options?)` | ✓ `ImageCapture` / `AVCapturePhotoOutput` | ✓ canvas |
305
+ | `flipCamera()` | ✓ | ✗ (throws) |
306
+ | `setFlash(mode)` | ✓ | ✗ (throws) |
307
+ | `setZoom(factor)` | ✓ | ✗ (throws) |
308
+ | `startRecording()` | ✓ `VideoCapture` / `AVCaptureMovieFileOutput` | ✓ `MediaRecorder` |
309
+ | `stopRecording()` | ✓ | ✓ |
310
+ | `saveToGallery(path)` | ✓ | ✗ (throws) |
311
+ | `checkPermissions()` | ✓ | ✓ |
312
+ | `requestPermissions()` | ✓ | ✓ |
313
+ | `openAppSettings()` | ✓ | ✗ (throws) |
314
+ | `getPreviewStream()` | `null` | `MediaStream` |
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+
3
+ var core = require('@tauri-apps/api/core');
4
+
5
+ /**
6
+ * Detects whether the current runtime is a Tauri mobile webview (Android/iOS).
7
+ * Desktop runtimes fall through to the getUserMedia-based backend.
8
+ */
9
+ function isMobile() {
10
+ if (typeof navigator === 'undefined')
11
+ return false;
12
+ const ua = navigator.userAgent;
13
+ return /android/i.test(ua) || /iphone|ipad|ipod/i.test(ua);
14
+ }
15
+ // ---------------------------------------------------------------------------
16
+ // Desktop backend state (getUserMedia + canvas + MediaRecorder)
17
+ // ---------------------------------------------------------------------------
18
+ let activeStream = null;
19
+ let activeVideo = null;
20
+ let mediaRecorder = null;
21
+ let recordedChunks = [];
22
+ let recordingStartedAt = 0;
23
+ async function ensureDesktopStream(options) {
24
+ if (activeStream)
25
+ return activeStream;
26
+ const constraints = {
27
+ video: {
28
+ facingMode: options?.camera === 'front' ? 'user' : 'environment'
29
+ },
30
+ audio: false
31
+ };
32
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
33
+ activeStream = stream;
34
+ const video = document.createElement('video');
35
+ video.muted = true;
36
+ video.playsInline = true;
37
+ video.srcObject = stream;
38
+ await video.play();
39
+ activeVideo = video;
40
+ return stream;
41
+ }
42
+ function teardownDesktopStream() {
43
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
44
+ mediaRecorder.stop();
45
+ }
46
+ mediaRecorder = null;
47
+ recordedChunks = [];
48
+ if (activeStream) {
49
+ activeStream.getTracks().forEach((t) => t.stop());
50
+ }
51
+ activeStream = null;
52
+ activeVideo = null;
53
+ }
54
+ function captureDesktopFrame() {
55
+ if (!activeVideo) {
56
+ throw new Error('Camera preview is not active. Call startPreview() first.');
57
+ }
58
+ const width = activeVideo.videoWidth;
59
+ const height = activeVideo.videoHeight;
60
+ const canvas = document.createElement('canvas');
61
+ canvas.width = width;
62
+ canvas.height = height;
63
+ const ctx = canvas.getContext('2d');
64
+ if (!ctx) {
65
+ throw new Error('Failed to create canvas context for capture.');
66
+ }
67
+ ctx.drawImage(activeVideo, 0, 0, width, height);
68
+ const dataUrl = canvas.toDataURL('image/jpeg');
69
+ return {
70
+ dataUrl,
71
+ width,
72
+ height,
73
+ orientation: 1,
74
+ format: 'jpeg',
75
+ thumbnail: dataUrl
76
+ };
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Preview
80
+ // ---------------------------------------------------------------------------
81
+ /**
82
+ * Starts the camera preview.
83
+ *
84
+ * On mobile this starts a native preview over the webview; pass `windowed: true`
85
+ * to keep the webview transparent so your UI renders on top of the camera.
86
+ * On desktop this acquires the camera via `getUserMedia` and stores the stream
87
+ * for `capture()` / `startRecording()`. Use `getPreviewStream()` to attach the
88
+ * desktop stream to a `<video>` element.
89
+ */
90
+ async function startPreview(options) {
91
+ if (isMobile()) {
92
+ await core.invoke('plugin:camera-kessdev|start_preview', { ...options });
93
+ return;
94
+ }
95
+ await ensureDesktopStream(options);
96
+ }
97
+ /** Stops the camera preview and releases the camera. */
98
+ async function stopPreview() {
99
+ if (isMobile()) {
100
+ await core.invoke('plugin:camera-kessdev|stop_preview');
101
+ return;
102
+ }
103
+ teardownDesktopStream();
104
+ }
105
+ /**
106
+ * Returns the active desktop preview stream so the application can render it in
107
+ * a `<video>` element. Returns `null` on mobile (the native preview is used).
108
+ */
109
+ async function getPreviewStream() {
110
+ if (isMobile())
111
+ return null;
112
+ return activeStream;
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Photo capture
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Captures a photo.
119
+ *
120
+ * On mobile returns the temporary file path (plus an optional base64
121
+ * thumbnail). Use `convertFileSrc(photo.path)` to display it. On desktop
122
+ * returns a base64 data URL and a Blob.
123
+ */
124
+ async function capture(options) {
125
+ if (isMobile()) {
126
+ return await core.invoke('plugin:camera-kessdev|capture', { ...options });
127
+ }
128
+ return captureDesktopFrame();
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Camera controls
132
+ // ---------------------------------------------------------------------------
133
+ /** Switches between front and back camera (mobile). */
134
+ async function flipCamera() {
135
+ if (isMobile()) {
136
+ await core.invoke('plugin:camera-kessdev|flip_camera');
137
+ return;
138
+ }
139
+ throw new Error('flipCamera is not supported on desktop. Restart the preview with a different camera direction.');
140
+ }
141
+ /** Sets the flash mode (mobile). */
142
+ async function setFlash(mode) {
143
+ if (isMobile()) {
144
+ await core.invoke('plugin:camera-kessdev|set_flash', { mode });
145
+ return;
146
+ }
147
+ throw new Error('setFlash is not supported on desktop.');
148
+ }
149
+ /** Sets the camera zoom factor (mobile). */
150
+ async function setZoom(factor) {
151
+ if (isMobile()) {
152
+ await core.invoke('plugin:camera-kessdev|set_zoom', { factor });
153
+ return;
154
+ }
155
+ throw new Error('setZoom is not supported on desktop.');
156
+ }
157
+ // ---------------------------------------------------------------------------
158
+ // Video recording
159
+ // ---------------------------------------------------------------------------
160
+ /** Starts video recording (with audio). */
161
+ async function startRecording() {
162
+ if (isMobile()) {
163
+ await core.invoke('plugin:camera-kessdev|start_recording');
164
+ return;
165
+ }
166
+ if (activeStream) {
167
+ activeStream.getTracks().forEach((t) => t.stop());
168
+ activeStream = null;
169
+ activeVideo = null;
170
+ }
171
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
172
+ activeStream = stream;
173
+ const video = document.createElement('video');
174
+ video.muted = true;
175
+ video.playsInline = true;
176
+ video.srcObject = stream;
177
+ await video.play();
178
+ activeVideo = video;
179
+ recordedChunks = [];
180
+ mediaRecorder = new MediaRecorder(stream);
181
+ mediaRecorder.ondataavailable = (event) => {
182
+ if (event.data.size > 0)
183
+ recordedChunks.push(event.data);
184
+ };
185
+ recordingStartedAt = performance.now();
186
+ mediaRecorder.start();
187
+ }
188
+ /** Stops video recording and returns the recorded media. */
189
+ async function stopRecording() {
190
+ if (isMobile()) {
191
+ return await core.invoke('plugin:camera-kessdev|stop_recording');
192
+ }
193
+ if (!mediaRecorder) {
194
+ throw new Error('Recording is not active. Call startRecording() first.');
195
+ }
196
+ const duration = (performance.now() - recordingStartedAt) / 1000;
197
+ return await new Promise((resolve, reject) => {
198
+ mediaRecorder.onstop = () => {
199
+ const type = mediaRecorder.mimeType || 'video/webm';
200
+ const blob = new Blob(recordedChunks, { type });
201
+ const url = URL.createObjectURL(blob);
202
+ recordedChunks = [];
203
+ mediaRecorder = null;
204
+ resolve({ duration, blob, url });
205
+ };
206
+ mediaRecorder.onerror = () => reject(new Error('Video recording failed.'));
207
+ mediaRecorder.stop();
208
+ });
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Gallery
212
+ // ---------------------------------------------------------------------------
213
+ /**
214
+ * Saves a captured image to the device photo gallery (mobile).
215
+ *
216
+ * @param path Absolute path to the image file.
217
+ */
218
+ async function saveToGallery(path) {
219
+ if (isMobile()) {
220
+ await core.invoke('plugin:camera-kessdev|save_to_gallery', { path });
221
+ return;
222
+ }
223
+ throw new Error('saveToGallery is not supported on desktop.');
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Permissions
227
+ // ---------------------------------------------------------------------------
228
+ /** Gets the current camera permission state. */
229
+ async function checkPermissions() {
230
+ if (isMobile()) {
231
+ return await core.checkPermissions('camera-kessdev').then((r) => r.camera);
232
+ }
233
+ try {
234
+ const status = await navigator.permissions.query({ name: 'camera' });
235
+ return status.state;
236
+ }
237
+ catch {
238
+ return 'prompt';
239
+ }
240
+ }
241
+ /** Requests camera permission. */
242
+ async function requestPermissions() {
243
+ if (isMobile()) {
244
+ return await core.requestPermissions('camera-kessdev').then((r) => r.camera);
245
+ }
246
+ try {
247
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
248
+ stream.getTracks().forEach((t) => t.stop());
249
+ return 'granted';
250
+ }
251
+ catch {
252
+ return 'denied';
253
+ }
254
+ }
255
+ /** Opens the application settings screen (mobile). */
256
+ async function openAppSettings() {
257
+ if (isMobile()) {
258
+ await core.invoke('plugin:camera-kessdev|open_app_settings');
259
+ return;
260
+ }
261
+ throw new Error('openAppSettings is not supported on desktop.');
262
+ }
263
+
264
+ Object.defineProperty(exports, "convertFileSrc", {
265
+ enumerable: true,
266
+ get: function () { return core.convertFileSrc; }
267
+ });
268
+ exports.capture = capture;
269
+ exports.checkPermissions = checkPermissions;
270
+ exports.flipCamera = flipCamera;
271
+ exports.getPreviewStream = getPreviewStream;
272
+ exports.openAppSettings = openAppSettings;
273
+ exports.requestPermissions = requestPermissions;
274
+ exports.saveToGallery = saveToGallery;
275
+ exports.setFlash = setFlash;
276
+ exports.setZoom = setZoom;
277
+ exports.startPreview = startPreview;
278
+ exports.startRecording = startRecording;
279
+ exports.stopPreview = stopPreview;
280
+ exports.stopRecording = stopRecording;
@@ -0,0 +1,84 @@
1
+ import { convertFileSrc } from '@tauri-apps/api/core';
2
+ import type { PermissionState } from '@tauri-apps/api/core';
3
+ export type { PermissionState } from '@tauri-apps/api/core';
4
+ export { convertFileSrc };
5
+ export type CameraDirection = 'front' | 'back';
6
+ export type FlashMode = 'on' | 'off' | 'auto';
7
+ export interface StartPreviewOptions {
8
+ camera?: CameraDirection;
9
+ windowed?: boolean;
10
+ }
11
+ export interface CaptureOptions {
12
+ flash?: FlashMode;
13
+ }
14
+ export interface Photo {
15
+ /** Absolute path to the captured image (mobile). */
16
+ path?: string;
17
+ /** Base64 data URL of the captured image (desktop). */
18
+ dataUrl?: string;
19
+ /** Raw image blob (desktop). */
20
+ blob?: Blob;
21
+ width: number;
22
+ height: number;
23
+ /** EXIF orientation value. */
24
+ orientation: number;
25
+ format: string;
26
+ /** Optional base64-encoded thumbnail (mobile). */
27
+ thumbnail?: string;
28
+ }
29
+ export interface Video {
30
+ /** Absolute path to the recorded video (mobile). */
31
+ path?: string;
32
+ duration: number;
33
+ /** Raw video blob (desktop). */
34
+ blob?: Blob;
35
+ /** Object URL for the recorded video (desktop). */
36
+ url?: string;
37
+ }
38
+ /**
39
+ * Starts the camera preview.
40
+ *
41
+ * On mobile this starts a native preview over the webview; pass `windowed: true`
42
+ * to keep the webview transparent so your UI renders on top of the camera.
43
+ * On desktop this acquires the camera via `getUserMedia` and stores the stream
44
+ * for `capture()` / `startRecording()`. Use `getPreviewStream()` to attach the
45
+ * desktop stream to a `<video>` element.
46
+ */
47
+ export declare function startPreview(options?: StartPreviewOptions): Promise<void>;
48
+ /** Stops the camera preview and releases the camera. */
49
+ export declare function stopPreview(): Promise<void>;
50
+ /**
51
+ * Returns the active desktop preview stream so the application can render it in
52
+ * a `<video>` element. Returns `null` on mobile (the native preview is used).
53
+ */
54
+ export declare function getPreviewStream(): Promise<MediaStream | null>;
55
+ /**
56
+ * Captures a photo.
57
+ *
58
+ * On mobile returns the temporary file path (plus an optional base64
59
+ * thumbnail). Use `convertFileSrc(photo.path)` to display it. On desktop
60
+ * returns a base64 data URL and a Blob.
61
+ */
62
+ export declare function capture(options?: CaptureOptions): Promise<Photo>;
63
+ /** Switches between front and back camera (mobile). */
64
+ export declare function flipCamera(): Promise<void>;
65
+ /** Sets the flash mode (mobile). */
66
+ export declare function setFlash(mode: FlashMode): Promise<void>;
67
+ /** Sets the camera zoom factor (mobile). */
68
+ export declare function setZoom(factor: number): Promise<void>;
69
+ /** Starts video recording (with audio). */
70
+ export declare function startRecording(): Promise<void>;
71
+ /** Stops video recording and returns the recorded media. */
72
+ export declare function stopRecording(): Promise<Video>;
73
+ /**
74
+ * Saves a captured image to the device photo gallery (mobile).
75
+ *
76
+ * @param path Absolute path to the image file.
77
+ */
78
+ export declare function saveToGallery(path: string): Promise<void>;
79
+ /** Gets the current camera permission state. */
80
+ export declare function checkPermissions(): Promise<PermissionState>;
81
+ /** Requests camera permission. */
82
+ export declare function requestPermissions(): Promise<PermissionState>;
83
+ /** Opens the application settings screen (mobile). */
84
+ export declare function openAppSettings(): Promise<void>;
@@ -0,0 +1,263 @@
1
+ import { invoke, checkPermissions as checkPermissions$1, requestPermissions as requestPermissions$1 } from '@tauri-apps/api/core';
2
+ export { convertFileSrc } from '@tauri-apps/api/core';
3
+
4
+ /**
5
+ * Detects whether the current runtime is a Tauri mobile webview (Android/iOS).
6
+ * Desktop runtimes fall through to the getUserMedia-based backend.
7
+ */
8
+ function isMobile() {
9
+ if (typeof navigator === 'undefined')
10
+ return false;
11
+ const ua = navigator.userAgent;
12
+ return /android/i.test(ua) || /iphone|ipad|ipod/i.test(ua);
13
+ }
14
+ // ---------------------------------------------------------------------------
15
+ // Desktop backend state (getUserMedia + canvas + MediaRecorder)
16
+ // ---------------------------------------------------------------------------
17
+ let activeStream = null;
18
+ let activeVideo = null;
19
+ let mediaRecorder = null;
20
+ let recordedChunks = [];
21
+ let recordingStartedAt = 0;
22
+ async function ensureDesktopStream(options) {
23
+ if (activeStream)
24
+ return activeStream;
25
+ const constraints = {
26
+ video: {
27
+ facingMode: options?.camera === 'front' ? 'user' : 'environment'
28
+ },
29
+ audio: false
30
+ };
31
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
32
+ activeStream = stream;
33
+ const video = document.createElement('video');
34
+ video.muted = true;
35
+ video.playsInline = true;
36
+ video.srcObject = stream;
37
+ await video.play();
38
+ activeVideo = video;
39
+ return stream;
40
+ }
41
+ function teardownDesktopStream() {
42
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
43
+ mediaRecorder.stop();
44
+ }
45
+ mediaRecorder = null;
46
+ recordedChunks = [];
47
+ if (activeStream) {
48
+ activeStream.getTracks().forEach((t) => t.stop());
49
+ }
50
+ activeStream = null;
51
+ activeVideo = null;
52
+ }
53
+ function captureDesktopFrame() {
54
+ if (!activeVideo) {
55
+ throw new Error('Camera preview is not active. Call startPreview() first.');
56
+ }
57
+ const width = activeVideo.videoWidth;
58
+ const height = activeVideo.videoHeight;
59
+ const canvas = document.createElement('canvas');
60
+ canvas.width = width;
61
+ canvas.height = height;
62
+ const ctx = canvas.getContext('2d');
63
+ if (!ctx) {
64
+ throw new Error('Failed to create canvas context for capture.');
65
+ }
66
+ ctx.drawImage(activeVideo, 0, 0, width, height);
67
+ const dataUrl = canvas.toDataURL('image/jpeg');
68
+ return {
69
+ dataUrl,
70
+ width,
71
+ height,
72
+ orientation: 1,
73
+ format: 'jpeg',
74
+ thumbnail: dataUrl
75
+ };
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // Preview
79
+ // ---------------------------------------------------------------------------
80
+ /**
81
+ * Starts the camera preview.
82
+ *
83
+ * On mobile this starts a native preview over the webview; pass `windowed: true`
84
+ * to keep the webview transparent so your UI renders on top of the camera.
85
+ * On desktop this acquires the camera via `getUserMedia` and stores the stream
86
+ * for `capture()` / `startRecording()`. Use `getPreviewStream()` to attach the
87
+ * desktop stream to a `<video>` element.
88
+ */
89
+ async function startPreview(options) {
90
+ if (isMobile()) {
91
+ await invoke('plugin:camera-kessdev|start_preview', { ...options });
92
+ return;
93
+ }
94
+ await ensureDesktopStream(options);
95
+ }
96
+ /** Stops the camera preview and releases the camera. */
97
+ async function stopPreview() {
98
+ if (isMobile()) {
99
+ await invoke('plugin:camera-kessdev|stop_preview');
100
+ return;
101
+ }
102
+ teardownDesktopStream();
103
+ }
104
+ /**
105
+ * Returns the active desktop preview stream so the application can render it in
106
+ * a `<video>` element. Returns `null` on mobile (the native preview is used).
107
+ */
108
+ async function getPreviewStream() {
109
+ if (isMobile())
110
+ return null;
111
+ return activeStream;
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Photo capture
115
+ // ---------------------------------------------------------------------------
116
+ /**
117
+ * Captures a photo.
118
+ *
119
+ * On mobile returns the temporary file path (plus an optional base64
120
+ * thumbnail). Use `convertFileSrc(photo.path)` to display it. On desktop
121
+ * returns a base64 data URL and a Blob.
122
+ */
123
+ async function capture(options) {
124
+ if (isMobile()) {
125
+ return await invoke('plugin:camera-kessdev|capture', { ...options });
126
+ }
127
+ return captureDesktopFrame();
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // Camera controls
131
+ // ---------------------------------------------------------------------------
132
+ /** Switches between front and back camera (mobile). */
133
+ async function flipCamera() {
134
+ if (isMobile()) {
135
+ await invoke('plugin:camera-kessdev|flip_camera');
136
+ return;
137
+ }
138
+ throw new Error('flipCamera is not supported on desktop. Restart the preview with a different camera direction.');
139
+ }
140
+ /** Sets the flash mode (mobile). */
141
+ async function setFlash(mode) {
142
+ if (isMobile()) {
143
+ await invoke('plugin:camera-kessdev|set_flash', { mode });
144
+ return;
145
+ }
146
+ throw new Error('setFlash is not supported on desktop.');
147
+ }
148
+ /** Sets the camera zoom factor (mobile). */
149
+ async function setZoom(factor) {
150
+ if (isMobile()) {
151
+ await invoke('plugin:camera-kessdev|set_zoom', { factor });
152
+ return;
153
+ }
154
+ throw new Error('setZoom is not supported on desktop.');
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Video recording
158
+ // ---------------------------------------------------------------------------
159
+ /** Starts video recording (with audio). */
160
+ async function startRecording() {
161
+ if (isMobile()) {
162
+ await invoke('plugin:camera-kessdev|start_recording');
163
+ return;
164
+ }
165
+ if (activeStream) {
166
+ activeStream.getTracks().forEach((t) => t.stop());
167
+ activeStream = null;
168
+ activeVideo = null;
169
+ }
170
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
171
+ activeStream = stream;
172
+ const video = document.createElement('video');
173
+ video.muted = true;
174
+ video.playsInline = true;
175
+ video.srcObject = stream;
176
+ await video.play();
177
+ activeVideo = video;
178
+ recordedChunks = [];
179
+ mediaRecorder = new MediaRecorder(stream);
180
+ mediaRecorder.ondataavailable = (event) => {
181
+ if (event.data.size > 0)
182
+ recordedChunks.push(event.data);
183
+ };
184
+ recordingStartedAt = performance.now();
185
+ mediaRecorder.start();
186
+ }
187
+ /** Stops video recording and returns the recorded media. */
188
+ async function stopRecording() {
189
+ if (isMobile()) {
190
+ return await invoke('plugin:camera-kessdev|stop_recording');
191
+ }
192
+ if (!mediaRecorder) {
193
+ throw new Error('Recording is not active. Call startRecording() first.');
194
+ }
195
+ const duration = (performance.now() - recordingStartedAt) / 1000;
196
+ return await new Promise((resolve, reject) => {
197
+ mediaRecorder.onstop = () => {
198
+ const type = mediaRecorder.mimeType || 'video/webm';
199
+ const blob = new Blob(recordedChunks, { type });
200
+ const url = URL.createObjectURL(blob);
201
+ recordedChunks = [];
202
+ mediaRecorder = null;
203
+ resolve({ duration, blob, url });
204
+ };
205
+ mediaRecorder.onerror = () => reject(new Error('Video recording failed.'));
206
+ mediaRecorder.stop();
207
+ });
208
+ }
209
+ // ---------------------------------------------------------------------------
210
+ // Gallery
211
+ // ---------------------------------------------------------------------------
212
+ /**
213
+ * Saves a captured image to the device photo gallery (mobile).
214
+ *
215
+ * @param path Absolute path to the image file.
216
+ */
217
+ async function saveToGallery(path) {
218
+ if (isMobile()) {
219
+ await invoke('plugin:camera-kessdev|save_to_gallery', { path });
220
+ return;
221
+ }
222
+ throw new Error('saveToGallery is not supported on desktop.');
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // Permissions
226
+ // ---------------------------------------------------------------------------
227
+ /** Gets the current camera permission state. */
228
+ async function checkPermissions() {
229
+ if (isMobile()) {
230
+ return await checkPermissions$1('camera-kessdev').then((r) => r.camera);
231
+ }
232
+ try {
233
+ const status = await navigator.permissions.query({ name: 'camera' });
234
+ return status.state;
235
+ }
236
+ catch {
237
+ return 'prompt';
238
+ }
239
+ }
240
+ /** Requests camera permission. */
241
+ async function requestPermissions() {
242
+ if (isMobile()) {
243
+ return await requestPermissions$1('camera-kessdev').then((r) => r.camera);
244
+ }
245
+ try {
246
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
247
+ stream.getTracks().forEach((t) => t.stop());
248
+ return 'granted';
249
+ }
250
+ catch {
251
+ return 'denied';
252
+ }
253
+ }
254
+ /** Opens the application settings screen (mobile). */
255
+ async function openAppSettings() {
256
+ if (isMobile()) {
257
+ await invoke('plugin:camera-kessdev|open_app_settings');
258
+ return;
259
+ }
260
+ throw new Error('openAppSettings is not supported on desktop.');
261
+ }
262
+
263
+ export { capture, checkPermissions, flipCamera, getPreviewStream, openAppSettings, requestPermissions, saveToGallery, setFlash, setZoom, startPreview, startRecording, stopPreview, stopRecording };
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "tauri-plugin-camera-kessdev",
3
+ "version": "0.1.1-beta.0",
4
+ "author": "kess",
5
+ "description": "Camera plugin for Tauri v2: photo capture, video recording and preview on Android, iOS and desktop.",
6
+ "license": "MIT",
7
+ "repository": "https://github.com/kessdev/tauri-plugin-camera",
8
+ "keywords": [
9
+ "tauri",
10
+ "tauri-plugin",
11
+ "camera",
12
+ "rust",
13
+ "android",
14
+ "ios"
15
+ ],
16
+ "type": "module",
17
+ "types": "./dist-js/index.d.ts",
18
+ "main": "./dist-js/index.cjs",
19
+ "module": "./dist-js/index.js",
20
+ "exports": {
21
+ "types": "./dist-js/index.d.ts",
22
+ "import": "./dist-js/index.js",
23
+ "require": "./dist-js/index.cjs"
24
+ },
25
+ "files": [
26
+ "dist-js",
27
+ "README.md"
28
+ ],
29
+ "scripts": {
30
+ "build": "rollup -c",
31
+ "test": "vitest run",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "@tauri-apps/api": "^2.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@rollup/plugin-typescript": "^12.0.0",
39
+ "rollup": "^4.9.6",
40
+ "typescript": "^5.5.0",
41
+ "tslib": "^2.6.2",
42
+ "vitest": "^2.1.0",
43
+ "covector": "^0.13.2"
44
+ }
45
+ }