Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v1.2.3] - 2026-02-27

### Added
- Add a `DisposableStack` to clean-up
- Add support for passing a `base` (document or Shadow Root) to get `<video>` from
- Support numerous forms of disposal

## [v1.2.2] - 2025-12-16

### Added
Expand Down
13 changes: 6 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,27 @@ document.adoptedStyleSheets = [reset, theme, btn];

preloadRxing();

function start() {
const controller = new AbortController();
const signal = controller.signal;
async function start() {
const btn = document.getElementById('stop');
const stack = new DisposableStack();

createBarcodeScanner(({ rawValue, format }) => {
const li = document.createElement('li');
li.textContent = `[${format}] ${rawValue}`;
document.getElementById('results').append(li);
}, { frameRate: 24, signal, video: 'scanner', chimeType: 'sawtooth', chimeFrequency: 4000, chimeDuration: 0.1 }).catch(err => {
controller.abort(err);
}, { frameRate: 24, video: 'scanner', chimeType: 'sawtooth', chimeFrequency: 4000, chimeDuration: 0.1, stack }).catch(err => {
stack.disposeAsync();
reportError(err);
const li = document.createElement('li');
li.textContent = err.message;
document.getElementById('results').append(li);
});

btn.addEventListener('click', ({ currentTarget }) => {
controller.abort();
stack.dispose();
currentTarget.disabled = true;
document.getElementById('start').disabled = false;
}, { once: true, signal });
}, { once: true });

btn.disabled = false;
}
Expand Down
22 changes: 14 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aegisjsproject/barcodescanner",
"version": "1.2.2",
"version": "1.2.3",
"description": "A simple barcode scanner module ",
"keywords": [
"barcode",
Expand Down Expand Up @@ -78,11 +78,11 @@
"@aegisjsproject/dev-server": "^1.0.5",
"@aegisjsproject/http-utils": "^1.0.4",
"@rollup/plugin-terser": "^0.4.4",
"@shgysk8zer0/eslint-config": "^1.0.4",
"@shgysk8zer0/eslint-config": "^1.0.5",
"@shgysk8zer0/http-server": "^1.1.1",
"@shgysk8zer0/importmap": "^1.7.7",
"@shgysk8zer0/polyfills": "^0.6.0",
"eslint": "^10.0.0",
"rollup": "^4.40.1"
"@shgysk8zer0/importmap": "^1.7.11",
"@shgysk8zer0/polyfills": "^0.6.2",
"eslint": "^10.0.2",
"rollup": "^4.59.0"
}
}
69 changes: 47 additions & 22 deletions scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ function _getConstraint(val) {
* @param {OscillatorType} [options.chimeType="sine"] Shape of the audio for the chime
* @param {number} [options.chimeVolume=0.2] Chime volume on detect
* @param {Function} [options.errorHandler=reportError] Callback for handling errors
* @param {DocumentOrShadowRoot} [options.base=document] Base to query for video ID if passed a string/id for a `<video>`
* @param {DisposableStack|AsyncDisposableStack} [options.stack] Optional stack to handle disposal
* @param {number} [options.width] Requested camera width
* @param {number} [options.height] Requested camera height
* @param {AbortSignal} [options.signal] Abort signal to abort the stream/video
Expand All @@ -91,6 +93,8 @@ export async function createBarcodeScanner(callback = console.log, {
chimeType = CHIME_TYPE,
chimeVolume = CHIME_VOLUME,
errorHandler = reportError,
base = document,
stack,
width,
height,
signal,
Expand All @@ -99,7 +103,7 @@ export async function createBarcodeScanner(callback = console.log, {

if (typeof video === 'string') {
return await createBarcodeScanner(callback, {
video: document.getElementById(video),
video: base.getElementById(video),
delay,
formats,
facingMode,
Expand All @@ -109,6 +113,7 @@ export async function createBarcodeScanner(callback = console.log, {
chimeType,
chimeVolume,
errorHandler,
stack,
width,
height,
signal,
Expand All @@ -117,28 +122,57 @@ export async function createBarcodeScanner(callback = console.log, {
reject(new TypeError(`Expected a <video> but got a ${typeof video}.`));
} else if (signal instanceof AbortSignal && signal.aborted) {
reject(signal.reason);
} else if (stack?.disposed) {
throw new DOMException('Stack was disposed.', 'AbortError');
} else {
let frame = NaN;
const controller = new AbortController();
const loadController = new AbortController();

/**
* @type {{ adopt: <T>(value: T, onDispose: (val: T) => void) => T, dispose: () => void }}
*/
const disposableStack = stack instanceof DisposableStack || stack instanceof AsyncDisposableStack
? stack.use(new DisposableStack())
: new DisposableStack();

const controller = disposableStack.adopt(
new AbortController(),
controller => controller.abort(new DOMException('Stack disposed.', 'AbortError'))
);

const loadController = disposableStack.adopt(
new AbortController(),
controller => controller.abort(new DOMException('Stack disposed.', 'AbortError'))
);

const sig = signal instanceof AbortSignal
? AbortSignal.any([signal, controller.signal])
: controller.signal;

const scanner = new BarcodeDetector({ formats });
const wakeLock = 'wakeLock' in navigator
? await navigator.wakeLock.request('screen').catch(() => undefined)
? disposableStack.adopt(
await navigator.wakeLock.request('screen').catch(() => undefined),
lock => lock?.released || lock?.release()
)
: undefined;

const stream = await navigator.mediaDevices.getUserMedia({
disposableStack.defer(() => {
video.srcObject = null;
video.cancelVideoFrameCallback(frame);
});

/**
* @type {MediaStream}
*/
const stream = disposableStack.adopt(await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
frameRate: _getConstraint(frameRate),
facingMode: _getConstraint(facingMode),
width: _getConstraint(width),
height: _getConstraint(height),
},
});
}), stream => stream.getTracks().forEach(track => track.stop()));

const [track] = stream.getVideoTracks();

Expand Down Expand Up @@ -169,7 +203,7 @@ export async function createBarcodeScanner(callback = console.log, {
const { width, height } = track.getSettings();
target.width = width;
target.height = height;
resolve({ controller, video, stream, wakeLock, signal: sig });
resolve({ controller, video, stream, wakeLock, signal: sig, [Symbol.dispose]: disposableStack[Symbol.dispose].bind(disposableStack), stack: disposableStack });
drawFrame();
loadController.abort();
}, { once: true, signal: sig });
Expand All @@ -181,22 +215,13 @@ export async function createBarcodeScanner(callback = console.log, {
reject(err);
}, { once: true, signal: sig });

sig.addEventListener('abort', async ({ target }) => {
video.cancelVideoFrameCallback(frame);
video.pause();
video.srcObject = null;
stream.getTracks().forEach(track => track.stop());
sig?.addEventListener('abort', disposableStack.dispose.bind(disposableStack), { once: true });

if (typeof wakeLock === 'object') {
await wakeLock.release();
}

if (! loadController.signal.aborted) {
loadController.abort(target.reason);
}
}, { once: true });

video.play();
video.play().catch(err => {
reject(err);
controller.abort(err);
disposableStack.dispose();
});
}

return promise;
Expand Down
Loading