diff --git a/README.md b/README.md index d8a8901a..6c9710aa 100644 --- a/README.md +++ b/README.md @@ -216,8 +216,7 @@ The wasm binary won't be fetched or instantiated unless a [read](#readbarcodesfr import { getZXingModule } from "zxing-wasm"; /** - * This function will trigger the download and - * instantiation of the wasm binary immediately + * This function will trigger the download and instantiation of the wasm binary immediately */ const zxingModulePromise1 = getZXingModule(); diff --git a/biome.json b/biome.json index a6a63c25..b0c8c6a5 100644 --- a/biome.json +++ b/biome.json @@ -33,6 +33,12 @@ "allowTrailingCommas": true } } + }, + { + "include": ["react/components"], + "formatter": { + "lineWidth": 120 + } } ] } diff --git a/copy-files-from-to.json b/copy-files-from-to.json index a33a23ac..f83dbdb5 100644 --- a/copy-files-from-to.json +++ b/copy-files-from-to.json @@ -11,6 +11,10 @@ { "from": "./src/full/*.wasm", "to": "./dist/full/" + }, + { + "from": "./src/stream/media-track-shims.d.ts", + "to": "./dist/" } ], "copyFilesSettings": { diff --git a/index.html b/index.html index f0464cdf..23d81393 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,13 @@ - + - ZXing Reader Demo + + + + ZXing WASM React -
- +
+ diff --git a/main.ts b/main.ts deleted file mode 100644 index d4750662..00000000 --- a/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - readBarcodesFromImageFile, - writeBarcodeToImageFile, -} from "./src/full/index"; - -// import { readBarcodesFromImageFile } from "./src/reader/index"; - -// import { writeBarcodeToImageFile } from "./src/writer/index"; - -const text = "Hello World!"; -const barcodeImage = (await writeBarcodeToImageFile(text)).image; -if (barcodeImage) { - const readResults = await readBarcodesFromImageFile(barcodeImage, { - formats: ["QRCode"], - }); - console.log(readResults); - console.log(readResults[0].text === text); -} diff --git a/main.tsx b/main.tsx new file mode 100644 index 00000000..920b3fc5 --- /dev/null +++ b/main.tsx @@ -0,0 +1,45 @@ +/// + +import React, { useState } from "react"; +import ReactDOM from "react-dom/client"; +import { StreamBarcodeDetector } from "./src/react/components/StreamBarcodeDetector.js"; + +import type { InitConstraints } from "./src/stream/index.js"; + +const App = () => { + const [initConstraints] = useState({ + video: { + aspectRatio: undefined, + }, + }); + + const [videoConstraints] = useState({ + advanced: [ + { + exposureMode: "continuous", + }, + ], + }); + + return ( + { + console.log(r); + }} + onStreamInspect={(c) => { + console.log(c); + }} + initConstraints={initConstraints} + videoConstraints={videoConstraints} + scanThrottle={0} + negativeDebounce={0} + formats={["QRCode"]} + /> + ); +}; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/package-lock.json b/package-lock.json index ddaa112f..5469d88e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,12 @@ "version": "1.2.6", "license": "MIT", "dependencies": { - "@types/emscripten": "^1.39.10" + "@types/emscripten": "^1.39.10", + "just-compare": "^2.3.0", + "sha1-uint8array": "^0.10.7", + "type-fest": "^4.10.2", + "webrtc-adapter": "^8.2.3", + "zustand": "^4.5.1" }, "devDependencies": { "@babel/core": "^7.24.0", @@ -18,12 +23,18 @@ "@changesets/cli": "^2.27.1", "@types/babel__core": "^7.20.5", "@types/node": "^20.11.28", + "@types/react": "^18.2.56", + "@types/react-dom": "^18.2.19", + "@vanilla-extract/css": "^1.14.1", + "@vanilla-extract/vite-plugin": "^4.0.4", "concurrently": "^8.2.2", "copy-files-from-to": "^3.9.1", "lint-staged": "^15.2.2", "npm-check-updates": "^16.14.17", "prettier": "^3.2.5", "pretty-quick": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rimraf": "^5.0.5", "simple-git-hooks": "^2.11.0", "tsx": "^4.7.1", @@ -33,6 +44,33 @@ "vite-plugin-babel": "^1.2.0" } }, + "../user-media-stream": { + "version": "0.1.1", + "extraneous": true, + "license": "MIT", + "dependencies": { + "just-compare": "^2.3.0", + "webrtc-adapter": "^8.2.3", + "zustand": "^4.5.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.5.3", + "@changesets/cli": "^2.27.1", + "@commitlint/cli": "^18.6.0", + "@commitlint/config-conventional": "^18.6.0", + "concurrently": "^8.2.2", + "copy-files-from-to": "^3.9.1", + "lint-staged": "^15.2.1", + "npm-check-updates": "^16.14.14", + "prettier": "^3.2.4", + "prettier-plugin-jsdoc": "^1.3.0", + "rimraf": "^5.0.5", + "simple-git-hooks": "^2.9.0", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -298,6 +336,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-simple-access": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", @@ -460,6 +507,21 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", @@ -1282,6 +1344,28 @@ "node": ">=0.1.90" } }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "dev": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.10.tgz", + "integrity": "sha512-Q+mk96KJ+FZ30h9fsJl+67IjNJm3x2eX+GBWGmocAKgzp27cowCOOqSdscX80s0SpdFXZnIv/+1xD1EctFx96Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.19.10", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.10.tgz", @@ -2049,12 +2133,123 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "devOptional": true + }, + "node_modules/@types/react": { + "version": "18.2.56", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.56.tgz", + "integrity": "sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==", + "devOptional": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", + "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "devOptional": true + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@vanilla-extract/babel-plugin-debug-ids": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.4.tgz", + "integrity": "sha512-mevYcVMwsT6960xnXRw/Rr2K7SOEwzwVBApg/2SJ3eg2KGsHfj1rN0oQ12WdoTT3RzThq+0551bVQKPvQnjeaA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.7" + } + }, + "node_modules/@vanilla-extract/css": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.14.1.tgz", + "integrity": "sha512-V4JUuHNjZgl64NGfkDJePqizkNgiSpphODtZEs4cCPuxLAzwOUJYATGpejwimJr1n529kq4DEKWexW22LMBokw==", + "dev": true, + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.3", + "chalk": "^4.1.1", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.0.7", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "outdent": "^0.8.0" + } + }, + "node_modules/@vanilla-extract/css/node_modules/outdent": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", + "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", + "dev": true + }, + "node_modules/@vanilla-extract/integration": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-7.1.0.tgz", + "integrity": "sha512-kCFn2IfnCHf4PCP538zBs5g6JJvqybJ4lU+ww2CeV/B2roze8drF7jVu2hDQUTtfiXgNe0Q3WpUfXdX27KFLsw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.7", + "@babel/plugin-syntax-typescript": "^7.20.0", + "@vanilla-extract/babel-plugin-debug-ids": "^1.0.4", + "@vanilla-extract/css": "^1.14.0", + "esbuild": "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0", + "eval": "0.1.8", + "find-up": "^5.0.0", + "javascript-stringify": "^2.0.1", + "lodash": "^4.17.21", + "mlly": "^1.4.2", + "outdent": "^0.8.0", + "vite": "^5.0.11", + "vite-node": "^1.2.0" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/outdent": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", + "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", + "dev": true + }, + "node_modules/@vanilla-extract/private": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.3.tgz", + "integrity": "sha512-17kVyLq3ePTKOkveHxXuIJZtGYs+cSoev7BlP+Lf4916qfDhk/HBjvlYDe8egrea7LNPHKwSZJK/bzZC+Q6AwQ==", + "dev": true + }, + "node_modules/@vanilla-extract/vite-plugin": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/vite-plugin/-/vite-plugin-4.0.4.tgz", + "integrity": "sha512-cfg4GK274xzwbVFh8YWvQXNnsCMemvMMwej7V93TTBP2O8qzyTgsx5VJuiAPov3oUU8JWGboaTs16Vnoe5bZ9w==", + "dev": true, + "dependencies": { + "@vanilla-extract/integration": "^7.1.0" + }, + "peerDependencies": { + "vite": "^4.0.3 || ^5.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2062,9 +2257,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2143,6 +2338,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2495,6 +2702,15 @@ "semver": "^7.0.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -2616,9 +2832,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001571", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz", - "integrity": "sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==", + "version": "1.0.30001583", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz", + "integrity": "sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==", "dev": true, "funding": [ { @@ -3084,6 +3300,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true + }, "node_modules/csv": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/csv/-/csv-5.5.3.tgz", @@ -3220,6 +3466,21 @@ "node": ">=4.0.0" } }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -3296,6 +3557,20 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3519,6 +3794,342 @@ "@esbuild/win32-x64": "0.19.10" } }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.10.tgz", + "integrity": "sha512-7W0bK7qfkw1fc2viBfrtAEkDKHatYfHzr/jKAHNr9BvkYDXPcC6bodtm8AyLJNNuqClLNaeTLuwURt4PRT9d7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.10.tgz", + "integrity": "sha512-1X4CClKhDgC3by7k8aOWZeBXQX8dHT5QAMCAQDArCLaYfkppoARvh0fit3X2Qs+MXDngKcHv6XXyQCpY0hkK1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.10.tgz", + "integrity": "sha512-O/nO/g+/7NlitUxETkUv/IvADKuZXyH4BHf/g/7laqKC4i/7whLpB0gvpPc2zpF0q9Q6FXS3TS75QHac9MvVWw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz", + "integrity": "sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.10.tgz", + "integrity": "sha512-alfGtT+IEICKtNE54hbvPg13xGBe4GkVxyGWtzr+yHO7HIiRJppPDhOKq3zstTcVf8msXb/t4eavW3jCDpMSmA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.10.tgz", + "integrity": "sha512-dMtk1wc7FSH8CCkE854GyGuNKCewlh+7heYP/sclpOG6Cectzk14qdUIY5CrKDbkA/OczXq9WesqnPl09mj5dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.10.tgz", + "integrity": "sha512-G5UPPspryHu1T3uX8WiOEUa6q6OlQh6gNl4CO4Iw5PS+Kg5bVggVFehzXBJY6X6RSOMS8iXDv2330VzaObm4Ag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.10.tgz", + "integrity": "sha512-j6gUW5aAaPgD416Hk9FHxn27On28H4eVI9rJ4az7oCGTFW48+LcgNDBN+9f8rKZz7EEowo889CPKyeaD0iw9Kg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.10.tgz", + "integrity": "sha512-QxaouHWZ+2KWEj7cGJmvTIHVALfhpGxo3WLmlYfJ+dA5fJB6lDEIg+oe/0//FuyVHuS3l79/wyBxbHr0NgtxJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.10.tgz", + "integrity": "sha512-4ub1YwXxYjj9h1UIZs2hYbnTZBtenPw5NfXCRgEkGb0b6OJ2gpkMvDqRDYIDRjRdWSe/TBiZltm3Y3Q8SN1xNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.10.tgz", + "integrity": "sha512-lo3I9k+mbEKoxtoIbM0yC/MZ1i2wM0cIeOejlVdZ3D86LAcFXFRdeuZmh91QJvUTW51bOK5W2BznGNIl4+mDaA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.10.tgz", + "integrity": "sha512-J4gH3zhHNbdZN0Bcr1QUGVNkHTdpijgx5VMxeetSk6ntdt+vR1DqGmHxQYHRmNb77tP6GVvD+K0NyO4xjd7y4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.10.tgz", + "integrity": "sha512-tgT/7u+QhV6ge8wFMzaklOY7KqiyitgT1AUHMApau32ZlvTB/+efeCtMk4eXS+uEymYK249JsoiklZN64xt6oQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.10.tgz", + "integrity": "sha512-0f/spw0PfBMZBNqtKe5FLzBDGo0SKZKvMl5PHYQr3+eiSscfJ96XEknCe+JoOayybWUFQbcJTrk946i3j9uYZA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.10.tgz", + "integrity": "sha512-pZFe0OeskMHzHa9U38g+z8Yx5FNCLFtUnJtQMpwhS+r4S566aK2ci3t4NCP4tjt6d5j5uo4h7tExZMjeKoehAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.10.tgz", + "integrity": "sha512-ACbZ0vXy9zksNArWlk2c38NdKg25+L9pr/mVaj9SUq6lHZu/35nx2xnQVRGLrC1KKQqJKRIB0q8GspiHI3J80Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.10.tgz", + "integrity": "sha512-PxcgvjdSjtgPMiPQrM3pwSaG4kGphP+bLSb+cihuP0LYdZv1epbAIecHVl5sD3npkfYBZ0ZnOjR878I7MdJDFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.10.tgz", + "integrity": "sha512-ZkIOtrRL8SEJjr+VHjmW0znkPs+oJXhlJbNwfI37rvgeMtk3sxOQevXPXjmAPZPigVTncvFqLMd+uV0IBSEzqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.10.tgz", + "integrity": "sha512-+Sa4oTDbpBfGpl3Hn3XiUe4f8TU2JF7aX8cOfqFYMMjXp6ma6NJDztl5FDG8Ezx0OjwGikIHw+iA54YLDNNVfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.10.tgz", + "integrity": "sha512-EOGVLK1oWMBXgfttJdPHDTiivYSjX6jDNaATeNOaCOFEVcfMjtbx7WVQwPSE1eIfCp/CaSF2nSrDtzc4I9f8TQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.10.tgz", + "integrity": "sha512-whqLG6Sc70AbU73fFYvuYzaE4MNMBIlR1Y/IrUeOXFrWHxBEjjbZaQ3IXIQS8wJdAzue2GwYZCjOrgrU1oUHoA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3553,6 +4164,19 @@ "node": ">=4" } }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -4905,6 +5529,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true + }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -4914,8 +5544,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5007,6 +5636,11 @@ "node >= 0.2.0" ] }, + "node_modules/just-compare": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", + "integrity": "sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5049,6 +5683,224 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lightningcss": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz", + "integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.23.0", + "lightningcss-darwin-x64": "1.23.0", + "lightningcss-freebsd-x64": "1.23.0", + "lightningcss-linux-arm-gnueabihf": "1.23.0", + "lightningcss-linux-arm64-gnu": "1.23.0", + "lightningcss-linux-arm64-musl": "1.23.0", + "lightningcss-linux-x64-gnu": "1.23.0", + "lightningcss-linux-x64-musl": "1.23.0", + "lightningcss-win32-x64-msvc": "1.23.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz", + "integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz", + "integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz", + "integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz", + "integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz", + "integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz", + "integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz", + "integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz", + "integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz", + "integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", @@ -5401,6 +6253,17 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -5489,6 +6352,15 @@ "is-buffer": "~1.1.6" } }, + "node_modules/media-query-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", + "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + } + }, "node_modules/meow": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", @@ -5894,6 +6766,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mlly": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", + "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" + } + }, + "node_modules/modern-ahocorasick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz", + "integrity": "sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==", + "dev": true + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -6859,6 +7749,12 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -6962,6 +7858,17 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -7308,6 +8215,30 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/read-package-json": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", @@ -7612,6 +8543,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -7875,6 +8815,20 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/sdp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz", + "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -7959,6 +8913,11 @@ "node": ">= 0.4" } }, + "node_modules/sha1-uint8array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/sha1-uint8array/-/sha1-uint8array-0.10.7.tgz", + "integrity": "sha512-COJRCUOuTgEEPyhcRncHlf3Z2/Nik0PGZ60/tA9Ni2jlwYJ2g/WgP8TV19gbllmZDs/DGV5YklZxreyMHFX8ww==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8864,12 +9823,11 @@ } }, "node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dev": true, + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.2.tgz", + "integrity": "sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==", "engines": { - "node": ">=14.16" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9007,6 +9965,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -9167,6 +10131,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9250,6 +10222,28 @@ } } }, + "node_modules/vite-node": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.0.tgz", + "integrity": "sha512-D/oiDVBw75XMnjAXne/4feCkCEwcbr2SU1bjAhCcfI5Bq3VoOHji8/wCPAfUkDIeohJ5nSZ39fNxM3dNZ6OBOA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-babel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/vite-plugin-babel/-/vite-plugin-babel-1.2.0.tgz", @@ -9281,6 +10275,18 @@ "defaults": "^1.0.3" } }, + "node_modules/webrtc-adapter": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz", + "integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9619,6 +10625,33 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.1.tgz", + "integrity": "sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 8b500839..4d2ba003 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,35 @@ }, "./reader/zxing_reader.wasm": "./dist/reader/zxing_reader.wasm", "./writer/zxing_writer.wasm": "./dist/writer/zxing_writer.wasm", - "./full/zxing_full.wasm": "./dist/full/zxing_full.wasm" + "./full/zxing_full.wasm": "./dist/full/zxing_full.wasm", + "./scanner": { + "import": "./dist/es/scanner/index.js", + "require": "./dist/cjs/scanner/index.js", + "default": "./dist/es/scanner/index.js" + }, + "./stream": { + "import": "./dist/es/stream/index.js", + "require": "./dist/cjs/stream/index.js", + "default": "./dist/es/stream/index.js" + }, + "./media-track-shims": { + "types": "./dist/media-track-shims.d.ts" + }, + "./react": { + "import": "./dist/es/react/index.js", + "require": "./dist/cjs/react/index.js", + "default": "./dist/es/react/index.js" + }, + "./react/components": { + "import": "./dist/es/react/components/index.js", + "require": "./dist/cjs/react/components/index.js", + "default": "./dist/es/react/components/index.js" + }, + "./react/hooks": { + "import": "./dist/es/react/hooks/index.js", + "require": "./dist/cjs/react/hooks/index.js", + "default": "./dist/es/react/hooks/index.js" + } }, "repository": { "type": "git", @@ -66,7 +94,7 @@ "submodule:update": "git submodule update --remote", "cmake": "emcmake cmake -S src/cpp -B build", "build:wasm": "cmake --build build -j$(($(nproc) - 1))", - "copy:wasm": "copy-files-from-to", + "copy": "copy-files-from-to", "docs:dev": "conc \"npm:docs:preview\" \"typedoc --watch --excludeInternal\"", "docs:build": "typedoc --excludeInternal", "docs:preview": "vite preview --outDir ./docs", @@ -84,9 +112,9 @@ "build:cjs": "tsx ./scripts/build-cjs.ts", "build:iife": "tsx ./scripts/build-iife.ts", "build": "conc \"npm:build:es\" \"npm:build:cjs\" \"npm:build:iife\"", - "postbuild:es": "tsc --declarationDir ./dist/es", - "postbuild:cjs": "tsc --declarationDir ./dist/cjs", - "postbuild": "conc \"npm:copy:wasm\" \"npm:docs:build\"", + "postbuild:es": "tsc --project ./tsconfig.production.json --declarationDir ./dist/es", + "postbuild:cjs": "tsc --project ./tsconfig.production.json --declarationDir ./dist/cjs", + "postbuild": "conc \"npm:copy\" \"npm:docs:build\"", "build:all": "npm run submodule:init && npm run cmake && npm run build:wasm && npm run build", "preview": "vite preview", "prepublishOnly": "npm run build:all", @@ -102,12 +130,18 @@ "@changesets/cli": "^2.27.1", "@types/babel__core": "^7.20.5", "@types/node": "^20.11.28", + "@types/react": "^18.2.56", + "@types/react-dom": "^18.2.19", + "@vanilla-extract/css": "^1.14.1", + "@vanilla-extract/vite-plugin": "^4.0.4", "concurrently": "^8.2.2", "copy-files-from-to": "^3.9.1", "lint-staged": "^15.2.2", "npm-check-updates": "^16.14.17", "prettier": "^3.2.5", "pretty-quick": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rimraf": "^5.0.5", "simple-git-hooks": "^2.11.0", "tsx": "^4.7.1", @@ -117,7 +151,12 @@ "vite-plugin-babel": "^1.2.0" }, "dependencies": { - "@types/emscripten": "^1.39.10" + "@types/emscripten": "^1.39.10", + "just-compare": "^2.3.0", + "sha1-uint8array": "^0.10.7", + "type-fest": "^4.10.2", + "webrtc-adapter": "^8.2.3", + "zustand": "^4.5.1" }, "overrides": { "typedoc": { diff --git a/scripts/build-iife.ts b/scripts/build-iife.ts index ab28abe7..6ce39d0d 100644 --- a/scripts/build-iife.ts +++ b/scripts/build-iife.ts @@ -1,12 +1,15 @@ import { rimraf } from "rimraf"; import { type LibraryOptions, build } from "vite"; import viteConfig from "../vite.config.js"; +import { emscriptenBun } from "./vite-plugin-emscripten-bun.js"; async function buildIife() { await rimraf("dist/iife"); await Promise.all( - Object.entries((viteConfig.build?.lib as LibraryOptions).entry).map( - ([entryAlias, entryPath]) => { + Object.entries((viteConfig.build?.lib as LibraryOptions).entry) + // TODO: pay attention to the order + .slice(0, 3) + .map(([entryAlias, entryPath]) => { return build({ ...viteConfig, build: { @@ -19,14 +22,21 @@ async function buildIife() { formats: ["iife"], name: "ZXingWASM", }, - rollupOptions: undefined, + rollupOptions: { + external: ["react"], + output: { + globals: { + react: "React", + }, + }, + }, outDir: "dist/iife", emptyOutDir: false, }, configFile: false, + plugins: [emscriptenBun()], }); - }, - ), + }), ); } diff --git a/scripts/vite-plugin-emscripten-bun.ts b/scripts/vite-plugin-emscripten-bun.ts new file mode 100644 index 00000000..b0b5b88e --- /dev/null +++ b/scripts/vite-plugin-emscripten-bun.ts @@ -0,0 +1,68 @@ +import type { PluginItem } from "@babel/core"; +import { + binaryExpression, + identifier, + logicalExpression, + stringLiteral, + unaryExpression, + variableDeclaration, + variableDeclarator, +} from "@babel/types"; +import babel from "vite-plugin-babel"; + +function emscriptenBunBabel(): PluginItem { + return { + visitor: { + VariableDeclaration(path) { + if ( + path.node.kind === "var" && + path.node.declarations[0]?.id.type === "Identifier" && + path.node.declarations[0]?.id.name === "ENVIRONMENT_IS_WEB" + ) { + path.insertAfter([ + variableDeclaration("var", [ + variableDeclarator( + identifier("ENVIRONMENT_IS_BUN"), + binaryExpression( + "!==", + unaryExpression("typeof", identifier("Bun")), + stringLiteral("undefined"), + ), + ), + ]), + ]); + } + }, + LogicalExpression(path) { + if ( + path.node.operator === "||" && + path.node.left.type === "Identifier" && + (path.node.left.name === "ENVIRONMENT_IS_WEB" || + path.node.left.name === "ENVIRONMENT_IS_WORKER") && + path.node.right.type === "Identifier" && + (path.node.right.name === "ENVIRONMENT_IS_WORKER" || + path.node.right.name === "ENVIRONMENT_IS_WEB") + ) { + path.replaceWith( + logicalExpression( + "||", + path.node, + identifier("ENVIRONMENT_IS_BUN"), + ), + ); + path.skip(); + } + }, + }, + }; +} + +export function emscriptenBun() { + return babel({ + babelConfig: { + plugins: [emscriptenBunBabel()], + }, + filter: /zxing_(reader|writer|full)\.js$/, + include: /zxing_(reader|writer|full)\.js$/, + }); +} diff --git a/src/bindings/barcodeFormat.ts b/src/bindings/barcodeFormat.ts index 2e0877cf..108e2c1b 100644 --- a/src/bindings/barcodeFormat.ts +++ b/src/bindings/barcodeFormat.ts @@ -29,12 +29,14 @@ export const barcodeFormats = [ export type BarcodeFormat = (typeof barcodeFormats)[number]; /** - * Barcode formats that can be used in {@link ReaderOptions.formats | `ReaderOptions.formats`} to read barcodes. + * Barcode formats that can be used in {@link ReaderOptions.formats | `ReaderOptions.formats`} to + * read barcodes. */ export type ReadInputBarcodeFormat = Exclude; /** - * Barcode formats that can be used in {@link WriterOptions.format | `WriterOptions.format`} to write barcodes. + * Barcode formats that can be used in {@link WriterOptions.format | `WriterOptions.format`} to write + * barcodes. */ export type WriteInputBarcodeFormat = Exclude< BarcodeFormat, @@ -73,7 +75,7 @@ export function formatFromString(format: string): BarcodeFormat { let end = barcodeFormats.length - 1; while (start <= end) { const mid = Math.floor((start + end) / 2); - const midElement = barcodeFormats[mid]; + const midElement = barcodeFormats[mid]!; const normalizedMidElement = normalizeFormatString(midElement); if (normalizedMidElement === normalizedTarget) { return midElement; diff --git a/src/bindings/binarizer.ts b/src/bindings/binarizer.ts index 63f4d320..202bf81f 100644 --- a/src/bindings/binarizer.ts +++ b/src/bindings/binarizer.ts @@ -23,5 +23,5 @@ export function binarizerToZXingEnum( } export function zxingEnumToBinarizer(zxingEnum: ZXingEnum): Binarizer { - return binarizers[zxingEnum.value]; + return binarizers[zxingEnum.value]!; } diff --git a/src/bindings/characterSet.ts b/src/bindings/characterSet.ts index 8b0dae48..877a42b3 100644 --- a/src/bindings/characterSet.ts +++ b/src/bindings/characterSet.ts @@ -53,5 +53,5 @@ export function characterSetToZXingEnum( } export function zxingEnumToCharacterSet(zxingEnum: ZXingEnum): CharacterSet { - return characterSets[zxingEnum.value]; + return characterSets[zxingEnum.value]!; } diff --git a/src/bindings/contentType.ts b/src/bindings/contentType.ts index 0a634f0f..d96a0d3b 100644 --- a/src/bindings/contentType.ts +++ b/src/bindings/contentType.ts @@ -25,5 +25,5 @@ export function contentTypeToZXingEnum( } export function zxingEnumToContentType(zxingEnum: ZXingEnum): ContentType { - return contentTypes[zxingEnum.value]; + return contentTypes[zxingEnum.value]!; } diff --git a/src/bindings/eanAddOnSymbol.ts b/src/bindings/eanAddOnSymbol.ts index 7856ebea..87382d2f 100644 --- a/src/bindings/eanAddOnSymbol.ts +++ b/src/bindings/eanAddOnSymbol.ts @@ -20,5 +20,5 @@ export function eanAddOnSymbolToZXingEnum( export function zxingEnumToEanAddOnSymbol( zxingEnum: ZXingEnum, ): EanAddOnSymbol { - return eanAddOnSymbols[zxingEnum.value]; + return eanAddOnSymbols[zxingEnum.value]!; } diff --git a/src/bindings/exposedReaderBindings.ts b/src/bindings/exposedReaderBindings.ts index d3c03aaf..de3346ad 100644 --- a/src/bindings/exposedReaderBindings.ts +++ b/src/bindings/exposedReaderBindings.ts @@ -1,57 +1,61 @@ -import { type ReaderOptions, defaultReaderOptions as ro } from "./index.js"; +import { + type ReaderOptions, + type ResolvedReaderOptions, + defaultReaderOptions as ro, +} from "./index.js"; -export const defaultReaderOptions: Required = { +export const defaultReaderOptions: ReaderOptions = { ...ro, formats: [...ro.formats], -}; +} satisfies ResolvedReaderOptions; export { barcodeFormats, + binarizers, + characterSets, + contentTypes, + eanAddOnSymbols, + readOutputEccLevels, + textModes, type BarcodeFormat, + type Binarizer, + type CharacterSet, + type ContentType, + type EanAddOnSymbol, + type Point, + type Position, type ReadInputBarcodeFormat, type ReadOutputBarcodeFormat, - binarizers, + type ReadOutputEccLevel, + type ReadResult, + type ReaderOptions, + type TextMode, type ZXingBinarizer, - type Binarizer, - characterSets, type ZXingCharacterSet, - type CharacterSet, - contentTypes, type ZXingContentType, - type ContentType, - type ZXingReaderOptions, - type ReaderOptions, - eanAddOnSymbols, type ZXingEanAddOnSymbol, - type EanAddOnSymbol, - readOutputEccLevels, - type ReadOutputEccLevel, type ZXingEnum, type ZXingPoint, type ZXingPosition, - type Point, - type Position, type ZXingReadResult, - type ReadResult, - textModes, + type ZXingReaderOptions, type ZXingTextMode, - type TextMode, type ZXingVector, } from "./index.js"; export { /** - * @deprecated renamed as `defaultReaderOptions` + * @deprecated Renamed as `ReaderOptions` */ - defaultReaderOptions as defaultDecodeHints, -}; -export { + type ReaderOptions as DecodeHints, /** - * @deprecated renamed as `ZXingReaderOptions` + * @deprecated Renamed as `ZXingReaderOptions` */ type ZXingReaderOptions as ZXingDecodeHints, +} from "./index.js"; +export { /** - * @deprecated renamed as `ReaderOptions` + * @deprecated Renamed as `defaultReaderOptions` */ - type ReaderOptions as DecodeHints, -} from "./index.js"; + defaultReaderOptions as defaultDecodeHints, +}; diff --git a/src/bindings/exposedWriterBindings.ts b/src/bindings/exposedWriterBindings.ts index 3daebdc8..8188afa9 100644 --- a/src/bindings/exposedWriterBindings.ts +++ b/src/bindings/exposedWriterBindings.ts @@ -1,36 +1,42 @@ -import { type WriterOptions, defaultWriterOptions as wo } from "./index.js"; +import { + type ResolvedWriterOptions, + type WriterOptions, + defaultWriterOptions as wo, +} from "./index.js"; -export const defaultWriterOptions: Required = { ...wo }; +export const defaultWriterOptions: WriterOptions = { + ...wo, +} satisfies ResolvedWriterOptions; export { barcodeFormats, - type BarcodeFormat, - type WriteInputBarcodeFormat, characterSets, - type ZXingCharacterSet, - type CharacterSet, writeInputEccLevels, + type BarcodeFormat, + type CharacterSet, + type WriteInputBarcodeFormat, type WriteInputEccLevel, - type ZXingWriterOptions, + type WriteResult, type WriterOptions, + type ZXingCharacterSet, type ZXingEnum, type ZXingWriteResult, - type WriteResult, + type ZXingWriterOptions, } from "./index.js"; export { /** - * @deprecated renamed as `defaultWriterOptions` + * @deprecated Renamed as `WriterOptions` */ - defaultWriterOptions as defaultEncodeHints, -}; -export { + type WriterOptions as EncodeHints, /** - * @deprecated renamed as `ZXingWriterOptions` + * @deprecated Renamed as `ZXingWriterOptions` */ type ZXingWriterOptions as ZXingEncodeHints, +} from "./index.js"; +export { /** - * @deprecated renamed as `WriterOptions` + * @deprecated Renamed as `defaultWriterOptions` */ - type WriterOptions as EncodeHints, -} from "./index.js"; + defaultWriterOptions as defaultEncodeHints, +}; diff --git a/src/bindings/index.ts b/src/bindings/index.ts index 0081c441..64de3f45 100644 --- a/src/bindings/index.ts +++ b/src/bindings/index.ts @@ -2,13 +2,13 @@ export * from "./barcodeFormat.js"; export * from "./binarizer.js"; export * from "./characterSet.js"; export * from "./contentType.js"; -export * from "./readerOptions.js"; export * from "./eanAddOnSymbol.js"; export * from "./eccLevel.js"; -export * from "./writerOptions.js"; export * from "./enum.js"; export * from "./position.js"; export * from "./readResult.js"; +export * from "./readerOptions.js"; export * from "./textMode.js"; export * from "./vector.js"; export * from "./writeResult.js"; +export * from "./writerOptions.js"; diff --git a/src/bindings/position.ts b/src/bindings/position.ts index b7637f65..0188399f 100644 --- a/src/bindings/position.ts +++ b/src/bindings/position.ts @@ -21,7 +21,6 @@ export interface ZXingPosition { * * @property x X coordinate. * @property y Y coordinate. - * * @see {@link Position | `Position`} */ export interface Point extends ZXingPoint {} diff --git a/src/bindings/readResult.ts b/src/bindings/readResult.ts index 28bc3c92..947e567d 100644 --- a/src/bindings/readResult.ts +++ b/src/bindings/readResult.ts @@ -63,9 +63,9 @@ export interface ZXingReadResult { /** * Number of symbols in a structured append sequence. * - * If this is not part of a structured append sequence, the returned value is `-1`. - * If it is a structured append symbol but the total number of symbols is unknown, the - * returned value is `0` (see PDF417 if optional "Segment Count" not given). + * If this is not part of a structured append sequence, the returned value is `-1`. If it is a + * structured append symbol but the total number of symbols is unknown, the returned value is `0` + * (see PDF417 if optional "Segment Count" not given). */ sequenceSize: number; /** @@ -75,9 +75,9 @@ export interface ZXingReadResult { /** * ID to check if a set of symbols belongs to the same structured append sequence. * - * If the symbology does not support this feature, the returned value is empty (see MaxiCode). - * For QR Code, this is the parity integer converted to a string. - * For PDF417 and DataMatrix, this is the `"fileId"`. + * If the symbology does not support this feature, the returned value is empty (see MaxiCode). For + * QR Code, this is the parity integer converted to a string. For PDF417 and DataMatrix, this is + * the `"fileId"`. */ sequenceId: string; /** @@ -104,14 +104,13 @@ export interface ReadResult "format" | "eccLevel" | "contentType" | "position" > { /** - * Format of the barcode, should be one of {@link ReadOutputBarcodeFormat | `ReadOutputBarcodeFormat`}. + * Format of the barcode, should be one of + * {@link ReadOutputBarcodeFormat | `ReadOutputBarcodeFormat`}. * - * Possible values are: - * `"Aztec"`, `"Codabar"`, `"Code128"`, `"Code39"`, `"Code93"`, - * `"DataBar"`, `"DataBarExpanded"`, `"DataMatrix"`, `"DXFilmEdge"`, - * `"EAN-13"`, `"EAN-8"`, `"ITF"`, - * `"MaxiCode"`, `"MicroQRCode"`, `"None"`, - * `"PDF417"`, `"QRCode"`, `"rMQRCode"`, `"UPC-A"`, `"UPC-E"` + * Possible values are: `"Aztec"`, `"Codabar"`, `"Code128"`, `"Code39"`, `"Code93"`, `"DataBar"`, + * `"DataBarExpanded"`, `"DataMatrix"`, `"DXFilmEdge"`, `"EAN-13"`, `"EAN-8"`, `"ITF"`, + * `"MaxiCode"`, `"MicroQRCode"`, `"None"`, `"PDF417"`, `"QRCode"`, `"rMQRCode"`, `"UPC-A"`, + * `"UPC-E"` */ format: ReadOutputBarcodeFormat; /** diff --git a/src/bindings/readerOptions.ts b/src/bindings/readerOptions.ts index 042c25b6..d49cb8b9 100644 --- a/src/bindings/readerOptions.ts +++ b/src/bindings/readerOptions.ts @@ -44,26 +44,27 @@ export interface ZXingReaderOptions { tryDownscale: boolean; binarizer: ZXingEnum; /** - * Set to `true` if the input contains nothing but a single perfectly aligned barcode (usually generated images). + * Set to `true` if the input contains nothing but a single perfectly aligned barcode (usually + * generated images). * * @defaultValue `false` */ isPure: boolean; /** - * Image size ( min(width, height) ) threshold at which to start downscaled scanning - * **WARNING**: this API is experimental and may change / disappear + * Image size ( min(width, height) ) threshold at which to start downscaled scanning **WARNING**: + * this API is experimental and may change / disappear * - * @experimental * @defaultValue `500` + * @experimental * @see {@link tryDownscale | `tryDownscale`} {@link downscaleFactor | `downscaleFactor`} */ downscaleThreshold: number; /** - * Scale factor to use during downscaling, meaningful values are `2`, `3` and `4`. - * **WARNING**: this API is experimental and may change / disappear + * Scale factor to use during downscaling, meaningful values are `2`, `3` and `4`. **WARNING**: + * this API is experimental and may change / disappear * - * @experimental * @defaultValue `3` + * @experimental * @see {@link tryDownscale | `tryDownscale`} {@link downscaleThreshold | `downscaleThreshold`} */ downscaleFactor: number; @@ -74,8 +75,8 @@ export interface ZXingReaderOptions { */ minLineCount: number; /** - * The maximum number of symbols / barcodes to detect / look for in the image. - * The upper limit of this number is 255. + * The maximum number of symbols / barcodes to detect / look for in the image. The upper limit of + * this number is 255. * * @defaultValue `255` */ @@ -132,32 +133,27 @@ export interface ReaderOptions * A set of {@link ReadInputBarcodeFormat | `ReadInputBarcodeFormat`}s that should be searched for. * An empty list `[]` indicates all supported formats. * - * Supported values in this list are: - * `"Aztec"`, `"Codabar"`, `"Code128"`, `"Code39"`, `"Code93"`, - * `"DataBar"`, `"DataBarExpanded"`, `"DataMatrix"`, `"DXFilmEdge"`, - * `"EAN-13"`, `"EAN-8"`, `"ITF"`, `"Linear-Codes"`, `"Matrix-Codes"`, - * `"MaxiCode"`, `"MicroQRCode"`, `"PDF417"`, `"QRCode"`, `"rMQRCode"`, `"UPC-A"`, `"UPC-E"` + * Supported values in this list are: `"Aztec"`, `"Codabar"`, `"Code128"`, `"Code39"`, `"Code93"`, + * `"DataBar"`, `"DataBarExpanded"`, `"DataMatrix"`, `"DXFilmEdge"`, `"EAN-13"`, `"EAN-8"`, + * `"ITF"`, `"Linear-Codes"`, `"Matrix-Codes"`, `"MaxiCode"`, `"MicroQRCode"`, `"PDF417"`, + * `"QRCode"`, `"rMQRCode"`, `"UPC-A"`, `"UPC-E"` * * @defaultValue `[]` */ formats?: ReadInputBarcodeFormat[]; /** - * Algorithm to use for the grayscale to binary transformation. - * The difference is how to get to a threshold value T - * which results in a bit value R = L <= T. + * Algorithm to use for the grayscale to binary transformation. The difference is how to get to a + * threshold value T which results in a bit value R = L <= T. * * - `"LocalAverage"` * * T = average of neighboring pixels for matrix and GlobalHistogram for linear - * * - `"GlobalHistogram"` * * T = valley between the 2 largest peaks in the histogram (per line in linear case) - * * - `"FixedThreshold"` * * T = 127 - * * - `"BoolCast"` * * T = 0, fastest possible @@ -166,16 +162,15 @@ export interface ReaderOptions */ binarizer?: Binarizer; /** - * Specify whether to ignore, read or require EAN-2 / 5 add-on symbols while scanning EAN / UPC codes. + * Specify whether to ignore, read or require EAN-2 / 5 add-on symbols while scanning EAN / UPC + * codes. * * - `"Ignore"` * * Ignore any Add-On symbol during read / scan - * * - `"Read"` * * Read EAN-2 / EAN-5 Add-On symbol if found - * * - `"Require"` * * Require EAN-2 / EAN-5 Add-On symbol to be present @@ -184,41 +179,43 @@ export interface ReaderOptions */ eanAddOnSymbol?: EanAddOnSymbol; /** - * Specifies the `TextMode` that controls the result of {@link ReadResult.text | `ReadResult.text`}. + * Specifies the `TextMode` that controls the result of + * {@link ReadResult.text | `ReadResult.text`}. * * - `"Plain"` * - * {@link ReadResult.bytes | `ReadResult.bytes`} transcoded to unicode based on ECI info or guessed character set - * + * {@link ReadResult.bytes | `ReadResult.bytes`} transcoded to unicode based on ECI info or guessed + * character set * - `"ECI"` * - * Standard content following the ECI protocol with every character set ECI segment transcoded to unicode - * + * Standard content following the ECI protocol with every character set ECI segment transcoded to + * unicode * - `"HRI"` * * Human Readable Interpretation (dependent on the ContentType) - * * - `"Hex"` * * {@link ReadResult.bytes | `ReadResult.bytes`} transcoded to ASCII string of HEX values - * * - `"Escaped"` * - * Escape non-graphical characters in angle brackets (e.g. ASCII `29` will be transcoded to `""`) + * Escape non-graphical characters in angle brackets (e.g. ASCII `29` will be transcoded to + * `""`) * * @defaultValue `"Plain"` */ textMode?: TextMode; /** - * Character set to use (when applicable). - * If this is set to `"Unknown"`, auto-detecting will be used. + * Character set to use (when applicable). If this is set to `"Unknown"`, auto-detecting will be + * used. * * @defaultValue `"Unknown"` */ characterSet?: CharacterSet; } -export const defaultReaderOptions: Required = { +export type ResolvedReaderOptions = Required; + +export const defaultReaderOptions: ResolvedReaderOptions = { formats: [], tryHarder: true, tryRotate: true, @@ -240,12 +237,55 @@ export const defaultReaderOptions: Required = { characterSet: "Unknown", }; +export function resolveReaderOptions( + readerOptions?: ReaderOptions, +): ResolvedReaderOptions { + return { + formats: readerOptions?.formats ?? defaultReaderOptions.formats, + tryHarder: readerOptions?.tryHarder ?? defaultReaderOptions.tryHarder, + tryRotate: readerOptions?.tryRotate ?? defaultReaderOptions.tryRotate, + tryInvert: readerOptions?.tryInvert ?? defaultReaderOptions.tryInvert, + tryDownscale: + readerOptions?.tryDownscale ?? defaultReaderOptions.tryDownscale, + binarizer: readerOptions?.binarizer ?? defaultReaderOptions.binarizer, + isPure: readerOptions?.isPure ?? defaultReaderOptions.isPure, + downscaleFactor: + readerOptions?.downscaleFactor ?? defaultReaderOptions.downscaleFactor, + downscaleThreshold: + readerOptions?.downscaleThreshold ?? + defaultReaderOptions.downscaleThreshold, + minLineCount: + readerOptions?.minLineCount ?? defaultReaderOptions.minLineCount, + maxNumberOfSymbols: + readerOptions?.maxNumberOfSymbols ?? + defaultReaderOptions.maxNumberOfSymbols, + tryCode39ExtendedMode: + readerOptions?.tryCode39ExtendedMode ?? + defaultReaderOptions.tryCode39ExtendedMode, + validateCode39CheckSum: + readerOptions?.validateCode39CheckSum ?? + defaultReaderOptions.validateCode39CheckSum, + validateITFCheckSum: + readerOptions?.validateITFCheckSum ?? + defaultReaderOptions.validateITFCheckSum, + returnCodabarStartEnd: + readerOptions?.returnCodabarStartEnd ?? + defaultReaderOptions.returnCodabarStartEnd, + returnErrors: + readerOptions?.returnErrors ?? defaultReaderOptions.returnErrors, + eanAddOnSymbol: + readerOptions?.eanAddOnSymbol ?? defaultReaderOptions.eanAddOnSymbol, + textMode: readerOptions?.textMode ?? defaultReaderOptions.textMode, + characterSet: + readerOptions?.characterSet ?? defaultReaderOptions.characterSet, + }; +} + export function readerOptionsToZXingReaderOptions( zxingModule: ZXingModule, - readerOptions: Required, + readerOptions: ResolvedReaderOptions, ): ZXingReaderOptions { - return { - ...readerOptions, + return Object.assign(readerOptions, { formats: formatsToString(readerOptions.formats), binarizer: binarizerToZXingEnum(zxingModule, readerOptions.binarizer), eanAddOnSymbol: eanAddOnSymbolToZXingEnum( @@ -257,5 +297,5 @@ export function readerOptionsToZXingReaderOptions( zxingModule, readerOptions.characterSet, ), - }; + }); } diff --git a/src/bindings/textMode.ts b/src/bindings/textMode.ts index 0fb2606b..edfcd3c5 100644 --- a/src/bindings/textMode.ts +++ b/src/bindings/textMode.ts @@ -18,5 +18,5 @@ export function textModeToZXingEnum( } export function zxingEnumToTextMode(zxingEnum: ZXingEnum): TextMode { - return textModes[zxingEnum.value]; + return textModes[zxingEnum.value]!; } diff --git a/src/bindings/writeResult.ts b/src/bindings/writeResult.ts index 3386294e..652a90df 100644 --- a/src/bindings/writeResult.ts +++ b/src/bindings/writeResult.ts @@ -4,8 +4,7 @@ export interface ZXingWriteResult { image: Uint8Array; /** - * Encoding error. - * If there's no error, this will be an empty string `""`. + * Encoding error. If there's no error, this will be an empty string `""`. * * @see {@link WriteResult.error | `WriteResult.error`} */ @@ -16,8 +15,7 @@ export interface ZXingWriteResult { export interface WriteResult extends Omit { /** - * The encoded barcode as an image blob. - * If some error happens, this will be `null`. + * The encoded barcode as an image blob. If some error happens, this will be `null`. * * @see {@link WriteResult.error | `WriteResult.error`} */ diff --git a/src/bindings/writerOptions.ts b/src/bindings/writerOptions.ts index 9a382852..394a4876 100644 --- a/src/bindings/writerOptions.ts +++ b/src/bindings/writerOptions.ts @@ -41,32 +41,29 @@ export interface WriterOptions /** * The format of the barcode to write. * - * Supported values are: - * `"Aztec"`, `"Codabar"`, `"Code128"`, `"Code39"`, `"Code93"`, - * `"DataMatrix"`, `"EAN-13"`, `"EAN-8"`, `"ITF"`, - * `"PDF417"`, `"QRCode"`, `"UPC-A"`, `"UPC-E"` + * Supported values are: `"Aztec"`, `"Codabar"`, `"Code128"`, `"Code39"`, `"Code93"`, + * `"DataMatrix"`, `"EAN-13"`, `"EAN-8"`, `"ITF"`, `"PDF417"`, `"QRCode"`, `"UPC-A"`, `"UPC-E"` * * @defaultValue `"QRCode"` */ format?: WriteInputBarcodeFormat; /** - * Character set to use for encoding the text. - * Used for Aztec, PDF417, and QRCode only. + * Character set to use for encoding the text. Used for Aztec, PDF417, and QRCode only. * * @defaultValue `"UTF8"` */ characterSet?: CharacterSet; /** - * Error correction level of the symbol. - * Used for Aztec, PDF417, and QRCode only. - * `-1` means auto. + * Error correction level of the symbol. Used for Aztec, PDF417, and QRCode only. `-1` means auto. * * @defaultValue `-1` */ eccLevel?: WriteInputEccLevel; } -export const defaultWriterOptions: Required = { +export type ResolvedWriterOptions = Required; + +export const defaultWriterOptions: ResolvedWriterOptions = { width: 200, height: 200, format: "QRCode", @@ -75,15 +72,28 @@ export const defaultWriterOptions: Required = { margin: 10, }; +export function resolveWriterOptions( + writerOptions?: WriterOptions, +): ResolvedWriterOptions { + return { + width: writerOptions?.width ?? defaultWriterOptions.width, + height: writerOptions?.height ?? defaultWriterOptions.height, + format: writerOptions?.format ?? defaultWriterOptions.format, + characterSet: + writerOptions?.characterSet ?? defaultWriterOptions.characterSet, + eccLevel: writerOptions?.eccLevel ?? defaultWriterOptions.eccLevel, + margin: writerOptions?.margin ?? defaultWriterOptions.margin, + }; +} + export function writerOptionsToZXingWriterOptions( zxingModule: ZXingModule, - writerOptions: Required, + writerOptions: ResolvedWriterOptions, ): ZXingWriterOptions { - return { - ...writerOptions, + return Object.assign(writerOptions, { characterSet: characterSetToZXingEnum( zxingModule, writerOptions.characterSet, ), - }; + }); } diff --git a/src/core.ts b/src/core.ts index 5e816101..953d52be 100644 --- a/src/core.ts +++ b/src/core.ts @@ -12,9 +12,9 @@ import { type ZXingVector, type ZXingWriteResult, type ZXingWriterOptions, - defaultReaderOptions, - defaultWriterOptions, readerOptionsToZXingReaderOptions, + resolveReaderOptions, + resolveWriterOptions, writerOptionsToZXingWriterOptions, zxingReadResultToReadResult, zxingWriteResultToWriteResult, @@ -93,7 +93,7 @@ export type ZXingModuleFactory = export type ZXingModuleOverrides = Partial; -const defaultModuleOverrides: ZXingModuleOverrides = import.meta.env.PROD +export const defaultModuleOverrides: ZXingModuleOverrides = import.meta.env.PROD ? { locateFile: (path, prefix) => { const match = path.match(/_(.+?)\.wasm$/); @@ -173,12 +173,9 @@ export async function readBarcodesFromImageFileWithFactory< >( zxingModuleFactory: ZXingModuleFactory, imageFile: Blob, - readerOptions: ReaderOptions = defaultReaderOptions, + readerOptions?: ReaderOptions, ) { - const requiredReaderOptions: Required = { - ...defaultReaderOptions, - ...readerOptions, - }; + const resolvedReaderOptions = resolveReaderOptions(readerOptions); const zxingModule = await getZXingModuleWithFactory(zxingModuleFactory); const { size } = imageFile; const buffer = new Uint8Array(await imageFile.arrayBuffer()); @@ -187,7 +184,7 @@ export async function readBarcodesFromImageFileWithFactory< const zxingReadResultVector = zxingModule.readBarcodesFromImage( bufferPtr, size, - readerOptionsToZXingReaderOptions(zxingModule, requiredReaderOptions), + readerOptionsToZXingReaderOptions(zxingModule, resolvedReaderOptions), ); zxingModule._free(bufferPtr); const readResults: ReadResult[] = []; @@ -204,12 +201,9 @@ export async function readBarcodesFromImageDataWithFactory< >( zxingModuleFactory: ZXingModuleFactory, imageData: ImageData, - readerOptions: ReaderOptions = defaultReaderOptions, + readerOptions?: ReaderOptions, ) { - const requiredReaderOptions: Required = { - ...defaultReaderOptions, - ...readerOptions, - }; + const resolvedReaderOptions = resolveReaderOptions(readerOptions); const zxingModule = await getZXingModuleWithFactory(zxingModuleFactory); const { data: buffer, @@ -223,7 +217,7 @@ export async function readBarcodesFromImageDataWithFactory< bufferPtr, width, height, - readerOptionsToZXingReaderOptions(zxingModule, requiredReaderOptions), + readerOptionsToZXingReaderOptions(zxingModule, resolvedReaderOptions), ); zxingModule._free(bufferPtr); const readResults: ReadResult[] = []; @@ -240,16 +234,13 @@ export async function writeBarcodeToImageFileWithFactory< >( zxingModuleFactory: ZXingModuleFactory, text: string, - writerOptions: WriterOptions = defaultWriterOptions, + writerOptions?: WriterOptions, ) { - const requiredWriterOptions: Required = { - ...defaultWriterOptions, - ...writerOptions, - }; + const resolvedWriterOptions = resolveWriterOptions(writerOptions); const zxingModule = await getZXingModuleWithFactory(zxingModuleFactory); const zxingWriteResult = zxingModule.writeBarcodeToImage( text, - writerOptionsToZXingWriterOptions(zxingModule, requiredWriterOptions), + writerOptionsToZXingWriterOptions(zxingModule, resolvedWriterOptions), ); return zxingWriteResultToWriteResult(zxingWriteResult); } diff --git a/src/index.css.ts b/src/index.css.ts new file mode 100644 index 00000000..837ed0ed --- /dev/null +++ b/src/index.css.ts @@ -0,0 +1,22 @@ +import { style } from "@vanilla-extract/css"; + +export const wrapperClass = style({ + display: "grid", + alignItems: "center", + justifyContent: "center", +}); + +const canvasClass = style({ + gridArea: "1 / 1", + maxWidth: "100%", + maxHeight: "100%", +}); + +export const frameClass = canvasClass; + +export const overlayClass = style([ + canvasClass, + { + pointerEvents: "none", + }, +]); diff --git a/src/react/components/StreamBarcodeDetector.tsx b/src/react/components/StreamBarcodeDetector.tsx new file mode 100644 index 00000000..3f824a36 --- /dev/null +++ b/src/react/components/StreamBarcodeDetector.tsx @@ -0,0 +1,186 @@ +import { type ComponentPropsWithRef, type ComponentPropsWithoutRef, memo, useEffect, useMemo, useRef } from "react"; +import { frameClass, overlayClass, wrapperClass } from "../../index.css.js"; +import type { ReaderOptions } from "../../reader/index.js"; +import { + type UseUserMediaStreamOptions, + type UseVideoScannerOptions, + useUserMediaStream, + useVideoScanner, +} from "../hooks/index.js"; + +export interface StreamBarcodeDetectorProps + extends UseUserMediaStreamOptions, + UseVideoScannerOptions, + ReaderOptions, + ComponentPropsWithRef<"div"> { + overlayCanvasProps?: ComponentPropsWithoutRef<"canvas">; + frameCanvasProps?: ComponentPropsWithoutRef<"canvas">; +} + +export const StreamBarcodeDetector = memo( + ({ + /** + * stream options + */ + streaming = true, + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, + + /** + * scanner options + */ + scanning = true, + wasmLocation, + readerOptions, + scanThrottle, + negativeDebounce, + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, + + /** + * reader options + */ + formats, + tryHarder, + tryRotate, + tryInvert, + tryDownscale, + binarizer, + isPure, + downscaleThreshold, + downscaleFactor, + minLineCount, + maxNumberOfSymbols, + tryCode39ExtendedMode, + validateCode39CheckSum, + validateITFCheckSum, + returnCodabarStartEnd, + returnErrors, + eanAddOnSymbol, + textMode, + characterSet, + + /** + * inner element props + */ + overlayCanvasProps, + frameCanvasProps, + + /** + * element options + */ + children, + ref, + ...wrapperProps + }: StreamBarcodeDetectorProps = {}) => { + const resolvedReaderOptions = useMemo( + () => ({ + formats: formats ?? readerOptions?.formats, + tryHarder: tryHarder ?? readerOptions?.tryHarder, + tryRotate: tryRotate ?? readerOptions?.tryRotate, + tryInvert: tryInvert ?? readerOptions?.tryInvert, + tryDownscale: tryDownscale ?? readerOptions?.tryDownscale, + binarizer: binarizer ?? readerOptions?.binarizer, + isPure: isPure ?? readerOptions?.isPure, + downscaleFactor: downscaleFactor ?? readerOptions?.downscaleFactor, + downscaleThreshold: downscaleThreshold ?? readerOptions?.downscaleThreshold, + minLineCount: minLineCount ?? readerOptions?.minLineCount, + maxNumberOfSymbols: maxNumberOfSymbols ?? readerOptions?.maxNumberOfSymbols, + tryCode39ExtendedMode: tryCode39ExtendedMode ?? readerOptions?.tryCode39ExtendedMode, + validateCode39CheckSum: validateCode39CheckSum ?? readerOptions?.validateCode39CheckSum, + validateITFCheckSum: validateITFCheckSum ?? readerOptions?.validateITFCheckSum, + returnCodabarStartEnd: returnCodabarStartEnd ?? readerOptions?.returnCodabarStartEnd, + returnErrors: returnErrors ?? readerOptions?.returnErrors, + eanAddOnSymbol: eanAddOnSymbol ?? readerOptions?.eanAddOnSymbol, + textMode: textMode ?? readerOptions?.textMode, + characterSet: characterSet ?? readerOptions?.characterSet, + }), + [ + readerOptions, + formats, + tryHarder, + tryRotate, + tryInvert, + tryDownscale, + binarizer, + isPure, + downscaleFactor, + downscaleThreshold, + minLineCount, + maxNumberOfSymbols, + tryCode39ExtendedMode, + validateCode39CheckSum, + validateITFCheckSum, + returnCodabarStartEnd, + returnErrors, + eanAddOnSymbol, + textMode, + characterSet, + ], + ); + + const streamVideoRefCallback = useUserMediaStream({ + streaming, + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, + }); + + const { + videoRefCallback: scannerVideoRefCallback, + overlayCanvasElementRef, + frameCanvasElementRef, + } = useVideoScanner({ + /** + * scanner options + */ + scanning, + wasmLocation, + readerOptions: resolvedReaderOptions, + scanThrottle, + negativeDebounce, + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, + }); + + const videoElementRef = useRef(document.createElement("video")); + + useEffect(() => { + streamVideoRefCallback(videoElementRef.current); + scannerVideoRefCallback(videoElementRef.current); + }, [streamVideoRefCallback, scannerVideoRefCallback]); + + return ( +
+ + {frameCanvasProps?.children} + + + {overlayCanvasProps?.children} + + {children} +
+ ); + }, +); + +export default StreamBarcodeDetector; diff --git a/src/react/components/index.ts b/src/react/components/index.ts new file mode 100644 index 00000000..4ed7de7a --- /dev/null +++ b/src/react/components/index.ts @@ -0,0 +1 @@ +export * from "./StreamBarcodeDetector.js"; diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts new file mode 100644 index 00000000..98c45ecd --- /dev/null +++ b/src/react/hooks/index.ts @@ -0,0 +1,3 @@ +export * from "./useUserMediaStream.js"; +export * from "./useHeadlessVideoScanner.js"; +export * from "./useVideoScanner.js"; diff --git a/src/react/hooks/useHeadlessVideoScanner.ts b/src/react/hooks/useHeadlessVideoScanner.ts new file mode 100644 index 00000000..80b20206 --- /dev/null +++ b/src/react/hooks/useHeadlessVideoScanner.ts @@ -0,0 +1,95 @@ +import { type RefCallback, useCallback, useEffect, useRef } from "react"; +import { + type VideoScanner, + type VideoScannerOptions, + createVideoScanner, +} from "../../scanner/index.js"; + +export interface UseHeadlessVideoScannerOptions extends VideoScannerOptions { + /** + * control the activation of scanning + */ + scanning?: boolean; +} + +export function useHeadlessVideoScanner({ + /** + * scanner options + */ + scanning = true, + wasmLocation, + readerOptions, + scanThrottle, + negativeDebounce, + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, +}: UseHeadlessVideoScannerOptions) { + const videoScannerRef = useRef(null); + + useEffect(() => { + if (scanning) { + videoScannerRef.current?.start(); + } else { + videoScannerRef.current?.stop(); + } + }, [scanning]); + + const createVideoScannerRef = useRef((videoElement: HTMLVideoElement) => + createVideoScanner(videoElement, { + wasmLocation, + readerOptions, + scanThrottle, + negativeDebounce, + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, + }), + ); + + const videoRefCallback = useCallback>( + (videoElement) => { + if (videoElement !== null) { + videoScannerRef.current = createVideoScannerRef.current(videoElement); + } else { + videoScannerRef.current?.close(); + videoScannerRef.current = null; + } + }, + [], + ); + + useEffect(() => { + videoScannerRef.current?.setOptions({ + wasmLocation, + readerOptions, + scanThrottle, + negativeDebounce, + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, + }); + }, [ + wasmLocation, + readerOptions, + scanThrottle, + negativeDebounce, + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, + ]); + + return videoRefCallback; +} diff --git a/src/react/hooks/useUserMediaStream.ts b/src/react/hooks/useUserMediaStream.ts new file mode 100644 index 00000000..2b57c4a4 --- /dev/null +++ b/src/react/hooks/useUserMediaStream.ts @@ -0,0 +1,97 @@ +import { + type RefCallback, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { + type UserMediaStream, + type UserMediaStreamOptions, + attachMediaStream, + createUserMediaStream, +} from "../../stream/index.js"; + +export interface UseUserMediaStreamOptions extends UserMediaStreamOptions { + /** + * control the activation of streaming + */ + streaming?: boolean; +} + +export function useUserMediaStream({ + /** + * stream options + */ + streaming = true, + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, +}: UseUserMediaStreamOptions) { + const userMediaStreamRef = useRef( + createUserMediaStream({ + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, + }), + ); + + const [stream, setStream] = useState(null); + + useEffect(() => { + (async () => { + if (streaming) { + const stream = await userMediaStreamRef.current.start(); + setStream(stream); + userMediaStreamRef.current.inspect(); + } else { + await userMediaStreamRef.current.stop(); + setStream(null); + } + })(); + }, [streaming]); + + const videoRefCallback = useCallback>( + (videoElement) => { + if (stream !== null && videoElement !== null) { + attachMediaStream(videoElement, stream); + videoElement.play(); + } + }, + [stream], + ); + + useEffect(() => { + userMediaStreamRef.current.setOptions({ + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, + }); + }, [ + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, + ]); + + return videoRefCallback; +} diff --git a/src/react/hooks/useVideoScanner.ts b/src/react/hooks/useVideoScanner.ts new file mode 100644 index 00000000..5319c116 --- /dev/null +++ b/src/react/hooks/useVideoScanner.ts @@ -0,0 +1,170 @@ +import { type RefCallback, useCallback, useRef } from "react"; +import type { ReadResult } from "../../reader/index.js"; +import { + type UseHeadlessVideoScannerOptions, + useHeadlessVideoScanner, +} from "./useHeadlessVideoScanner.js"; + +export interface UseVideoScannerOptions + extends UseHeadlessVideoScannerOptions {} + +export function useVideoScanner({ + onScanUpdate, + onRepaint, + ...restUseVideoScannerOptions +}: UseVideoScannerOptions) { + const videoElementRef = useRef(null); + + const scannerVideoRefCallback = useCallback>( + (videoElement) => { + videoElementRef.current = videoElement; + }, + [], + ); + + const overlayCanvasElementRef = useRef(null); + + const handleOverlayUpdate = useCallback( + (() => { + let context: CanvasRenderingContext2D | undefined = undefined; + return (readResults: ReadResult[]) => { + // check canvas ready state + if ( + overlayCanvasElementRef.current === null || + videoElementRef.current === null + ) { + return; + } + // update canvas dimension + const { videoWidth, videoHeight } = videoElementRef.current; + if (context === undefined) { + context = overlayCanvasElementRef.current.getContext("2d")!; + } + if (context.canvas.width !== videoWidth) { + context.canvas.width = videoWidth; + } + if (context.canvas.height !== videoHeight) { + context.canvas.height = videoHeight; + } + // draw overlay + context.clearRect(0, 0, videoWidth, videoHeight); + context.beginPath(); + + context.moveTo(0, 0); + context.lineTo(videoWidth, 0); + context.lineTo(videoWidth, videoHeight); + context.lineTo(0, videoHeight); + context.closePath(); + + for (const readResult of readResults) { + if (readResult.isValid) { + context.moveTo( + readResult.position.topLeft.x, + readResult.position.topLeft.y, + ); + context.lineTo( + readResult.position.bottomLeft.x, + readResult.position.bottomLeft.y, + ); + context.lineTo( + readResult.position.bottomRight.x, + readResult.position.bottomRight.y, + ); + context.lineTo( + readResult.position.topRight.x, + readResult.position.topRight.y, + ); + context.closePath(); + } + } + + context.fillStyle = "rgba(0, 0, 0, 0.8)"; + context.fill(); + // context.clearRect(0, 0, videoWidth, videoHeight); + // for (const readResult of readResults) { + // if (readResult.isValid) { + // context.strokeStyle = "#f00"; + // context.lineWidth = 1; + // context.beginPath(); + // context.moveTo( + // readResult.position.topLeft.x, + // readResult.position.topLeft.y, + // ); + // context.lineTo( + // readResult.position.topRight.x, + // readResult.position.topRight.y, + // ); + // context.lineTo( + // readResult.position.bottomRight.x, + // readResult.position.bottomRight.y, + // ); + // context.lineTo( + // readResult.position.bottomLeft.x, + // readResult.position.bottomLeft.y, + // ); + // context.closePath(); + // context.stroke(); + // } + // } + }; + })(), + [], + ); + + const handleScanUpdate = useCallback( + (readResults: ReadResult[]) => { + handleOverlayUpdate(readResults); + onScanUpdate?.(readResults); + }, + [handleOverlayUpdate, onScanUpdate], + ); + + const frameCanvasElementRef = useRef(null); + + const handleFrameUpdate = useCallback( + (() => { + let context: CanvasRenderingContext2D | undefined = undefined; + return () => { + if ( + frameCanvasElementRef.current === null || + videoElementRef.current === null + ) { + return; + } + const { videoWidth, videoHeight } = videoElementRef.current; + if (context === undefined) { + context = frameCanvasElementRef.current.getContext("2d")!; + } + if (context.canvas.width !== videoWidth) { + context.canvas.width = videoWidth; + } + if (context.canvas.height !== videoHeight) { + context.canvas.height = videoHeight; + } + context.drawImage(videoElementRef.current, 0, 0); + }; + })(), + [], + ); + + const handleRepaint = useCallback(() => { + handleFrameUpdate(); + onRepaint?.(); + }, [handleFrameUpdate, onRepaint]); + + const headlessScannerVideoRefCallback = useHeadlessVideoScanner({ + onScanUpdate: handleScanUpdate, + onRepaint: handleRepaint, + ...restUseVideoScannerOptions, + }); + + const videoRefCallback = useCallback>( + (videoElement) => { + scannerVideoRefCallback(videoElement); + headlessScannerVideoRefCallback(videoElement); + }, + [scannerVideoRefCallback, headlessScannerVideoRefCallback], + ); + + return { videoRefCallback, overlayCanvasElementRef, frameCanvasElementRef }; +} diff --git a/src/react/index.ts b/src/react/index.ts new file mode 100644 index 00000000..c67e1a1a --- /dev/null +++ b/src/react/index.ts @@ -0,0 +1,2 @@ +export * from "./components/index.js"; +export * from "./hooks/index.js"; diff --git a/src/reader/index.ts b/src/reader/index.ts index 1ffa08e6..2925907a 100644 --- a/src/reader/index.ts +++ b/src/reader/index.ts @@ -50,6 +50,6 @@ export async function readBarcodesFromImageData( export * from "../bindings/exposedReaderBindings.js"; export { purgeZXingModule, - type ZXingReaderModule, type ZXingModuleOverrides, + type ZXingReaderModule, } from "../core.js"; diff --git a/src/scanner/index.ts b/src/scanner/index.ts new file mode 100644 index 00000000..e7327e1b --- /dev/null +++ b/src/scanner/index.ts @@ -0,0 +1 @@ +export * from "./videoScanner.js"; diff --git a/src/scanner/videoScanner.ts b/src/scanner/videoScanner.ts new file mode 100644 index 00000000..bfa170af --- /dev/null +++ b/src/scanner/videoScanner.ts @@ -0,0 +1,510 @@ +import { createHash } from "sha1-uint8array"; +import type { SetRequired } from "type-fest"; +import { subscribeWithSelector } from "zustand/middleware"; +import { createStore } from "zustand/vanilla"; +import { defaultModuleOverrides } from "../core.js"; +import { + type ReadResult, + type ReaderOptions, + barcodeFormats, + contentTypes, + readBarcodesFromImageData, + setZXingModuleOverrides, +} from "../reader/index.js"; + +/** + * The default minimum interval in milliseconds for scanning operations. + */ +const SCAN_THROTTLE = 40; + +/** + * The default minimum interval in milliseconds to confirm a negative result. + */ +const NEGATIVE_DEBOUNCE = 0; + +/** + * Symbols used as unique keys for internal state properties to avoid overriden. + */ +const scanSymbol = Symbol("scan"); +const closeSymbol = Symbol("close"); + +/** + * Options for configuring the VideoScanner. + */ +export interface VideoScannerOptions { + /** + * Location of the ZXing WebAssembly (WASM) file used for barcode decoding. + */ + wasmLocation?: string; + /** + * Configuration options for the ZXing barcode reader. + */ + readerOptions?: ReaderOptions; + /** + * Minimum interval in milliseconds between scan operations to throttle the scan frequency. + */ + scanThrottle?: number; + /** + * Minimum duration in milliseconds before a negative result is confirmed. + */ + negativeDebounce?: number; + /** + * Callback function that is triggered when a new barcode is detected. + * + * @param readResults - Array of detected barcodes. + */ + onScanDetect?: (readResults: ReadResult[]) => unknown; + /** + * Callback function that is triggered on each scan update, regardless of new detections. + * + * @param readResults - Array of detected barcodes. + */ + onScanUpdate?: (readResults: ReadResult[]) => unknown; + /** + * Callback function that is triggered when the scanning process starts. + */ + onScanStart?: () => unknown; + /** + * Callback function that is triggered when the scanning process stops. + */ + onScanStop?: () => unknown; + /** + * Callback function that is triggered when the scanning process is closed. + */ + onScanClose?: () => unknown; + /** + * Callback function that is triggered when the browser repaints. + */ + onRepaint?: () => unknown; +} + +/** + * Internal state and options for VideoScanner. + */ +interface VideoScannerState extends ResolvedVideoScannerOptions { + /** + * Indicates whether the scanner is currently scanning. + */ + [scanSymbol]: boolean; + /** + * Indicates whether the scanner has been closed. + */ + [closeSymbol]: boolean; +} + +/** + * Resolved options for the VideoScanner with default values provided. + */ +type ResolvedVideoScannerOptions = SetRequired< + VideoScannerOptions, + "scanThrottle" | "negativeDebounce" +>; + +/** + * Resolves and merges the provided VideoScannerOptions with the default values for unspecified options. + * + * @param videoScannerOptions - The user-provided configuration options for the VideoScanner. + * @returns The resolved configuration options with default values for unspecified options. + */ +function resolveVideoScannerOptions( + videoScannerOptions: VideoScannerOptions, +): ResolvedVideoScannerOptions { + return { + wasmLocation: + "wasmLocation" in videoScannerOptions + ? videoScannerOptions.wasmLocation + : undefined, + readerOptions: + "readerOptions" in videoScannerOptions + ? videoScannerOptions.readerOptions + : undefined, + scanThrottle: videoScannerOptions.scanThrottle ?? SCAN_THROTTLE, + negativeDebounce: videoScannerOptions.negativeDebounce ?? NEGATIVE_DEBOUNCE, + onScanDetect: + "onScanDetect" in videoScannerOptions + ? videoScannerOptions.onScanDetect + : undefined, + onScanUpdate: + "onScanUpdate" in videoScannerOptions + ? videoScannerOptions.onScanUpdate + : undefined, + onScanStart: + "onScanStart" in videoScannerOptions + ? videoScannerOptions.onScanStart + : undefined, + onScanStop: + "onScanStop" in videoScannerOptions + ? videoScannerOptions.onScanStop + : undefined, + onScanClose: + "onScanClose" in videoScannerOptions + ? videoScannerOptions.onScanClose + : undefined, + onRepaint: + "onRepaint" in videoScannerOptions + ? videoScannerOptions.onRepaint + : undefined, + }; +} + +/** + * Represents a video scanner that can start, stop, and close the scanning process, and update its configuration. + */ +export interface VideoScanner { + /** + * Starts the scanning process. + */ + start: () => void; + /** + * Stops the scanning process. + */ + stop: () => void; + /** + * Closes the scanner and performs cleanup operations. + */ + close: () => void; + /** + * Update the configuration options of the VideoScanner. + * + * @param videoScannerOptions - New configuration options. + */ + setOptions: (videoScannerOptions: VideoScannerOptions) => void; +} + +/** + * Creates and returns a VideoScanner object. + * + * Initializes a VideoScanner with the specified HTMLVideoElement and configuration options, providing control methods + * for starting, stopping, and closing the video scanner, along with updating its options. + * + * @param videoElement - The HTMLVideoElement used for scanning. + * @param videoScannerOptions - Configuration options for the VideoScanner. + * @returns A VideoScanner object providing control methods for video scanning. + */ +export function createVideoScanner( + videoElement: HTMLVideoElement, + videoScannerOptions: VideoScannerOptions, +): VideoScanner { + const { + wasmLocation, + readerOptions, + scanThrottle, + negativeDebounce, + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, + } = resolveVideoScannerOptions(videoScannerOptions); + + // request animation frame id + let requestAnimationFrameId: number; + + // create a state store + const videoScannerStore = createStore()( + subscribeWithSelector(() => ({ + [scanSymbol]: false, + [closeSymbol]: false, + + wasmLocation, + readerOptions, + scanThrottle, + negativeDebounce, + + onScanDetect, + onScanUpdate, + onScanStart, + onScanStop, + onScanClose, + onRepaint, + })), + ); + + const start = () => { + videoScannerStore.setState({ + [scanSymbol]: true, + }); + }; + + const stop = () => { + videoScannerStore.setState({ + [scanSymbol]: false, + }); + }; + + const close = () => { + videoScannerStore.setState({ + [closeSymbol]: true, + }); + }; + + // subscribe to scan start and stop actions + // so we can call the event handlers + const unsubScan = videoScannerStore.subscribe( + (options) => options[scanSymbol], + (scan) => { + if (scan) { + videoScannerStore.getState().onScanStart?.(); + } else { + videoScannerStore.getState().onScanStop?.(); + } + }, + ); + + // subscribe to the close action + // so we can do some cleanups + const unsubClose = videoScannerStore.subscribe( + (options) => options[closeSymbol], + (close) => { + if (close) { + globalThis.cancelAnimationFrame(requestAnimationFrameId); + stop(); + unsubScan(); + unsubClose(); + unsubWasmLocation(); + videoScannerStore.getState().onScanClose?.(); + } + }, + { + fireImmediately: true, + }, + ); + + // subscribe to wasm location change + const unsubWasmLocation = videoScannerStore.subscribe( + (options) => options.wasmLocation, + (wasmLocation) => { + if (wasmLocation === undefined) { + setZXingModuleOverrides(defaultModuleOverrides); + return; + } + setZXingModuleOverrides({ + locateFile: (path, prefix) => { + if (path.endsWith(".wasm")) { + return wasmLocation; + } + return prefix + path; + }, + }); + }, + { + fireImmediately: true, + }, + ); + + // define the frame request callback function + const frameRequestCallback = (() => { + // keep the context values in the closure + let detecting = false; + let prevTimestamp = 0; + + type SignatureMap = Map< + string, + { timestamp: number; readResult: ReadResult } + >; + let prevSignatureMap: SignatureMap = new Map(); + + // return the actual callback function + return async (currTimestamp: DOMHighResTimeStamp) => { + // get options snapshot + const { + [scanSymbol]: scan, + [closeSymbol]: close, + readerOptions, + scanThrottle, + onScanDetect, + onScanUpdate, + onRepaint, + } = videoScannerStore.getState(); + + // return if closed + if (close) { + return; + } + + // call repaint event handler + onRepaint?.(); + + // skip if the following conditions are met + if (!scan || detecting || currTimestamp - prevTimestamp < scanThrottle) { + if (!scan) { + // clear previous signature map + prevSignatureMap.clear(); + // clear previouse timestamp + prevTimestamp = 0; + } + // trigger next cycle + requestAnimationFrameId = + globalThis.requestAnimationFrame(frameRequestCallback); + return; + } + + // set detecting status + detecting = true; + + // detect + const imageData = getImageDataFromVideoElement(videoElement); + let readResults: ReadResult[] = []; + if (imageData !== null) { + readResults = await readBarcodesFromImageData(imageData, readerOptions); + } + + // populate the signature map and set the flag + let newSymbolDetected = false; + const currSignatureMap: SignatureMap = new Map(); + for (const readResult of readResults) { + const signature = getSignature(readResult); + if (!newSymbolDetected && !prevSignatureMap.has(signature)) { + newSymbolDetected = true; + } + currSignatureMap.set(signature, { + timestamp: currTimestamp, + readResult, + }); + } + for (const [prevSignature, prevResult] of prevSignatureMap) { + if ( + !currSignatureMap.has(prevSignature) && + currTimestamp - prevResult.timestamp < negativeDebounce + ) { + currSignatureMap.set(prevSignature, prevResult); + readResults.push(prevResult.readResult); + } + } + prevSignatureMap = currSignatureMap; + + // call onScanDetect event handler + if (newSymbolDetected) { + onScanDetect?.(readResults); + } + + // call onScanUpdate event handler + onScanUpdate?.(readResults); + + // update prevTimestamp + prevTimestamp = currTimestamp; + + // clear detecting status + detecting = false; + + // trigger next cycle + requestAnimationFrameId = + globalThis.requestAnimationFrame(frameRequestCallback); + }; + })(); + + requestAnimationFrameId = + globalThis.requestAnimationFrame(frameRequestCallback); + + return { + start, + stop, + close, + setOptions: (videoScannerOptions) => + videoScannerStore.setState( + resolveVideoScannerOptions(videoScannerOptions), + ), + }; +} + +/** + * Retrieves image data from the provided HTMLVideoElement for processing. + * + * Ensures the video element is in a ready state and has non-zero dimensions before extracting the image data. + * + * @param videoElement - The HTMLVideoElement to extract image data from. + * @returns ImageData if successful, or null in case of failure or invalid state. + */ +export const getImageDataFromVideoElement = (() => { + let context: + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D + | undefined = undefined; + return (videoElement: HTMLVideoElement) => { + if (videoElement.readyState === 0 || videoElement.readyState === 1) { + return null; + } + const width = videoElement.videoWidth; + const height = videoElement.videoHeight; + if (width === 0 || height === 0) { + return null; + } + if (context === undefined) { + const canvas = createCanvas(width, height); + context = canvas.getContext("2d", { willReadFrequently: true }) as + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D; + } else { + if (context.canvas.width !== width) { + context.canvas.width = width; + } + if (context.canvas.height !== height) { + context.canvas.height = height; + } + } + context.drawImage(videoElement, 0, 0); + try { + const imageData = context.getImageData(0, 0, width, height); + return imageData; + } catch (e) { + return null; + } + }; +})(); + +/** + * Dynamically creates a canvas element suitable for the environment. + * + * Prefers OffscreenCanvas but falls back to HTMLCanvasElement where OffscreenCanvas is not supported. + * + * @param width - The width of the canvas. + * @param height - The height of the canvas. + * @returns A canvas element, either OffscreenCanvas or HTMLCanvasElement based on environment support. + */ +function createCanvas( + width: number, + height: number, +): OffscreenCanvas | HTMLCanvasElement { + try { + const canvas = new OffscreenCanvas(width, height); + if ( + canvas.getContext("2d", { willReadFrequently: true }) instanceof + OffscreenCanvasRenderingContext2D + ) { + return canvas; + } + throw undefined; + } catch { + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + return canvas; + } +} + +/** + * Generates a unique signature for a given ReadResult. + * + * Creates a signature string for a ReadResult object to uniquely identify a read barcode. + * + * @param readResult - The ReadResult object for which to generate the signature. + * @returns A unique signature string for the given ReadResult. + */ +function getSignature(readResult: ReadResult) { + return createHash() + .update( + new Uint8Array( + (+readResult.isValid << 0) + + (+readResult.isMirrored << 1) + + (+readResult.isInverted << 2) + + (+readResult.hasECI << 3) + + (+readResult.readerInit << 4), + ), + ) + .update(new Uint8Array(barcodeFormats.indexOf(readResult.format) + 1)) + .update(new Uint8Array(contentTypes.indexOf(readResult.contentType) + 1)) + .update(readResult.symbologyIdentifier + readResult.version) + .update(readResult.bytes) + .digest("hex"); +} diff --git a/src/stream/compatibility.d.ts b/src/stream/compatibility.d.ts new file mode 100644 index 00000000..2e6d5684 --- /dev/null +++ b/src/stream/compatibility.d.ts @@ -0,0 +1,11 @@ +declare type CreateObjectURLCompat = ( + obj: Blob | MediaSource | MediaStream, +) => string; + +declare interface HTMLVideoElement { + mozSrcObject?: HTMLVideoElement["srcObject"]; +} + +declare interface MediaStreamTrack { + getCapabilities?: MediaStreamTrack["getCapabilities"]; +} diff --git a/src/stream/index.ts b/src/stream/index.ts new file mode 100644 index 00000000..0729d42f --- /dev/null +++ b/src/stream/index.ts @@ -0,0 +1 @@ +export * from "./userMediaStream.js"; diff --git a/src/stream/media-track-shims.d.ts b/src/stream/media-track-shims.d.ts new file mode 100644 index 00000000..5c0d38b1 --- /dev/null +++ b/src/stream/media-track-shims.d.ts @@ -0,0 +1,61 @@ +// TODO: complete constraints and capabilities + +declare interface MediaTrackSupportedConstraints { + brightness?: boolean; + browserWindow?: boolean; + colorTemperature?: boolean; + contrast?: boolean; + exposureCompensation?: boolean; + exposureMode?: boolean; + exposureTime?: boolean; + focusDistance?: boolean; + focusMode?: boolean; + iso?: boolean; + latency?: boolean; + mediaSource?: boolean; + pan?: boolean; + pointsOfInterest?: boolean; + resizeMode?: boolean; + saturation?: boolean; + scrollWithPage?: boolean; + sharpness?: boolean; + suppressLocalAudioPlayback?: boolean; + tilt?: boolean; + torch?: boolean; + viewPortHeight?: boolean; + viewPortOffsetX?: boolean; + viewPortOffsetY?: boolean; + viewportWidth?: boolean; + whiteBalanceMode?: boolean; + zoom?: boolean; +} + +interface NumberRangeWithStep { + min: number; + max: number; + step: number; +} + +declare interface MediaTrackCapabilities { + brightness?: NumberRangeWithStep; + colorTemperature?: NumberRangeWithStep; + contrast?: NumberRangeWithStep; + exposureMode?: string[]; + exposureTime?: NumberRangeWithStep; + resizeMode?: string[]; + saturation?: NumberRangeWithStep; + sharpness?: NumberRangeWithStep; + whiteBalanceMode?: string[]; +} + +declare interface MediaTrackConstraintSet { + brightness?: ConstrainULong; + colorTemperature?: ConstrainULong; + contrast?: ConstrainULong; + exposureMode?: ConstrainDOMString; + exposureTime?: ConstrainDouble; + resizeMode?: ConstrainDOMString; + saturation?: ConstrainULong; + sharpness?: ConstrainULong; + whiteBalanceMode?: ConstrainDOMString; +} diff --git a/src/stream/shimGetUserMedia.ts b/src/stream/shimGetUserMedia.ts new file mode 100644 index 00000000..2212e910 --- /dev/null +++ b/src/stream/shimGetUserMedia.ts @@ -0,0 +1,50 @@ +import { detectBrowser } from "webrtc-adapter/dist/utils"; + +/** + * Provide a shim for getUserMedia API across different browsers. + * + * This function ensures compatibility of the getUserMedia API with various browsers by applying + * necessary shims provided by the 'webrtc-adapter' package. It detects the browser and applies the + * appropriate shim. The function is designed to be called once to set up the shims and will not + * re-apply the shims on subsequent calls. + * + * @returns A promise that resolves once the appropriate shim (if any) has been applied. It resolves + * to `void` as its purpose is to configure the environment rather than return a value. + */ +export const shimGetUserMedia = (() => { + let called = false; + return async () => { + // Ensure the shim is applied only once + if (called) { + return; + } + + // Detect the current browser + const browserDetails = detectBrowser(window); + + // Apply browser-specific shim for getUserMedia + switch (browserDetails.browser) { + case "chrome": + ( + await import("webrtc-adapter/dist/chrome/getusermedia") + ).shimGetUserMedia(window, browserDetails); + break; + case "firefox": + ( + await import("webrtc-adapter/dist/firefox/getusermedia") + ).shimGetUserMedia(window, browserDetails); + break; + case "safari": + ( + await import("webrtc-adapter/dist/safari/safari_shim") + ).shimGetUserMedia(window); + break; + default: + // Handle other browsers or runtimes in a non-disruptive way + break; + } + + // Mark the shim as applied + called = true; + }; +})(); diff --git a/src/stream/userMediaStream.ts b/src/stream/userMediaStream.ts new file mode 100644 index 00000000..637a7464 --- /dev/null +++ b/src/stream/userMediaStream.ts @@ -0,0 +1,834 @@ +import compare from "just-compare"; +import type { SetRequired } from "type-fest"; +import { subscribeWithSelector } from "zustand/middleware"; +import { shallow } from "zustand/shallow"; +import { createStore } from "zustand/vanilla"; +import { shimGetUserMedia } from "./shimGetUserMedia.js"; + +/** + * The default maximum time in milliseconds to wait for getting the capabilities of a media track. + */ +const GET_CAPABILITIES_TIMEOUT = 500; + +/** + * Media stream constraints for initialization. + * + * This can either be a standard `MediaStreamConstraints` object or a function that returns + * `MediaStreamConstraints`. The function is provided with an argument, + * `MediaTrackSupportedConstraints`, which represents constraints supported by the **user agent**, + * and should return either `MediaStreamConstraints` or a promise that resolves to it. + * + * Note that `MediaTrackSupportedConstraints` doesn't reflect the constraints supported by the + * device. It only provides information about which constraint can be understood by the user agent. + * + * - [`MediaStreamConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#constraints) + * - [`MediaTrackSupportedConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints) + */ +export type InitConstraints = + | MediaStreamConstraints + | (( + supportedConstraints: MediaTrackSupportedConstraints, + ) => MediaStreamConstraints | Promise); + +/** + * Media track constraints specific to video tracks. + * + * Can be a `MediaTrackConstraints` object or a function that returns `MediaTrackConstraints`. The + * function is provided with an argument, `MediaTrackCapabilities`, which reprensents the media + * track capabilities, and should return either `MediaTrackConstraints` or a promise that resolves + * to it. + * + * Unlike `MediaTrackSupportedConstraints`, `MediaTrackCapabilities` provides the accurate + * information of the track capabilities supported by your device. + * + * - [`MediaTrackConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints) + * - [`MediaTrackCapabilities`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities#return_value) + */ +export type VideoConstraints = + | MediaTrackConstraints + | undefined + | (( + capabilities: MediaTrackCapabilities, + ) => + | MediaTrackConstraints + | undefined + | Promise); + +/** + * Media track constraints specific to audio tracks. + * + * Can be a `MediaTrackConstraints` object or a function that returns `MediaTrackConstraints`. The + * function is provided with an argument, `MediaTrackCapabilities`, which reprensents the media + * track capabilities, and should return either `MediaTrackConstraints` or a promise that resolves + * to it. + * + * Unlike `MediaTrackSupportedConstraints`, `MediaTrackCapabilities` provides the accurate + * information of the track capabilities supported by your device. + * + * - [`MediaTrackConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints) + * - [`MediaTrackCapabilities`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities#return_value) + */ +export type AudioConstraints = + | MediaTrackConstraints + | undefined + | (( + capabilities: MediaTrackCapabilities, + ) => + | MediaTrackConstraints + | undefined + | Promise); + +/** + * Details the result of a "start" action, including the options used, and the resulting media stream. + */ +interface StartActionResult { + type: "start"; + options: StartMediaStreamOptions; + stream: MediaStream; +} + +/** + * Details the result of an "inspect" action, including options used, the media stream inspected, + * and the capabilities of video and audio tracks. + */ +interface InspectActionResult { + type: "inspect"; + options: InspectMediaStreamOptions; + stream: MediaStream; + videoTracksCapabilities: MediaTrackCapabilities[]; + audioTracksCapabilities: MediaTrackCapabilities[]; +} + +/** + * Details the result of a "constrain" action, including the options used and the constrained media stream. + */ +interface ConstrainActionResult { + type: "constrain"; + options: ConstrainMediaStreamOptions; + stream: MediaStream; +} + +/** + * Represents the result of a "stop" action. + */ +interface StopActionResult { + type: "stop"; +} + +/** + * Union type representing the possible outcomes of actions performed on a media stream. + */ +type ActionResult = + | StartActionResult + | InspectActionResult + | ConstrainActionResult + | StopActionResult; + +/** + * Configuration options for the UserMediaStream object, including constraints for initialization, + * video, and audio tracks, a timeout for capability retrieval, and callbacks for stream lifecycle events. + */ +export interface UserMediaStreamOptions { + /** + * Media stream constraints for initialization. + */ + initConstraints?: InitConstraints; + /** + * Media track constraints specific to video tracks. + */ + videoConstraints?: VideoConstraints; + /** + * Media track constraints specific to audio tracks. + */ + audioConstraints?: AudioConstraints; + /** + * The default maximum time (in milliseconds) to wait for getting the capabilities of a media + * track. + */ + getCapabilitiesTimeout?: number; + /** + * Callback function that is triggered when the stream starts. + * + * @param stream - Media stream + */ + onStreamStart?: (stream: MediaStream) => unknown; + /** + * Callback function that is triggered when the stream stops. + */ + onStreamStop?: () => unknown; + /** + * Callback function that is triggered on each application of constraints. + * + * @param stream - Media stream + */ + onStreamUpdate?: (stream: MediaStream) => unknown; + /** + * Callback function that is triggered when the stream is inspected. + * + * @param streamCapablities - User media stream capabilities + */ + onStreamInspect?: ( + streamCapablities?: UserMediaStreamCapabilities, + ) => unknown; +} + +/** + * Represents the capabilities of video and audio tracks within a user media stream. + */ +export interface UserMediaStreamCapabilities { + videoTracksCapabilities: MediaTrackCapabilities[]; + audioTracksCapabilities: MediaTrackCapabilities[]; +} + +/** + * Interface for controlling and interacting with a user media stream, including starting, + * inspecting, stopping the stream, and updating configuration options. + */ +export interface UserMediaStream { + start: () => Promise; + inspect: () => Promise; + stop: () => Promise; + setOptions: (userMediaStreamOptions: UserMediaStreamOptions) => void; +} + +/** + * Resolved options for the UserMediaStream with default values provided. + */ +type ResolvedUserMediaStreamOptions = SetRequired< + UserMediaStreamOptions, + "initConstraints" | "getCapabilitiesTimeout" +>; + +/** + * Resolves and merges the provided UserMediaStreamOptions with default values for unspecified options. + * + * @param userMediaStreamOptions - The user-provided configuration options for the UserMediaStream. + * @returns The resolved configuration options with default values for unspecified options. + */ +function resolveUserMediaStreamOptions( + userMediaStreamOptions: UserMediaStreamOptions, +): ResolvedUserMediaStreamOptions { + return { + initConstraints: userMediaStreamOptions.initConstraints ?? { + video: true, + audio: false, + }, + videoConstraints: + "videoConstraints" in userMediaStreamOptions + ? userMediaStreamOptions.videoConstraints + : undefined, + audioConstraints: + "audioConstraints" in userMediaStreamOptions + ? userMediaStreamOptions.audioConstraints + : undefined, + getCapabilitiesTimeout: + userMediaStreamOptions.getCapabilitiesTimeout ?? GET_CAPABILITIES_TIMEOUT, + onStreamStart: + "onStreamStart" in userMediaStreamOptions + ? userMediaStreamOptions.onStreamStart + : undefined, + onStreamStop: + "onStreamStop" in userMediaStreamOptions + ? userMediaStreamOptions.onStreamStop + : undefined, + onStreamUpdate: + "onStreamUpdate" in userMediaStreamOptions + ? userMediaStreamOptions.onStreamUpdate + : undefined, + onStreamInspect: + "onStreamInspect" in userMediaStreamOptions + ? userMediaStreamOptions.onStreamInspect + : undefined, + }; +} + +/** + * Creates and returns a UserMediaStream object with the specified configuration options. + * Provides methods to start, inspect, and stop the media stream, as well as to update its options. + * + * @param userMediaStreamOptions - Optional configuration options for the UserMediaStream. + * @returns An object providing control methods for managing a user media stream. + */ +export function createUserMediaStream( + userMediaStreamOptions: UserMediaStreamOptions = {}, +): UserMediaStream { + const { + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, + } = resolveUserMediaStreamOptions(userMediaStreamOptions); + + // initialize a queue + let queue: Promise = Promise.resolve({ + type: "stop", + }); + + // queueify start + const queuefiedStart = async ( + startMediaStreamOptions: StartMediaStreamOptions, + ) => { + // update the queue + queue = queue.then((prevActionResult) => { + switch (prevActionResult.type) { + // previous action is a start action + case "start": { + // if options are the same, reuse the previous action result + if (compare(prevActionResult.options, startMediaStreamOptions)) { + return prevActionResult; + } + // otherwise stop and then start + return stopMediaStream(prevActionResult.stream).then(() => + startMediaStream(startMediaStreamOptions), + ); + } + // previous action is an inspect action + case "inspect": { + // stop and then start + return stopMediaStream(prevActionResult.stream).then(() => + startMediaStream(startMediaStreamOptions), + ); + } + // previous action is a constrain action + case "constrain": { + // stop and then start + return stopMediaStream(prevActionResult.stream).then(() => + startMediaStream(startMediaStreamOptions), + ); + } + // previous action is a stop action + case "stop": { + // just start + return startMediaStream(startMediaStreamOptions); + } + // unknown action + default: { + throw new TypeError( + `Unknown action result: ${prevActionResult satisfies never}`, + ); + } + } + }); + // execute the queue and get the result + const actionResult = (await queue) as StartActionResult; + // return the stream + return actionResult.stream; + }; + + // queueify inspect + const queuefiedInspect = async ( + inspectMediaStreamOptions: InspectMediaStreamOptions, + ) => { + // update the queue + queue = queue.then((prevActionResult) => { + switch (prevActionResult.type) { + // previous action is a start action + case "start": { + // just inspect + return inspectMediaStream( + prevActionResult.stream, + inspectMediaStreamOptions, + ); + } + // previous action is an inspect action + case "inspect": { + // if options are the same, reuse the previous action result + if (compare(prevActionResult.options, inspectMediaStreamOptions)) { + return prevActionResult; + } + // otherwise just inspect + return inspectMediaStream( + prevActionResult.stream, + inspectMediaStreamOptions, + ); + } + // previous action is a constrain action + case "constrain": { + // just inspect + return inspectMediaStream( + prevActionResult.stream, + inspectMediaStreamOptions, + ); + } + // previous action is a stop action + case "stop": { + // we cannot inspect a stopped stream + // so do nothing and reuse the previous action + return prevActionResult; + } + // unknown action + default: { + throw new TypeError( + `Unknown action result: ${prevActionResult satisfies never}`, + ); + } + } + }); + // execute the queue and get the result + const actionResult = await queue; + if (actionResult.type === "inspect") { + // return the stream if it is not stopped + return { + videoTracksCapabilities: actionResult.videoTracksCapabilities, + audioTracksCapabilities: actionResult.audioTracksCapabilities, + }; + } + }; + + // queueify constrain + const queuefiedConstrain = async ( + constrainMediaStreamOptions: ConstrainMediaStreamOptions, + ) => { + // update the queue + queue = queue.then((prevActionResult) => { + switch (prevActionResult.type) { + // previous action is a start action + case "start": { + // just apply constraints + return constrainMediaStream( + prevActionResult.stream, + constrainMediaStreamOptions, + ); + } + // previous action is an inspect action + case "inspect": { + // just apply constraints + return constrainMediaStream( + prevActionResult.stream, + constrainMediaStreamOptions, + ); + } + // previous action is a constrain action + case "constrain": { + // if options are the same, reuse the previous action result + if (compare(prevActionResult.options, constrainMediaStreamOptions)) { + return prevActionResult; + } + // otherwise just apply constraints + return constrainMediaStream( + prevActionResult.stream, + constrainMediaStreamOptions, + ); + } + // previous action is a stop action + case "stop": { + // we cannot apply constraints to a stopped stream + // so do nothing and reuse the previous action + return prevActionResult; + } + // unknown action + default: { + throw new TypeError( + `Unknown action result: ${prevActionResult satisfies never}`, + ); + } + } + }); + // execute the queue and get the result + const actionResult = await queue; + if (actionResult.type === "constrain") { + // return the stream if it is not stopped + return actionResult.stream; + } + }; + + // queueify stop + const queuefiedStop = async () => { + queue = queue.then((prevActionResult) => { + switch (prevActionResult.type) { + // previous action is a start action + case "start": { + // just stop the stream + return stopMediaStream(prevActionResult.stream); + } + // previous action is an inspect action + case "inspect": { + // just stop the stream + return stopMediaStream(prevActionResult.stream); + } + // previous action is a constrain action + case "constrain": { + // just stop the stream + return stopMediaStream(prevActionResult.stream); + } + // previous action is a stop action + case "stop": { + // reuse the previous action + return prevActionResult; + } + // unknown action + default: { + throw new TypeError( + `Unknown action result: ${prevActionResult satisfies never}`, + ); + } + } + }); + // execute the queue + await queue; + }; + + // create a state store + const userMediaStreamStore = createStore()( + subscribeWithSelector(() => ({ + initConstraints, + videoConstraints, + audioConstraints, + + getCapabilitiesTimeout, + + onStreamStart, + onStreamStop, + onStreamUpdate, + onStreamInspect, + })), + ); + + // invoke constrain when constraints update + userMediaStreamStore.subscribe( + (options) => [options.videoConstraints, options.audioConstraints] as const, + async ([videoConstraints, audioConstraints]) => { + const { getCapabilitiesTimeout, onStreamUpdate } = + userMediaStreamStore.getState(); + const stream = await queuefiedConstrain({ + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + }); + stream && onStreamUpdate?.(stream); + return stream; + }, + { equalityFn: shallow }, + ); + + // the exposed control function to start a stream + const start = async () => { + const { + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + onStreamStart, + } = userMediaStreamStore.getState(); + const stream = await queuefiedStart({ + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + }); + onStreamStart?.(stream); + return stream; + }; + + // the exposed inspect function to inspect a stream + const inspect = async () => { + const { getCapabilitiesTimeout, onStreamInspect } = + userMediaStreamStore.getState(); + const streamCapablities = await queuefiedInspect({ + getCapabilitiesTimeout, + }); + onStreamInspect?.(streamCapablities ? { ...streamCapablities } : undefined); + return streamCapablities; + }; + + // the exposed control function to stop a stream + const stop = async () => { + const { onStreamStop } = userMediaStreamStore.getState(); + await queuefiedStop(); + onStreamStop?.(); + }; + + // return the exposed control functions + return { + start, + inspect, + stop, + setOptions: (userMediaStreamOptions) => + userMediaStreamStore.setState( + resolveUserMediaStreamOptions(userMediaStreamOptions), + ), + }; +} + +interface StartMediaStreamOptions { + initConstraints: InitConstraints; + videoConstraints?: VideoConstraints; + audioConstraints?: AudioConstraints; + getCapabilitiesTimeout: number; +} + +/** + * Start a media stream with specified constraints. + * + * This function does some checks mainly for compatibility. + * + * @param startMediaStreamOptions - The constraints (initial, video, and audio) to apply along with + * meta configuration options. + * @returns A promise that resolves to an StartActionResult for later queueing. + */ +async function startMediaStream({ + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, +}: StartMediaStreamOptions): Promise { + // check if we are in the secure context + if (window.isSecureContext !== true) { + throw new DOMException( + "Cannot use navigator.mediaDevices in insecure contexts. Please use HTTPS or localhost.", + "NotAllowedError", + ); + } + + // check if we can use the getUserMedia API + if (navigator.mediaDevices?.getUserMedia === undefined) { + throw new DOMException( + "The method navigator.mediaDevices.getUserMedia is not defined. Does your runtime support this API?", + "NotSupportedError", + ); + } + + // shim WebRTC APIs in the client runtime + await shimGetUserMedia(); + + // resolve initial constraints + const resolvedInitConstraints = + typeof initConstraints === "function" + ? // callback constraints + await initConstraints(navigator.mediaDevices.getSupportedConstraints()) + : initConstraints; + + // apply initial constraints and get the media stream + const stream = await navigator.mediaDevices.getUserMedia( + resolvedInitConstraints, + ); + + // apply video and audio constraints + await constrainMediaStream(stream, { + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + }); + + return { + type: "start", + options: { + initConstraints, + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + }, + stream: stream, + }; +} + +interface ConstrainMediaStreamOptions { + videoConstraints?: VideoConstraints; + audioConstraints?: AudioConstraints; + getCapabilitiesTimeout: number; +} + +/** + * Apply video and audio constraints to a given media stream. + * + * This function processes both video and audio tracks of the media stream, applying the specified + * constraints to each kind of tracks. + * + * @param stream - The MediaStream to which constraints will be applied. + * @param constrainMediaStreamOptions - The video and audio constraints to apply along with meta + * configuration options. + * @returns A promise that resolves to a ConstrainActionResult for later queueing. + */ +async function constrainMediaStream( + stream: MediaStream, + { + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + }: ConstrainMediaStreamOptions, +): Promise { + // get video tracks + const videoTracks = stream.getVideoTracks(); + + // get audio tracks + const audioTracks = stream.getAudioTracks(); + + // apply media track constraints + await Promise.all([ + // apply video constraints + Promise.all( + videoTracks.map(async (videoTrack) => { + const resolvedVideoConstraints = + typeof videoConstraints === "function" + ? // callback constraints + await videoConstraints( + await getCapabilities(videoTrack, getCapabilitiesTimeout), + ) + : videoConstraints; + await videoTrack.applyConstraints(resolvedVideoConstraints); + }), + ), + // apply audio constraints + Promise.all( + audioTracks.map(async (audioTrack) => { + const resolvedAudioConstraints = + typeof audioConstraints === "function" + ? // callback constraints + await audioConstraints( + await getCapabilities(audioTrack, getCapabilitiesTimeout), + ) + : audioConstraints; + await audioTrack.applyConstraints(resolvedAudioConstraints); + }), + ), + ]); + + return { + type: "constrain", + options: { + videoConstraints, + audioConstraints, + getCapabilitiesTimeout, + }, + stream, + }; +} + +interface InspectMediaStreamOptions { + getCapabilitiesTimeout: number; +} + +async function inspectMediaStream( + stream: MediaStream, + { getCapabilitiesTimeout }: InspectMediaStreamOptions, +): Promise { + // get video tracks + const videoTracks = stream.getVideoTracks(); + + // get audio tracks + const audioTracks = stream.getAudioTracks(); + + const [videoTracksCapabilities, audioTracksCapabilities] = await Promise.all([ + Promise.all( + videoTracks.map(async (videoTrack) => + getCapabilities(videoTrack, getCapabilitiesTimeout), + ), + ), + Promise.all( + audioTracks.map(async (audioTrack) => + getCapabilities(audioTrack, getCapabilitiesTimeout), + ), + ), + ]); + + return { + type: "inspect", + options: { + getCapabilitiesTimeout, + }, + stream, + videoTracksCapabilities, + audioTracksCapabilities, + }; +} + +/** + * Stop a given media stream. + * + * This function stops all tracks of the given media stream. It ensures that the media stream is + * properly terminated. + * + * @param stream - The MediaStream to be stopped. + * @returns A promise that resolves to a StopActionResult for later queueing. + */ +async function stopMediaStream(stream: MediaStream): Promise { + for (const track of stream.getTracks()) { + stream.removeTrack(track); + track.stop(); + } + + return { + type: "stop", + }; +} + +/** + * Attach a given media stream to a HTMLVideoElement. + * + * This function sets the source of the video element to the provided media stream. It supports + * different ways of attaching the stream based on browser compatibility. + * + * @param videoElement - The HTMLVideoElement to which the media stream will be attached. + * @param stream - The MediaStream to be attached to the video element. + */ +export function attachMediaStream( + videoElement: HTMLVideoElement, + stream: MediaStream, +) { + // attach the stream to the video element + if (videoElement.srcObject !== undefined) { + videoElement.srcObject = stream; + } else if (videoElement.mozSrcObject !== undefined) { + videoElement.mozSrcObject = stream; + } else if (window.URL.createObjectURL) { + videoElement.src = (window.URL.createObjectURL as CreateObjectURLCompat)( + stream, + ); + } else if (window.webkitURL) { + videoElement.src = ( + window.webkitURL.createObjectURL as CreateObjectURLCompat + )(stream); + } else { + videoElement.src = stream.id; + } +} + +/** + * Asynchronously retrieve the capabilities of a given media stream track. + * + * This function attempts to fetch the capabilities of a MediaStreamTrack. If called too early, it + * may return an empty object as [the capabilities might not be available + * immediately](https://oberhofer.co/mediastreamtrack-and-its-capabilities/#queryingcapabilities). + * Because capabilities are allowed to be an empty object, a timeout mechanism is implemented to + * ensure the function returns even if capabilities are not obtained within the specified time, + * preventing indefinite waiting. + * + * @param track - The MediaStreamTrack whose capabilities are to be fetched. + * @param timeout - The maximum time (in milliseconds) to wait for fetching capabilities. + * @returns A promise that resolves to the track's capabilities, which may be an empty object. + */ +export async function getCapabilities( + track: MediaStreamTrack, + timeout = GET_CAPABILITIES_TIMEOUT, +): Promise { + return new Promise((resolve) => { + // timeout, return empty capabilities + let timeoutId: number | undefined = setTimeout(() => { + resolve({}); + timeoutId = undefined; + return; + }, timeout); + + // not supported, return empty capabilities + if (!track.getCapabilities) { + clearTimeout(timeoutId); + resolve({}); + timeoutId = undefined; + return; + } + + // poll to check capabilities + let capabilities: MediaTrackCapabilities = {}; + while (Object.keys(capabilities).length === 0 && timeoutId !== undefined) { + capabilities = track.getCapabilities(); + } + clearTimeout(timeoutId); + resolve(capabilities); + timeoutId = undefined; + return; + }); +} diff --git a/src/stream/webrtc-adapter.d.ts b/src/stream/webrtc-adapter.d.ts new file mode 100644 index 00000000..ad4f11e3 --- /dev/null +++ b/src/stream/webrtc-adapter.d.ts @@ -0,0 +1,30 @@ +/** + * This file declares types for the non-public exports from "webrtc-adapter". As we're only + * interested in the getUserMedia shims, we use this approach to avoid importing the whole library. + * + * This type declaration file is only meant to be used internally. + */ + +declare module "webrtc-adapter/dist/chrome/getusermedia" { + function shimGetUserMedia( + window: Window, + browserDetails: import("webrtc-adapter").IAdapter["browserDetails"], + ): void; +} + +declare module "webrtc-adapter/dist/firefox/getusermedia" { + function shimGetUserMedia( + window: Window, + browserDetails: import("webrtc-adapter").IAdapter["browserDetails"], + ): void; +} + +declare module "webrtc-adapter/dist/safari/safari_shim" { + function shimGetUserMedia(window: Window): void; +} + +declare module "webrtc-adapter/dist/utils" { + function detectBrowser( + window: Window, + ): import("webrtc-adapter").IAdapter["browserDetails"]; +} diff --git a/src/writer/index.ts b/src/writer/index.ts index 0d43db84..1b8b1955 100644 --- a/src/writer/index.ts +++ b/src/writer/index.ts @@ -1,6 +1,6 @@ /** - * The writer part API of this package is subject to change a lot. - * Please track the status of [this issue](https://github.com/zxing-cpp/zxing-cpp/issues/332). + * The writer part API of this package is subject to change a lot. Please track the status of [this + * issue](https://github.com/zxing-cpp/zxing-cpp/issues/332). * * @packageDocumentation */ @@ -45,6 +45,6 @@ export async function writeBarcodeToImageFile( export * from "../bindings/exposedWriterBindings.js"; export { purgeZXingModule, - type ZXingWriterModule, type ZXingModuleOverrides, + type ZXingWriterModule, } from "../core.js"; diff --git a/tsconfig.base.json b/tsconfig.base.json index f1a2ef8f..ffaea4a9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,6 +1,8 @@ { "compilerOptions": { "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", "useDefineForClassFields": true, "skipLibCheck": true, "resolveJsonModule": true, diff --git a/tsconfig.json b/tsconfig.json index 07dcdbbb..05b705a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "module": "NodeNext", + "jsx": "react-jsx", "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["emscripten"], - "moduleResolution": "NodeNext" + "noUncheckedIndexedAccess": true }, - "include": ["./src"], + "include": ["./src", "./main.ts", "./main.tsx"], "references": [ { "path": "./tsconfig.node.json" diff --git a/tsconfig.node.json b/tsconfig.node.json index 2c6111d3..521555ee 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -2,9 +2,7 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "composite": true, - "module": "ESNext", "types": ["node"], - "moduleResolution": "Bundler", "resolveJsonModule": true, "allowJs": true }, diff --git a/tsconfig.production.json b/tsconfig.production.json new file mode 100644 index 00000000..a16641f0 --- /dev/null +++ b/tsconfig.production.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["./src"], + "ignore": ["./src/index.css.ts"] +} diff --git a/typedoc.json b/typedoc.json index dddddcdd..9d9a3f37 100644 --- a/typedoc.json +++ b/typedoc.json @@ -3,7 +3,11 @@ "entryPoints": [ "./src/full/index.ts", "./src/reader/index.ts", - "./src/writer/index.ts" + "./src/writer/index.ts", + "./src/scanner/index.ts", + "./src/stream/index.ts", + "./src/react/hooks/index.ts" ], + "sortEntryPoints": false, "out": "docs" } diff --git a/vite.config.ts b/vite.config.ts index 2cc173f9..db7e32b1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; import { defineConfig } from "vite"; import babel from "vite-plugin-babel"; import { version } from "./package-lock.json"; @@ -11,14 +12,35 @@ export default defineConfig({ "reader/index": "src/reader/index.ts", "writer/index": "src/writer/index.ts", "full/index": "src/full/index.ts", + "scanner/index": "src/scanner/index.ts", + "stream/index": "src/stream/index.ts", + "react/index": "src/react/index.ts", + "react/hooks/index": "src/react/hooks/index.ts", + "react/components/index": "src/react/components/index.ts", }, formats: ["es"], fileName: (_, entryName) => `${entryName}.js`, }, outDir: "dist/es", rollupOptions: { + external: ["react"], output: { manualChunks: (id) => { + if (/webrtc-adapter\/dist\/utils/.test(id)) { + return "shim-utils"; + } + if (/webrtc-adapter\/dist\/chrome/.test(id)) { + return "shim-chrome"; + } + if (/webrtc-adapter\/dist\/firefox/.test(id)) { + return "shim-firefox"; + } + if (/webrtc-adapter\/dist\/safari/.test(id)) { + return "shim-safari"; + } + if (/zustand/.test(id)) { + return "zustand"; + } if ( /core\.ts|exposedReaderBindings\.ts|exposedWriterBindings\.ts/.test( id, @@ -26,11 +48,34 @@ export default defineConfig({ ) { return "core"; } + if (/src\/reader\/index\.ts/.test(id)) { + return "reader"; + } + if (/src\/writer\/index\.ts/.test(id)) { + return "writer"; + } + if (/src\/full\/index\.ts/.test(id)) { + return "full"; + } + if (/src\/scanner\/index\.ts/.test(id)) { + return "scanner"; + } + if (/src\/stream\/index\.ts/.test(id)) { + return "stream"; + } + if (/src\/react\/hooks\/index\.ts/.test(id)) { + return "react-hooks"; + } + if (/src\/react\/components\/index\.ts/.test(id)) { + return "react-components"; + } }, + chunkFileNames: "[name]-[hash].js", }, }, }, plugins: [ + vanillaExtractPlugin(), babel({ babelConfig: { plugins: [emscriptenPatch()], @@ -41,5 +86,6 @@ export default defineConfig({ ], define: { NPM_PACKAGE_VERSION: JSON.stringify(version), + "process.env.NODE_ENV": JSON.stringify("production"), }, });