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 +314 -0
- package/dist-js/index.cjs +280 -0
- package/dist-js/index.d.ts +84 -0
- package/dist-js/index.js +263 -0
- package/dist-js/index.test.d.ts +1 -0
- package/package.json +45 -0
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>;
|
package/dist-js/index.js
ADDED
|
@@ -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
|
+
}
|