Bunch of updates in preparation for publish
This commit is contained in:
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,2 +1 @@
|
|||||||
build/* binary
|
build/* linguist-generated=true
|
||||||
build/* linguist-generated=true
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
/.vscode/c_cpp_properties.json
|
/.vscode/c_cpp_properties.json
|
||||||
/node_modules
|
node_modules
|
||||||
/deps/*
|
/deps/*
|
||||||
!/deps/libgphoto2
|
!/deps/libgphoto2
|
||||||
!/deps/libusb
|
!/deps/libusb
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -15,15 +15,15 @@ export LDFLAGS += $(COMMON_FLAGS)
|
|||||||
|
|
||||||
## Main API module
|
## Main API module
|
||||||
|
|
||||||
build/libapi.mjs: api.o $(SYSROOT)/lib/libltdl.la $(SYSROOT)/lib/libgphoto2.la
|
build/libapi.mjs: src/api.o $(SYSROOT)/lib/libltdl.la $(SYSROOT)/lib/libgphoto2.la
|
||||||
libtool --verbose --mode=link $(LD) $(LDFLAGS) -o $@ $+ \
|
libtool --verbose --mode=link $(LD) $(LDFLAGS) -o $@ $+ \
|
||||||
-fexceptions --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH \
|
-fexceptions --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH \
|
||||||
-dlpreopen $(SYSROOT)/lib/libgphoto2/2.5.28.1/ptp2.la \
|
-dlpreopen $(SYSROOT)/lib/libgphoto2/2.5.28.1/ptp2.la \
|
||||||
-dlpreopen $(SYSROOT)/lib/libgphoto2_port/0.12.0/usb1.la
|
-dlpreopen $(SYSROOT)/lib/libgphoto2_port/0.12.0/usb1.la
|
||||||
|
|
||||||
api.o: deps/libgphoto2/configure.ac
|
src/api.o: deps/libgphoto2/configure.ac
|
||||||
api.o: CPPFLAGS += -Ideps/libgphoto2 -Ideps/libgphoto2/libgphoto2_port
|
src/api.o: CPPFLAGS += -Ideps/libgphoto2 -Ideps/libgphoto2/libgphoto2_port
|
||||||
api.o: CXXFLAGS += -std=c++17 -fexceptions -pthread
|
src/api.o: CXXFLAGS += -std=c++17 -fexceptions -pthread
|
||||||
|
|
||||||
## Generic rules for deps
|
## Generic rules for deps
|
||||||
|
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -4,16 +4,14 @@ A gPhoto2 implementation using WebAssembly to control DSLR cameras from the brow
|
|||||||
|
|
||||||
Powered by a [custom fork](https://github.com/RReverser/libgphoto2) of [libgphoto2](https://github.com/gphoto/libgphoto2), the [WebUSB](https://github.com/WICG/webusb) backend of [libusb](https://github.com/libusb/libusb), and WebAssembly via [Emscripten](https://emscripten.org/).
|
Powered by a [custom fork](https://github.com/RReverser/libgphoto2) of [libgphoto2](https://github.com/gphoto/libgphoto2), the [WebUSB](https://github.com/WICG/webusb) backend of [libusb](https://github.com/libusb/libusb), and WebAssembly via [Emscripten](https://emscripten.org/).
|
||||||
|
|
||||||
# NPM
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install web-gphoto2
|
npm install web-gphoto2
|
||||||
// or
|
|
||||||
yarn add web-gphoto2
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
A short example on how to use this package:
|
A short example on how to use this package:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -55,7 +53,7 @@ async function captureImageAsFile() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Demo
|
## Demo
|
||||||
|
|
||||||
This repository also contains a [demo app](https://web.dev/porting-libusb-to-webusb/) running gPhoto2 on the Web:
|
This repository also contains a [demo app](https://web.dev/porting-libusb-to-webusb/) running gPhoto2 on the Web:
|
||||||

|

|
||||||
@@ -64,20 +62,21 @@ For the detailed technical write-up, see [the official blog post](https://web.de
|
|||||||
|
|
||||||
If you don't have a DSLR, you can check out a recording of the demo below:
|
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
|
<https://user-images.githubusercontent.com/557590/152155035-a1664656-a7d9-411f-8cb3-5f04320f1391.mp4>
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
To build, you'll need Docker. Then:
|
To build the WebAssembly part of the repo, you'll need Docker. Then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh # runs build in Docker
|
./build.sh # runs build in Docker
|
||||||
npx serve examples/preact # starts a local server with COOP/COEP
|
npx serve examples/preact # starts a local server with COOP/COEP
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, navigate to http://localhost:3000/ in Chrome.
|
Then, navigate to <http://localhost:3000/> in Chrome.
|
||||||
|
|
||||||
## Common Issues
|
## Common Issues
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
SharedArrayBuffer can not be found
|
SharedArrayBuffer can not be found
|
||||||
@@ -123,6 +122,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|||||||
24
build/camera.d.ts
generated
vendored
Normal file
24
build/camera.d.ts
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export type { Config, SupportedOps } from '../build/libapi.mjs';
|
||||||
|
export declare function rethrowIfCritical(err: any): void;
|
||||||
|
export declare class Camera {
|
||||||
|
#private;
|
||||||
|
static showPicker(): Promise<void>;
|
||||||
|
connect(): Promise<void>;
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
getConfig(): Promise<{
|
||||||
|
name: string;
|
||||||
|
info: string;
|
||||||
|
label: string;
|
||||||
|
readonly: boolean;
|
||||||
|
} & {
|
||||||
|
type: "window";
|
||||||
|
children: Record<string, import("../build/libapi.mjs").Config>;
|
||||||
|
} & {
|
||||||
|
type: "window";
|
||||||
|
}>;
|
||||||
|
getSupportedOps(): Promise<import("../build/libapi.mjs").SupportedOps>;
|
||||||
|
setConfigValue(name: string, value: string | number | boolean): Promise<void>;
|
||||||
|
capturePreviewAsBlob(): Promise<Blob>;
|
||||||
|
captureImageAsFile(): Promise<File>;
|
||||||
|
consumeEvents(): Promise<boolean>;
|
||||||
|
}
|
||||||
89
build/camera.js
generated
Normal file
89
build/camera.js
generated
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
import initModule from '../build/libapi.mjs';
|
||||||
|
// A helper that allows to distinguish critical errors from library errors.
|
||||||
|
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) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const INTERFACE_CLASS = 6; // PTP
|
||||||
|
const INTERFACE_SUBCLASS = 1; // MTP
|
||||||
|
let ModulePromise;
|
||||||
|
export class Camera {
|
||||||
|
#queue = Promise.resolve();
|
||||||
|
#context = null;
|
||||||
|
static async showPicker() {
|
||||||
|
// @ts-ignore
|
||||||
|
await navigator.usb.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
classCode: INTERFACE_CLASS,
|
||||||
|
subclassCode: INTERFACE_SUBCLASS
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async connect() {
|
||||||
|
if (!ModulePromise) {
|
||||||
|
ModulePromise = initModule();
|
||||||
|
}
|
||||||
|
let Module = await ModulePromise;
|
||||||
|
this.#context = await new Module.Context();
|
||||||
|
}
|
||||||
|
async #schedule(op) {
|
||||||
|
let res = this.#queue.then(() => op(this.#context));
|
||||||
|
this.#queue = res.catch(rethrowIfCritical);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
async disconnect() {
|
||||||
|
if (this.#context && !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) {
|
||||||
|
let uiTimeout;
|
||||||
|
await this.#schedule(context => {
|
||||||
|
// This is terrible, yes... but some configs return too quickly before they're actually updated.
|
||||||
|
// We want to wait some time before updating the UI in that case, but not block subsequent ops.
|
||||||
|
uiTimeout = new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
return context.setConfigValue(name, value);
|
||||||
|
});
|
||||||
|
await 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/libapi.mjs.d.ts → build/libapi.mjs.d.ts
generated
vendored
130
src/libapi.mjs.d.ts → build/libapi.mjs.d.ts
generated
vendored
@@ -1,65 +1,65 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2021 Google LLC
|
* Copyright 2021 Google LLC
|
||||||
*
|
*
|
||||||
* This library is free software; you can redistribute it and/or
|
* This library is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
* License as published by the Free Software Foundation; either
|
* License as published by the Free Software Foundation; either
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
* 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,
|
* This library is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
* Lesser General Public License for more details.
|
* Lesser General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
* License along with this library; if not, write to the Free Software
|
* License along with this library; if not, write to the Free Software
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
name: string;
|
name: string;
|
||||||
info: string;
|
info: string;
|
||||||
label: string;
|
label: string;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
} & (
|
} & (
|
||||||
| { type: 'range'; value: number; min: number; max: number; step: number }
|
| { type: 'range'; value: number; min: number; max: number; step: number }
|
||||||
| { type: 'menu' | 'radio'; value: string; choices: string[] }
|
| { type: 'menu' | 'radio'; value: string; choices: string[] }
|
||||||
| { type: 'toggle'; value: boolean }
|
| { type: 'toggle'; value: boolean }
|
||||||
| { type: 'text'; value: string }
|
| { type: 'text'; value: string }
|
||||||
| { type: 'window'; children: Record<string, Config> }
|
| { type: 'window'; children: Record<string, Config> }
|
||||||
| { type: 'section'; children: Record<string, Config> }
|
| { type: 'section'; children: Record<string, Config> }
|
||||||
| { type: 'datetime'; value: number }
|
| { type: 'datetime'; value: number }
|
||||||
);
|
);
|
||||||
|
|
||||||
declare interface SupportedOps {
|
declare interface SupportedOps {
|
||||||
captureImage: boolean;
|
captureImage: boolean;
|
||||||
captureVideo: boolean;
|
captureVideo: boolean;
|
||||||
captureAudio: boolean;
|
captureAudio: boolean;
|
||||||
capturePreview: boolean;
|
capturePreview: boolean;
|
||||||
config: boolean;
|
config: boolean;
|
||||||
triggerCapture: boolean;
|
triggerCapture: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class Context {
|
declare class Context {
|
||||||
configToJS(): Promise<Config & { type: 'window' }>;
|
configToJS(): Promise<Config & { type: 'window' }>;
|
||||||
setConfigValue(
|
setConfigValue(
|
||||||
name: string,
|
name: string,
|
||||||
value: number | string | boolean
|
value: number | string | boolean
|
||||||
): Promise<undefined>;
|
): Promise<undefined>;
|
||||||
capturePreviewAsBlob(): Promise<Blob>;
|
capturePreviewAsBlob(): Promise<Blob>;
|
||||||
captureImageAsFile(): Promise<File>;
|
captureImageAsFile(): Promise<File>;
|
||||||
consumeEvents(): Promise<boolean>;
|
consumeEvents(): Promise<boolean>;
|
||||||
supportedOps(): SupportedOps;
|
supportedOps(): SupportedOps;
|
||||||
|
|
||||||
delete(): void;
|
delete(): void;
|
||||||
isDeleted(): boolean;
|
isDeleted(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Module extends EmscriptenModule {
|
export interface Module extends EmscriptenModule {
|
||||||
Context: typeof Context;
|
Context: typeof Context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { Config, Context, SupportedOps };
|
export type { Config, Context, SupportedOps };
|
||||||
|
|
||||||
declare const initModule: EmscriptenModuleFactory<Module>;
|
declare const initModule: EmscriptenModuleFactory<Module>;
|
||||||
export default initModule;
|
export default initModule;
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../build
|
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"web-gphoto2": "../../src/camera.js",
|
||||||
"preact": "https://unpkg.com/preact@10.6.4/dist/preact.module.js",
|
"preact": "https://unpkg.com/preact@10.6.4/dist/preact.module.js",
|
||||||
"preact/debug": "https://unpkg.com/preact@10.6.4/debug/dist/debug.module.js",
|
"preact/debug": "https://unpkg.com/preact@10.6.4/debug/dist/debug.module.js",
|
||||||
"preact/devtools": "https://unpkg.com/preact@10.6.4/devtools/dist/devtools.module.js",
|
"preact/devtools": "https://unpkg.com/preact@10.6.4/devtools/dist/devtools.module.js",
|
||||||
|
|||||||
@@ -18,29 +18,21 @@
|
|||||||
|
|
||||||
import { h, render, Component } from 'preact';
|
import { h, render, Component } from 'preact';
|
||||||
import { CaptureButton } from './capture-button.js';
|
import { CaptureButton } from './capture-button.js';
|
||||||
import { connect, rethrowIfCritical } from './ops.js';
|
import { Camera, rethrowIfCritical } from 'web-gphoto2';
|
||||||
import { Preview } from './preview.js';
|
import { Preview } from './preview.js';
|
||||||
import { Widget } from './widget.js';
|
import { Widget } from './widget.js';
|
||||||
|
|
||||||
/** @typedef {import('./build/libapi.mjs').Context} Context */
|
|
||||||
/** @typedef {import('./build/libapi.mjs').Config} Config */
|
|
||||||
/** @typedef {import('./ops.js').Connection} Connection */
|
|
||||||
|
|
||||||
export const isDebug = new URLSearchParams(location.search).has('debug');
|
export const isDebug = new URLSearchParams(location.search).has('debug');
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
|
// @ts-ignore
|
||||||
await import('preact/debug');
|
await import('preact/debug');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @typedef {{ type: 'CameraPicker' } | { type: 'Status', message: string } | { type: 'Config', config: Config }} AppState */
|
/** @extends Component<{}, AppState> */
|
||||||
|
|
||||||
const INTERFACE_CLASS = 6; // PTP
|
|
||||||
const INTERFACE_SUBCLASS = 1; // MTP
|
|
||||||
|
|
||||||
/** @extends Component<null, AppState> */
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
/** @type {Connection} */
|
/** @type {Camera | undefined} */
|
||||||
connection;
|
camera;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
addEventListener('error', ({ message }) =>
|
addEventListener('error', ({ message }) =>
|
||||||
@@ -52,9 +44,9 @@ class App extends Component {
|
|||||||
addEventListener(
|
addEventListener(
|
||||||
'beforeunload',
|
'beforeunload',
|
||||||
() => {
|
() => {
|
||||||
if (!this.connection) return;
|
if (!this.camera) return;
|
||||||
this.connection.disconnect();
|
this.camera.disconnect();
|
||||||
this.connection = undefined;
|
this.camera = undefined;
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
@@ -66,39 +58,45 @@ class App extends Component {
|
|||||||
|
|
||||||
selectDevice = async () => {
|
selectDevice = async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await navigator.usb.requestDevice({
|
await Camera.showPicker();
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
classCode: INTERFACE_CLASS,
|
|
||||||
subclassCode: INTERFACE_SUBCLASS
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
this.setState({ type: 'Status', message: '⌛ Connecting...' });
|
this.setState({ type: 'Status', message: '⌛ Connecting...' });
|
||||||
await this.tryToConnectToCamera();
|
await this.tryToConnectToCamera();
|
||||||
};
|
};
|
||||||
|
|
||||||
async tryToConnectToCamera() {
|
async tryToConnectToCamera() {
|
||||||
|
/** @type {Camera} */
|
||||||
|
let camera;
|
||||||
try {
|
try {
|
||||||
this.connection = await connect();
|
camera = new Camera();
|
||||||
|
await camera.connect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
this.setState({ type: 'CameraPicker' });
|
this.setState({ type: 'CameraPicker' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.camera = camera;
|
||||||
|
let supportedOps = await camera.getSupportedOps();
|
||||||
|
let capturePreview;
|
||||||
|
if (supportedOps.capturePreview) {
|
||||||
|
capturePreview = () => camera.capturePreviewAsBlob();
|
||||||
|
}
|
||||||
|
let triggerCapture;
|
||||||
|
if (supportedOps.captureImage) {
|
||||||
|
triggerCapture = () => camera.captureImageAsFile();
|
||||||
|
}
|
||||||
// We should reach this only once.
|
// We should reach this only once.
|
||||||
while (this.connection) {
|
while (this.camera) {
|
||||||
try {
|
try {
|
||||||
let config = await this.connection.schedule(context =>
|
let config = await this.camera.getConfig();
|
||||||
context.configToJS()
|
|
||||||
);
|
|
||||||
if (!isDebug) {
|
if (!isDebug) {
|
||||||
delete config.children.actions;
|
delete config.children.actions;
|
||||||
delete config.children.other;
|
delete config.children.other;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
type: 'Config',
|
type: 'Config',
|
||||||
config
|
config,
|
||||||
|
capturePreview,
|
||||||
|
triggerCapture
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
rethrowIfCritical(err);
|
rethrowIfCritical(err);
|
||||||
@@ -109,9 +107,7 @@ class App extends Component {
|
|||||||
requestIdleCallback(resolve, { timeout: 500 })
|
requestIdleCallback(resolve, { timeout: 500 })
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
let hadEvents = await this.connection.schedule(context =>
|
let hadEvents = await this.camera.consumeEvents();
|
||||||
context.consumeEvents()
|
|
||||||
);
|
|
||||||
if (hadEvents) {
|
if (hadEvents) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -128,23 +124,7 @@ class App extends Component {
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {*} value
|
* @param {*} value
|
||||||
*/
|
*/
|
||||||
setValue = async (name, value) => {
|
setValue = async (name, value) => this.camera?.setConfigValue(name, value);
|
||||||
/** @type {Promise<void>} */
|
|
||||||
let uiTimeout;
|
|
||||||
await this.connection.schedule(context => {
|
|
||||||
// This is terrible, yes... but some configs return too quickly before they're actually updated.
|
|
||||||
// We want to wait some time before updating the UI in that case, but not block subsequent ops.
|
|
||||||
uiTimeout = new Promise(resolve => setTimeout(resolve, 800));
|
|
||||||
return context.setConfigValue(name, value);
|
|
||||||
});
|
|
||||||
await uiTimeout;
|
|
||||||
};
|
|
||||||
|
|
||||||
capturePreview = () =>
|
|
||||||
this.connection.schedule(context => context.capturePreviewAsBlob());
|
|
||||||
|
|
||||||
captureImage = () =>
|
|
||||||
this.connection.schedule(context => context.captureImageAsFile());
|
|
||||||
|
|
||||||
render(/** @type {App['props']} */ props, /** @type {App['state']} */ state) {
|
render(/** @type {App['props']} */ props, /** @type {App['state']} */ state) {
|
||||||
switch (state.type) {
|
switch (state.type) {
|
||||||
@@ -195,9 +175,7 @@ class App extends Component {
|
|||||||
'div',
|
'div',
|
||||||
{ class: 'pure-u-2-3' },
|
{ class: 'pure-u-2-3' },
|
||||||
h(Preview, {
|
h(Preview, {
|
||||||
getPreview: this.connection.supportedOps.capturePreview
|
getPreview: state.capturePreview
|
||||||
? this.capturePreview
|
|
||||||
: undefined
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
@@ -209,8 +187,8 @@ class App extends Component {
|
|||||||
h(
|
h(
|
||||||
'fieldset',
|
'fieldset',
|
||||||
null,
|
null,
|
||||||
this.connection.supportedOps.triggerCapture
|
state.triggerCapture
|
||||||
? h(CaptureButton, { getFile: this.captureImage })
|
? h(CaptureButton, { getFile: state.triggerCapture })
|
||||||
: undefined,
|
: undefined,
|
||||||
' ',
|
' ',
|
||||||
h(
|
h(
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
import initModule from './build/libapi.mjs';
|
|
||||||
|
|
||||||
/** @typedef {import('../libapi.mjs').Context} Context */
|
|
||||||
|
|
||||||
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) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function connect() {
|
|
||||||
const Module = await ModulePromise;
|
|
||||||
|
|
||||||
let context = await new Module.Context();
|
|
||||||
let supportedOps = await context.supportedOps();
|
|
||||||
|
|
||||||
/** @type {Promise<unknown>} */
|
|
||||||
let queue = Promise.resolve();
|
|
||||||
|
|
||||||
/** Schedules an exclusive async operation on the global context.
|
|
||||||
* @template T
|
|
||||||
* @param {(ctx: Context) => Promise<T>} op
|
|
||||||
* @returns {Promise<T>}
|
|
||||||
*/
|
|
||||||
function schedule(op) {
|
|
||||||
let res = queue.then(() => op(context));
|
|
||||||
queue = res.catch(rethrowIfCritical);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
supportedOps,
|
|
||||||
schedule,
|
|
||||||
disconnect() {
|
|
||||||
context.delete();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @typedef {ReturnType<typeof connect> extends Promise<infer Connection> ? Connection : never} Connection */
|
|
||||||
75
examples/preact/package-lock.json
generated
Normal file
75
examples/preact/package-lock.json
generated
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"name": "preact",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/requestidlecallback": "^0.3.4",
|
||||||
|
"@types/stats.js": "^0.17.0",
|
||||||
|
"preact": "^10.5.14",
|
||||||
|
"web-gphoto2": "file:../../"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../..": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "LGPL-2.1-or-later",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/emscripten": "^1.39.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/requestidlecallback": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/requestidlecallback/-/requestidlecallback-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-Uh49VrVTPfU0y/qIvXXYuRmd/sKLfVgQWZU1t8FWH22AIJyQbCei1aSmXdMDAijwGUFhBDpJmksiHEDsfiE/cg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.16.0.tgz",
|
||||||
|
"integrity": "sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-gphoto2": {
|
||||||
|
"resolved": "../..",
|
||||||
|
"link": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/requestidlecallback": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/requestidlecallback/-/requestidlecallback-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-Uh49VrVTPfU0y/qIvXXYuRmd/sKLfVgQWZU1t8FWH22AIJyQbCei1aSmXdMDAijwGUFhBDpJmksiHEDsfiE/cg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/stats.js": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"preact": {
|
||||||
|
"version": "10.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.16.0.tgz",
|
||||||
|
"integrity": "sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"web-gphoto2": {
|
||||||
|
"version": "file:../..",
|
||||||
|
"requires": {
|
||||||
|
"@types/emscripten": "^1.39.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
examples/preact/package.json
Normal file
9
examples/preact/package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"web-gphoto2": "file:../../",
|
||||||
|
"@types/requestidlecallback": "^0.3.4",
|
||||||
|
"@types/stats.js": "^0.17.0",
|
||||||
|
"preact": "^10.5.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
examples/preact/tsconfig.json
Normal file
9
examples/preact/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"checkJs": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
9
examples/preact/types.d.ts
vendored
Normal file
9
examples/preact/types.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
type AppState =
|
||||||
|
| { type: 'CameraPicker' }
|
||||||
|
| { type: 'Status'; message: string }
|
||||||
|
| {
|
||||||
|
type: 'Config';
|
||||||
|
config: import('web-gphoto2').Config;
|
||||||
|
capturePreview: (() => Promise<Blob>) | undefined;
|
||||||
|
triggerCapture: (() => Promise<File>) | undefined;
|
||||||
|
};
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import { h, Component, createRef } from 'preact';
|
import { h, Component, createRef } from 'preact';
|
||||||
|
|
||||||
/** @typedef {import('./build/libapi.mjs').Config} Config */
|
/** @typedef {import('web-gphoto2').Config} Config */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Config} config
|
* @param {Config} config
|
||||||
|
|||||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -1,6 +1,39 @@
|
|||||||
{
|
{
|
||||||
|
"name": "web-gphoto2",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"lockfileVersion": 1,
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "web-gphoto2",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "LGPL-2.1-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"typescript": "^5.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/emscripten": "^1.39.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/emscripten": {
|
||||||
|
"version": "1.39.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.5.tgz",
|
||||||
|
"integrity": "sha512-DIOOg+POSrYl+OlNRHQuIEqCd8DCtynG57H862UCce16nXJX7J8eWxNGgOcf8Eyge8zXeSs27mz1UcFu8L/L7g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/emscripten": {
|
"@types/emscripten": {
|
||||||
"version": "1.39.5",
|
"version": "1.39.5",
|
||||||
@@ -8,23 +41,10 @@
|
|||||||
"integrity": "sha512-DIOOg+POSrYl+OlNRHQuIEqCd8DCtynG57H862UCce16nXJX7J8eWxNGgOcf8Eyge8zXeSs27mz1UcFu8L/L7g==",
|
"integrity": "sha512-DIOOg+POSrYl+OlNRHQuIEqCd8DCtynG57H862UCce16nXJX7J8eWxNGgOcf8Eyge8zXeSs27mz1UcFu8L/L7g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/requestidlecallback": {
|
"typescript": {
|
||||||
"version": "0.3.4",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/requestidlecallback/-/requestidlecallback-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
|
||||||
"integrity": "sha512-aTSyiZuRemRLTQkJPb25L7A4/eR2Teo5l4yJ1V6P3+MFxEZckTDkNKNtr/V1zEOMzS6H8DgxF22U6jPAPrzQvw==",
|
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA=="
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/stats.js": {
|
|
||||||
"version": "0.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz",
|
|
||||||
"integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"preact": {
|
|
||||||
"version": "10.5.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.14.tgz",
|
|
||||||
"integrity": "sha512-KojoltCrshZ099ksUZ2OQKfbH66uquFoxHSbnwKbTJHeQNvx42EmC7wQVWNuDt6vC5s3nudRHFtKbpY4ijKlaQ==",
|
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "web-gphoto2",
|
"name": "web-gphoto2",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"description": "WebAssembly implementation of gphoto2 and libusb to control DSLR cameras over USB on the Web",
|
"description": "WebAssembly implementation of gphoto2 and libusb to control DSLR cameras over USB on the Web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/camera.js",
|
"main": "build/camera.js",
|
||||||
"types": "src/types.d.ts",
|
"types": "build/camera.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"build:ts": "tsc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -35,13 +35,12 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/GoogleChromeLabs/web-gphoto2#readme",
|
"homepage": "https://github.com/GoogleChromeLabs/web-gphoto2#readme",
|
||||||
"files": [
|
"files": [
|
||||||
"src/*",
|
|
||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/emscripten": "^1.39.5",
|
"@types/emscripten": "^1.39.5"
|
||||||
"@types/requestidlecallback": "^0.3.4",
|
},
|
||||||
"@types/stats.js": "^0.17.0",
|
"dependencies": {
|
||||||
"preact": "^10.5.14"
|
"typescript": "^5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
src/camera.js
103
src/camera.js
@@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
import initModule from "../build/libapi.mjs";
|
|
||||||
|
|
||||||
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) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const INTERFACE_CLASS = 6; // PTP
|
|
||||||
const INTERFACE_SUBCLASS = 1; // MTP
|
|
||||||
|
|
||||||
let ModulePromise = null;
|
|
||||||
|
|
||||||
export class Camera {
|
|
||||||
constructor() {
|
|
||||||
this.queue = Promise.resolve();
|
|
||||||
this.Module = null;
|
|
||||||
this.context = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async showPicker() {
|
|
||||||
// @ts-ignore
|
|
||||||
await navigator.usb.requestDevice({
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
classCode: INTERFACE_CLASS,
|
|
||||||
subclassCode: INTERFACE_SUBCLASS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect() {
|
|
||||||
if (!ModulePromise) {
|
|
||||||
ModulePromise = initModule()
|
|
||||||
}
|
|
||||||
this.Module = await ModulePromise;
|
|
||||||
this.context = await new this.Module.Context();
|
|
||||||
}
|
|
||||||
|
|
||||||
async schedule(op) {
|
|
||||||
let res = this.queue.then(() => op(this.context));
|
|
||||||
this.queue = res.catch(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)
|
|
||||||
);
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
106
src/camera.ts
Normal file
106
src/camera.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
import initModule, { Context, Module } from '../build/libapi.mjs';
|
||||||
|
|
||||||
|
export type { Config, SupportedOps } from '../build/libapi.mjs';
|
||||||
|
|
||||||
|
// A helper that allows to distinguish critical errors from library errors.
|
||||||
|
export function rethrowIfCritical(err: any) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const INTERFACE_CLASS = 6; // PTP
|
||||||
|
const INTERFACE_SUBCLASS = 1; // MTP
|
||||||
|
|
||||||
|
let ModulePromise: Promise<Module>;
|
||||||
|
|
||||||
|
export class Camera {
|
||||||
|
#queue: Promise<any> = Promise.resolve();
|
||||||
|
#context: Context | null = null;
|
||||||
|
|
||||||
|
static async showPicker() {
|
||||||
|
// @ts-ignore
|
||||||
|
await navigator.usb.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
classCode: INTERFACE_CLASS,
|
||||||
|
subclassCode: INTERFACE_SUBCLASS
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (!ModulePromise) {
|
||||||
|
ModulePromise = initModule();
|
||||||
|
}
|
||||||
|
let Module = await ModulePromise;
|
||||||
|
this.#context = await new Module.Context();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #schedule<T>(op: (context: Context) => Promise<T>): Promise<T> {
|
||||||
|
let res = this.#queue.then(() => op(this.#context!));
|
||||||
|
this.#queue = res.catch(rethrowIfCritical);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
if (this.#context && !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: string, value: string | number | boolean) {
|
||||||
|
let uiTimeout: Promise<void> | undefined;
|
||||||
|
await this.#schedule(context => {
|
||||||
|
// This is terrible, yes... but some configs return too quickly before they're actually updated.
|
||||||
|
// We want to wait some time before updating the UI in that case, but not block subsequent ops.
|
||||||
|
uiTimeout = new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
return context.setConfigValue(name, value);
|
||||||
|
});
|
||||||
|
await 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/types.d.ts
vendored
25
src/types.d.ts
vendored
@@ -1,25 +0,0 @@
|
|||||||
// Import types Context and SupportedOps ./libapi.mjs.d.ts
|
|
||||||
import type { Config, Context, SupportedOps } from './libapi.mjs.d.ts';
|
|
||||||
|
|
||||||
export declare class Camera {
|
|
||||||
constructor();
|
|
||||||
|
|
||||||
static showPicker(): Promise<void>;
|
|
||||||
connect(): Promise<void>;
|
|
||||||
disconnect(): Promise<void>;
|
|
||||||
|
|
||||||
getConfig(): Promise<Config>;
|
|
||||||
|
|
||||||
getSupportedOps(): SupportedOps;
|
|
||||||
|
|
||||||
setConfigValue(name: string, value: number | string | boolean): Promise<void>;
|
|
||||||
|
|
||||||
capturePreviewAsBlob(): Promise<Blob>;
|
|
||||||
captureImageAsFile(): Promise<File>;
|
|
||||||
consumeEvents(): Promise<boolean>;
|
|
||||||
|
|
||||||
private rethrowIfCritical(err: any): void; // any error type can be replaced by more specific if available
|
|
||||||
private schedule<T>(op: (ctx: any) => Promise<T>): Promise<T>; // ctx and T types can be replaced by more specific if available
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { Config, Context, SupportedOps };
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"checkJs": true,
|
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"noEmit": true
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "build"
|
||||||
},
|
},
|
||||||
"exclude": ["libapi.*"]
|
"include": ["src/camera.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user