tauri-plugin-camera-kessdev 0.2.0-beta.0 → 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 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), saving to the
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.1.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`, `camera:allow-start-recording`,
71
- `camera:allow-check-permissions`, …). The full list is generated under
72
- `permissions/autogenerated/commands/` as `allow-<command>` / `deny-<command>`.
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
 
@@ -135,6 +137,7 @@ import {
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
@@ -240,6 +261,31 @@ const zoom = await getZoom() // number
240
261
  On desktop `getZoom()` rejects with a descriptive error (consistent with
241
262
  `setZoom`).
242
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
+
243
289
  ### Camera introspection
244
290
 
245
291
  Use `getAvailableCameras()` to list the cameras (physical/logical lenses) and
@@ -252,7 +298,8 @@ const cameras = await getAvailableCameras()
252
298
  // No argument: the active camera, or the default (back) camera without a preview.
253
299
  const caps = await getCapabilities(cameras[0].id)
254
300
  // { cameraId, hasFlash, hasTorch, zoomRange?, supportsFocus, focusModes?,
255
- // photoFormats?, supportsPause }
301
+ // exposureRange?, exposureModes?, whiteBalanceModes?, supportsWhiteBalanceTemperature?,
302
+ // photoResolutions?, photoAspects?, videoQualities?, videoFps?, photoFormats?, supportsPause }
256
303
  ```
257
304
 
258
305
  The plugin follows one rule to avoid ambiguity:
@@ -282,6 +329,9 @@ webm, 3gp, mkv are treated as video); pass `{ type }` to force it.
282
329
  ```ts
283
330
  const state = await checkPermissions() // 'granted' | 'denied' | 'prompt'
284
331
  await requestPermissions()
332
+
333
+ // Mobile: open the OS settings screen (e.g. after a permanent denial).
334
+ await openAppSettings()
285
335
  ```
286
336
 
287
337
  ## Image return pattern
@@ -305,7 +355,7 @@ hardware, emulator or device is required.
305
355
  | Rust | `src/models.rs` (inline tests) | `cargo test` | plugin root | Rust + Cargo |
306
356
  | JS | `guest-js/index.test.ts` | `npm test` | plugin root | Node.js 18+ + npm |
307
357
  | Android | `android/src/test/java/CameraPluginTest.kt`| `gradle test` | `android/` (or generated app) | JDK 17+, Android SDK, Gradle |
308
- | iOS | `ios/Tests/PluginTests/PluginTests.swift` | `swift test` | `ios/` | macOS + Xcode |
358
+ | iOS | `ios/Tests/PluginTests/PluginTests.swift` | `xcodebuild test` | iOS Simulator (generated app) | macOS + Xcode |
309
359
 
310
360
  ### Prerequisites
311
361
 
@@ -417,6 +467,12 @@ destination from the generated Xcode project (`src-tauri/gen/apple`).
417
467
  | `setFlash(mode)` | ✓ | ✗ (throws) |
418
468
  | `setZoom(factor)` | ✓ | ✗ (throws) |
419
469
  | `getZoom()` | ✓ | ✗ (throws) |
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) |
420
476
  | `getAvailableCameras()` | ✓ | ✓ |
421
477
  | `getCapabilities(cameraId?)` | ✓ | ✓ (partial) |
422
478
  | `startRecording(options?)` | ✓ `VideoCapture` / `AVCaptureMovieFileOutput` | ✓ `MediaRecorder` |
@@ -428,3 +484,88 @@ destination from the generated Xcode project (`src-tauri/gen/apple`).
428
484
  | `requestPermissions()` | ✓ | ✓ |
429
485
  | `openAppSettings()` | ✓ | ✗ (throws) |
430
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 constraints = {
52
- video: {
53
- facingMode: options?.camera === 'front' ? 'user' : 'environment'
54
- },
55
- audio: false
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
- function captureDesktopFrame() {
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 width = activeVideo.videoWidth;
86
- const height = activeVideo.videoHeight;
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('image/jpeg');
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: 'jpeg',
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
@@ -273,6 +318,57 @@ async function getCapabilities(cameraId) {
273
318
  return desktopCapabilities(cameraId);
274
319
  }
275
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;
368
+ }
369
+ throw new Error('setWhiteBalance is not supported on desktop.');
370
+ }
371
+ // ---------------------------------------------------------------------------
276
372
  // Video recording
277
373
  // ---------------------------------------------------------------------------
278
374
  /** Starts video recording (with audio). */
@@ -286,7 +382,12 @@ async function startRecording(options) {
286
382
  activeStream = null;
287
383
  activeVideo = null;
288
384
  }
289
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
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 });
290
391
  activeStream = stream;
291
392
  const video = document.createElement('video');
292
393
  video.muted = true;
@@ -296,7 +397,10 @@ async function startRecording(options) {
296
397
  activeVideo = video;
297
398
  recordedChunks = [];
298
399
  recordedBytes = 0;
299
- mediaRecorder = new MediaRecorder(stream);
400
+ const recorderOptions = {};
401
+ if (options?.bitrate)
402
+ recorderOptions.videoBitsPerSecond = options.bitrate;
403
+ mediaRecorder = new MediaRecorder(stream, recorderOptions);
300
404
  mediaRecorder.ondataavailable = (event) => {
301
405
  if (event.data.size > 0) {
302
406
  recordedChunks.push(event.data);
@@ -436,7 +540,13 @@ exports.getZoom = getZoom;
436
540
  exports.openAppSettings = openAppSettings;
437
541
  exports.requestPermissions = requestPermissions;
438
542
  exports.saveToGallery = saveToGallery;
543
+ exports.setExposureCompensation = setExposureCompensation;
544
+ exports.setExposureMode = setExposureMode;
439
545
  exports.setFlash = setFlash;
546
+ exports.setFocusMode = setFocusMode;
547
+ exports.setFocusPoint = setFocusPoint;
548
+ exports.setTorch = setTorch;
549
+ exports.setWhiteBalance = setWhiteBalance;
440
550
  exports.setZoom = setZoom;
441
551
  exports.startPreview = startPreview;
442
552
  exports.startRecording = startRecording;
@@ -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). */
@@ -49,6 +66,23 @@ export type CameraFacing = 'front' | 'back' | 'external';
49
66
  * determine it (notably desktop) omit the field.
50
67
  */
51
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
+ }
52
86
  /** A camera (physical or logical lens) available on the device. */
53
87
  export interface CameraInfo {
54
88
  /** Native, session-stable identifier (usable to reference this camera). */
@@ -75,6 +109,15 @@ export interface CameraCapabilities {
75
109
  zoomRange?: ZoomRange;
76
110
  supportsFocus: boolean;
77
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[];
78
121
  photoFormats?: string[];
79
122
  supportsPause: boolean;
80
123
  }
@@ -96,6 +139,12 @@ export interface RecordingProgress {
96
139
  export interface StartRecordingOptions {
97
140
  /** Receives live recording telemetry while the recording is active. */
98
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;
99
148
  }
100
149
  export interface StopRecordingOptions {
101
150
  /** When true, the result includes a base64-encoded `thumbnail`. */
@@ -154,6 +203,18 @@ export declare function getAvailableCameras(): Promise<CameraInfo[]>;
154
203
  * Live values are exposed by dedicated getters (e.g. {@link getZoom}).
155
204
  */
156
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>;
157
218
  /** Starts video recording (with audio). */
158
219
  export declare function startRecording(options?: StartRecordingOptions): Promise<void>;
159
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 constraints = {
51
- video: {
52
- facingMode: options?.camera === 'front' ? 'user' : 'environment'
53
- },
54
- audio: false
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
- function captureDesktopFrame() {
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 width = activeVideo.videoWidth;
85
- const height = activeVideo.videoHeight;
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('image/jpeg');
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: 'jpeg',
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
@@ -272,6 +317,57 @@ async function getCapabilities(cameraId) {
272
317
  return desktopCapabilities(cameraId);
273
318
  }
274
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;
367
+ }
368
+ throw new Error('setWhiteBalance is not supported on desktop.');
369
+ }
370
+ // ---------------------------------------------------------------------------
275
371
  // Video recording
276
372
  // ---------------------------------------------------------------------------
277
373
  /** Starts video recording (with audio). */
@@ -285,7 +381,12 @@ async function startRecording(options) {
285
381
  activeStream = null;
286
382
  activeVideo = null;
287
383
  }
288
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
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 });
289
390
  activeStream = stream;
290
391
  const video = document.createElement('video');
291
392
  video.muted = true;
@@ -295,7 +396,10 @@ async function startRecording(options) {
295
396
  activeVideo = video;
296
397
  recordedChunks = [];
297
398
  recordedBytes = 0;
298
- mediaRecorder = new MediaRecorder(stream);
399
+ const recorderOptions = {};
400
+ if (options?.bitrate)
401
+ recorderOptions.videoBitsPerSecond = options.bitrate;
402
+ mediaRecorder = new MediaRecorder(stream, recorderOptions);
299
403
  mediaRecorder.ondataavailable = (event) => {
300
404
  if (event.data.size > 0) {
301
405
  recordedChunks.push(event.data);
@@ -421,4 +525,4 @@ async function openAppSettings() {
421
525
  throw new Error('openAppSettings is not supported on desktop.');
422
526
  }
423
527
 
424
- export { capture, checkPermissions, flipCamera, getAvailableCameras, getCapabilities, getPreviewStream, getZoom, openAppSettings, requestPermissions, saveToGallery, setFlash, setZoom, startPreview, startRecording, stopPreview, stopRecording };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-plugin-camera-kessdev",
3
- "version": "0.2.0-beta.0",
3
+ "version": "0.2.0-beta.1",
4
4
  "author": "kess",
5
5
  "description": "Camera plugin for Tauri v2: photo capture, video recording and preview on Android, iOS and desktop.",
6
6
  "license": "MIT",