From 6e09c9d03795f4ca40e2eea5451c327f0b5f4862 Mon Sep 17 00:00:00 2001 From: ICheered Date: Thu, 3 Aug 2023 18:18:43 +0200 Subject: [PATCH] Create camera wrapper around functions --- package.json | 5 +- ui/camera.js | 114 +++++++++++++++++++++++++++++++++++++++++++++ ui/libapi.mjs.d.ts | 39 ++++++++++++---- 3 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 ui/camera.js diff --git a/package.json b/package.json index f94cae1..ccee349 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "web-gphoto2", "version": "0.1.0", "description": "WebAssembly implementation of gphoto2 and libusb to control DSLR cameras over USB on the Web", - "main": "ui/libapi.mjs", + "main": "ui/camera.js", "types": "ui/libapi.mjs.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -23,7 +23,8 @@ }, "homepage": "https://github.com/GoogleChromeLabs/web-gphoto2#readme", "files": [ - "ui/libapi.*" + "ui/libapi.*", + "ui/camera.js" ], "devDependencies": { "@types/emscripten": "^1.39.5", diff --git a/ui/camera.js b/ui/camera.js new file mode 100644 index 0000000..7c4a1e1 --- /dev/null +++ b/ui/camera.js @@ -0,0 +1,114 @@ +/* + * Copyright 2021 Google LLC + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + + +/** @typedef {import('./libapi.mjs').Context} Context */ + +// To avoid errors for users who use SSR or Hybrid rendering (e.g. Nuxt.js), we need to check if we're in the browser. +let initModule; + +async function initializeModule() { + if (typeof window !== 'undefined') { + const module = await import('./libapi.mjs'); + initModule = module.default; + return initModule; + } else { + console.warn("web-gphoto2 is only available in the browser"); + return null; + } +} + +const ModulePromise = initializeModule().then(initModule => { + if (initModule) { + return initModule(); + } else { + return null; + } +}); + +class Camera { + constructor() { + /** @type {Promise} */ + this.queue = Promise.resolve(); + this.Module = null; + this.context = null; + } + + rethrowIfCritical(err) { + // If it's precisely Error, it's a custom error; anything else - SyntaxError, + // WebAssembly.RuntimeError, TypeError, etc. - is treated as critical here. + if (err.constructor !== Error) { + throw err; + } + } + + async connect() { + this.Module = await ModulePromise; + this.context = await new this.Module.Context(); + } + + /** Schedules an exclusive async operation on the global context. + * @template T + * @param {(ctx: Context) => Promise} op + * @returns {Promise} + */ + async schedule(op) { + let res = this.queue.then(() => op(this.context)); + this.queue = res.catch(this.rethrowIfCritical); + return res; + } + + async disconnect() { + if (!this.context.isDeleted()) { + this.context.delete(); + } + } + + async getConfig() { + return this.schedule((context) => context.configToJS()); + } + + async getSupportedOps() { + if (this.context) { + return await this.context.supportedOps(); + } + throw new Error("You need to connect to the camera first"); + } + + async setConfigValue(name, value) { + const uiTimeout = new Promise((resolve) => setTimeout(resolve, 800)); + const setResult = this.schedule((context) => context.setConfigValue(name, value)); + // wait for both the config set operation and the timeout to complete + return Promise.all([setResult, uiTimeout]); + } + + + async capturePreviewAsBlob() { + return this.schedule((context) => context.capturePreviewAsBlob()); + } + + async captureImageAsFile() { + return this.schedule((context) => context.captureImageAsFile()); + } + + async consumeEvents() { + return this.schedule((context) => context.consumeEvents()); + } +} + +export default Camera; diff --git a/ui/libapi.mjs.d.ts b/ui/libapi.mjs.d.ts index b94c6ca..855202d 100644 --- a/ui/libapi.mjs.d.ts +++ b/ui/libapi.mjs.d.ts @@ -22,14 +22,14 @@ export type Config = { label: string; readonly: boolean; } & ( - | { type: 'range'; value: number; min: number; max: number; step: number } - | { type: 'menu' | 'radio'; value: string; choices: string[] } - | { type: 'toggle'; value: boolean } - | { type: 'text'; value: string } - | { type: 'window'; children: Record } - | { type: 'section'; children: Record } - | { type: 'datetime'; value: number } -); + | { type: 'range'; value: number; min: number; max: number; step: number } + | { type: 'menu' | 'radio'; value: string; choices: string[] } + | { type: 'toggle'; value: boolean } + | { type: 'text'; value: string } + | { type: 'window'; children: Record } + | { type: 'section'; children: Record } + | { type: 'datetime'; value: number } + ); declare interface SupportedOps { captureImage: boolean; @@ -41,6 +41,9 @@ declare interface SupportedOps { } declare class Context { + context(): Promise { + throw new Error('Method not implemented.'); + } configToJS(): Promise; setConfigValue( name: string, @@ -55,6 +58,26 @@ declare class Context { isDeleted(): boolean; } +export declare class Camera { + constructor(); + + connect(): Promise; + disconnect(): Promise; + + getConfig(): Promise; // replace "any" with the actual type of the config if you have it + + getSupportedOps(): SupportedOps; + + setConfigValue(name: string, value: number | string | boolean): Promise; + + capturePreviewAsBlob(): Promise; + captureImageAsFile(): Promise; + consumeEvents(): Promise; + + private rethrowIfCritical(err: any): void; // any error type can be replaced by more specific if available + private schedule(op: (ctx: any) => Promise): Promise; // ctx and T types can be replaced by more specific if available +} + export interface Module extends EmscriptenModule { Context: typeof Context; }