tauri-plugin-camera-kessdev 0.1.1-beta.3 → 0.2.0-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 +62 -3
- package/dist-js/index.cjs +127 -7
- package/dist-js/index.d.ts +76 -4
- package/dist-js/index.js +125 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,6 +129,9 @@ import {
|
|
|
129
129
|
flipCamera,
|
|
130
130
|
setFlash,
|
|
131
131
|
setZoom,
|
|
132
|
+
getZoom,
|
|
133
|
+
getAvailableCameras,
|
|
134
|
+
getCapabilities,
|
|
132
135
|
saveToGallery,
|
|
133
136
|
checkPermissions,
|
|
134
137
|
requestPermissions,
|
|
@@ -168,8 +171,23 @@ await startRecording()
|
|
|
168
171
|
// ... user records ...
|
|
169
172
|
const video = await stopRecording()
|
|
170
173
|
// video: { path, duration } (mobile) | { duration, blob, url } (desktop)
|
|
174
|
+
|
|
175
|
+
// Optionally request a base64 JPEG thumbnail of the recording:
|
|
176
|
+
const withThumbnail = await stopRecording({ thumbnail: true })
|
|
177
|
+
// withThumbnail.thumbnail: string | undefined
|
|
171
178
|
```
|
|
172
179
|
|
|
180
|
+
### Video thumbnail
|
|
181
|
+
|
|
182
|
+
Pass `{ thumbnail: true }` to `stopRecording()` to include a base64-encoded JPEG
|
|
183
|
+
poster frame in the `Video` result. It is opt-in because it costs a frame decode
|
|
184
|
+
and encode. When it cannot be generated the field is omitted (the recording still
|
|
185
|
+
succeeds).
|
|
186
|
+
|
|
187
|
+
- **Android**: extracted with `MediaMetadataRetriever` on the recording executor.
|
|
188
|
+
- **iOS**: extracted with `AVAssetImageGenerator` at ~0.1 s (avoids a black first frame).
|
|
189
|
+
- **Desktop**: captured from the live preview stream at stop time.
|
|
190
|
+
|
|
173
191
|
### Recording progress (telemetry)
|
|
174
192
|
|
|
175
193
|
Pass an `onProgress` channel to `startRecording()` to receive live telemetry while
|
|
@@ -214,14 +232,51 @@ ends. Telemetry is never emitted after `stopRecording()` resolves.
|
|
|
214
232
|
await flipCamera()
|
|
215
233
|
await setFlash('on') // 'on' | 'off' | 'auto'
|
|
216
234
|
await setZoom(2.0)
|
|
235
|
+
|
|
236
|
+
// Live state (mobile):
|
|
237
|
+
const zoom = await getZoom() // number
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
On desktop `getZoom()` rejects with a descriptive error (consistent with
|
|
241
|
+
`setZoom`).
|
|
242
|
+
|
|
243
|
+
### Camera introspection
|
|
244
|
+
|
|
245
|
+
Use `getAvailableCameras()` to list the cameras (physical/logical lenses) and
|
|
246
|
+
`getCapabilities()` to know what a camera *can* do, before trying:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const cameras = await getAvailableCameras()
|
|
250
|
+
// [{ id: '0', facing: 'back', lens: 'wide' }, { id: '1', facing: 'front' }, ...]
|
|
251
|
+
|
|
252
|
+
// No argument: the active camera, or the default (back) camera without a preview.
|
|
253
|
+
const caps = await getCapabilities(cameras[0].id)
|
|
254
|
+
// { cameraId, hasFlash, hasTorch, zoomRange?, supportsFocus, focusModes?,
|
|
255
|
+
// photoFormats?, supportsPause }
|
|
217
256
|
```
|
|
218
257
|
|
|
258
|
+
The plugin follows one rule to avoid ambiguity:
|
|
259
|
+
|
|
260
|
+
- **What the hardware can do** -> `getCapabilities()` (declarative).
|
|
261
|
+
- **What a value is right now** -> a dedicated live getter (e.g. `getZoom()`).
|
|
262
|
+
|
|
263
|
+
Both work without an active preview on mobile. On desktop `getAvailableCameras()`
|
|
264
|
+
works cold (labels may stay empty until camera permission has been granted once),
|
|
265
|
+
while the hardware capabilities `zoomRange`, `hasTorch` and `focusModes` are
|
|
266
|
+
omitted until a preview/recording stream is active; `hasFlash` is always `false`
|
|
267
|
+
and `supportsPause` is `true`.
|
|
268
|
+
|
|
219
269
|
### Gallery
|
|
220
270
|
|
|
221
271
|
```ts
|
|
222
|
-
await saveToGallery(photo.path)
|
|
272
|
+
await saveToGallery(photo.path) // image
|
|
273
|
+
await saveToGallery(video.path) // video
|
|
274
|
+
await saveToGallery(media.path, { type: 'video' }) // forced type override
|
|
223
275
|
```
|
|
224
276
|
|
|
277
|
+
Mobile only. The media type is inferred from the file extension (mp4, mov, m4v,
|
|
278
|
+
webm, 3gp, mkv are treated as video); pass `{ type }` to force it.
|
|
279
|
+
|
|
225
280
|
### Permissions
|
|
226
281
|
|
|
227
282
|
```ts
|
|
@@ -361,10 +416,14 @@ destination from the generated Xcode project (`src-tauri/gen/apple`).
|
|
|
361
416
|
| `flipCamera()` | ✓ | ✗ (throws) |
|
|
362
417
|
| `setFlash(mode)` | ✓ | ✗ (throws) |
|
|
363
418
|
| `setZoom(factor)` | ✓ | ✗ (throws) |
|
|
419
|
+
| `getZoom()` | ✓ | ✗ (throws) |
|
|
420
|
+
| `getAvailableCameras()` | ✓ | ✓ |
|
|
421
|
+
| `getCapabilities(cameraId?)` | ✓ | ✓ (partial) |
|
|
364
422
|
| `startRecording(options?)` | ✓ `VideoCapture` / `AVCaptureMovieFileOutput` | ✓ `MediaRecorder` |
|
|
365
|
-
| `stopRecording()` | ✓ | ✓ |
|
|
423
|
+
| `stopRecording(options?)` | ✓ | ✓ |
|
|
366
424
|
| `RecordingProgress` telemetry (`onProgress` channel) | ✓ | ✓ |
|
|
367
|
-
| `
|
|
425
|
+
| `Video.thumbnail` (`stopRecording({ thumbnail: true })`) | ✓ | ✓ |
|
|
426
|
+
| `saveToGallery(path, options?)` | ✓ image & video | ✗ (throws) |
|
|
368
427
|
| `checkPermissions()` | ✓ | ✓ |
|
|
369
428
|
| `requestPermissions()` | ✓ | ✓ |
|
|
370
429
|
| `openAppSettings()` | ✓ | ✗ (throws) |
|
package/dist-js/index.cjs
CHANGED
|
@@ -181,6 +181,97 @@ async function setZoom(factor) {
|
|
|
181
181
|
}
|
|
182
182
|
throw new Error('setZoom is not supported on desktop.');
|
|
183
183
|
}
|
|
184
|
+
/** Gets the current camera zoom factor (mobile). */
|
|
185
|
+
async function getZoom() {
|
|
186
|
+
if (isMobile()) {
|
|
187
|
+
return await core.invoke('plugin:camera-kessdev|get_zoom');
|
|
188
|
+
}
|
|
189
|
+
throw new Error('getZoom is not supported on desktop.');
|
|
190
|
+
}
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Introspection (available cameras + declarative capabilities)
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
/** Best-effort facing detection from a device label (desktop). */
|
|
195
|
+
function facingFromLabel(label) {
|
|
196
|
+
const value = label.toLowerCase();
|
|
197
|
+
if (value.includes('front') || value.includes('user'))
|
|
198
|
+
return 'front';
|
|
199
|
+
if (value.includes('back') || value.includes('rear') || value.includes('environment'))
|
|
200
|
+
return 'back';
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
/** Enumerates video input devices on desktop via `enumerateDevices()`. */
|
|
204
|
+
async function enumerateDesktopCameras() {
|
|
205
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
206
|
+
return devices
|
|
207
|
+
.filter((device) => device.kind === 'videoinput')
|
|
208
|
+
.map((device) => {
|
|
209
|
+
const info = { id: device.deviceId };
|
|
210
|
+
const facing = facingFromLabel(device.label);
|
|
211
|
+
if (facing)
|
|
212
|
+
info.facing = facing;
|
|
213
|
+
if (device.label)
|
|
214
|
+
info.label = device.label;
|
|
215
|
+
return info;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Declarative capabilities on desktop. Hardware fields (`zoomRange`, `hasTorch`,
|
|
220
|
+
* `focusModes`) are only known while a preview/recording stream is active; they
|
|
221
|
+
* are omitted otherwise.
|
|
222
|
+
*/
|
|
223
|
+
function desktopCapabilities(cameraId) {
|
|
224
|
+
const caps = {
|
|
225
|
+
hasFlash: false,
|
|
226
|
+
hasTorch: false,
|
|
227
|
+
supportsFocus: false,
|
|
228
|
+
supportsPause: true,
|
|
229
|
+
photoFormats: ['jpeg', 'png', 'webp']
|
|
230
|
+
};
|
|
231
|
+
if (cameraId)
|
|
232
|
+
caps.cameraId = cameraId;
|
|
233
|
+
const track = activeStream?.getVideoTracks?.()[0];
|
|
234
|
+
const trackCaps = track?.getCapabilities?.();
|
|
235
|
+
if (trackCaps) {
|
|
236
|
+
if (trackCaps.zoom) {
|
|
237
|
+
caps.zoomRange = { min: trackCaps.zoom.min, max: trackCaps.zoom.max };
|
|
238
|
+
}
|
|
239
|
+
if (typeof trackCaps.torch === 'boolean') {
|
|
240
|
+
caps.hasTorch = trackCaps.torch;
|
|
241
|
+
}
|
|
242
|
+
if (trackCaps.focusMode && trackCaps.focusMode.length > 0) {
|
|
243
|
+
caps.supportsFocus = true;
|
|
244
|
+
caps.focusModes = trackCaps.focusMode;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return caps;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Lists the cameras (physical or logical lenses) available on the device.
|
|
251
|
+
*
|
|
252
|
+
* Works without an active preview on every platform. On desktop the labels may
|
|
253
|
+
* be empty until camera permission has been granted at least once.
|
|
254
|
+
*/
|
|
255
|
+
async function getAvailableCameras() {
|
|
256
|
+
if (isMobile()) {
|
|
257
|
+
return await core.invoke('plugin:camera-kessdev|get_available_cameras');
|
|
258
|
+
}
|
|
259
|
+
return enumerateDesktopCameras();
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Returns the declarative capabilities of a camera.
|
|
263
|
+
*
|
|
264
|
+
* Without `cameraId` it describes the active camera, or the default (back)
|
|
265
|
+
* camera when no preview is active. On mobile it works without an active
|
|
266
|
+
* preview; on desktop hardware fields are omitted until a stream is active.
|
|
267
|
+
* Live values are exposed by dedicated getters (e.g. {@link getZoom}).
|
|
268
|
+
*/
|
|
269
|
+
async function getCapabilities(cameraId) {
|
|
270
|
+
if (isMobile()) {
|
|
271
|
+
return await core.invoke('plugin:camera-kessdev|get_capabilities', { cameraId });
|
|
272
|
+
}
|
|
273
|
+
return desktopCapabilities(cameraId);
|
|
274
|
+
}
|
|
184
275
|
// ---------------------------------------------------------------------------
|
|
185
276
|
// Video recording
|
|
186
277
|
// ---------------------------------------------------------------------------
|
|
@@ -228,14 +319,15 @@ async function startRecording(options) {
|
|
|
228
319
|
}
|
|
229
320
|
}
|
|
230
321
|
/** Stops video recording and returns the recorded media. */
|
|
231
|
-
async function stopRecording() {
|
|
322
|
+
async function stopRecording(options) {
|
|
232
323
|
if (isMobile()) {
|
|
233
|
-
return await core.invoke('plugin:camera-kessdev|stop_recording');
|
|
324
|
+
return await core.invoke('plugin:camera-kessdev|stop_recording', { ...options });
|
|
234
325
|
}
|
|
235
326
|
if (!mediaRecorder) {
|
|
236
327
|
throw new Error('Recording is not active. Call startRecording() first.');
|
|
237
328
|
}
|
|
238
329
|
const duration = (performance.now() - recordingStartedAt) / 1000;
|
|
330
|
+
const thumbnail = options?.thumbnail ? captureDesktopThumbnail() : undefined;
|
|
239
331
|
stopProgressTimer();
|
|
240
332
|
return await new Promise((resolve, reject) => {
|
|
241
333
|
mediaRecorder.onstop = () => {
|
|
@@ -245,23 +337,48 @@ async function stopRecording() {
|
|
|
245
337
|
recordedChunks = [];
|
|
246
338
|
recordedBytes = 0;
|
|
247
339
|
mediaRecorder = null;
|
|
248
|
-
|
|
340
|
+
const result = { duration, blob, url };
|
|
341
|
+
if (thumbnail)
|
|
342
|
+
result.thumbnail = thumbnail;
|
|
343
|
+
resolve(result);
|
|
249
344
|
};
|
|
250
345
|
mediaRecorder.onerror = () => reject(new Error('Video recording failed.'));
|
|
251
346
|
mediaRecorder.stop();
|
|
252
347
|
});
|
|
253
348
|
}
|
|
349
|
+
/** Captures the current desktop preview frame as a base64 JPEG thumbnail. */
|
|
350
|
+
function captureDesktopThumbnail() {
|
|
351
|
+
if (!activeVideo || !activeVideo.videoWidth || !activeVideo.videoHeight) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const canvas = document.createElement('canvas');
|
|
356
|
+
canvas.width = activeVideo.videoWidth;
|
|
357
|
+
canvas.height = activeVideo.videoHeight;
|
|
358
|
+
const ctx = canvas.getContext('2d');
|
|
359
|
+
if (!ctx)
|
|
360
|
+
return undefined;
|
|
361
|
+
ctx.drawImage(activeVideo, 0, 0, canvas.width, canvas.height);
|
|
362
|
+
return canvas.toDataURL('image/jpeg');
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
254
368
|
// ---------------------------------------------------------------------------
|
|
255
369
|
// Gallery
|
|
256
370
|
// ---------------------------------------------------------------------------
|
|
257
371
|
/**
|
|
258
|
-
* Saves a captured
|
|
372
|
+
* Saves a captured photo or video to the device gallery (mobile).
|
|
373
|
+
*
|
|
374
|
+
* The media type is inferred from the file extension; pass `options.type` to
|
|
375
|
+
* force it. On desktop this is not supported.
|
|
259
376
|
*
|
|
260
|
-
* @param path Absolute path to the
|
|
377
|
+
* @param path Absolute path to the media file.
|
|
261
378
|
*/
|
|
262
|
-
async function saveToGallery(path) {
|
|
379
|
+
async function saveToGallery(path, options) {
|
|
263
380
|
if (isMobile()) {
|
|
264
|
-
await core.invoke('plugin:camera-kessdev|save_to_gallery', { path });
|
|
381
|
+
await core.invoke('plugin:camera-kessdev|save_to_gallery', { path, ...options });
|
|
265
382
|
return;
|
|
266
383
|
}
|
|
267
384
|
throw new Error('saveToGallery is not supported on desktop.');
|
|
@@ -312,7 +429,10 @@ Object.defineProperty(exports, "convertFileSrc", {
|
|
|
312
429
|
exports.capture = capture;
|
|
313
430
|
exports.checkPermissions = checkPermissions;
|
|
314
431
|
exports.flipCamera = flipCamera;
|
|
432
|
+
exports.getAvailableCameras = getAvailableCameras;
|
|
433
|
+
exports.getCapabilities = getCapabilities;
|
|
315
434
|
exports.getPreviewStream = getPreviewStream;
|
|
435
|
+
exports.getZoom = getZoom;
|
|
316
436
|
exports.openAppSettings = openAppSettings;
|
|
317
437
|
exports.requestPermissions = requestPermissions;
|
|
318
438
|
exports.saveToGallery = saveToGallery;
|
package/dist-js/index.d.ts
CHANGED
|
@@ -34,6 +34,49 @@ export interface Video {
|
|
|
34
34
|
blob?: Blob;
|
|
35
35
|
/** Object URL for the recorded video (desktop). */
|
|
36
36
|
url?: string;
|
|
37
|
+
/** Optional base64-encoded thumbnail, when requested via `stopRecording`. */
|
|
38
|
+
thumbnail?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Supported zoom range of a camera (declarative, without the live value). */
|
|
41
|
+
export interface ZoomRange {
|
|
42
|
+
min: number;
|
|
43
|
+
max: number;
|
|
44
|
+
}
|
|
45
|
+
/** Orientation of a camera. */
|
|
46
|
+
export type CameraFacing = 'front' | 'back' | 'external';
|
|
47
|
+
/**
|
|
48
|
+
* Best-effort hint of which lens a camera maps to. Platforms that cannot
|
|
49
|
+
* determine it (notably desktop) omit the field.
|
|
50
|
+
*/
|
|
51
|
+
export type CameraLens = 'wide' | 'ultraWide' | 'telephoto' | (string & {});
|
|
52
|
+
/** A camera (physical or logical lens) available on the device. */
|
|
53
|
+
export interface CameraInfo {
|
|
54
|
+
/** Native, session-stable identifier (usable to reference this camera). */
|
|
55
|
+
id: string;
|
|
56
|
+
/** Orientation, when the platform can determine it. */
|
|
57
|
+
facing?: CameraFacing;
|
|
58
|
+
/** Best-effort lens hint, when the platform can determine it. */
|
|
59
|
+
lens?: CameraLens;
|
|
60
|
+
/** Human-readable label, when available. */
|
|
61
|
+
label?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Declarative capabilities of a camera: what the hardware/platform *can* do.
|
|
65
|
+
*
|
|
66
|
+
* Optional fields are omitted when the platform cannot determine them. Live
|
|
67
|
+
* values (e.g. the current zoom factor) are exposed by dedicated getters such
|
|
68
|
+
* as {@link getZoom}, never here.
|
|
69
|
+
*/
|
|
70
|
+
export interface CameraCapabilities {
|
|
71
|
+
/** Identifier of the camera these capabilities describe, when known. */
|
|
72
|
+
cameraId?: string;
|
|
73
|
+
hasFlash: boolean;
|
|
74
|
+
hasTorch: boolean;
|
|
75
|
+
zoomRange?: ZoomRange;
|
|
76
|
+
supportsFocus: boolean;
|
|
77
|
+
focusModes?: string[];
|
|
78
|
+
photoFormats?: string[];
|
|
79
|
+
supportsPause: boolean;
|
|
37
80
|
}
|
|
38
81
|
/**
|
|
39
82
|
* Live telemetry emitted while a video recording is in progress.
|
|
@@ -54,6 +97,14 @@ export interface StartRecordingOptions {
|
|
|
54
97
|
/** Receives live recording telemetry while the recording is active. */
|
|
55
98
|
onProgress?: Channel<RecordingProgress>;
|
|
56
99
|
}
|
|
100
|
+
export interface StopRecordingOptions {
|
|
101
|
+
/** When true, the result includes a base64-encoded `thumbnail`. */
|
|
102
|
+
thumbnail?: boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface SaveToGalleryOptions {
|
|
105
|
+
/** Force the media type instead of inferring it from the file. */
|
|
106
|
+
type?: 'image' | 'video';
|
|
107
|
+
}
|
|
57
108
|
/**
|
|
58
109
|
* Starts the camera preview.
|
|
59
110
|
*
|
|
@@ -85,16 +136,37 @@ export declare function flipCamera(): Promise<void>;
|
|
|
85
136
|
export declare function setFlash(mode: FlashMode): Promise<void>;
|
|
86
137
|
/** Sets the camera zoom factor (mobile). */
|
|
87
138
|
export declare function setZoom(factor: number): Promise<void>;
|
|
139
|
+
/** Gets the current camera zoom factor (mobile). */
|
|
140
|
+
export declare function getZoom(): Promise<number>;
|
|
141
|
+
/**
|
|
142
|
+
* Lists the cameras (physical or logical lenses) available on the device.
|
|
143
|
+
*
|
|
144
|
+
* Works without an active preview on every platform. On desktop the labels may
|
|
145
|
+
* be empty until camera permission has been granted at least once.
|
|
146
|
+
*/
|
|
147
|
+
export declare function getAvailableCameras(): Promise<CameraInfo[]>;
|
|
148
|
+
/**
|
|
149
|
+
* Returns the declarative capabilities of a camera.
|
|
150
|
+
*
|
|
151
|
+
* Without `cameraId` it describes the active camera, or the default (back)
|
|
152
|
+
* camera when no preview is active. On mobile it works without an active
|
|
153
|
+
* preview; on desktop hardware fields are omitted until a stream is active.
|
|
154
|
+
* Live values are exposed by dedicated getters (e.g. {@link getZoom}).
|
|
155
|
+
*/
|
|
156
|
+
export declare function getCapabilities(cameraId?: string): Promise<CameraCapabilities>;
|
|
88
157
|
/** Starts video recording (with audio). */
|
|
89
158
|
export declare function startRecording(options?: StartRecordingOptions): Promise<void>;
|
|
90
159
|
/** Stops video recording and returns the recorded media. */
|
|
91
|
-
export declare function stopRecording(): Promise<Video>;
|
|
160
|
+
export declare function stopRecording(options?: StopRecordingOptions): Promise<Video>;
|
|
92
161
|
/**
|
|
93
|
-
* Saves a captured
|
|
162
|
+
* Saves a captured photo or video to the device gallery (mobile).
|
|
163
|
+
*
|
|
164
|
+
* The media type is inferred from the file extension; pass `options.type` to
|
|
165
|
+
* force it. On desktop this is not supported.
|
|
94
166
|
*
|
|
95
|
-
* @param path Absolute path to the
|
|
167
|
+
* @param path Absolute path to the media file.
|
|
96
168
|
*/
|
|
97
|
-
export declare function saveToGallery(path: string): Promise<void>;
|
|
169
|
+
export declare function saveToGallery(path: string, options?: SaveToGalleryOptions): Promise<void>;
|
|
98
170
|
/** Gets the current camera permission state. */
|
|
99
171
|
export declare function checkPermissions(): Promise<PermissionState>;
|
|
100
172
|
/** Requests camera permission. */
|
package/dist-js/index.js
CHANGED
|
@@ -180,6 +180,97 @@ async function setZoom(factor) {
|
|
|
180
180
|
}
|
|
181
181
|
throw new Error('setZoom is not supported on desktop.');
|
|
182
182
|
}
|
|
183
|
+
/** Gets the current camera zoom factor (mobile). */
|
|
184
|
+
async function getZoom() {
|
|
185
|
+
if (isMobile()) {
|
|
186
|
+
return await invoke('plugin:camera-kessdev|get_zoom');
|
|
187
|
+
}
|
|
188
|
+
throw new Error('getZoom is not supported on desktop.');
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Introspection (available cameras + declarative capabilities)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
/** Best-effort facing detection from a device label (desktop). */
|
|
194
|
+
function facingFromLabel(label) {
|
|
195
|
+
const value = label.toLowerCase();
|
|
196
|
+
if (value.includes('front') || value.includes('user'))
|
|
197
|
+
return 'front';
|
|
198
|
+
if (value.includes('back') || value.includes('rear') || value.includes('environment'))
|
|
199
|
+
return 'back';
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
/** Enumerates video input devices on desktop via `enumerateDevices()`. */
|
|
203
|
+
async function enumerateDesktopCameras() {
|
|
204
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
205
|
+
return devices
|
|
206
|
+
.filter((device) => device.kind === 'videoinput')
|
|
207
|
+
.map((device) => {
|
|
208
|
+
const info = { id: device.deviceId };
|
|
209
|
+
const facing = facingFromLabel(device.label);
|
|
210
|
+
if (facing)
|
|
211
|
+
info.facing = facing;
|
|
212
|
+
if (device.label)
|
|
213
|
+
info.label = device.label;
|
|
214
|
+
return info;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Declarative capabilities on desktop. Hardware fields (`zoomRange`, `hasTorch`,
|
|
219
|
+
* `focusModes`) are only known while a preview/recording stream is active; they
|
|
220
|
+
* are omitted otherwise.
|
|
221
|
+
*/
|
|
222
|
+
function desktopCapabilities(cameraId) {
|
|
223
|
+
const caps = {
|
|
224
|
+
hasFlash: false,
|
|
225
|
+
hasTorch: false,
|
|
226
|
+
supportsFocus: false,
|
|
227
|
+
supportsPause: true,
|
|
228
|
+
photoFormats: ['jpeg', 'png', 'webp']
|
|
229
|
+
};
|
|
230
|
+
if (cameraId)
|
|
231
|
+
caps.cameraId = cameraId;
|
|
232
|
+
const track = activeStream?.getVideoTracks?.()[0];
|
|
233
|
+
const trackCaps = track?.getCapabilities?.();
|
|
234
|
+
if (trackCaps) {
|
|
235
|
+
if (trackCaps.zoom) {
|
|
236
|
+
caps.zoomRange = { min: trackCaps.zoom.min, max: trackCaps.zoom.max };
|
|
237
|
+
}
|
|
238
|
+
if (typeof trackCaps.torch === 'boolean') {
|
|
239
|
+
caps.hasTorch = trackCaps.torch;
|
|
240
|
+
}
|
|
241
|
+
if (trackCaps.focusMode && trackCaps.focusMode.length > 0) {
|
|
242
|
+
caps.supportsFocus = true;
|
|
243
|
+
caps.focusModes = trackCaps.focusMode;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return caps;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Lists the cameras (physical or logical lenses) available on the device.
|
|
250
|
+
*
|
|
251
|
+
* Works without an active preview on every platform. On desktop the labels may
|
|
252
|
+
* be empty until camera permission has been granted at least once.
|
|
253
|
+
*/
|
|
254
|
+
async function getAvailableCameras() {
|
|
255
|
+
if (isMobile()) {
|
|
256
|
+
return await invoke('plugin:camera-kessdev|get_available_cameras');
|
|
257
|
+
}
|
|
258
|
+
return enumerateDesktopCameras();
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Returns the declarative capabilities of a camera.
|
|
262
|
+
*
|
|
263
|
+
* Without `cameraId` it describes the active camera, or the default (back)
|
|
264
|
+
* camera when no preview is active. On mobile it works without an active
|
|
265
|
+
* preview; on desktop hardware fields are omitted until a stream is active.
|
|
266
|
+
* Live values are exposed by dedicated getters (e.g. {@link getZoom}).
|
|
267
|
+
*/
|
|
268
|
+
async function getCapabilities(cameraId) {
|
|
269
|
+
if (isMobile()) {
|
|
270
|
+
return await invoke('plugin:camera-kessdev|get_capabilities', { cameraId });
|
|
271
|
+
}
|
|
272
|
+
return desktopCapabilities(cameraId);
|
|
273
|
+
}
|
|
183
274
|
// ---------------------------------------------------------------------------
|
|
184
275
|
// Video recording
|
|
185
276
|
// ---------------------------------------------------------------------------
|
|
@@ -227,14 +318,15 @@ async function startRecording(options) {
|
|
|
227
318
|
}
|
|
228
319
|
}
|
|
229
320
|
/** Stops video recording and returns the recorded media. */
|
|
230
|
-
async function stopRecording() {
|
|
321
|
+
async function stopRecording(options) {
|
|
231
322
|
if (isMobile()) {
|
|
232
|
-
return await invoke('plugin:camera-kessdev|stop_recording');
|
|
323
|
+
return await invoke('plugin:camera-kessdev|stop_recording', { ...options });
|
|
233
324
|
}
|
|
234
325
|
if (!mediaRecorder) {
|
|
235
326
|
throw new Error('Recording is not active. Call startRecording() first.');
|
|
236
327
|
}
|
|
237
328
|
const duration = (performance.now() - recordingStartedAt) / 1000;
|
|
329
|
+
const thumbnail = options?.thumbnail ? captureDesktopThumbnail() : undefined;
|
|
238
330
|
stopProgressTimer();
|
|
239
331
|
return await new Promise((resolve, reject) => {
|
|
240
332
|
mediaRecorder.onstop = () => {
|
|
@@ -244,23 +336,48 @@ async function stopRecording() {
|
|
|
244
336
|
recordedChunks = [];
|
|
245
337
|
recordedBytes = 0;
|
|
246
338
|
mediaRecorder = null;
|
|
247
|
-
|
|
339
|
+
const result = { duration, blob, url };
|
|
340
|
+
if (thumbnail)
|
|
341
|
+
result.thumbnail = thumbnail;
|
|
342
|
+
resolve(result);
|
|
248
343
|
};
|
|
249
344
|
mediaRecorder.onerror = () => reject(new Error('Video recording failed.'));
|
|
250
345
|
mediaRecorder.stop();
|
|
251
346
|
});
|
|
252
347
|
}
|
|
348
|
+
/** Captures the current desktop preview frame as a base64 JPEG thumbnail. */
|
|
349
|
+
function captureDesktopThumbnail() {
|
|
350
|
+
if (!activeVideo || !activeVideo.videoWidth || !activeVideo.videoHeight) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const canvas = document.createElement('canvas');
|
|
355
|
+
canvas.width = activeVideo.videoWidth;
|
|
356
|
+
canvas.height = activeVideo.videoHeight;
|
|
357
|
+
const ctx = canvas.getContext('2d');
|
|
358
|
+
if (!ctx)
|
|
359
|
+
return undefined;
|
|
360
|
+
ctx.drawImage(activeVideo, 0, 0, canvas.width, canvas.height);
|
|
361
|
+
return canvas.toDataURL('image/jpeg');
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
253
367
|
// ---------------------------------------------------------------------------
|
|
254
368
|
// Gallery
|
|
255
369
|
// ---------------------------------------------------------------------------
|
|
256
370
|
/**
|
|
257
|
-
* Saves a captured
|
|
371
|
+
* Saves a captured photo or video to the device gallery (mobile).
|
|
372
|
+
*
|
|
373
|
+
* The media type is inferred from the file extension; pass `options.type` to
|
|
374
|
+
* force it. On desktop this is not supported.
|
|
258
375
|
*
|
|
259
|
-
* @param path Absolute path to the
|
|
376
|
+
* @param path Absolute path to the media file.
|
|
260
377
|
*/
|
|
261
|
-
async function saveToGallery(path) {
|
|
378
|
+
async function saveToGallery(path, options) {
|
|
262
379
|
if (isMobile()) {
|
|
263
|
-
await invoke('plugin:camera-kessdev|save_to_gallery', { path });
|
|
380
|
+
await invoke('plugin:camera-kessdev|save_to_gallery', { path, ...options });
|
|
264
381
|
return;
|
|
265
382
|
}
|
|
266
383
|
throw new Error('saveToGallery is not supported on desktop.');
|
|
@@ -304,4 +421,4 @@ async function openAppSettings() {
|
|
|
304
421
|
throw new Error('openAppSettings is not supported on desktop.');
|
|
305
422
|
}
|
|
306
423
|
|
|
307
|
-
export { capture, checkPermissions, flipCamera, getPreviewStream, openAppSettings, requestPermissions, saveToGallery, setFlash, setZoom, startPreview, startRecording, stopPreview, stopRecording };
|
|
424
|
+
export { capture, checkPermissions, flipCamera, getAvailableCameras, getCapabilities, getPreviewStream, getZoom, openAppSettings, requestPermissions, saveToGallery, setFlash, setZoom, startPreview, startRecording, stopPreview, stopRecording };
|
package/package.json
CHANGED