@elizaos/capacitor-camera 1.0.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/ElizaosCapacitorCamera.podspec +18 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/ai/eliza/plugins/camera/CameraPlugin.kt +1002 -0
- package/dist/esm/definitions.d.ts +191 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +58 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +541 -0
- package/dist/plugin.cjs.js +557 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +560 -0
- package/dist/plugin.js.map +1 -0
- package/electrobun/src/index.ts +13 -0
- package/electrobun/tsconfig.json +18 -0
- package/ios/Sources/CameraPlugin/CameraPlugin.swift +1225 -0
- package/package.json +81 -0
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
const VIDEO_MIME_TYPES = [
|
|
3
|
+
"video/webm;codecs=vp9,opus",
|
|
4
|
+
"video/webm;codecs=vp8,opus",
|
|
5
|
+
"video/webm",
|
|
6
|
+
"video/mp4",
|
|
7
|
+
];
|
|
8
|
+
const getSupportedMimeType = () => VIDEO_MIME_TYPES.find((m) => MediaRecorder.isTypeSupported(m)) ?? null;
|
|
9
|
+
export class CameraWeb extends WebPlugin {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.mediaStream = null;
|
|
13
|
+
this.videoElement = null;
|
|
14
|
+
this.previewElement = null;
|
|
15
|
+
this.currentDeviceId = null;
|
|
16
|
+
this.mediaRecorder = null;
|
|
17
|
+
this.recordedChunks = [];
|
|
18
|
+
this.recordingStartTime = 0;
|
|
19
|
+
this.recordingStateInterval = null;
|
|
20
|
+
this.isRecording = false;
|
|
21
|
+
this.currentSettings = {
|
|
22
|
+
flash: "off",
|
|
23
|
+
zoom: 1,
|
|
24
|
+
focusMode: "continuous",
|
|
25
|
+
exposureMode: "continuous",
|
|
26
|
+
exposureCompensation: 0,
|
|
27
|
+
whiteBalance: "auto",
|
|
28
|
+
};
|
|
29
|
+
this.pluginListeners = [];
|
|
30
|
+
}
|
|
31
|
+
async getDevices() {
|
|
32
|
+
// enumerateDevices() returns device stubs without labels unless the user
|
|
33
|
+
// has already granted camera permission via a prior getUserMedia() call.
|
|
34
|
+
// We intentionally do NOT call getUserMedia() here because it requires a
|
|
35
|
+
// user gesture and would throw NotAllowedError if called programmatically.
|
|
36
|
+
const allDevices = await navigator.mediaDevices.enumerateDevices();
|
|
37
|
+
const videoDevices = allDevices.filter((d) => d.kind === "videoinput");
|
|
38
|
+
const devices = await Promise.all(videoDevices.map(async (device, index) => {
|
|
39
|
+
const capabilities = await this.getDeviceCapabilities(device.deviceId);
|
|
40
|
+
return {
|
|
41
|
+
deviceId: device.deviceId,
|
|
42
|
+
label: device.label || `Camera ${index + 1}`,
|
|
43
|
+
direction: this.inferDirection(device.label),
|
|
44
|
+
// Flash detection not available via MediaDevices API on web
|
|
45
|
+
hasFlash: capabilities?.hasFlash ?? false,
|
|
46
|
+
hasZoom: !!capabilities?.zoom,
|
|
47
|
+
maxZoom: capabilities?.zoom?.max ?? 1,
|
|
48
|
+
// Return actual capabilities or empty array to indicate unknown
|
|
49
|
+
supportedResolutions: capabilities?.resolutions ?? [],
|
|
50
|
+
supportedFrameRates: capabilities?.frameRates ?? [],
|
|
51
|
+
};
|
|
52
|
+
}));
|
|
53
|
+
return { devices };
|
|
54
|
+
}
|
|
55
|
+
inferDirection(label) {
|
|
56
|
+
const lowerLabel = label.toLowerCase();
|
|
57
|
+
if (lowerLabel.includes("front") ||
|
|
58
|
+
lowerLabel.includes("facetime") ||
|
|
59
|
+
lowerLabel.includes("user")) {
|
|
60
|
+
return "front";
|
|
61
|
+
}
|
|
62
|
+
if (lowerLabel.includes("back") ||
|
|
63
|
+
lowerLabel.includes("rear") ||
|
|
64
|
+
lowerLabel.includes("environment")) {
|
|
65
|
+
return "back";
|
|
66
|
+
}
|
|
67
|
+
return "external";
|
|
68
|
+
}
|
|
69
|
+
async getDeviceCapabilities(deviceId) {
|
|
70
|
+
let stream;
|
|
71
|
+
try {
|
|
72
|
+
stream = await navigator.mediaDevices.getUserMedia({
|
|
73
|
+
video: { deviceId: { exact: deviceId } },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null; // Device not accessible
|
|
78
|
+
}
|
|
79
|
+
const track = stream.getVideoTracks()[0];
|
|
80
|
+
if (!track) {
|
|
81
|
+
stream.getTracks().forEach((t) => {
|
|
82
|
+
t.stop();
|
|
83
|
+
});
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const capabilities = track.getCapabilities ? track.getCapabilities() : {};
|
|
87
|
+
stream.getTracks().forEach((t) => {
|
|
88
|
+
t.stop();
|
|
89
|
+
});
|
|
90
|
+
const caps = capabilities;
|
|
91
|
+
// Build resolutions from actual device capabilities only
|
|
92
|
+
const resolutions = [];
|
|
93
|
+
if (caps.width?.max && caps.height?.max) {
|
|
94
|
+
resolutions.push({ width: caps.width.max, height: caps.height.max });
|
|
95
|
+
// Add common lower resolutions only if device supports them
|
|
96
|
+
if (caps.width.max >= 1280 && caps.height.max >= 720) {
|
|
97
|
+
resolutions.push({ width: 1280, height: 720 });
|
|
98
|
+
}
|
|
99
|
+
if (caps.width.max >= 640 && caps.height.max >= 480) {
|
|
100
|
+
resolutions.push({ width: 640, height: 480 });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Build frameRates from actual device capabilities only
|
|
104
|
+
const frameRates = [];
|
|
105
|
+
if (caps.frameRate?.max) {
|
|
106
|
+
if (caps.frameRate.max >= 60)
|
|
107
|
+
frameRates.push(60);
|
|
108
|
+
if (caps.frameRate.max >= 30)
|
|
109
|
+
frameRates.push(30);
|
|
110
|
+
if (caps.frameRate.max >= 24)
|
|
111
|
+
frameRates.push(24);
|
|
112
|
+
if (caps.frameRate.max >= 15)
|
|
113
|
+
frameRates.push(15);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
zoom: caps.zoom,
|
|
117
|
+
resolutions: resolutions.length > 0 ? resolutions : undefined,
|
|
118
|
+
frameRates: frameRates.length > 0 ? frameRates : undefined,
|
|
119
|
+
hasFlash: caps.torch === true, // Torch capability indicates flash support
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async startPreview(options) {
|
|
123
|
+
await this.stopPreview();
|
|
124
|
+
const constraints = {
|
|
125
|
+
video: {
|
|
126
|
+
deviceId: options.deviceId ? { exact: options.deviceId } : undefined,
|
|
127
|
+
facingMode: options.direction === "front"
|
|
128
|
+
? "user"
|
|
129
|
+
: options.direction === "back"
|
|
130
|
+
? "environment"
|
|
131
|
+
: undefined,
|
|
132
|
+
width: options.resolution?.width
|
|
133
|
+
? { ideal: options.resolution.width }
|
|
134
|
+
: { ideal: 1920 },
|
|
135
|
+
height: options.resolution?.height
|
|
136
|
+
? { ideal: options.resolution.height }
|
|
137
|
+
: { ideal: 1080 },
|
|
138
|
+
frameRate: options.frameRate
|
|
139
|
+
? { ideal: options.frameRate }
|
|
140
|
+
: { ideal: 30 },
|
|
141
|
+
},
|
|
142
|
+
audio: false,
|
|
143
|
+
};
|
|
144
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
145
|
+
this.previewElement = options.element;
|
|
146
|
+
this.videoElement = document.createElement("video");
|
|
147
|
+
this.videoElement.srcObject = this.mediaStream;
|
|
148
|
+
this.videoElement.autoplay = true;
|
|
149
|
+
this.videoElement.playsInline = true;
|
|
150
|
+
this.videoElement.muted = true;
|
|
151
|
+
this.videoElement.style.width = "100%";
|
|
152
|
+
this.videoElement.style.height = "100%";
|
|
153
|
+
this.videoElement.style.objectFit = "cover";
|
|
154
|
+
if (options.mirror) {
|
|
155
|
+
this.videoElement.style.transform = "scaleX(-1)";
|
|
156
|
+
}
|
|
157
|
+
this.previewElement.appendChild(this.videoElement);
|
|
158
|
+
await this.videoElement.play();
|
|
159
|
+
const track = this.mediaStream.getVideoTracks()[0];
|
|
160
|
+
const settings = track.getSettings();
|
|
161
|
+
this.currentDeviceId = settings.deviceId || options.deviceId || "";
|
|
162
|
+
return {
|
|
163
|
+
width: settings.width || options.resolution?.width || 1920,
|
|
164
|
+
height: settings.height || options.resolution?.height || 1080,
|
|
165
|
+
deviceId: this.currentDeviceId,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async stopPreview() {
|
|
169
|
+
if (this.isRecording) {
|
|
170
|
+
await this.stopRecording();
|
|
171
|
+
}
|
|
172
|
+
if (this.mediaStream) {
|
|
173
|
+
this.mediaStream.getTracks().forEach((track) => {
|
|
174
|
+
track.stop();
|
|
175
|
+
});
|
|
176
|
+
this.mediaStream = null;
|
|
177
|
+
}
|
|
178
|
+
if (this.videoElement && this.previewElement) {
|
|
179
|
+
this.previewElement.removeChild(this.videoElement);
|
|
180
|
+
this.videoElement = null;
|
|
181
|
+
}
|
|
182
|
+
this.previewElement = null;
|
|
183
|
+
this.currentDeviceId = null;
|
|
184
|
+
}
|
|
185
|
+
async switchCamera(options) {
|
|
186
|
+
if (!this.previewElement) {
|
|
187
|
+
throw new Error("Preview not started");
|
|
188
|
+
}
|
|
189
|
+
const mirror = options.direction === "front";
|
|
190
|
+
return this.startPreview({
|
|
191
|
+
element: this.previewElement,
|
|
192
|
+
deviceId: options.deviceId,
|
|
193
|
+
direction: options.direction,
|
|
194
|
+
mirror,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async capturePhoto(options) {
|
|
198
|
+
if (!this.videoElement || !this.mediaStream) {
|
|
199
|
+
throw new Error("Preview not started");
|
|
200
|
+
}
|
|
201
|
+
const track = this.mediaStream.getVideoTracks()[0];
|
|
202
|
+
const settings = track.getSettings();
|
|
203
|
+
const videoWidth = settings.width || this.videoElement.videoWidth;
|
|
204
|
+
const videoHeight = settings.height || this.videoElement.videoHeight;
|
|
205
|
+
const targetWidth = options?.width || videoWidth;
|
|
206
|
+
const targetHeight = options?.height || videoHeight;
|
|
207
|
+
const canvas = document.createElement("canvas");
|
|
208
|
+
canvas.width = targetWidth;
|
|
209
|
+
canvas.height = targetHeight;
|
|
210
|
+
const ctx = canvas.getContext("2d");
|
|
211
|
+
if (!ctx) {
|
|
212
|
+
throw new Error("Failed to get canvas context");
|
|
213
|
+
}
|
|
214
|
+
const scaleX = targetWidth / videoWidth;
|
|
215
|
+
const scaleY = targetHeight / videoHeight;
|
|
216
|
+
const scale = Math.max(scaleX, scaleY);
|
|
217
|
+
const drawWidth = videoWidth * scale;
|
|
218
|
+
const drawHeight = videoHeight * scale;
|
|
219
|
+
const drawX = (targetWidth - drawWidth) / 2;
|
|
220
|
+
const drawY = (targetHeight - drawHeight) / 2;
|
|
221
|
+
ctx.drawImage(this.videoElement, drawX, drawY, drawWidth, drawHeight);
|
|
222
|
+
const quality = (options?.quality ?? 90) / 100;
|
|
223
|
+
const format = options?.format || "jpeg";
|
|
224
|
+
const mimeType = format === "png"
|
|
225
|
+
? "image/png"
|
|
226
|
+
: format === "webp"
|
|
227
|
+
? "image/webp"
|
|
228
|
+
: "image/jpeg";
|
|
229
|
+
const base64 = canvas.toDataURL(mimeType, quality).split(",")[1];
|
|
230
|
+
return {
|
|
231
|
+
base64,
|
|
232
|
+
format,
|
|
233
|
+
width: targetWidth,
|
|
234
|
+
height: targetHeight,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async startRecording(options) {
|
|
238
|
+
if (!this.mediaStream) {
|
|
239
|
+
throw new Error("Preview not started");
|
|
240
|
+
}
|
|
241
|
+
if (this.isRecording) {
|
|
242
|
+
throw new Error("Recording already in progress");
|
|
243
|
+
}
|
|
244
|
+
let streamToRecord = this.mediaStream;
|
|
245
|
+
if (options?.audio !== false) {
|
|
246
|
+
const audioStream = await navigator.mediaDevices.getUserMedia({
|
|
247
|
+
audio: true,
|
|
248
|
+
});
|
|
249
|
+
streamToRecord = new MediaStream([
|
|
250
|
+
...this.mediaStream.getVideoTracks(),
|
|
251
|
+
...audioStream.getAudioTracks(),
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
const mimeType = getSupportedMimeType();
|
|
255
|
+
if (!mimeType)
|
|
256
|
+
throw new Error("No supported video mime type found");
|
|
257
|
+
const recorderOptions = { mimeType };
|
|
258
|
+
if (options?.bitrate)
|
|
259
|
+
recorderOptions.videoBitsPerSecond = options.bitrate;
|
|
260
|
+
this.recordedChunks = [];
|
|
261
|
+
this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
|
|
262
|
+
this.mediaRecorder.ondataavailable = (event) => {
|
|
263
|
+
if (event.data.size > 0) {
|
|
264
|
+
this.recordedChunks.push(event.data);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
this.mediaRecorder.onerror = (event) => {
|
|
268
|
+
this.notifyListeners("error", {
|
|
269
|
+
code: "RECORDING_ERROR",
|
|
270
|
+
message: `Recording error: ${event.message || "Unknown error"}`,
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
this.recordingStartTime = Date.now();
|
|
274
|
+
this.isRecording = true;
|
|
275
|
+
this.mediaRecorder.start(1000);
|
|
276
|
+
this.notifyListeners("recordingState", {
|
|
277
|
+
isRecording: true,
|
|
278
|
+
duration: 0,
|
|
279
|
+
fileSize: 0,
|
|
280
|
+
});
|
|
281
|
+
let autoStopping = false;
|
|
282
|
+
this.recordingStateInterval = setInterval(() => {
|
|
283
|
+
if (!this.isRecording || autoStopping)
|
|
284
|
+
return;
|
|
285
|
+
const duration = (Date.now() - this.recordingStartTime) / 1000;
|
|
286
|
+
const fileSize = this.recordedChunks.reduce((acc, chunk) => acc + chunk.size, 0);
|
|
287
|
+
this.notifyListeners("recordingState", {
|
|
288
|
+
isRecording: true,
|
|
289
|
+
duration,
|
|
290
|
+
fileSize,
|
|
291
|
+
});
|
|
292
|
+
const overLimit = (options?.maxDuration && duration >= options.maxDuration) ||
|
|
293
|
+
(options?.maxFileSize && fileSize >= options.maxFileSize);
|
|
294
|
+
if (overLimit) {
|
|
295
|
+
autoStopping = true;
|
|
296
|
+
this.stopRecording().catch((err) => {
|
|
297
|
+
console.error("[Camera] Auto-stop recording failed:", err);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}, 500);
|
|
301
|
+
}
|
|
302
|
+
async stopRecording() {
|
|
303
|
+
if (!this.isRecording || !this.mediaRecorder) {
|
|
304
|
+
throw new Error("Not recording");
|
|
305
|
+
}
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
if (!this.mediaRecorder) {
|
|
308
|
+
reject(new Error("MediaRecorder not initialized"));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const duration = (Date.now() - this.recordingStartTime) / 1000;
|
|
312
|
+
this.mediaRecorder.onstop = () => {
|
|
313
|
+
if (this.recordingStateInterval) {
|
|
314
|
+
clearInterval(this.recordingStateInterval);
|
|
315
|
+
this.recordingStateInterval = null;
|
|
316
|
+
}
|
|
317
|
+
this.isRecording = false;
|
|
318
|
+
const blob = new Blob(this.recordedChunks, {
|
|
319
|
+
type: this.mediaRecorder?.mimeType || "video/webm",
|
|
320
|
+
});
|
|
321
|
+
const url = URL.createObjectURL(blob);
|
|
322
|
+
const video = document.createElement("video");
|
|
323
|
+
video.src = url;
|
|
324
|
+
video.onloadedmetadata = () => {
|
|
325
|
+
resolve({
|
|
326
|
+
path: url,
|
|
327
|
+
duration,
|
|
328
|
+
width: video.videoWidth,
|
|
329
|
+
height: video.videoHeight,
|
|
330
|
+
fileSize: blob.size,
|
|
331
|
+
mimeType: this.mediaRecorder?.mimeType || "video/webm",
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
video.onerror = () => {
|
|
335
|
+
resolve({
|
|
336
|
+
path: url,
|
|
337
|
+
duration,
|
|
338
|
+
width: 0,
|
|
339
|
+
height: 0,
|
|
340
|
+
fileSize: blob.size,
|
|
341
|
+
mimeType: this.mediaRecorder?.mimeType || "video/webm",
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
this.notifyListeners("recordingState", {
|
|
345
|
+
isRecording: false,
|
|
346
|
+
duration,
|
|
347
|
+
fileSize: blob.size,
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
this.mediaRecorder.stop();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async getRecordingState() {
|
|
354
|
+
const duration = this.isRecording
|
|
355
|
+
? (Date.now() - this.recordingStartTime) / 1000
|
|
356
|
+
: 0;
|
|
357
|
+
const fileSize = this.recordedChunks.reduce((acc, chunk) => acc + chunk.size, 0);
|
|
358
|
+
return {
|
|
359
|
+
isRecording: this.isRecording,
|
|
360
|
+
duration,
|
|
361
|
+
fileSize,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
async getSettings() {
|
|
365
|
+
return { settings: { ...this.currentSettings } };
|
|
366
|
+
}
|
|
367
|
+
async setSettings(options) {
|
|
368
|
+
this.currentSettings = { ...this.currentSettings, ...options.settings };
|
|
369
|
+
if (this.mediaStream && options.settings.zoom !== undefined) {
|
|
370
|
+
await this.applyZoom(options.settings.zoom);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async setZoom(options) {
|
|
374
|
+
if (!Number.isFinite(options.zoom) || options.zoom < 0) {
|
|
375
|
+
throw new Error(`Invalid zoom value: ${options.zoom}. Must be a non-negative finite number.`);
|
|
376
|
+
}
|
|
377
|
+
await this.applyZoom(options.zoom);
|
|
378
|
+
this.currentSettings.zoom = options.zoom;
|
|
379
|
+
}
|
|
380
|
+
async applyZoom(zoom) {
|
|
381
|
+
if (!this.mediaStream)
|
|
382
|
+
return;
|
|
383
|
+
const track = this.mediaStream.getVideoTracks()[0];
|
|
384
|
+
if (!track)
|
|
385
|
+
return;
|
|
386
|
+
const capabilities = track.getCapabilities ? track.getCapabilities() : {};
|
|
387
|
+
const caps = capabilities;
|
|
388
|
+
if (caps.zoom) {
|
|
389
|
+
const clampedZoom = Math.max(caps.zoom.min, Math.min(caps.zoom.max, zoom));
|
|
390
|
+
await track.applyConstraints({
|
|
391
|
+
advanced: [{ zoom: clampedZoom }],
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async setFocusPoint(options) {
|
|
396
|
+
if (!this.mediaStream)
|
|
397
|
+
throw new Error("Preview not started");
|
|
398
|
+
const track = this.mediaStream.getVideoTracks()[0];
|
|
399
|
+
if (!track)
|
|
400
|
+
throw new Error("No video track available");
|
|
401
|
+
// Check if focus control is supported
|
|
402
|
+
const caps = track.getCapabilities ? track.getCapabilities() : {};
|
|
403
|
+
if (!caps.focusMode?.includes("manual")) {
|
|
404
|
+
throw new Error("Manual focus not supported by this camera");
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
await track.applyConstraints({
|
|
408
|
+
advanced: [
|
|
409
|
+
{
|
|
410
|
+
focusMode: "manual",
|
|
411
|
+
pointsOfInterest: [{ x: options.x, y: options.y }],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
throw new Error(`Failed to set focus point: ${e instanceof Error ? e.message : "unknown error"}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async setExposurePoint(options) {
|
|
421
|
+
if (!this.mediaStream)
|
|
422
|
+
throw new Error("Preview not started");
|
|
423
|
+
const track = this.mediaStream.getVideoTracks()[0];
|
|
424
|
+
if (!track)
|
|
425
|
+
throw new Error("No video track available");
|
|
426
|
+
// Check if exposure control is supported
|
|
427
|
+
const caps = track.getCapabilities ? track.getCapabilities() : {};
|
|
428
|
+
if (!caps.exposureMode?.includes("manual")) {
|
|
429
|
+
throw new Error("Manual exposure not supported by this camera");
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await track.applyConstraints({
|
|
433
|
+
advanced: [
|
|
434
|
+
{
|
|
435
|
+
exposureMode: "manual",
|
|
436
|
+
pointsOfInterest: [{ x: options.x, y: options.y }],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
throw new Error(`Failed to set exposure point: ${e instanceof Error ? e.message : "unknown error"}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async checkPermissions() {
|
|
446
|
+
let cameraStatus = "prompt";
|
|
447
|
+
let microphoneStatus = "prompt";
|
|
448
|
+
try {
|
|
449
|
+
const cameraResult = await navigator.permissions.query({
|
|
450
|
+
name: "camera",
|
|
451
|
+
});
|
|
452
|
+
cameraStatus = cameraResult.state;
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
console.debug("[Camera] permissions.query('camera') not supported:", err);
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const micResult = await navigator.permissions.query({
|
|
459
|
+
name: "microphone",
|
|
460
|
+
});
|
|
461
|
+
microphoneStatus = micResult.state;
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
console.debug("[Camera] permissions.query('microphone') not supported:", err);
|
|
465
|
+
}
|
|
466
|
+
// Note: Web platform doesn't have a "photos" permission concept.
|
|
467
|
+
// Photos are captured from camera stream, so camera permission covers this.
|
|
468
|
+
return {
|
|
469
|
+
camera: cameraStatus,
|
|
470
|
+
microphone: microphoneStatus,
|
|
471
|
+
photos: cameraStatus, // Photos access follows camera permission on web
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async requestPermissions() {
|
|
475
|
+
let cameraStatus = "denied";
|
|
476
|
+
let microphoneStatus = "denied";
|
|
477
|
+
try {
|
|
478
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
479
|
+
video: true,
|
|
480
|
+
audio: true,
|
|
481
|
+
});
|
|
482
|
+
stream.getTracks().forEach((track) => {
|
|
483
|
+
track.stop();
|
|
484
|
+
});
|
|
485
|
+
cameraStatus = "granted";
|
|
486
|
+
microphoneStatus = "granted";
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
try {
|
|
490
|
+
const videoStream = await navigator.mediaDevices.getUserMedia({
|
|
491
|
+
video: true,
|
|
492
|
+
});
|
|
493
|
+
videoStream.getTracks().forEach((track) => {
|
|
494
|
+
track.stop();
|
|
495
|
+
});
|
|
496
|
+
cameraStatus = "granted";
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
cameraStatus = "denied";
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const audioStream = await navigator.mediaDevices.getUserMedia({
|
|
503
|
+
audio: true,
|
|
504
|
+
});
|
|
505
|
+
audioStream.getTracks().forEach((track) => {
|
|
506
|
+
track.stop();
|
|
507
|
+
});
|
|
508
|
+
microphoneStatus = "granted";
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
microphoneStatus = "denied";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
camera: cameraStatus,
|
|
516
|
+
microphone: microphoneStatus,
|
|
517
|
+
photos: cameraStatus, // Photos access follows camera permission on web
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async addListener(eventName, listenerFunc) {
|
|
521
|
+
const entry = { eventName, callback: listenerFunc };
|
|
522
|
+
this.pluginListeners.push(entry);
|
|
523
|
+
return {
|
|
524
|
+
remove: async () => {
|
|
525
|
+
const i = this.pluginListeners.indexOf(entry);
|
|
526
|
+
if (i >= 0)
|
|
527
|
+
this.pluginListeners.splice(i, 1);
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async removeAllListeners() {
|
|
532
|
+
this.pluginListeners = [];
|
|
533
|
+
}
|
|
534
|
+
notifyListeners(eventName, data) {
|
|
535
|
+
this.pluginListeners
|
|
536
|
+
.filter((l) => l.eventName === eventName)
|
|
537
|
+
.forEach((l) => {
|
|
538
|
+
l.callback(data);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|