From 2e50c46b3f5721a6d94844806cc7137b4dae9005 Mon Sep 17 00:00:00 2001 From: Patrick Cason Date: Tue, 10 Sep 2024 19:58:41 -0500 Subject: [PATCH] chore: brought some things into the frame processor and wrote better docs for the reader --- packages/reader/README.md | 42 ++++++++- .../reader/src/processors/file-processor.ts | 65 -------------- .../reader/src/processors/frame-processor.ts | 87 +++++++++++++++++++ .../reader/src/processors/webrtc-processor.ts | 72 +-------------- 4 files changed, 127 insertions(+), 139 deletions(-) diff --git a/packages/reader/README.md b/packages/reader/README.md index 07450e7..7810341 100644 --- a/packages/reader/README.md +++ b/packages/reader/README.md @@ -30,7 +30,11 @@ bun add @flipbookqr/reader ## Usage -```typescript +### Screenshare + +The following will read a QR code via the `getDisplayMedia` (screenshare) API: + +```ts import { Reader } from '@flipbookqr/reader'; // Create a new instance of the Flipbook reader @@ -42,20 +46,50 @@ const result = await reader.read(); The `result` is a is the original payload that was encoded into the series of QR codes. -**Please note:** The default configuration of the reader is to read the QR codes visible on the screen using WebRTC's `getUserMedia` API. This means that the reader will ask for permission to view the user's screen. You may want to use a different `frameProcessor` if you want to read the QR codes from a different source, such as a file. You can change the `frameProcessor` like this: +### Camera + +The following will read a QR code via the `getUserMedia` (camera) API: + +```ts +import { Reader, WebRTCProcessor } from '@flipbookqr/reader'; + +// Create a new instance of the Flipbook reader +const reader = new Reader({ + frameProcessor: new WebRTCProcessor('camera') +}); + +// Get a list of all available camera sources +const sources = await reader.opts.frameProcessor.getStreamTracks(); + +// Select a camera source +reader.opts.frameProcessor.setStreamTrack(sources[0]); + +// Note: If you don't do the above two commands, it will default to the first camera source + +// Read the Flipbook visible on the screen +const result = await reader.read(); +``` + +The `result` is a is the original payload that was encoded into the series of QR codes. + +### File upload + +The following will read a QR code from a file: ```ts import { Reader, FileProcessor } from '@flipbookqr/reader'; -const file = // some file +const file = new File(); // some file const reader = new Reader({ frameProcessor: new FileProcessor(file) }); -const result = await reader.read() +const result = await reader.read(); ``` +The `result` is a is the original payload that was encoded into the series of QR codes. + ## Configuration The `Writer` class accepts an optional configuration object that can be used to customize the behavior of the writer. The following options are available: diff --git a/packages/reader/src/processors/file-processor.ts b/packages/reader/src/processors/file-processor.ts index 990ccca..8b2f6d8 100644 --- a/packages/reader/src/processors/file-processor.ts +++ b/packages/reader/src/processors/file-processor.ts @@ -1,6 +1,3 @@ -import jsQR, { type QRCode } from 'jsqr'; -import { getLogger } from '@flipbookqr/shared'; -import { type Logger } from 'loglevel'; import { parseGIF, decompressFrames } from 'gifuct-js'; import { sliceFrames, sortFrames } from '../helpers'; import { FrameProcessor } from './frame-processor'; @@ -11,11 +8,6 @@ import { FrameProcessor } from './frame-processor'; * @extends FrameProcessor */ export class FileProcessor extends FrameProcessor { - protected _ctx: CanvasRenderingContext2D | null; - protected _canvas: HTMLCanvasElement; - protected _width: number; - protected _height: number; - protected _log: Logger; protected _file: File; /** @@ -27,65 +19,8 @@ export class FileProcessor extends FrameProcessor { // Initialize the processor super(); - // Set up logger - this._log = getLogger(); - // Set the file to process this._file = file; - - // Create canvas element - const canvas = document.createElement('canvas'); - this._width = 1920; - this._height = 1080; - this._canvas = canvas; - this._ctx = canvas.getContext('2d'); - } - - /** - * Sets the specified frame on the canvas. - * - * @param {HTMLImageElement | ImageBitmap} frame - The frame to draw on the canvas. - * @throws Will throw an error if an unsupported frame type is provided. - */ - protected setFrame(frame: HTMLImageElement | ImageBitmap): void { - // Store the frame dimensions - let width, height; - - // Get the frame dimensions - if (frame instanceof HTMLImageElement || frame instanceof ImageBitmap) { - width = frame.width; - height = frame.height; - } else { - throw new Error('Unsupported frame type'); - } - - // Update the canvas dimensions - this._width = width; - this._height = height; - this._canvas.width = width; - this._canvas.height = height; - - // Draw the frame on the canvas - this._ctx?.drawImage(frame, 0, 0, this._width, this._height); - } - - /** - * Retrieves QR code data from the current canvas frame. - * - * @returns {QRCode | null} The decoded QR code data, or null if no data is found. - */ - protected getFrameData(): QRCode | null { - // Get the image data from the canvas - const imageData = this._ctx?.getImageData(0, 0, this._width, this._height); - - // Decode the image data to extract QR code data - let decodedData: null | QRCode = null; - if (imageData && 'data' in imageData) { - decodedData = jsQR(imageData.data, this._width, this._height); - } - - // Return the decoded data - return decodedData; } /** diff --git a/packages/reader/src/processors/frame-processor.ts b/packages/reader/src/processors/frame-processor.ts index b47346d..1942792 100644 --- a/packages/reader/src/processors/frame-processor.ts +++ b/packages/reader/src/processors/frame-processor.ts @@ -1,3 +1,90 @@ +import { getLogger } from '@flipbookqr/shared'; +import jsQR, { type QRCode } from 'jsqr'; +import type { Logger } from 'loglevel'; + export abstract class FrameProcessor { + protected _ctx: CanvasRenderingContext2D | null; + protected _canvas: HTMLCanvasElement; + protected _width: number; + protected _height: number; + protected _log: Logger; + + constructor() { + // Set up logger + this._log = getLogger(); + + // Create canvas element + const canvas = document.createElement('canvas'); + this._width = 1920; + this._height = 1080; + this._canvas = canvas; + this._ctx = canvas.getContext('2d'); + } + + /** + * Sets the specified frame on the canvas. + * + * @param {HTMLVideoElement | HTMLImageElement | ImageBitmap} source - The source to draw on the canvas. + * @throws Will throw an error if an unsupported frame type is provided. + */ + protected setFrame( + source: HTMLVideoElement | HTMLImageElement | ImageBitmap + ): void { + // Store the source dimensions + let width, height; + + // Get the source dimensions + if (source instanceof HTMLImageElement || source instanceof ImageBitmap) { + width = source.width; + height = source.height; + } else if (source instanceof HTMLVideoElement) { + width = source.videoWidth; + height = source.videoHeight; + } else { + throw new Error('Unsupported frame type'); + } + + // Update the canvas dimensions + this._width = width; + this._height = height; + this._canvas.width = width; + this._canvas.height = height; + + // Draw the frame on the canvas + this._ctx?.drawImage(source, 0, 0, this._width, this._height); + } + + /** + * Retrieves QR code data from the current frame. + * + * @returns {QRCode | null} The decoded QR code data, or null if no data was found. + */ + protected getFrameData(): QRCode | null { + // Get the frame data from the canvas + const results = this._ctx?.getImageData( + 0, + 0, + this._canvas.width, + this._canvas.height + ); + + // If no data is found, return null + if (!results) return null; + + this._log.debug('Got frame data', results); + + // Decode the frame data using a QR code reader + const decodedData = jsQR( + results.data, + this._canvas.width, + this._canvas.height + ); + + this._log.debug('Decoded frame data', decodedData); + + // Return the decoded data + return decodedData; + } + abstract read(): Promise; } diff --git a/packages/reader/src/processors/webrtc-processor.ts b/packages/reader/src/processors/webrtc-processor.ts index 018168f..33e2ed7 100644 --- a/packages/reader/src/processors/webrtc-processor.ts +++ b/packages/reader/src/processors/webrtc-processor.ts @@ -1,6 +1,4 @@ -import jsQR, { type QRCode } from 'jsqr'; -import { getHeadLength, getLogger } from '@flipbookqr/shared'; -import type { Logger } from 'loglevel'; +import { getHeadLength } from '@flipbookqr/shared'; import { sliceFrames, sortFrames } from '../helpers'; import { FrameProcessor } from './frame-processor'; @@ -14,15 +12,10 @@ type MediaOptions = DisplayMediaStreamOptions | MediaStreamConstraints; * @extends FrameProcessor */ export class WebRTCProcessor extends FrameProcessor { - protected _ctx: CanvasRenderingContext2D | null; - protected _canvas: HTMLCanvasElement; - protected _width: number; - protected _height: number; protected _video: HTMLVideoElement | undefined; protected _track: MediaStreamTrack | undefined; protected _mediaType: MediaType; protected _mediaOptions: MediaOptions; - protected _log: Logger; /** * Creates an instance of WebRTCProcessor. @@ -34,16 +27,6 @@ export class WebRTCProcessor extends FrameProcessor { // Initialize the processor super(); - // Set up logger - this._log = getLogger(); - - // Create canvas element - const canvas = document.createElement('canvas'); - this._width = 1920; - this._height = 1080; - this._canvas = canvas; - this._ctx = canvas.getContext('2d'); - // Initialize video and track this._video = undefined; this._track = undefined; @@ -56,57 +39,6 @@ export class WebRTCProcessor extends FrameProcessor { }; } - /** - * Sets the current video frame onto the canvas. - * If the video element is present, it draws the video frame onto the canvas. - */ - protected setFrame(): void { - // If the video and context are available, draw the video frame onto the canvas - if (this._video && this._ctx) { - // Get the video dimensions - const { videoWidth, videoHeight } = this._video; - - // Set the canvas dimensions - this._canvas.width = videoWidth; - this._canvas.height = videoHeight; - - // Draw the video frame onto the canvas - this._ctx.drawImage(this._video, 0, 0, videoWidth, videoHeight); - } - } - - /** - * Retrieves QR code data from the current frame. - * - * @returns {QRCode | null} The decoded QR code data, or null if no data was found. - */ - protected getFrameData(): QRCode | null { - // Get the frame data from the canvas - const results = this._ctx?.getImageData( - 0, - 0, - this._canvas.width, - this._canvas.height - ); - - // If no data is found, return null - if (!results) return null; - - this._log.debug('Got frame data', results); - - // Decode the frame data using a QR code reader - const decodedData = jsQR( - results.data, - this._canvas.width, - this._canvas.height - ); - - this._log.debug('Decoded frame data', decodedData); - - // Return the decoded data - return decodedData; - } - /** * Cleans up by removing the canvas and video elements. */ @@ -140,7 +72,7 @@ export class WebRTCProcessor extends FrameProcessor { const processFrame = (): void => { try { // Set the current frame onto the canvas - this.setFrame(); + this.setFrame(this._video!); // Get the QR code data from the frame const result = this.getFrameData();