tauri-plugin-camera-kessdev 0.1.1-beta.4 → 0.2.0-beta.1
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 +182 -17
- package/dist-js/index.cjs +201 -25
- package/dist-js/index.d.ts +111 -7
- package/dist-js/index.js +194 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A [Tauri v2](https://v2.tauri.app) plugin that provides a **unified JavaScript API** for
|
|
4
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),
|
|
5
|
+
video recording (with audio), camera controls (front/back, flash, zoom), camera
|
|
6
|
+
introspection (list the available cameras and query their capabilities), saving to the
|
|
6
7
|
photo gallery, and camera permission management.
|
|
7
8
|
|
|
8
9
|
| Platform | Supported |
|
|
@@ -25,7 +26,7 @@ Add the Rust crate to `src-tauri/Cargo.toml`:
|
|
|
25
26
|
|
|
26
27
|
```toml
|
|
27
28
|
[dependencies]
|
|
28
|
-
tauri-plugin-camera-kessdev = "0.
|
|
29
|
+
tauri-plugin-camera-kessdev = "0.2.0-beta.0" # beta
|
|
29
30
|
# or from a git/path source:
|
|
30
31
|
# tauri-plugin-camera-kessdev = { path = "../tauri-plugin-camera" }
|
|
31
32
|
```
|
|
@@ -61,15 +62,16 @@ Add the default permission set to `src-tauri/capabilities/default.json`:
|
|
|
61
62
|
{
|
|
62
63
|
"permissions": [
|
|
63
64
|
"core:default",
|
|
64
|
-
"camera:default"
|
|
65
|
+
"camera-kessdev:default"
|
|
65
66
|
]
|
|
66
67
|
}
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
To allow only specific commands, use the granular permissions instead of
|
|
70
|
-
`camera:default` (e.g. `camera:allow-capture`,
|
|
71
|
-
`camera:allow-check-permissions`, …).
|
|
72
|
-
`permissions/autogenerated/commands/` as
|
|
71
|
+
`camera-kessdev:default` (e.g. `camera-kessdev:allow-capture`,
|
|
72
|
+
`camera-kessdev:allow-start-recording`, `camera-kessdev:allow-check-permissions`, …).
|
|
73
|
+
The full list is generated under `permissions/autogenerated/commands/` as
|
|
74
|
+
`allow-<command>` / `deny-<command>`.
|
|
73
75
|
|
|
74
76
|
### Android
|
|
75
77
|
|
|
@@ -128,13 +130,14 @@ import {
|
|
|
128
130
|
stopRecording,
|
|
129
131
|
flipCamera,
|
|
130
132
|
setFlash,
|
|
131
|
-
isFlashAvailable,
|
|
132
133
|
setZoom,
|
|
133
134
|
getZoom,
|
|
134
|
-
|
|
135
|
+
getAvailableCameras,
|
|
136
|
+
getCapabilities,
|
|
135
137
|
saveToGallery,
|
|
136
138
|
checkPermissions,
|
|
137
139
|
requestPermissions,
|
|
140
|
+
openAppSettings,
|
|
138
141
|
getPreviewStream,
|
|
139
142
|
convertFileSrc
|
|
140
143
|
} from 'tauri-plugin-camera-kessdev'
|
|
@@ -146,6 +149,10 @@ import {
|
|
|
146
149
|
// windowed: true keeps the webview transparent so your UI renders over the camera.
|
|
147
150
|
await startPreview({ camera: 'back', windowed: true })
|
|
148
151
|
|
|
152
|
+
// Or pick a specific lens by id (from getAvailableCameras()); cameraId takes precedence.
|
|
153
|
+
const cameras = await getAvailableCameras()
|
|
154
|
+
await startPreview({ cameraId: cameras[0].id })
|
|
155
|
+
|
|
149
156
|
// Desktop: attach the stream to a <video> element.
|
|
150
157
|
const stream = await getPreviewStream()
|
|
151
158
|
if (stream) videoEl.srcObject = stream
|
|
@@ -164,6 +171,17 @@ const photo = await capture({ flash: 'auto' })
|
|
|
164
171
|
imageEl.src = convertFileSrc(photo.path)
|
|
165
172
|
```
|
|
166
173
|
|
|
174
|
+
Configurable (mobile and desktop):
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
await capture({
|
|
178
|
+
resolution: { width: 1920, height: 1080 }, // nearest supported size (best-effort)
|
|
179
|
+
aspectRatio: '16:9', // '4:3' | '16:9' | '1:1' | ...
|
|
180
|
+
format: 'jpeg', // 'jpeg' | 'png' | 'heif' (mobile jpeg; desktop jpeg/png)
|
|
181
|
+
quality: 0.9 // 0..1 for lossy formats
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
167
185
|
### Video recording (with audio)
|
|
168
186
|
|
|
169
187
|
```ts
|
|
@@ -175,6 +193,9 @@ const video = await stopRecording()
|
|
|
175
193
|
// Optionally request a base64 JPEG thumbnail of the recording:
|
|
176
194
|
const withThumbnail = await stopRecording({ thumbnail: true })
|
|
177
195
|
// withThumbnail.thumbnail: string | undefined
|
|
196
|
+
|
|
197
|
+
// Configure quality/fps/bitrate (mobile and desktop):
|
|
198
|
+
await startRecording({ quality: 'fhd', fps: 30, bitrate: 2_000_000 }) // quality: 'sd' | 'hd' | 'fhd' | 'uhd'
|
|
178
199
|
```
|
|
179
200
|
|
|
180
201
|
### Video thumbnail
|
|
@@ -233,14 +254,64 @@ await flipCamera()
|
|
|
233
254
|
await setFlash('on') // 'on' | 'off' | 'auto'
|
|
234
255
|
await setZoom(2.0)
|
|
235
256
|
|
|
236
|
-
//
|
|
237
|
-
const zoom = await getZoom()
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
// Live state (mobile):
|
|
258
|
+
const zoom = await getZoom() // number
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
On desktop `getZoom()` rejects with a descriptive error (consistent with
|
|
262
|
+
`setZoom`).
|
|
263
|
+
|
|
264
|
+
### Fine controls
|
|
265
|
+
|
|
266
|
+
Torch, tap-to-focus, exposure and white balance (mobile; desktop throws):
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
await setTorch(true) // continuous light (independent from capture flash)
|
|
270
|
+
await setFocusPoint(0.5, 0.5) // normalized 0..1, top-left origin
|
|
271
|
+
await setFocusMode('continuous') // 'auto' | 'continuous' | 'locked'
|
|
272
|
+
await setExposureCompensation(1.5) // EV, clamped to getCapabilities().exposureRange
|
|
273
|
+
await setExposureMode('locked') // 'auto' | 'locked'
|
|
274
|
+
await setWhiteBalance({ mode: 'locked', temperatureK: 5000 }) // Kelvin; 'auto' | 'locked'
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Details:
|
|
278
|
+
|
|
279
|
+
- **Torch** is independent from the capture flash (`setFlash`); query `hasTorch`.
|
|
280
|
+
- **Focus** uses normalized coordinates (0..1, top-left origin); query `supportsFocus` / `focusModes`.
|
|
281
|
+
- **Exposure**: `ev` is clamped to `exposureRange`; `exposureModes` is `['auto', 'locked']`.
|
|
282
|
+
- **White balance** is uniform across platforms: `mode: 'auto' | 'locked'`. In `locked`, `temperatureK` (Kelvin) is applied where the hardware supports it (`supportsWhiteBalanceTemperature`); when unsupported, the plugin locks the current gains (or rejects) instead of inventing a temperature. `auto` uses the platform's continuous AWB.
|
|
283
|
+
|
|
284
|
+
On desktop all of these controls reject with a descriptive error. Query the
|
|
285
|
+
supported values with `getCapabilities()` first: `hasTorch`,
|
|
286
|
+
`supportsFocus`/`focusModes`, `exposureRange`/`exposureModes`,
|
|
287
|
+
`whiteBalanceModes`/`supportsWhiteBalanceTemperature`.
|
|
288
|
+
|
|
289
|
+
### Camera introspection
|
|
290
|
+
|
|
291
|
+
Use `getAvailableCameras()` to list the cameras (physical/logical lenses) and
|
|
292
|
+
`getCapabilities()` to know what a camera *can* do, before trying:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const cameras = await getAvailableCameras()
|
|
296
|
+
// [{ id: '0', facing: 'back', lens: 'wide' }, { id: '1', facing: 'front' }, ...]
|
|
297
|
+
|
|
298
|
+
// No argument: the active camera, or the default (back) camera without a preview.
|
|
299
|
+
const caps = await getCapabilities(cameras[0].id)
|
|
300
|
+
// { cameraId, hasFlash, hasTorch, zoomRange?, supportsFocus, focusModes?,
|
|
301
|
+
// exposureRange?, exposureModes?, whiteBalanceModes?, supportsWhiteBalanceTemperature?,
|
|
302
|
+
// photoResolutions?, photoAspects?, videoQualities?, videoFps?, photoFormats?, supportsPause }
|
|
240
303
|
```
|
|
241
304
|
|
|
242
|
-
|
|
243
|
-
|
|
305
|
+
The plugin follows one rule to avoid ambiguity:
|
|
306
|
+
|
|
307
|
+
- **What the hardware can do** -> `getCapabilities()` (declarative).
|
|
308
|
+
- **What a value is right now** -> a dedicated live getter (e.g. `getZoom()`).
|
|
309
|
+
|
|
310
|
+
Both work without an active preview on mobile. On desktop `getAvailableCameras()`
|
|
311
|
+
works cold (labels may stay empty until camera permission has been granted once),
|
|
312
|
+
while the hardware capabilities `zoomRange`, `hasTorch` and `focusModes` are
|
|
313
|
+
omitted until a preview/recording stream is active; `hasFlash` is always `false`
|
|
314
|
+
and `supportsPause` is `true`.
|
|
244
315
|
|
|
245
316
|
### Gallery
|
|
246
317
|
|
|
@@ -258,6 +329,9 @@ webm, 3gp, mkv are treated as video); pass `{ type }` to force it.
|
|
|
258
329
|
```ts
|
|
259
330
|
const state = await checkPermissions() // 'granted' | 'denied' | 'prompt'
|
|
260
331
|
await requestPermissions()
|
|
332
|
+
|
|
333
|
+
// Mobile: open the OS settings screen (e.g. after a permanent denial).
|
|
334
|
+
await openAppSettings()
|
|
261
335
|
```
|
|
262
336
|
|
|
263
337
|
## Image return pattern
|
|
@@ -281,7 +355,7 @@ hardware, emulator or device is required.
|
|
|
281
355
|
| Rust | `src/models.rs` (inline tests) | `cargo test` | plugin root | Rust + Cargo |
|
|
282
356
|
| JS | `guest-js/index.test.ts` | `npm test` | plugin root | Node.js 18+ + npm |
|
|
283
357
|
| Android | `android/src/test/java/CameraPluginTest.kt`| `gradle test` | `android/` (or generated app) | JDK 17+, Android SDK, Gradle |
|
|
284
|
-
| iOS | `ios/Tests/PluginTests/PluginTests.swift` | `
|
|
358
|
+
| iOS | `ios/Tests/PluginTests/PluginTests.swift` | `xcodebuild test` | iOS Simulator (generated app) | macOS + Xcode |
|
|
285
359
|
|
|
286
360
|
### Prerequisites
|
|
287
361
|
|
|
@@ -391,10 +465,16 @@ destination from the generated Xcode project (`src-tauri/gen/apple`).
|
|
|
391
465
|
| `capture(options?)` | ✓ `ImageCapture` / `AVCapturePhotoOutput` | ✓ canvas |
|
|
392
466
|
| `flipCamera()` | ✓ | ✗ (throws) |
|
|
393
467
|
| `setFlash(mode)` | ✓ | ✗ (throws) |
|
|
394
|
-
| `isFlashAvailable()` | ✓ | `false` |
|
|
395
468
|
| `setZoom(factor)` | ✓ | ✗ (throws) |
|
|
396
469
|
| `getZoom()` | ✓ | ✗ (throws) |
|
|
397
|
-
| `
|
|
470
|
+
| `setTorch(on)` | ✓ | ✗ (throws) |
|
|
471
|
+
| `setFocusPoint(x, y)` | ✓ | ✗ (throws) |
|
|
472
|
+
| `setFocusMode(mode)` | ✓ | ✗ (throws) |
|
|
473
|
+
| `setExposureCompensation(ev)` | ✓ | ✗ (throws) |
|
|
474
|
+
| `setExposureMode(mode)` | ✓ | ✗ (throws) |
|
|
475
|
+
| `setWhiteBalance(options)` | ✓ | ✗ (throws) |
|
|
476
|
+
| `getAvailableCameras()` | ✓ | ✓ |
|
|
477
|
+
| `getCapabilities(cameraId?)` | ✓ | ✓ (partial) |
|
|
398
478
|
| `startRecording(options?)` | ✓ `VideoCapture` / `AVCaptureMovieFileOutput` | ✓ `MediaRecorder` |
|
|
399
479
|
| `stopRecording(options?)` | ✓ | ✓ |
|
|
400
480
|
| `RecordingProgress` telemetry (`onProgress` channel) | ✓ | ✓ |
|
|
@@ -404,3 +484,88 @@ destination from the generated Xcode project (`src-tauri/gen/apple`).
|
|
|
404
484
|
| `requestPermissions()` | ✓ | ✓ |
|
|
405
485
|
| `openAppSettings()` | ✓ | ✗ (throws) |
|
|
406
486
|
| `getPreviewStream()` | `null` | `MediaStream` |
|
|
487
|
+
|
|
488
|
+
## Types
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
type CameraDirection = 'front' | 'back'
|
|
492
|
+
type FlashMode = 'on' | 'off' | 'auto'
|
|
493
|
+
type CameraFacing = 'front' | 'back' | 'external'
|
|
494
|
+
type CameraLens = 'wide' | 'ultraWide' | 'telephoto' | (string & {})
|
|
495
|
+
type FocusMode = 'auto' | 'continuous' | 'locked'
|
|
496
|
+
type ExposureMode = 'auto' | 'locked'
|
|
497
|
+
type WhiteBalanceMode = 'auto' | 'locked'
|
|
498
|
+
type PhotoFormat = 'jpeg' | 'png' | 'heif'
|
|
499
|
+
type VideoQuality = 'sd' | 'hd' | 'fhd' | 'uhd'
|
|
500
|
+
type PermissionState = 'granted' | 'denied' | 'prompt' // re-exported from @tauri-apps/api/core
|
|
501
|
+
|
|
502
|
+
interface StartPreviewOptions { camera?: CameraDirection; cameraId?: string; windowed?: boolean }
|
|
503
|
+
interface CaptureOptions {
|
|
504
|
+
flash?: FlashMode
|
|
505
|
+
resolution?: Resolution
|
|
506
|
+
aspectRatio?: string
|
|
507
|
+
format?: PhotoFormat
|
|
508
|
+
quality?: number
|
|
509
|
+
}
|
|
510
|
+
interface Resolution { width: number; height: number }
|
|
511
|
+
interface ExposureRange { minEv: number; maxEv: number }
|
|
512
|
+
interface WhiteBalanceOptions { mode: WhiteBalanceMode; temperatureK?: number }
|
|
513
|
+
|
|
514
|
+
interface Photo {
|
|
515
|
+
path?: string // mobile: absolute path to the captured file
|
|
516
|
+
dataUrl?: string // desktop: base64 data URL
|
|
517
|
+
blob?: Blob // desktop
|
|
518
|
+
width: number
|
|
519
|
+
height: number
|
|
520
|
+
orientation: number // EXIF orientation
|
|
521
|
+
format: string
|
|
522
|
+
thumbnail?: string // base64 JPEG thumbnail (mobile; desktop reuses dataUrl)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
interface Video {
|
|
526
|
+
path?: string // mobile
|
|
527
|
+
duration: number
|
|
528
|
+
blob?: Blob // desktop
|
|
529
|
+
url?: string // desktop object URL
|
|
530
|
+
thumbnail?: string // base64 JPEG when requested via stopRecording({ thumbnail: true })
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
interface ZoomRange { min: number; max: number }
|
|
534
|
+
|
|
535
|
+
interface CameraInfo {
|
|
536
|
+
id: string // native, session-stable identifier
|
|
537
|
+
facing?: CameraFacing
|
|
538
|
+
lens?: CameraLens
|
|
539
|
+
label?: string
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
interface CameraCapabilities {
|
|
543
|
+
cameraId?: string
|
|
544
|
+
hasFlash: boolean
|
|
545
|
+
hasTorch: boolean
|
|
546
|
+
zoomRange?: ZoomRange
|
|
547
|
+
supportsFocus: boolean
|
|
548
|
+
focusModes?: string[]
|
|
549
|
+
exposureRange?: ExposureRange
|
|
550
|
+
exposureModes?: ExposureMode[]
|
|
551
|
+
whiteBalanceModes?: WhiteBalanceMode[]
|
|
552
|
+
supportsWhiteBalanceTemperature?: boolean
|
|
553
|
+
photoResolutions?: Resolution[]
|
|
554
|
+
photoAspects?: string[]
|
|
555
|
+
videoQualities?: VideoQuality[]
|
|
556
|
+
videoFps?: number[]
|
|
557
|
+
photoFormats?: string[]
|
|
558
|
+
supportsPause: boolean
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
interface RecordingProgress { duration: number; bytesRecorded?: number; bitrate?: number }
|
|
562
|
+
|
|
563
|
+
interface StartRecordingOptions {
|
|
564
|
+
onProgress?: Channel<RecordingProgress>
|
|
565
|
+
quality?: VideoQuality
|
|
566
|
+
fps?: number
|
|
567
|
+
bitrate?: number
|
|
568
|
+
}
|
|
569
|
+
interface StopRecordingOptions { thumbnail?: boolean }
|
|
570
|
+
interface SaveToGalleryOptions { type?: 'image' | 'video' }
|
|
571
|
+
```
|
package/dist-js/index.cjs
CHANGED
|
@@ -48,12 +48,14 @@ function stopProgressTimer() {
|
|
|
48
48
|
async function ensureDesktopStream(options) {
|
|
49
49
|
if (activeStream)
|
|
50
50
|
return activeStream;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const videoConstraints = {};
|
|
52
|
+
if (options?.cameraId) {
|
|
53
|
+
videoConstraints.deviceId = { exact: options.cameraId };
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
videoConstraints.facingMode = options?.camera === 'front' ? 'user' : 'environment';
|
|
57
|
+
}
|
|
58
|
+
const constraints = { video: videoConstraints, audio: false };
|
|
57
59
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
58
60
|
activeStream = stream;
|
|
59
61
|
const video = document.createElement('video');
|
|
@@ -78,12 +80,55 @@ function teardownDesktopStream() {
|
|
|
78
80
|
activeStream = null;
|
|
79
81
|
activeVideo = null;
|
|
80
82
|
}
|
|
81
|
-
|
|
83
|
+
/** Parses an aspect ratio like '4:3' into a number (4/3), or null. */
|
|
84
|
+
function parseAspectRatio(value) {
|
|
85
|
+
const [w, h] = value.split(':').map((n) => Number(n));
|
|
86
|
+
if (!w || !h)
|
|
87
|
+
return null;
|
|
88
|
+
return w / h;
|
|
89
|
+
}
|
|
90
|
+
/** Target capture height (px) for a video quality preset (desktop best-effort). */
|
|
91
|
+
function qualityHeight(quality) {
|
|
92
|
+
switch (quality) {
|
|
93
|
+
case 'sd':
|
|
94
|
+
return 480;
|
|
95
|
+
case 'hd':
|
|
96
|
+
return 720;
|
|
97
|
+
case 'fhd':
|
|
98
|
+
return 1080;
|
|
99
|
+
case 'uhd':
|
|
100
|
+
return 2160;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** MIME type for a photo format on desktop, or null when unsupported. */
|
|
104
|
+
function desktopPhotoMime(format) {
|
|
105
|
+
switch (format) {
|
|
106
|
+
case 'jpeg':
|
|
107
|
+
return 'image/jpeg';
|
|
108
|
+
case 'png':
|
|
109
|
+
return 'image/png';
|
|
110
|
+
default:
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function captureDesktopFrame(options) {
|
|
82
115
|
if (!activeVideo) {
|
|
83
116
|
throw new Error('Camera preview is not active. Call startPreview() first.');
|
|
84
117
|
}
|
|
85
|
-
const
|
|
86
|
-
const
|
|
118
|
+
const format = options?.format ?? 'jpeg';
|
|
119
|
+
const mime = desktopPhotoMime(format);
|
|
120
|
+
if (!mime) {
|
|
121
|
+
throw new Error(`${format} is not supported on desktop.`);
|
|
122
|
+
}
|
|
123
|
+
const sourceWidth = activeVideo.videoWidth;
|
|
124
|
+
const sourceHeight = activeVideo.videoHeight;
|
|
125
|
+
let width = options?.resolution?.width ?? sourceWidth;
|
|
126
|
+
let height = options?.resolution?.height ?? sourceHeight;
|
|
127
|
+
if (!options?.resolution && options?.aspectRatio) {
|
|
128
|
+
const ratio = parseAspectRatio(options.aspectRatio);
|
|
129
|
+
if (ratio)
|
|
130
|
+
height = Math.round(width / ratio);
|
|
131
|
+
}
|
|
87
132
|
const canvas = document.createElement('canvas');
|
|
88
133
|
canvas.width = width;
|
|
89
134
|
canvas.height = height;
|
|
@@ -92,13 +137,13 @@ function captureDesktopFrame() {
|
|
|
92
137
|
throw new Error('Failed to create canvas context for capture.');
|
|
93
138
|
}
|
|
94
139
|
ctx.drawImage(activeVideo, 0, 0, width, height);
|
|
95
|
-
const dataUrl = canvas.toDataURL(
|
|
140
|
+
const dataUrl = canvas.toDataURL(mime, options?.quality);
|
|
96
141
|
return {
|
|
97
142
|
dataUrl,
|
|
98
143
|
width,
|
|
99
144
|
height,
|
|
100
145
|
orientation: 1,
|
|
101
|
-
format
|
|
146
|
+
format,
|
|
102
147
|
thumbnail: dataUrl
|
|
103
148
|
};
|
|
104
149
|
}
|
|
@@ -152,7 +197,7 @@ async function capture(options) {
|
|
|
152
197
|
if (isMobile()) {
|
|
153
198
|
return await core.invoke('plugin:camera-kessdev|capture', { ...options });
|
|
154
199
|
}
|
|
155
|
-
return captureDesktopFrame();
|
|
200
|
+
return captureDesktopFrame(options);
|
|
156
201
|
}
|
|
157
202
|
// ---------------------------------------------------------------------------
|
|
158
203
|
// Camera controls
|
|
@@ -188,23 +233,140 @@ async function getZoom() {
|
|
|
188
233
|
}
|
|
189
234
|
throw new Error('getZoom is not supported on desktop.');
|
|
190
235
|
}
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Introspection (available cameras + declarative capabilities)
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
/** Best-effort facing detection from a device label (desktop). */
|
|
240
|
+
function facingFromLabel(label) {
|
|
241
|
+
const value = label.toLowerCase();
|
|
242
|
+
if (value.includes('front') || value.includes('user'))
|
|
243
|
+
return 'front';
|
|
244
|
+
if (value.includes('back') || value.includes('rear') || value.includes('environment'))
|
|
245
|
+
return 'back';
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
/** Enumerates video input devices on desktop via `enumerateDevices()`. */
|
|
249
|
+
async function enumerateDesktopCameras() {
|
|
250
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
251
|
+
return devices
|
|
252
|
+
.filter((device) => device.kind === 'videoinput')
|
|
253
|
+
.map((device) => {
|
|
254
|
+
const info = { id: device.deviceId };
|
|
255
|
+
const facing = facingFromLabel(device.label);
|
|
256
|
+
if (facing)
|
|
257
|
+
info.facing = facing;
|
|
258
|
+
if (device.label)
|
|
259
|
+
info.label = device.label;
|
|
260
|
+
return info;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Declarative capabilities on desktop. Hardware fields (`zoomRange`, `hasTorch`,
|
|
265
|
+
* `focusModes`) are only known while a preview/recording stream is active; they
|
|
266
|
+
* are omitted otherwise.
|
|
267
|
+
*/
|
|
268
|
+
function desktopCapabilities(cameraId) {
|
|
269
|
+
const caps = {
|
|
270
|
+
hasFlash: false,
|
|
271
|
+
hasTorch: false,
|
|
272
|
+
supportsFocus: false,
|
|
273
|
+
supportsPause: true,
|
|
274
|
+
photoFormats: ['jpeg', 'png', 'webp']
|
|
275
|
+
};
|
|
276
|
+
if (cameraId)
|
|
277
|
+
caps.cameraId = cameraId;
|
|
278
|
+
const track = activeStream?.getVideoTracks?.()[0];
|
|
279
|
+
const trackCaps = track?.getCapabilities?.();
|
|
280
|
+
if (trackCaps) {
|
|
281
|
+
if (trackCaps.zoom) {
|
|
282
|
+
caps.zoomRange = { min: trackCaps.zoom.min, max: trackCaps.zoom.max };
|
|
283
|
+
}
|
|
284
|
+
if (typeof trackCaps.torch === 'boolean') {
|
|
285
|
+
caps.hasTorch = trackCaps.torch;
|
|
286
|
+
}
|
|
287
|
+
if (trackCaps.focusMode && trackCaps.focusMode.length > 0) {
|
|
288
|
+
caps.supportsFocus = true;
|
|
289
|
+
caps.focusModes = trackCaps.focusMode;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return caps;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Lists the cameras (physical or logical lenses) available on the device.
|
|
296
|
+
*
|
|
297
|
+
* Works without an active preview on every platform. On desktop the labels may
|
|
298
|
+
* be empty until camera permission has been granted at least once.
|
|
299
|
+
*/
|
|
300
|
+
async function getAvailableCameras() {
|
|
193
301
|
if (isMobile()) {
|
|
194
|
-
return await core.invoke('plugin:camera-kessdev|
|
|
302
|
+
return await core.invoke('plugin:camera-kessdev|get_available_cameras');
|
|
195
303
|
}
|
|
196
|
-
|
|
304
|
+
return enumerateDesktopCameras();
|
|
197
305
|
}
|
|
198
306
|
/**
|
|
199
|
-
* Returns
|
|
307
|
+
* Returns the declarative capabilities of a camera.
|
|
200
308
|
*
|
|
201
|
-
*
|
|
309
|
+
* Without `cameraId` it describes the active camera, or the default (back)
|
|
310
|
+
* camera when no preview is active. On mobile it works without an active
|
|
311
|
+
* preview; on desktop hardware fields are omitted until a stream is active.
|
|
312
|
+
* Live values are exposed by dedicated getters (e.g. {@link getZoom}).
|
|
202
313
|
*/
|
|
203
|
-
async function
|
|
314
|
+
async function getCapabilities(cameraId) {
|
|
204
315
|
if (isMobile()) {
|
|
205
|
-
return await core.invoke('plugin:camera-kessdev|
|
|
316
|
+
return await core.invoke('plugin:camera-kessdev|get_capabilities', { cameraId });
|
|
317
|
+
}
|
|
318
|
+
return desktopCapabilities(cameraId);
|
|
319
|
+
}
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Fine controls (torch, focus, exposure, white balance)
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
/** Turns the torch (continuous light) on or off (mobile). */
|
|
324
|
+
async function setTorch(on) {
|
|
325
|
+
if (isMobile()) {
|
|
326
|
+
await core.invoke('plugin:camera-kessdev|set_torch', { on });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
throw new Error('setTorch is not supported on desktop.');
|
|
330
|
+
}
|
|
331
|
+
/** Sets the focus point in normalized coordinates (0..1, top-left origin) (mobile). */
|
|
332
|
+
async function setFocusPoint(x, y) {
|
|
333
|
+
if (isMobile()) {
|
|
334
|
+
await core.invoke('plugin:camera-kessdev|set_focus_point', { x, y });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
throw new Error('setFocusPoint is not supported on desktop.');
|
|
338
|
+
}
|
|
339
|
+
/** Sets the focus mode (mobile). */
|
|
340
|
+
async function setFocusMode(mode) {
|
|
341
|
+
if (isMobile()) {
|
|
342
|
+
await core.invoke('plugin:camera-kessdev|set_focus_mode', { mode });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
throw new Error('setFocusMode is not supported on desktop.');
|
|
346
|
+
}
|
|
347
|
+
/** Sets the exposure compensation in EV (mobile). */
|
|
348
|
+
async function setExposureCompensation(ev) {
|
|
349
|
+
if (isMobile()) {
|
|
350
|
+
await core.invoke('plugin:camera-kessdev|set_exposure_compensation', { ev });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
throw new Error('setExposureCompensation is not supported on desktop.');
|
|
354
|
+
}
|
|
355
|
+
/** Sets the exposure mode (mobile). */
|
|
356
|
+
async function setExposureMode(mode) {
|
|
357
|
+
if (isMobile()) {
|
|
358
|
+
await core.invoke('plugin:camera-kessdev|set_exposure_mode', { mode });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
throw new Error('setExposureMode is not supported on desktop.');
|
|
362
|
+
}
|
|
363
|
+
/** Sets the white balance mode (with an optional Kelvin temperature when `locked`) (mobile). */
|
|
364
|
+
async function setWhiteBalance(options) {
|
|
365
|
+
if (isMobile()) {
|
|
366
|
+
await core.invoke('plugin:camera-kessdev|set_white_balance', { ...options });
|
|
367
|
+
return;
|
|
206
368
|
}
|
|
207
|
-
|
|
369
|
+
throw new Error('setWhiteBalance is not supported on desktop.');
|
|
208
370
|
}
|
|
209
371
|
// ---------------------------------------------------------------------------
|
|
210
372
|
// Video recording
|
|
@@ -220,7 +382,12 @@ async function startRecording(options) {
|
|
|
220
382
|
activeStream = null;
|
|
221
383
|
activeVideo = null;
|
|
222
384
|
}
|
|
223
|
-
const
|
|
385
|
+
const videoConstraints = {};
|
|
386
|
+
if (options?.quality)
|
|
387
|
+
videoConstraints.height = { ideal: qualityHeight(options.quality) };
|
|
388
|
+
if (options?.fps)
|
|
389
|
+
videoConstraints.frameRate = { ideal: options.fps };
|
|
390
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: true });
|
|
224
391
|
activeStream = stream;
|
|
225
392
|
const video = document.createElement('video');
|
|
226
393
|
video.muted = true;
|
|
@@ -230,7 +397,10 @@ async function startRecording(options) {
|
|
|
230
397
|
activeVideo = video;
|
|
231
398
|
recordedChunks = [];
|
|
232
399
|
recordedBytes = 0;
|
|
233
|
-
|
|
400
|
+
const recorderOptions = {};
|
|
401
|
+
if (options?.bitrate)
|
|
402
|
+
recorderOptions.videoBitsPerSecond = options.bitrate;
|
|
403
|
+
mediaRecorder = new MediaRecorder(stream, recorderOptions);
|
|
234
404
|
mediaRecorder.ondataavailable = (event) => {
|
|
235
405
|
if (event.data.size > 0) {
|
|
236
406
|
recordedChunks.push(event.data);
|
|
@@ -363,14 +533,20 @@ Object.defineProperty(exports, "convertFileSrc", {
|
|
|
363
533
|
exports.capture = capture;
|
|
364
534
|
exports.checkPermissions = checkPermissions;
|
|
365
535
|
exports.flipCamera = flipCamera;
|
|
536
|
+
exports.getAvailableCameras = getAvailableCameras;
|
|
537
|
+
exports.getCapabilities = getCapabilities;
|
|
366
538
|
exports.getPreviewStream = getPreviewStream;
|
|
367
539
|
exports.getZoom = getZoom;
|
|
368
|
-
exports.getZoomRange = getZoomRange;
|
|
369
|
-
exports.isFlashAvailable = isFlashAvailable;
|
|
370
540
|
exports.openAppSettings = openAppSettings;
|
|
371
541
|
exports.requestPermissions = requestPermissions;
|
|
372
542
|
exports.saveToGallery = saveToGallery;
|
|
543
|
+
exports.setExposureCompensation = setExposureCompensation;
|
|
544
|
+
exports.setExposureMode = setExposureMode;
|
|
373
545
|
exports.setFlash = setFlash;
|
|
546
|
+
exports.setFocusMode = setFocusMode;
|
|
547
|
+
exports.setFocusPoint = setFocusPoint;
|
|
548
|
+
exports.setTorch = setTorch;
|
|
549
|
+
exports.setWhiteBalance = setWhiteBalance;
|
|
374
550
|
exports.setZoom = setZoom;
|
|
375
551
|
exports.startPreview = startPreview;
|
|
376
552
|
exports.startRecording = startRecording;
|
package/dist-js/index.d.ts
CHANGED
|
@@ -6,10 +6,27 @@ export type CameraDirection = 'front' | 'back';
|
|
|
6
6
|
export type FlashMode = 'on' | 'off' | 'auto';
|
|
7
7
|
export interface StartPreviewOptions {
|
|
8
8
|
camera?: CameraDirection;
|
|
9
|
+
/** Specific camera/lens id from `getAvailableCameras()`. Takes precedence over `camera`. */
|
|
10
|
+
cameraId?: string;
|
|
9
11
|
windowed?: boolean;
|
|
10
12
|
}
|
|
13
|
+
export type PhotoFormat = 'jpeg' | 'png' | 'heif';
|
|
14
|
+
export type VideoQuality = 'sd' | 'hd' | 'fhd' | 'uhd';
|
|
15
|
+
/** A width/height pair in pixels. */
|
|
16
|
+
export interface Resolution {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
11
20
|
export interface CaptureOptions {
|
|
12
21
|
flash?: FlashMode;
|
|
22
|
+
/** Target resolution (best-effort: the nearest supported is used). */
|
|
23
|
+
resolution?: Resolution;
|
|
24
|
+
/** Target aspect ratio (e.g. '4:3', '16:9', '1:1'). */
|
|
25
|
+
aspectRatio?: string;
|
|
26
|
+
/** Image format. Mobile supports `jpeg`; desktop `jpeg`/`png`. */
|
|
27
|
+
format?: PhotoFormat;
|
|
28
|
+
/** Compression quality (0..1) for lossy formats. */
|
|
29
|
+
quality?: number;
|
|
13
30
|
}
|
|
14
31
|
export interface Photo {
|
|
15
32
|
/** Absolute path to the captured image (mobile). */
|
|
@@ -37,11 +54,72 @@ export interface Video {
|
|
|
37
54
|
/** Optional base64-encoded thumbnail, when requested via `stopRecording`. */
|
|
38
55
|
thumbnail?: string;
|
|
39
56
|
}
|
|
40
|
-
/** Supported zoom range of the
|
|
57
|
+
/** Supported zoom range of a camera (declarative, without the live value). */
|
|
41
58
|
export interface ZoomRange {
|
|
42
59
|
min: number;
|
|
43
60
|
max: number;
|
|
44
|
-
|
|
61
|
+
}
|
|
62
|
+
/** Orientation of a camera. */
|
|
63
|
+
export type CameraFacing = 'front' | 'back' | 'external';
|
|
64
|
+
/**
|
|
65
|
+
* Best-effort hint of which lens a camera maps to. Platforms that cannot
|
|
66
|
+
* determine it (notably desktop) omit the field.
|
|
67
|
+
*/
|
|
68
|
+
export type CameraLens = 'wide' | 'ultraWide' | 'telephoto' | (string & {});
|
|
69
|
+
/** Camera focus mode. */
|
|
70
|
+
export type FocusMode = 'auto' | 'continuous' | 'locked';
|
|
71
|
+
/** Camera exposure mode. */
|
|
72
|
+
export type ExposureMode = 'auto' | 'locked';
|
|
73
|
+
/** White balance mode. */
|
|
74
|
+
export type WhiteBalanceMode = 'auto' | 'locked';
|
|
75
|
+
/** Supported exposure compensation range (EV). */
|
|
76
|
+
export interface ExposureRange {
|
|
77
|
+
minEv: number;
|
|
78
|
+
maxEv: number;
|
|
79
|
+
}
|
|
80
|
+
/** Options for `setWhiteBalance`. */
|
|
81
|
+
export interface WhiteBalanceOptions {
|
|
82
|
+
mode: WhiteBalanceMode;
|
|
83
|
+
/** Color temperature in Kelvin; only meaningful when `mode` is `locked`. */
|
|
84
|
+
temperatureK?: number;
|
|
85
|
+
}
|
|
86
|
+
/** A camera (physical or logical lens) available on the device. */
|
|
87
|
+
export interface CameraInfo {
|
|
88
|
+
/** Native, session-stable identifier (usable to reference this camera). */
|
|
89
|
+
id: string;
|
|
90
|
+
/** Orientation, when the platform can determine it. */
|
|
91
|
+
facing?: CameraFacing;
|
|
92
|
+
/** Best-effort lens hint, when the platform can determine it. */
|
|
93
|
+
lens?: CameraLens;
|
|
94
|
+
/** Human-readable label, when available. */
|
|
95
|
+
label?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Declarative capabilities of a camera: what the hardware/platform *can* do.
|
|
99
|
+
*
|
|
100
|
+
* Optional fields are omitted when the platform cannot determine them. Live
|
|
101
|
+
* values (e.g. the current zoom factor) are exposed by dedicated getters such
|
|
102
|
+
* as {@link getZoom}, never here.
|
|
103
|
+
*/
|
|
104
|
+
export interface CameraCapabilities {
|
|
105
|
+
/** Identifier of the camera these capabilities describe, when known. */
|
|
106
|
+
cameraId?: string;
|
|
107
|
+
hasFlash: boolean;
|
|
108
|
+
hasTorch: boolean;
|
|
109
|
+
zoomRange?: ZoomRange;
|
|
110
|
+
supportsFocus: boolean;
|
|
111
|
+
focusModes?: string[];
|
|
112
|
+
exposureRange?: ExposureRange;
|
|
113
|
+
exposureModes?: ExposureMode[];
|
|
114
|
+
whiteBalanceModes?: WhiteBalanceMode[];
|
|
115
|
+
/** Whether an arbitrary color temperature (Kelvin) can be applied. */
|
|
116
|
+
supportsWhiteBalanceTemperature?: boolean;
|
|
117
|
+
photoResolutions?: Resolution[];
|
|
118
|
+
photoAspects?: string[];
|
|
119
|
+
videoQualities?: VideoQuality[];
|
|
120
|
+
videoFps?: number[];
|
|
121
|
+
photoFormats?: string[];
|
|
122
|
+
supportsPause: boolean;
|
|
45
123
|
}
|
|
46
124
|
/**
|
|
47
125
|
* Live telemetry emitted while a video recording is in progress.
|
|
@@ -61,6 +139,12 @@ export interface RecordingProgress {
|
|
|
61
139
|
export interface StartRecordingOptions {
|
|
62
140
|
/** Receives live recording telemetry while the recording is active. */
|
|
63
141
|
onProgress?: Channel<RecordingProgress>;
|
|
142
|
+
/** Video quality preset. */
|
|
143
|
+
quality?: VideoQuality;
|
|
144
|
+
/** Target frame rate. */
|
|
145
|
+
fps?: number;
|
|
146
|
+
/** Target bitrate in bits per second. */
|
|
147
|
+
bitrate?: number;
|
|
64
148
|
}
|
|
65
149
|
export interface StopRecordingOptions {
|
|
66
150
|
/** When true, the result includes a base64-encoded `thumbnail`. */
|
|
@@ -103,14 +187,34 @@ export declare function setFlash(mode: FlashMode): Promise<void>;
|
|
|
103
187
|
export declare function setZoom(factor: number): Promise<void>;
|
|
104
188
|
/** Gets the current camera zoom factor (mobile). */
|
|
105
189
|
export declare function getZoom(): Promise<number>;
|
|
106
|
-
/** Gets the supported zoom range of the active camera (mobile). */
|
|
107
|
-
export declare function getZoomRange(): Promise<ZoomRange>;
|
|
108
190
|
/**
|
|
109
|
-
*
|
|
191
|
+
* Lists the cameras (physical or logical lenses) available on the device.
|
|
192
|
+
*
|
|
193
|
+
* Works without an active preview on every platform. On desktop the labels may
|
|
194
|
+
* be empty until camera permission has been granted at least once.
|
|
195
|
+
*/
|
|
196
|
+
export declare function getAvailableCameras(): Promise<CameraInfo[]>;
|
|
197
|
+
/**
|
|
198
|
+
* Returns the declarative capabilities of a camera.
|
|
110
199
|
*
|
|
111
|
-
*
|
|
200
|
+
* Without `cameraId` it describes the active camera, or the default (back)
|
|
201
|
+
* camera when no preview is active. On mobile it works without an active
|
|
202
|
+
* preview; on desktop hardware fields are omitted until a stream is active.
|
|
203
|
+
* Live values are exposed by dedicated getters (e.g. {@link getZoom}).
|
|
112
204
|
*/
|
|
113
|
-
export declare function
|
|
205
|
+
export declare function getCapabilities(cameraId?: string): Promise<CameraCapabilities>;
|
|
206
|
+
/** Turns the torch (continuous light) on or off (mobile). */
|
|
207
|
+
export declare function setTorch(on: boolean): Promise<void>;
|
|
208
|
+
/** Sets the focus point in normalized coordinates (0..1, top-left origin) (mobile). */
|
|
209
|
+
export declare function setFocusPoint(x: number, y: number): Promise<void>;
|
|
210
|
+
/** Sets the focus mode (mobile). */
|
|
211
|
+
export declare function setFocusMode(mode: FocusMode): Promise<void>;
|
|
212
|
+
/** Sets the exposure compensation in EV (mobile). */
|
|
213
|
+
export declare function setExposureCompensation(ev: number): Promise<void>;
|
|
214
|
+
/** Sets the exposure mode (mobile). */
|
|
215
|
+
export declare function setExposureMode(mode: ExposureMode): Promise<void>;
|
|
216
|
+
/** Sets the white balance mode (with an optional Kelvin temperature when `locked`) (mobile). */
|
|
217
|
+
export declare function setWhiteBalance(options: WhiteBalanceOptions): Promise<void>;
|
|
114
218
|
/** Starts video recording (with audio). */
|
|
115
219
|
export declare function startRecording(options?: StartRecordingOptions): Promise<void>;
|
|
116
220
|
/** Stops video recording and returns the recorded media. */
|
package/dist-js/index.js
CHANGED
|
@@ -47,12 +47,14 @@ function stopProgressTimer() {
|
|
|
47
47
|
async function ensureDesktopStream(options) {
|
|
48
48
|
if (activeStream)
|
|
49
49
|
return activeStream;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
const videoConstraints = {};
|
|
51
|
+
if (options?.cameraId) {
|
|
52
|
+
videoConstraints.deviceId = { exact: options.cameraId };
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
videoConstraints.facingMode = options?.camera === 'front' ? 'user' : 'environment';
|
|
56
|
+
}
|
|
57
|
+
const constraints = { video: videoConstraints, audio: false };
|
|
56
58
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
57
59
|
activeStream = stream;
|
|
58
60
|
const video = document.createElement('video');
|
|
@@ -77,12 +79,55 @@ function teardownDesktopStream() {
|
|
|
77
79
|
activeStream = null;
|
|
78
80
|
activeVideo = null;
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
/** Parses an aspect ratio like '4:3' into a number (4/3), or null. */
|
|
83
|
+
function parseAspectRatio(value) {
|
|
84
|
+
const [w, h] = value.split(':').map((n) => Number(n));
|
|
85
|
+
if (!w || !h)
|
|
86
|
+
return null;
|
|
87
|
+
return w / h;
|
|
88
|
+
}
|
|
89
|
+
/** Target capture height (px) for a video quality preset (desktop best-effort). */
|
|
90
|
+
function qualityHeight(quality) {
|
|
91
|
+
switch (quality) {
|
|
92
|
+
case 'sd':
|
|
93
|
+
return 480;
|
|
94
|
+
case 'hd':
|
|
95
|
+
return 720;
|
|
96
|
+
case 'fhd':
|
|
97
|
+
return 1080;
|
|
98
|
+
case 'uhd':
|
|
99
|
+
return 2160;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** MIME type for a photo format on desktop, or null when unsupported. */
|
|
103
|
+
function desktopPhotoMime(format) {
|
|
104
|
+
switch (format) {
|
|
105
|
+
case 'jpeg':
|
|
106
|
+
return 'image/jpeg';
|
|
107
|
+
case 'png':
|
|
108
|
+
return 'image/png';
|
|
109
|
+
default:
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function captureDesktopFrame(options) {
|
|
81
114
|
if (!activeVideo) {
|
|
82
115
|
throw new Error('Camera preview is not active. Call startPreview() first.');
|
|
83
116
|
}
|
|
84
|
-
const
|
|
85
|
-
const
|
|
117
|
+
const format = options?.format ?? 'jpeg';
|
|
118
|
+
const mime = desktopPhotoMime(format);
|
|
119
|
+
if (!mime) {
|
|
120
|
+
throw new Error(`${format} is not supported on desktop.`);
|
|
121
|
+
}
|
|
122
|
+
const sourceWidth = activeVideo.videoWidth;
|
|
123
|
+
const sourceHeight = activeVideo.videoHeight;
|
|
124
|
+
let width = options?.resolution?.width ?? sourceWidth;
|
|
125
|
+
let height = options?.resolution?.height ?? sourceHeight;
|
|
126
|
+
if (!options?.resolution && options?.aspectRatio) {
|
|
127
|
+
const ratio = parseAspectRatio(options.aspectRatio);
|
|
128
|
+
if (ratio)
|
|
129
|
+
height = Math.round(width / ratio);
|
|
130
|
+
}
|
|
86
131
|
const canvas = document.createElement('canvas');
|
|
87
132
|
canvas.width = width;
|
|
88
133
|
canvas.height = height;
|
|
@@ -91,13 +136,13 @@ function captureDesktopFrame() {
|
|
|
91
136
|
throw new Error('Failed to create canvas context for capture.');
|
|
92
137
|
}
|
|
93
138
|
ctx.drawImage(activeVideo, 0, 0, width, height);
|
|
94
|
-
const dataUrl = canvas.toDataURL(
|
|
139
|
+
const dataUrl = canvas.toDataURL(mime, options?.quality);
|
|
95
140
|
return {
|
|
96
141
|
dataUrl,
|
|
97
142
|
width,
|
|
98
143
|
height,
|
|
99
144
|
orientation: 1,
|
|
100
|
-
format
|
|
145
|
+
format,
|
|
101
146
|
thumbnail: dataUrl
|
|
102
147
|
};
|
|
103
148
|
}
|
|
@@ -151,7 +196,7 @@ async function capture(options) {
|
|
|
151
196
|
if (isMobile()) {
|
|
152
197
|
return await invoke('plugin:camera-kessdev|capture', { ...options });
|
|
153
198
|
}
|
|
154
|
-
return captureDesktopFrame();
|
|
199
|
+
return captureDesktopFrame(options);
|
|
155
200
|
}
|
|
156
201
|
// ---------------------------------------------------------------------------
|
|
157
202
|
// Camera controls
|
|
@@ -187,23 +232,140 @@ async function getZoom() {
|
|
|
187
232
|
}
|
|
188
233
|
throw new Error('getZoom is not supported on desktop.');
|
|
189
234
|
}
|
|
190
|
-
|
|
191
|
-
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Introspection (available cameras + declarative capabilities)
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
/** Best-effort facing detection from a device label (desktop). */
|
|
239
|
+
function facingFromLabel(label) {
|
|
240
|
+
const value = label.toLowerCase();
|
|
241
|
+
if (value.includes('front') || value.includes('user'))
|
|
242
|
+
return 'front';
|
|
243
|
+
if (value.includes('back') || value.includes('rear') || value.includes('environment'))
|
|
244
|
+
return 'back';
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
/** Enumerates video input devices on desktop via `enumerateDevices()`. */
|
|
248
|
+
async function enumerateDesktopCameras() {
|
|
249
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
250
|
+
return devices
|
|
251
|
+
.filter((device) => device.kind === 'videoinput')
|
|
252
|
+
.map((device) => {
|
|
253
|
+
const info = { id: device.deviceId };
|
|
254
|
+
const facing = facingFromLabel(device.label);
|
|
255
|
+
if (facing)
|
|
256
|
+
info.facing = facing;
|
|
257
|
+
if (device.label)
|
|
258
|
+
info.label = device.label;
|
|
259
|
+
return info;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Declarative capabilities on desktop. Hardware fields (`zoomRange`, `hasTorch`,
|
|
264
|
+
* `focusModes`) are only known while a preview/recording stream is active; they
|
|
265
|
+
* are omitted otherwise.
|
|
266
|
+
*/
|
|
267
|
+
function desktopCapabilities(cameraId) {
|
|
268
|
+
const caps = {
|
|
269
|
+
hasFlash: false,
|
|
270
|
+
hasTorch: false,
|
|
271
|
+
supportsFocus: false,
|
|
272
|
+
supportsPause: true,
|
|
273
|
+
photoFormats: ['jpeg', 'png', 'webp']
|
|
274
|
+
};
|
|
275
|
+
if (cameraId)
|
|
276
|
+
caps.cameraId = cameraId;
|
|
277
|
+
const track = activeStream?.getVideoTracks?.()[0];
|
|
278
|
+
const trackCaps = track?.getCapabilities?.();
|
|
279
|
+
if (trackCaps) {
|
|
280
|
+
if (trackCaps.zoom) {
|
|
281
|
+
caps.zoomRange = { min: trackCaps.zoom.min, max: trackCaps.zoom.max };
|
|
282
|
+
}
|
|
283
|
+
if (typeof trackCaps.torch === 'boolean') {
|
|
284
|
+
caps.hasTorch = trackCaps.torch;
|
|
285
|
+
}
|
|
286
|
+
if (trackCaps.focusMode && trackCaps.focusMode.length > 0) {
|
|
287
|
+
caps.supportsFocus = true;
|
|
288
|
+
caps.focusModes = trackCaps.focusMode;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return caps;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Lists the cameras (physical or logical lenses) available on the device.
|
|
295
|
+
*
|
|
296
|
+
* Works without an active preview on every platform. On desktop the labels may
|
|
297
|
+
* be empty until camera permission has been granted at least once.
|
|
298
|
+
*/
|
|
299
|
+
async function getAvailableCameras() {
|
|
192
300
|
if (isMobile()) {
|
|
193
|
-
return await invoke('plugin:camera-kessdev|
|
|
301
|
+
return await invoke('plugin:camera-kessdev|get_available_cameras');
|
|
194
302
|
}
|
|
195
|
-
|
|
303
|
+
return enumerateDesktopCameras();
|
|
196
304
|
}
|
|
197
305
|
/**
|
|
198
|
-
* Returns
|
|
306
|
+
* Returns the declarative capabilities of a camera.
|
|
199
307
|
*
|
|
200
|
-
*
|
|
308
|
+
* Without `cameraId` it describes the active camera, or the default (back)
|
|
309
|
+
* camera when no preview is active. On mobile it works without an active
|
|
310
|
+
* preview; on desktop hardware fields are omitted until a stream is active.
|
|
311
|
+
* Live values are exposed by dedicated getters (e.g. {@link getZoom}).
|
|
201
312
|
*/
|
|
202
|
-
async function
|
|
313
|
+
async function getCapabilities(cameraId) {
|
|
203
314
|
if (isMobile()) {
|
|
204
|
-
return await invoke('plugin:camera-kessdev|
|
|
315
|
+
return await invoke('plugin:camera-kessdev|get_capabilities', { cameraId });
|
|
316
|
+
}
|
|
317
|
+
return desktopCapabilities(cameraId);
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Fine controls (torch, focus, exposure, white balance)
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
/** Turns the torch (continuous light) on or off (mobile). */
|
|
323
|
+
async function setTorch(on) {
|
|
324
|
+
if (isMobile()) {
|
|
325
|
+
await invoke('plugin:camera-kessdev|set_torch', { on });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
throw new Error('setTorch is not supported on desktop.');
|
|
329
|
+
}
|
|
330
|
+
/** Sets the focus point in normalized coordinates (0..1, top-left origin) (mobile). */
|
|
331
|
+
async function setFocusPoint(x, y) {
|
|
332
|
+
if (isMobile()) {
|
|
333
|
+
await invoke('plugin:camera-kessdev|set_focus_point', { x, y });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
throw new Error('setFocusPoint is not supported on desktop.');
|
|
337
|
+
}
|
|
338
|
+
/** Sets the focus mode (mobile). */
|
|
339
|
+
async function setFocusMode(mode) {
|
|
340
|
+
if (isMobile()) {
|
|
341
|
+
await invoke('plugin:camera-kessdev|set_focus_mode', { mode });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
throw new Error('setFocusMode is not supported on desktop.');
|
|
345
|
+
}
|
|
346
|
+
/** Sets the exposure compensation in EV (mobile). */
|
|
347
|
+
async function setExposureCompensation(ev) {
|
|
348
|
+
if (isMobile()) {
|
|
349
|
+
await invoke('plugin:camera-kessdev|set_exposure_compensation', { ev });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
throw new Error('setExposureCompensation is not supported on desktop.');
|
|
353
|
+
}
|
|
354
|
+
/** Sets the exposure mode (mobile). */
|
|
355
|
+
async function setExposureMode(mode) {
|
|
356
|
+
if (isMobile()) {
|
|
357
|
+
await invoke('plugin:camera-kessdev|set_exposure_mode', { mode });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
throw new Error('setExposureMode is not supported on desktop.');
|
|
361
|
+
}
|
|
362
|
+
/** Sets the white balance mode (with an optional Kelvin temperature when `locked`) (mobile). */
|
|
363
|
+
async function setWhiteBalance(options) {
|
|
364
|
+
if (isMobile()) {
|
|
365
|
+
await invoke('plugin:camera-kessdev|set_white_balance', { ...options });
|
|
366
|
+
return;
|
|
205
367
|
}
|
|
206
|
-
|
|
368
|
+
throw new Error('setWhiteBalance is not supported on desktop.');
|
|
207
369
|
}
|
|
208
370
|
// ---------------------------------------------------------------------------
|
|
209
371
|
// Video recording
|
|
@@ -219,7 +381,12 @@ async function startRecording(options) {
|
|
|
219
381
|
activeStream = null;
|
|
220
382
|
activeVideo = null;
|
|
221
383
|
}
|
|
222
|
-
const
|
|
384
|
+
const videoConstraints = {};
|
|
385
|
+
if (options?.quality)
|
|
386
|
+
videoConstraints.height = { ideal: qualityHeight(options.quality) };
|
|
387
|
+
if (options?.fps)
|
|
388
|
+
videoConstraints.frameRate = { ideal: options.fps };
|
|
389
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: true });
|
|
223
390
|
activeStream = stream;
|
|
224
391
|
const video = document.createElement('video');
|
|
225
392
|
video.muted = true;
|
|
@@ -229,7 +396,10 @@ async function startRecording(options) {
|
|
|
229
396
|
activeVideo = video;
|
|
230
397
|
recordedChunks = [];
|
|
231
398
|
recordedBytes = 0;
|
|
232
|
-
|
|
399
|
+
const recorderOptions = {};
|
|
400
|
+
if (options?.bitrate)
|
|
401
|
+
recorderOptions.videoBitsPerSecond = options.bitrate;
|
|
402
|
+
mediaRecorder = new MediaRecorder(stream, recorderOptions);
|
|
233
403
|
mediaRecorder.ondataavailable = (event) => {
|
|
234
404
|
if (event.data.size > 0) {
|
|
235
405
|
recordedChunks.push(event.data);
|
|
@@ -355,4 +525,4 @@ async function openAppSettings() {
|
|
|
355
525
|
throw new Error('openAppSettings is not supported on desktop.');
|
|
356
526
|
}
|
|
357
527
|
|
|
358
|
-
export { capture, checkPermissions, flipCamera,
|
|
528
|
+
export { capture, checkPermissions, flipCamera, getAvailableCameras, getCapabilities, getPreviewStream, getZoom, openAppSettings, requestPermissions, saveToGallery, setExposureCompensation, setExposureMode, setFlash, setFocusMode, setFocusPoint, setTorch, setWhiteBalance, setZoom, startPreview, startRecording, stopPreview, stopRecording };
|
package/package.json
CHANGED