From b0c044aa3a4085900ebe16cb31c3c0755bba597f Mon Sep 17 00:00:00 2001 From: ICheered Date: Sat, 5 Aug 2023 13:43:44 +0200 Subject: [PATCH] Fixes for review --- .gitignore | 1 + README.md | 54 +++++++++++++------------- examples/preact/ops.js | 6 +-- examples/preact/preview.js | 10 ++--- package.json | 4 +- src/camera.js | 77 +++++++++++++++++++++++++------------- src/libapi.mjs.d.ts | 25 +------------ 7 files changed, 92 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index 242d092..3c7a763 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ !/deps/libgphoto2 !/deps/libusb /api.o +examples/svelte # Local Netlify folder .netlify diff --git a/README.md b/README.md index c9722be..280c381 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ Powered by a [custom fork](https://github.com/RReverser/libgphoto2) of [libgphot # NPM ## Installation - ```bash npm install web-gphoto2 // or @@ -15,7 +14,6 @@ yarn add web-gphoto2 ``` ## Usage - A short example on how to use this package: ```ts @@ -43,16 +41,42 @@ async function updateConfig() { } async function capturePreviewAsBlob() { + // Capture a frame while in live view mode const blob = await camera.capturePreviewAsBlob(); - console.log("Blob:", blob); + imageUrl = URL.createObjectURL(blob); + // Set the imageUrl as the src of an image element in your HTML } async function captureImageAsFile() { + // Capture an image const file = await camera.captureImageAsFile(); - console.log("File:", file); + imageUrl = URL.createObjectURL(file); + // Set the imageUrl as the src of an image element in your HTML } ``` +# Demo + +This repository also contains a [demo app](https://web.dev/porting-libusb-to-webusb/) running gPhoto2 on the Web: +![A picture of DSLR camera connected via a USB cable to a laptop. The laptop is running the Web demo mentioned in the article, which mirrors a live video feed from the camera as well as allows to tweak its settings via form controls.](https://web-dev.imgix.net/image/9oK23mr86lhFOwKaoYZ4EySNFp02/MR4YGRvl0Z9AWT6vv3sQ.jpg?auto=format&w=1600) + +For the detailed technical write-up, see [the official blog post](https://web.dev/porting-libusb-to-webusb/). To see the demo in action, visit the hosted version [here](https://web-gphoto2.rreverser.com/) (but make sure to read the [cross-platform compatibility notes](https://web.dev/porting-libusb-to-webusb/#important-cross-platform-compatibility-notes) first). + +If you don't have a DSLR, you can check out a recording of the demo below: + +https://user-images.githubusercontent.com/557590/152155035-a1664656-a7d9-411f-8cb3-5f04320f1391.mp4 + +## Building + +To build, you'll need Docker. Then: + +```bash +./build.sh # runs build in Docker +npx serve examples/preact # starts a local server with COOP/COEP +``` + +Then, navigate to http://localhost:3000/ in Chrome. + ## Common Issues ### SharedArrayBuffer can not be found @@ -96,28 +120,6 @@ export default defineConfig({ }); ``` -# Demo - -This repository also contains a [demo app](https://web.dev/porting-libusb-to-webusb/) running gPhoto2 on the Web: -![A picture of DSLR camera connected via a USB cable to a laptop. The laptop is running the Web demo mentioned in the article, which mirrors a live video feed from the camera as well as allows to tweak its settings via form controls.](https://web-dev.imgix.net/image/9oK23mr86lhFOwKaoYZ4EySNFp02/MR4YGRvl0Z9AWT6vv3sQ.jpg?auto=format&w=1600) - -For the detailed technical write-up, see [the official blog post](https://web.dev/porting-libusb-to-webusb/). To see the demo in action, visit the hosted version [here](https://web-gphoto2.rreverser.com/) (but make sure to read the [cross-platform compatibility notes](https://web.dev/porting-libusb-to-webusb/#important-cross-platform-compatibility-notes) first). - -If you don't have a DSLR, you can check out a recording of the demo below: - -https://user-images.githubusercontent.com/557590/152155035-a1664656-a7d9-411f-8cb3-5f04320f1391.mp4 - -## Building - -To build, you'll need Docker. Then: - -```bash -./build.sh # runs build in Docker -npx serve examples/preact # starts a local server with COOP/COEP -``` - -Then, navigate to http://localhost:3000/ in Chrome. - ## See also [RReverser/eos-remote-web](https://github.com/RReverser/eos-remote-web) - my other project for controlling Canon cameras over Web Bluetooth. diff --git a/examples/preact/ops.js b/examples/preact/ops.js index 566e264..7294f75 100644 --- a/examples/preact/ops.js +++ b/examples/preact/ops.js @@ -25,8 +25,7 @@ const ModulePromise = initModule(); export function 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) - { + if (err.constructor !== Error) { throw err; } } @@ -45,8 +44,7 @@ export async function connect() { * @param {(ctx: Context) => Promise} op * @returns {Promise} */ - function schedule(op) - { + function schedule(op) { let res = queue.then(() => op(context)); queue = res.catch(rethrowIfCritical); return res; diff --git a/examples/preact/preview.js b/examples/preact/preview.js index 7d6eaa1..c66d8a6 100644 --- a/examples/preact/preview.js +++ b/examples/preact/preview.js @@ -23,8 +23,8 @@ export const isDebug = new URLSearchParams(location.search).has('debug'); const Stats = isDebug ? await import('stats.js').then( - res => /** @type {typeof import('stats.js')} */ (res['default']) - ) + res => /** @type {typeof import('stats.js')} */(res['default']) + ) : null; /** @extends Component<{ getPreview?: () => Promise }, { error?: string }> */ @@ -96,9 +96,9 @@ export class Preview extends Component { blob, ratio ? { - resizeWidth: canvas.width, - resizeHeight: canvas.height - } + resizeWidth: canvas.width, + resizeHeight: canvas.height + } : {} ); if (!ratio) { diff --git a/package.json b/package.json index b76f041..f35dbe4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "web-gphoto2", - "version": "1.0.0", + "name": "web-gphoto2-npm-test", + "version": "0.14.0", "description": "WebAssembly implementation of gphoto2 and libusb to control DSLR cameras over USB on the Web", "type": "module", "main": "src/camera.js", diff --git a/src/camera.js b/src/camera.js index d6c7585..537ff3c 100644 --- a/src/camera.js +++ b/src/camera.js @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2023 Google LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,21 +16,12 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ -/** @typedef {import('./libapi.mjs.d.ts').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("../build/libapi.mjs"); - initModule = module.default; - return initModule; - } else { - console.warn("web-gphoto2 is only available in the browser"); - return null; - } -} +/** + * @typedef {import('./libapi.mjs').Config} Config + * @typedef {import('./libapi.mjs').SupportedOps} SupportedOps + * @typedef {import('./libapi.mjs').Context} Context + */ +import initModule from "../build/libapi.mjs"; export function rethrowIfCritical(err) { // If it's precisely Error, it's a custom error; anything else - SyntaxError, @@ -43,6 +34,9 @@ export function rethrowIfCritical(err) { const INTERFACE_CLASS = 6; // PTP const INTERFACE_SUBCLASS = 1; // MTP +/** + * This class provides methods for interacting with the camera. + */ class Camera { constructor() { /** @type {Promise} */ @@ -52,6 +46,10 @@ class Camera { this.ModulePromise = null; } + /** + * This method shows the camera picker. + * @returns {Promise} + */ async showCameraPicker() { // @ts-ignore await navigator.usb.requestDevice({ @@ -64,24 +62,24 @@ class Camera { }); } + /** + * This method connects to the camera. + * @returns {Promise} + */ async connect() { if (!this.ModulePromise) { - this.ModulePromise = initializeModule().then((initModule) => { - if (initModule) { - return initModule(); - } else { - return null; - } - }); + this.ModulePromise = initModule() } this.Module = await this.ModulePromise; this.context = await new this.Module.Context(); } - /** Schedules an exclusive async operation on the global context. + /** + * This method schedules an exclusive async operation on the global context. * @template T * @param {(ctx: Context) => Promise} op * @returns {Promise} + * @private */ async schedule(op) { let res = this.queue.then(() => op(this.context)); @@ -89,16 +87,28 @@ class Camera { return res; } + /** + * This method disconnects from the camera. + * @returns {Promise} + */ async disconnect() { if (!this.context.isDeleted()) { this.context.delete(); } } + /** + * This method gets the camera configuration. + * @returns {Promise} + */ async getConfig() { return this.schedule((context) => context.configToJS()); } + /** + * This method gets the supported operations of the camera. + * @returns {Promise} + */ async getSupportedOps() { if (this.context) { return await this.context.supportedOps(); @@ -106,23 +116,40 @@ class Camera { throw new Error("You need to connect to the camera first"); } + /** + * This method sets a configuration value on the camera. + * @param {string} name + * @param {number | string | boolean} value + * @returns {Promise} + */ 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]); } + /** + * This method captures a preview as a Blob. + * @returns {Promise} + */ async capturePreviewAsBlob() { return this.schedule((context) => context.capturePreviewAsBlob()); } + /** + * This method captures an image as a File. + * @returns {Promise} + */ async captureImageAsFile() { return this.schedule((context) => context.captureImageAsFile()); } + /** + * This method consumes camera events. + * @returns {Promise} + */ async consumeEvents() { return this.schedule((context) => context.consumeEvents()); } diff --git a/src/libapi.mjs.d.ts b/src/libapi.mjs.d.ts index c73dbc9..5bbce49 100644 --- a/src/libapi.mjs.d.ts +++ b/src/libapi.mjs.d.ts @@ -16,7 +16,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ -export type Config = { +type Config = { name: string; info: string; label: string; @@ -58,32 +58,11 @@ declare class Context { isDeleted(): boolean; } -export declare class Camera { - constructor(); - - showCameraPicker(): Promise; - 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; } -export type { Context }; +export type { Config, Context, SupportedOps }; declare const initModule: EmscriptenModuleFactory; export default initModule;