qrcode-transmitter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +107 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +226 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +190 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-03-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `encodeBytesToQRCodes` to split `Uint8Array` payloads into protocol frames and generate QR SVG output
|
|
15
|
+
- Added `startVideoQRReceiver` to scan QR frames from camera input and reconstruct complete data automatically
|
|
16
|
+
- Added chunk protocol and aggregation logic (frame parsing, deduplication, and out-of-order reassembly)
|
|
17
|
+
- Added example app and tests covering protocol and main flow
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# qrcode-transmitter
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/qrcode-transmitter)
|
|
4
|
+
|
|
5
|
+
A lightweight browser-oriented library for splitting arbitrary binary data into multiple QR frames and reassembling it on scan.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Splits `Uint8Array` payloads into protocol frames and encodes each frame as QR SVG
|
|
10
|
+
- Scans QR codes from camera input and reassembles payloads automatically
|
|
11
|
+
- Handles duplicate frames and out-of-order frame delivery (`msgId + frameIndex` aggregation)
|
|
12
|
+
- Includes a runnable example app for local testing and demos
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add qrcode-transmitter
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You can also install it with `npm` or `yarn`.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { encodeBytesToQRCodes, startVideoQRReceiver } from "qrcode-transmitter";
|
|
26
|
+
|
|
27
|
+
// 1) Sender: encode data into multiple QR frames
|
|
28
|
+
const bytes = new TextEncoder().encode("Hello QR");
|
|
29
|
+
const frames = encodeBytesToQRCodes(bytes);
|
|
30
|
+
// frames[i].svg can be injected into the DOM directly
|
|
31
|
+
|
|
32
|
+
// 2) Receiver: scan with a video element and reassemble
|
|
33
|
+
const video = document.querySelector("video") as HTMLVideoElement;
|
|
34
|
+
const receiver = startVideoQRReceiver(video, {
|
|
35
|
+
onFrame: (progress) => {
|
|
36
|
+
console.log(progress.frameIndex + 1, "/", progress.totalFrames);
|
|
37
|
+
},
|
|
38
|
+
onComplete: (data) => {
|
|
39
|
+
console.log(new TextDecoder().decode(data));
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Stop scanning when needed
|
|
44
|
+
receiver.stop();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[]`
|
|
50
|
+
|
|
51
|
+
Encodes a raw byte array and returns QR frame objects:
|
|
52
|
+
|
|
53
|
+
- `frameIndex`: frame index (starting from 0)
|
|
54
|
+
- `totalFrames`: total number of frames
|
|
55
|
+
- `svg`: QR code SVG string for this frame
|
|
56
|
+
- `payload`: base64 payload of this frame
|
|
57
|
+
|
|
58
|
+
Encoding settings are fixed to:
|
|
59
|
+
|
|
60
|
+
- QR `TypeNumber = 15`
|
|
61
|
+
- error correction level `L`
|
|
62
|
+
- `Byte` mode for writing payload data
|
|
63
|
+
|
|
64
|
+
### `startVideoQRReceiver(video, options): { stop(): void }`
|
|
65
|
+
|
|
66
|
+
Starts scanning and reassembles data by protocol:
|
|
67
|
+
|
|
68
|
+
- `video`: `HTMLVideoElement`
|
|
69
|
+
- `options.onFrame`: called when each new (non-duplicate) frame is parsed
|
|
70
|
+
- `options.onComplete`: called when all frames are received, with the complete `Uint8Array`
|
|
71
|
+
|
|
72
|
+
Returns an object with `stop()` to stop and destroy the scanner.
|
|
73
|
+
|
|
74
|
+
## Protocol
|
|
75
|
+
|
|
76
|
+
Frame text format:
|
|
77
|
+
|
|
78
|
+
`msgId|idx/total|payloadBase64`
|
|
79
|
+
|
|
80
|
+
- `msgId`: 8-character random hexadecimal string
|
|
81
|
+
- `idx`: current chunk index
|
|
82
|
+
- `total`: total chunk count
|
|
83
|
+
- `payloadBase64`: base64 payload of the chunk
|
|
84
|
+
|
|
85
|
+
## Local Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pnpm install
|
|
89
|
+
pnpm build
|
|
90
|
+
pnpm test
|
|
91
|
+
pnpm example
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Common scripts:
|
|
95
|
+
|
|
96
|
+
- `pnpm build`: build to `dist/` with `tsup`
|
|
97
|
+
- `pnpm dev`: watch build
|
|
98
|
+
- `pnpm typecheck`: run TypeScript type checks
|
|
99
|
+
- `pnpm test`: run Vitest
|
|
100
|
+
- `pnpm example`: start the example app (Vite)
|
|
101
|
+
|
|
102
|
+
## Run the Example
|
|
103
|
+
|
|
104
|
+
After running `pnpm example`:
|
|
105
|
+
|
|
106
|
+
- In the "Encode" section, enter text or select a file to render QR frames at 5fps
|
|
107
|
+
- In the "Decode" section, allow camera permission and scan to see reconstructed output
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface EncodedFrame {
|
|
2
|
+
frameIndex: number;
|
|
3
|
+
totalFrames: number;
|
|
4
|
+
svg: string;
|
|
5
|
+
payload: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Encode byte array to QR codes with TypeNumber=15 and L error correction
|
|
9
|
+
* @param bytes Raw byte array
|
|
10
|
+
* @returns Array of encoded results with frame indices
|
|
11
|
+
*/
|
|
12
|
+
declare function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[];
|
|
13
|
+
interface FrameProgress {
|
|
14
|
+
frameIndex: number;
|
|
15
|
+
totalFrames: number;
|
|
16
|
+
msgId: string;
|
|
17
|
+
receivedCount: number;
|
|
18
|
+
}
|
|
19
|
+
interface VideoQRReceiverOptions {
|
|
20
|
+
/** Fired when each frame is parsed */
|
|
21
|
+
onFrame?: (progress: FrameProgress) => void;
|
|
22
|
+
/** Fired when the complete message is received */
|
|
23
|
+
onComplete?: (data: Uint8Array) => void;
|
|
24
|
+
}
|
|
25
|
+
interface VideoQRReceiver {
|
|
26
|
+
stop(): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Start video stream QR code parsing, automatically reassemble complete message per protocol
|
|
30
|
+
* @param video Video element for camera rendering
|
|
31
|
+
* @param options Callbacks and configuration
|
|
32
|
+
* @returns Returns { stop } to stop scanning
|
|
33
|
+
*/
|
|
34
|
+
declare function startVideoQRReceiver(video: HTMLVideoElement, options: VideoQRReceiverOptions): VideoQRReceiver;
|
|
35
|
+
|
|
36
|
+
export { type EncodedFrame, type FrameProgress, type VideoQRReceiver, type VideoQRReceiverOptions, encodeBytesToQRCodes, startVideoQRReceiver };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface EncodedFrame {
|
|
2
|
+
frameIndex: number;
|
|
3
|
+
totalFrames: number;
|
|
4
|
+
svg: string;
|
|
5
|
+
payload: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Encode byte array to QR codes with TypeNumber=15 and L error correction
|
|
9
|
+
* @param bytes Raw byte array
|
|
10
|
+
* @returns Array of encoded results with frame indices
|
|
11
|
+
*/
|
|
12
|
+
declare function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[];
|
|
13
|
+
interface FrameProgress {
|
|
14
|
+
frameIndex: number;
|
|
15
|
+
totalFrames: number;
|
|
16
|
+
msgId: string;
|
|
17
|
+
receivedCount: number;
|
|
18
|
+
}
|
|
19
|
+
interface VideoQRReceiverOptions {
|
|
20
|
+
/** Fired when each frame is parsed */
|
|
21
|
+
onFrame?: (progress: FrameProgress) => void;
|
|
22
|
+
/** Fired when the complete message is received */
|
|
23
|
+
onComplete?: (data: Uint8Array) => void;
|
|
24
|
+
}
|
|
25
|
+
interface VideoQRReceiver {
|
|
26
|
+
stop(): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Start video stream QR code parsing, automatically reassemble complete message per protocol
|
|
30
|
+
* @param video Video element for camera rendering
|
|
31
|
+
* @param options Callbacks and configuration
|
|
32
|
+
* @returns Returns { stop } to stop scanning
|
|
33
|
+
*/
|
|
34
|
+
declare function startVideoQRReceiver(video: HTMLVideoElement, options: VideoQRReceiverOptions): VideoQRReceiver;
|
|
35
|
+
|
|
36
|
+
export { type EncodedFrame, type FrameProgress, type VideoQRReceiver, type VideoQRReceiverOptions, encodeBytesToQRCodes, startVideoQRReceiver };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
encodeBytesToQRCodes: () => encodeBytesToQRCodes,
|
|
34
|
+
startVideoQRReceiver: () => startVideoQRReceiver
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_qrcode_generator = __toESM(require("qrcode-generator"));
|
|
38
|
+
var import_qr_scanner = __toESM(require("qr-scanner"));
|
|
39
|
+
|
|
40
|
+
// src/protocol.ts
|
|
41
|
+
var TYPE15_BYTE_CAPACITY = 520;
|
|
42
|
+
function randomMsgId() {
|
|
43
|
+
return Math.random().toString(16).slice(2, 10);
|
|
44
|
+
}
|
|
45
|
+
function estimateHeaderLength(total) {
|
|
46
|
+
const idxDigits = String(Math.max(0, total - 1)).length;
|
|
47
|
+
const totalDigits = String(total).length;
|
|
48
|
+
return 8 + 1 + idxDigits + 1 + totalDigits + 1;
|
|
49
|
+
}
|
|
50
|
+
function splitIntoFrames(bytes) {
|
|
51
|
+
if (bytes.length === 0) {
|
|
52
|
+
return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, ""), payloadBase64: "" }];
|
|
53
|
+
}
|
|
54
|
+
const msgId = randomMsgId();
|
|
55
|
+
const headerOverhead = estimateHeaderLength(1) + 4;
|
|
56
|
+
const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;
|
|
57
|
+
const maxPayloadBytes = Math.floor(maxPayloadChars * 3 / 4);
|
|
58
|
+
if (maxPayloadBytes <= 0) {
|
|
59
|
+
throw new Error("Protocol header too large for Type 15 QR capacity");
|
|
60
|
+
}
|
|
61
|
+
const total = Math.ceil(bytes.length / maxPayloadBytes);
|
|
62
|
+
const headerLen = estimateHeaderLength(total) + 4;
|
|
63
|
+
const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;
|
|
64
|
+
const maxPayloadBytesActual = Math.floor(maxPayloadCharsActual * 3 / 4);
|
|
65
|
+
const frames = [];
|
|
66
|
+
let offset = 0;
|
|
67
|
+
while (offset < bytes.length) {
|
|
68
|
+
const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));
|
|
69
|
+
const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));
|
|
70
|
+
const frame = encodeFrame(msgId, frames.length, total, chunkBase64);
|
|
71
|
+
frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });
|
|
72
|
+
offset += chunk.length;
|
|
73
|
+
}
|
|
74
|
+
return frames;
|
|
75
|
+
}
|
|
76
|
+
function encodeFrame(msgId, idx, total, payloadBase64) {
|
|
77
|
+
return `${msgId}|${idx}/${total}|${payloadBase64}`;
|
|
78
|
+
}
|
|
79
|
+
function parseFrame(text) {
|
|
80
|
+
const pipe1 = text.indexOf("|");
|
|
81
|
+
if (pipe1 < 0) return null;
|
|
82
|
+
const msgId = text.slice(0, pipe1);
|
|
83
|
+
const rest2 = text.slice(pipe1 + 1);
|
|
84
|
+
const slash = rest2.indexOf("/");
|
|
85
|
+
const pipe2 = rest2.indexOf("|");
|
|
86
|
+
if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;
|
|
87
|
+
const idx = parseInt(rest2.slice(0, slash), 10);
|
|
88
|
+
const total = parseInt(rest2.slice(slash + 1, pipe2), 10);
|
|
89
|
+
const payloadBase64 = rest2.slice(pipe2 + 1);
|
|
90
|
+
if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;
|
|
91
|
+
return { msgId, idx, total, payloadBase64 };
|
|
92
|
+
}
|
|
93
|
+
function base64ToBytes(base64) {
|
|
94
|
+
const binary = atob(base64);
|
|
95
|
+
const bytes = new Uint8Array(binary.length);
|
|
96
|
+
for (let i = 0; i < binary.length; i++) {
|
|
97
|
+
bytes[i] = binary.charCodeAt(i);
|
|
98
|
+
}
|
|
99
|
+
return bytes;
|
|
100
|
+
}
|
|
101
|
+
var FrameAggregator = class {
|
|
102
|
+
constructor() {
|
|
103
|
+
this.chunks = /* @__PURE__ */ new Map();
|
|
104
|
+
}
|
|
105
|
+
add(parsed) {
|
|
106
|
+
const { msgId, idx, total, payloadBase64 } = parsed;
|
|
107
|
+
let map = this.chunks.get(msgId);
|
|
108
|
+
if (!map) {
|
|
109
|
+
map = /* @__PURE__ */ new Map();
|
|
110
|
+
this.chunks.set(msgId, map);
|
|
111
|
+
}
|
|
112
|
+
const isNew = !map.has(idx);
|
|
113
|
+
if (isNew) map.set(idx, payloadBase64);
|
|
114
|
+
const receivedCount = map.size;
|
|
115
|
+
if (receivedCount !== total) {
|
|
116
|
+
return {
|
|
117
|
+
isNew,
|
|
118
|
+
frameIndex: idx,
|
|
119
|
+
totalFrames: total,
|
|
120
|
+
msgId,
|
|
121
|
+
receivedCount,
|
|
122
|
+
complete: false
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const parts = [];
|
|
126
|
+
for (let i = 0; i < total; i++) {
|
|
127
|
+
const b64 = map.get(i);
|
|
128
|
+
if (!b64) {
|
|
129
|
+
return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };
|
|
130
|
+
}
|
|
131
|
+
parts.push(base64ToBytes(b64));
|
|
132
|
+
}
|
|
133
|
+
this.chunks.delete(msgId);
|
|
134
|
+
const totalLen = parts.reduce((s, p) => s + p.length, 0);
|
|
135
|
+
const data = new Uint8Array(totalLen);
|
|
136
|
+
let off = 0;
|
|
137
|
+
for (const p of parts) {
|
|
138
|
+
data.set(p, off);
|
|
139
|
+
off += p.length;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
isNew,
|
|
143
|
+
frameIndex: idx,
|
|
144
|
+
totalFrames: total,
|
|
145
|
+
msgId,
|
|
146
|
+
receivedCount,
|
|
147
|
+
complete: true,
|
|
148
|
+
data
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/index.ts
|
|
154
|
+
var TYPE_NUMBER = 15;
|
|
155
|
+
var ERROR_CORRECTION = "L";
|
|
156
|
+
function encodeBytesToQRCodes(bytes) {
|
|
157
|
+
const frames = splitIntoFrames(bytes);
|
|
158
|
+
const result = [];
|
|
159
|
+
for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {
|
|
160
|
+
const qr = (0, import_qrcode_generator.default)(TYPE_NUMBER, ERROR_CORRECTION);
|
|
161
|
+
qr.addData(frame, "Byte");
|
|
162
|
+
qr.make();
|
|
163
|
+
result.push({
|
|
164
|
+
frameIndex,
|
|
165
|
+
totalFrames,
|
|
166
|
+
svg: qr.createSvgTag(),
|
|
167
|
+
payload: payloadBase64
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
function startVideoQRReceiver(video, options) {
|
|
173
|
+
const { onFrame, onComplete } = options;
|
|
174
|
+
const aggregator = new FrameAggregator();
|
|
175
|
+
const qrScanner = new import_qr_scanner.default(
|
|
176
|
+
video,
|
|
177
|
+
(result) => {
|
|
178
|
+
const text = typeof result === "string" ? result : result.data;
|
|
179
|
+
const parsed = parseFrame(text);
|
|
180
|
+
if (!parsed) return;
|
|
181
|
+
const res = aggregator.add(parsed);
|
|
182
|
+
if (res.isNew && onFrame) {
|
|
183
|
+
onFrame({
|
|
184
|
+
frameIndex: res.frameIndex,
|
|
185
|
+
totalFrames: res.totalFrames,
|
|
186
|
+
msgId: res.msgId,
|
|
187
|
+
receivedCount: res.receivedCount
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (res.complete && res.data && onComplete) {
|
|
191
|
+
onComplete(res.data);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
returnDetailedScanResult: true,
|
|
196
|
+
maxScansPerSecond: 20,
|
|
197
|
+
calculateScanRegion: (v) => {
|
|
198
|
+
const w = v.videoWidth;
|
|
199
|
+
const h = v.videoHeight;
|
|
200
|
+
const target = 1e3;
|
|
201
|
+
const scale = w >= h ? target / w : target / h;
|
|
202
|
+
return {
|
|
203
|
+
x: 0,
|
|
204
|
+
y: 0,
|
|
205
|
+
width: w,
|
|
206
|
+
height: h,
|
|
207
|
+
downScaledWidth: Math.round(w * scale),
|
|
208
|
+
downScaledHeight: Math.round(h * scale)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
qrScanner.start();
|
|
214
|
+
return {
|
|
215
|
+
stop() {
|
|
216
|
+
qrScanner.stop();
|
|
217
|
+
qrScanner.destroy();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
222
|
+
0 && (module.exports = {
|
|
223
|
+
encodeBytesToQRCodes,
|
|
224
|
+
startVideoQRReceiver
|
|
225
|
+
});
|
|
226
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/protocol.ts"],"sourcesContent":["import qrcode from \"qrcode-generator\";\nimport QrScanner from \"qr-scanner\";\nimport { splitIntoFrames, parseFrame, FrameAggregator } from \"./protocol\";\n\nconst TYPE_NUMBER = 15;\nconst ERROR_CORRECTION = \"L\";\n\nexport interface EncodedFrame {\n frameIndex: number;\n totalFrames: number;\n svg: string;\n payload: string;\n}\n\n/**\n * Encode byte array to QR codes with TypeNumber=15 and L error correction\n * @param bytes Raw byte array\n * @returns Array of encoded results with frame indices\n */\nexport function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[] {\n const frames = splitIntoFrames(bytes);\n const result: EncodedFrame[] = [];\n\n for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {\n const qr = qrcode(TYPE_NUMBER, ERROR_CORRECTION);\n qr.addData(frame, \"Byte\");\n qr.make();\n result.push({\n frameIndex,\n totalFrames,\n svg: qr.createSvgTag(),\n payload: payloadBase64,\n });\n }\n\n return result;\n}\n\nexport interface FrameProgress {\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n}\n\nexport interface VideoQRReceiverOptions {\n /** Fired when each frame is parsed */\n onFrame?: (progress: FrameProgress) => void;\n /** Fired when the complete message is received */\n onComplete?: (data: Uint8Array) => void;\n}\n\nexport interface VideoQRReceiver {\n stop(): void;\n}\n\n/**\n * Start video stream QR code parsing, automatically reassemble complete message per protocol\n * @param video Video element for camera rendering\n * @param options Callbacks and configuration\n * @returns Returns { stop } to stop scanning\n */\nexport function startVideoQRReceiver(\n video: HTMLVideoElement,\n options: VideoQRReceiverOptions\n): VideoQRReceiver {\n const { onFrame, onComplete } = options;\n const aggregator = new FrameAggregator();\n\n const qrScanner = new QrScanner(\n video,\n (result) => {\n const text = typeof result === \"string\" ? result : result.data;\n const parsed = parseFrame(text);\n if (!parsed) return;\n const res = aggregator.add(parsed);\n if (res.isNew && onFrame) {\n onFrame({\n frameIndex: res.frameIndex,\n totalFrames: res.totalFrames,\n msgId: res.msgId,\n receivedCount: res.receivedCount,\n });\n }\n if (res.complete && res.data && onComplete) {\n onComplete(res.data);\n }\n },\n {\n returnDetailedScanResult: true,\n maxScansPerSecond: 20,\n calculateScanRegion: (v: HTMLVideoElement) => {\n const w = v.videoWidth;\n const h = v.videoHeight;\n const target = 1000;\n const scale = w >= h ? target / w : target / h;\n return {\n x: 0,\n y: 0,\n width: w,\n height: h,\n downScaledWidth: Math.round(w * scale),\n downScaledHeight: Math.round(h * scale),\n };\n },\n }\n );\n\n qrScanner.start();\n\n return {\n stop() {\n qrScanner.stop();\n qrScanner.destroy();\n },\n };\n}\n","/**\n * Chunking protocol\n * Frame format: msgId|idx/total|payloadBase64\n */\n\nconst TYPE15_BYTE_CAPACITY = 520; // TypeNumber 15, L error correction\n\nfunction randomMsgId(): string {\n return Math.random().toString(16).slice(2, 10);\n}\n\nfunction estimateHeaderLength(total: number): number {\n const idxDigits = String(Math.max(0, total - 1)).length;\n const totalDigits = String(total).length;\n return 8 + 1 + idxDigits + 1 + totalDigits + 1;\n}\n\nexport interface FrameInfo {\n frameIndex: number;\n totalFrames: number;\n frame: string;\n payloadBase64: string;\n}\n\n/**\n * Split byte array into chunks, each encoded as a protocol frame\n */\nexport function splitIntoFrames(bytes: Uint8Array): FrameInfo[] {\n if (bytes.length === 0) {\n return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, \"\"), payloadBase64: \"\" }];\n }\n\n const msgId = randomMsgId();\n const headerOverhead = estimateHeaderLength(1) + 4;\n const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;\n const maxPayloadBytes = Math.floor((maxPayloadChars * 3) / 4);\n\n if (maxPayloadBytes <= 0) {\n throw new Error(\"Protocol header too large for Type 15 QR capacity\");\n }\n\n const total = Math.ceil(bytes.length / maxPayloadBytes);\n const headerLen = estimateHeaderLength(total) + 4;\n const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;\n const maxPayloadBytesActual = Math.floor((maxPayloadCharsActual * 3) / 4);\n\n const frames: FrameInfo[] = [];\n let offset = 0;\n\n while (offset < bytes.length) {\n const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));\n const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));\n const frame = encodeFrame(msgId, frames.length, total, chunkBase64);\n frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });\n offset += chunk.length;\n }\n\n return frames;\n}\n\nfunction encodeFrame(msgId: string, idx: number, total: number, payloadBase64: string): string {\n return `${msgId}|${idx}/${total}|${payloadBase64}`;\n}\n\nexport interface ParsedFrame {\n msgId: string;\n idx: number;\n total: number;\n payloadBase64: string;\n}\n\n/**\n * Parse protocol frame, returns null for invalid format\n */\nexport function parseFrame(text: string): ParsedFrame | null {\n const pipe1 = text.indexOf(\"|\");\n if (pipe1 < 0) return null;\n const msgId = text.slice(0, pipe1);\n const rest2 = text.slice(pipe1 + 1);\n const slash = rest2.indexOf(\"/\");\n const pipe2 = rest2.indexOf(\"|\");\n if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;\n const idx = parseInt(rest2.slice(0, slash), 10);\n const total = parseInt(rest2.slice(slash + 1, pipe2), 10);\n const payloadBase64 = rest2.slice(pipe2 + 1);\n if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;\n return { msgId, idx, total, payloadBase64 };\n}\n\n/**\n * Decode base64 to Uint8Array\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\nexport interface AddFrameResult {\n isNew: boolean;\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n complete: boolean;\n data?: Uint8Array;\n}\n\n/**\n * Message aggregation state: collect frames by msgId, reassemble when complete\n */\nexport class FrameAggregator {\n private chunks = new Map<string, Map<number, string>>();\n\n add(parsed: ParsedFrame): AddFrameResult {\n const { msgId, idx, total, payloadBase64 } = parsed;\n let map = this.chunks.get(msgId);\n if (!map) {\n map = new Map();\n this.chunks.set(msgId, map);\n }\n const isNew = !map.has(idx);\n if (isNew) map.set(idx, payloadBase64);\n const receivedCount = map.size;\n\n if (receivedCount !== total) {\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: false,\n };\n }\n\n const parts: Uint8Array[] = [];\n for (let i = 0; i < total; i++) {\n const b64 = map.get(i);\n if (!b64) {\n return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };\n }\n parts.push(base64ToBytes(b64));\n }\n this.chunks.delete(msgId);\n const totalLen = parts.reduce((s, p) => s + p.length, 0);\n const data = new Uint8Array(totalLen);\n let off = 0;\n for (const p of parts) {\n data.set(p, off);\n off += p.length;\n }\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: true,\n data,\n };\n }\n}\n"],"mappings":"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","names":["qrcode","QrScanner"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import qrcode from "qrcode-generator";
|
|
3
|
+
import QrScanner from "qr-scanner";
|
|
4
|
+
|
|
5
|
+
// src/protocol.ts
|
|
6
|
+
var TYPE15_BYTE_CAPACITY = 520;
|
|
7
|
+
function randomMsgId() {
|
|
8
|
+
return Math.random().toString(16).slice(2, 10);
|
|
9
|
+
}
|
|
10
|
+
function estimateHeaderLength(total) {
|
|
11
|
+
const idxDigits = String(Math.max(0, total - 1)).length;
|
|
12
|
+
const totalDigits = String(total).length;
|
|
13
|
+
return 8 + 1 + idxDigits + 1 + totalDigits + 1;
|
|
14
|
+
}
|
|
15
|
+
function splitIntoFrames(bytes) {
|
|
16
|
+
if (bytes.length === 0) {
|
|
17
|
+
return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, ""), payloadBase64: "" }];
|
|
18
|
+
}
|
|
19
|
+
const msgId = randomMsgId();
|
|
20
|
+
const headerOverhead = estimateHeaderLength(1) + 4;
|
|
21
|
+
const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;
|
|
22
|
+
const maxPayloadBytes = Math.floor(maxPayloadChars * 3 / 4);
|
|
23
|
+
if (maxPayloadBytes <= 0) {
|
|
24
|
+
throw new Error("Protocol header too large for Type 15 QR capacity");
|
|
25
|
+
}
|
|
26
|
+
const total = Math.ceil(bytes.length / maxPayloadBytes);
|
|
27
|
+
const headerLen = estimateHeaderLength(total) + 4;
|
|
28
|
+
const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;
|
|
29
|
+
const maxPayloadBytesActual = Math.floor(maxPayloadCharsActual * 3 / 4);
|
|
30
|
+
const frames = [];
|
|
31
|
+
let offset = 0;
|
|
32
|
+
while (offset < bytes.length) {
|
|
33
|
+
const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));
|
|
34
|
+
const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));
|
|
35
|
+
const frame = encodeFrame(msgId, frames.length, total, chunkBase64);
|
|
36
|
+
frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });
|
|
37
|
+
offset += chunk.length;
|
|
38
|
+
}
|
|
39
|
+
return frames;
|
|
40
|
+
}
|
|
41
|
+
function encodeFrame(msgId, idx, total, payloadBase64) {
|
|
42
|
+
return `${msgId}|${idx}/${total}|${payloadBase64}`;
|
|
43
|
+
}
|
|
44
|
+
function parseFrame(text) {
|
|
45
|
+
const pipe1 = text.indexOf("|");
|
|
46
|
+
if (pipe1 < 0) return null;
|
|
47
|
+
const msgId = text.slice(0, pipe1);
|
|
48
|
+
const rest2 = text.slice(pipe1 + 1);
|
|
49
|
+
const slash = rest2.indexOf("/");
|
|
50
|
+
const pipe2 = rest2.indexOf("|");
|
|
51
|
+
if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;
|
|
52
|
+
const idx = parseInt(rest2.slice(0, slash), 10);
|
|
53
|
+
const total = parseInt(rest2.slice(slash + 1, pipe2), 10);
|
|
54
|
+
const payloadBase64 = rest2.slice(pipe2 + 1);
|
|
55
|
+
if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;
|
|
56
|
+
return { msgId, idx, total, payloadBase64 };
|
|
57
|
+
}
|
|
58
|
+
function base64ToBytes(base64) {
|
|
59
|
+
const binary = atob(base64);
|
|
60
|
+
const bytes = new Uint8Array(binary.length);
|
|
61
|
+
for (let i = 0; i < binary.length; i++) {
|
|
62
|
+
bytes[i] = binary.charCodeAt(i);
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
var FrameAggregator = class {
|
|
67
|
+
constructor() {
|
|
68
|
+
this.chunks = /* @__PURE__ */ new Map();
|
|
69
|
+
}
|
|
70
|
+
add(parsed) {
|
|
71
|
+
const { msgId, idx, total, payloadBase64 } = parsed;
|
|
72
|
+
let map = this.chunks.get(msgId);
|
|
73
|
+
if (!map) {
|
|
74
|
+
map = /* @__PURE__ */ new Map();
|
|
75
|
+
this.chunks.set(msgId, map);
|
|
76
|
+
}
|
|
77
|
+
const isNew = !map.has(idx);
|
|
78
|
+
if (isNew) map.set(idx, payloadBase64);
|
|
79
|
+
const receivedCount = map.size;
|
|
80
|
+
if (receivedCount !== total) {
|
|
81
|
+
return {
|
|
82
|
+
isNew,
|
|
83
|
+
frameIndex: idx,
|
|
84
|
+
totalFrames: total,
|
|
85
|
+
msgId,
|
|
86
|
+
receivedCount,
|
|
87
|
+
complete: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const parts = [];
|
|
91
|
+
for (let i = 0; i < total; i++) {
|
|
92
|
+
const b64 = map.get(i);
|
|
93
|
+
if (!b64) {
|
|
94
|
+
return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };
|
|
95
|
+
}
|
|
96
|
+
parts.push(base64ToBytes(b64));
|
|
97
|
+
}
|
|
98
|
+
this.chunks.delete(msgId);
|
|
99
|
+
const totalLen = parts.reduce((s, p) => s + p.length, 0);
|
|
100
|
+
const data = new Uint8Array(totalLen);
|
|
101
|
+
let off = 0;
|
|
102
|
+
for (const p of parts) {
|
|
103
|
+
data.set(p, off);
|
|
104
|
+
off += p.length;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
isNew,
|
|
108
|
+
frameIndex: idx,
|
|
109
|
+
totalFrames: total,
|
|
110
|
+
msgId,
|
|
111
|
+
receivedCount,
|
|
112
|
+
complete: true,
|
|
113
|
+
data
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/index.ts
|
|
119
|
+
var TYPE_NUMBER = 15;
|
|
120
|
+
var ERROR_CORRECTION = "L";
|
|
121
|
+
function encodeBytesToQRCodes(bytes) {
|
|
122
|
+
const frames = splitIntoFrames(bytes);
|
|
123
|
+
const result = [];
|
|
124
|
+
for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {
|
|
125
|
+
const qr = qrcode(TYPE_NUMBER, ERROR_CORRECTION);
|
|
126
|
+
qr.addData(frame, "Byte");
|
|
127
|
+
qr.make();
|
|
128
|
+
result.push({
|
|
129
|
+
frameIndex,
|
|
130
|
+
totalFrames,
|
|
131
|
+
svg: qr.createSvgTag(),
|
|
132
|
+
payload: payloadBase64
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function startVideoQRReceiver(video, options) {
|
|
138
|
+
const { onFrame, onComplete } = options;
|
|
139
|
+
const aggregator = new FrameAggregator();
|
|
140
|
+
const qrScanner = new QrScanner(
|
|
141
|
+
video,
|
|
142
|
+
(result) => {
|
|
143
|
+
const text = typeof result === "string" ? result : result.data;
|
|
144
|
+
const parsed = parseFrame(text);
|
|
145
|
+
if (!parsed) return;
|
|
146
|
+
const res = aggregator.add(parsed);
|
|
147
|
+
if (res.isNew && onFrame) {
|
|
148
|
+
onFrame({
|
|
149
|
+
frameIndex: res.frameIndex,
|
|
150
|
+
totalFrames: res.totalFrames,
|
|
151
|
+
msgId: res.msgId,
|
|
152
|
+
receivedCount: res.receivedCount
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (res.complete && res.data && onComplete) {
|
|
156
|
+
onComplete(res.data);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
returnDetailedScanResult: true,
|
|
161
|
+
maxScansPerSecond: 20,
|
|
162
|
+
calculateScanRegion: (v) => {
|
|
163
|
+
const w = v.videoWidth;
|
|
164
|
+
const h = v.videoHeight;
|
|
165
|
+
const target = 1e3;
|
|
166
|
+
const scale = w >= h ? target / w : target / h;
|
|
167
|
+
return {
|
|
168
|
+
x: 0,
|
|
169
|
+
y: 0,
|
|
170
|
+
width: w,
|
|
171
|
+
height: h,
|
|
172
|
+
downScaledWidth: Math.round(w * scale),
|
|
173
|
+
downScaledHeight: Math.round(h * scale)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
qrScanner.start();
|
|
179
|
+
return {
|
|
180
|
+
stop() {
|
|
181
|
+
qrScanner.stop();
|
|
182
|
+
qrScanner.destroy();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
encodeBytesToQRCodes,
|
|
188
|
+
startVideoQRReceiver
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/protocol.ts"],"sourcesContent":["import qrcode from \"qrcode-generator\";\nimport QrScanner from \"qr-scanner\";\nimport { splitIntoFrames, parseFrame, FrameAggregator } from \"./protocol\";\n\nconst TYPE_NUMBER = 15;\nconst ERROR_CORRECTION = \"L\";\n\nexport interface EncodedFrame {\n frameIndex: number;\n totalFrames: number;\n svg: string;\n payload: string;\n}\n\n/**\n * Encode byte array to QR codes with TypeNumber=15 and L error correction\n * @param bytes Raw byte array\n * @returns Array of encoded results with frame indices\n */\nexport function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[] {\n const frames = splitIntoFrames(bytes);\n const result: EncodedFrame[] = [];\n\n for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {\n const qr = qrcode(TYPE_NUMBER, ERROR_CORRECTION);\n qr.addData(frame, \"Byte\");\n qr.make();\n result.push({\n frameIndex,\n totalFrames,\n svg: qr.createSvgTag(),\n payload: payloadBase64,\n });\n }\n\n return result;\n}\n\nexport interface FrameProgress {\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n}\n\nexport interface VideoQRReceiverOptions {\n /** Fired when each frame is parsed */\n onFrame?: (progress: FrameProgress) => void;\n /** Fired when the complete message is received */\n onComplete?: (data: Uint8Array) => void;\n}\n\nexport interface VideoQRReceiver {\n stop(): void;\n}\n\n/**\n * Start video stream QR code parsing, automatically reassemble complete message per protocol\n * @param video Video element for camera rendering\n * @param options Callbacks and configuration\n * @returns Returns { stop } to stop scanning\n */\nexport function startVideoQRReceiver(\n video: HTMLVideoElement,\n options: VideoQRReceiverOptions\n): VideoQRReceiver {\n const { onFrame, onComplete } = options;\n const aggregator = new FrameAggregator();\n\n const qrScanner = new QrScanner(\n video,\n (result) => {\n const text = typeof result === \"string\" ? result : result.data;\n const parsed = parseFrame(text);\n if (!parsed) return;\n const res = aggregator.add(parsed);\n if (res.isNew && onFrame) {\n onFrame({\n frameIndex: res.frameIndex,\n totalFrames: res.totalFrames,\n msgId: res.msgId,\n receivedCount: res.receivedCount,\n });\n }\n if (res.complete && res.data && onComplete) {\n onComplete(res.data);\n }\n },\n {\n returnDetailedScanResult: true,\n maxScansPerSecond: 20,\n calculateScanRegion: (v: HTMLVideoElement) => {\n const w = v.videoWidth;\n const h = v.videoHeight;\n const target = 1000;\n const scale = w >= h ? target / w : target / h;\n return {\n x: 0,\n y: 0,\n width: w,\n height: h,\n downScaledWidth: Math.round(w * scale),\n downScaledHeight: Math.round(h * scale),\n };\n },\n }\n );\n\n qrScanner.start();\n\n return {\n stop() {\n qrScanner.stop();\n qrScanner.destroy();\n },\n };\n}\n","/**\n * Chunking protocol\n * Frame format: msgId|idx/total|payloadBase64\n */\n\nconst TYPE15_BYTE_CAPACITY = 520; // TypeNumber 15, L error correction\n\nfunction randomMsgId(): string {\n return Math.random().toString(16).slice(2, 10);\n}\n\nfunction estimateHeaderLength(total: number): number {\n const idxDigits = String(Math.max(0, total - 1)).length;\n const totalDigits = String(total).length;\n return 8 + 1 + idxDigits + 1 + totalDigits + 1;\n}\n\nexport interface FrameInfo {\n frameIndex: number;\n totalFrames: number;\n frame: string;\n payloadBase64: string;\n}\n\n/**\n * Split byte array into chunks, each encoded as a protocol frame\n */\nexport function splitIntoFrames(bytes: Uint8Array): FrameInfo[] {\n if (bytes.length === 0) {\n return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, \"\"), payloadBase64: \"\" }];\n }\n\n const msgId = randomMsgId();\n const headerOverhead = estimateHeaderLength(1) + 4;\n const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;\n const maxPayloadBytes = Math.floor((maxPayloadChars * 3) / 4);\n\n if (maxPayloadBytes <= 0) {\n throw new Error(\"Protocol header too large for Type 15 QR capacity\");\n }\n\n const total = Math.ceil(bytes.length / maxPayloadBytes);\n const headerLen = estimateHeaderLength(total) + 4;\n const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;\n const maxPayloadBytesActual = Math.floor((maxPayloadCharsActual * 3) / 4);\n\n const frames: FrameInfo[] = [];\n let offset = 0;\n\n while (offset < bytes.length) {\n const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));\n const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));\n const frame = encodeFrame(msgId, frames.length, total, chunkBase64);\n frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });\n offset += chunk.length;\n }\n\n return frames;\n}\n\nfunction encodeFrame(msgId: string, idx: number, total: number, payloadBase64: string): string {\n return `${msgId}|${idx}/${total}|${payloadBase64}`;\n}\n\nexport interface ParsedFrame {\n msgId: string;\n idx: number;\n total: number;\n payloadBase64: string;\n}\n\n/**\n * Parse protocol frame, returns null for invalid format\n */\nexport function parseFrame(text: string): ParsedFrame | null {\n const pipe1 = text.indexOf(\"|\");\n if (pipe1 < 0) return null;\n const msgId = text.slice(0, pipe1);\n const rest2 = text.slice(pipe1 + 1);\n const slash = rest2.indexOf(\"/\");\n const pipe2 = rest2.indexOf(\"|\");\n if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;\n const idx = parseInt(rest2.slice(0, slash), 10);\n const total = parseInt(rest2.slice(slash + 1, pipe2), 10);\n const payloadBase64 = rest2.slice(pipe2 + 1);\n if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;\n return { msgId, idx, total, payloadBase64 };\n}\n\n/**\n * Decode base64 to Uint8Array\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\nexport interface AddFrameResult {\n isNew: boolean;\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n complete: boolean;\n data?: Uint8Array;\n}\n\n/**\n * Message aggregation state: collect frames by msgId, reassemble when complete\n */\nexport class FrameAggregator {\n private chunks = new Map<string, Map<number, string>>();\n\n add(parsed: ParsedFrame): AddFrameResult {\n const { msgId, idx, total, payloadBase64 } = parsed;\n let map = this.chunks.get(msgId);\n if (!map) {\n map = new Map();\n this.chunks.set(msgId, map);\n }\n const isNew = !map.has(idx);\n if (isNew) map.set(idx, payloadBase64);\n const receivedCount = map.size;\n\n if (receivedCount !== total) {\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: false,\n };\n }\n\n const parts: Uint8Array[] = [];\n for (let i = 0; i < total; i++) {\n const b64 = map.get(i);\n if (!b64) {\n return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };\n }\n parts.push(base64ToBytes(b64));\n }\n this.chunks.delete(msgId);\n const totalLen = parts.reduce((s, p) => s + p.length, 0);\n const data = new Uint8Array(totalLen);\n let off = 0;\n for (const p of parts) {\n data.set(p, off);\n off += p.length;\n }\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: true,\n data,\n };\n }\n}\n"],"mappings":";AAAA,OAAO,YAAY;AACnB,OAAO,eAAe;;;ACItB,IAAM,uBAAuB;AAE7B,SAAS,cAAsB;AAC7B,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/C;AAEA,SAAS,qBAAqB,OAAuB;AACnD,QAAM,YAAY,OAAO,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE;AACjD,QAAM,cAAc,OAAO,KAAK,EAAE;AAClC,SAAO,IAAI,IAAI,YAAY,IAAI,cAAc;AAC/C;AAYO,SAAS,gBAAgB,OAAgC;AAC9D,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,CAAC,EAAE,YAAY,GAAG,aAAa,GAAG,OAAO,YAAY,YAAY,GAAG,GAAG,GAAG,EAAE,GAAG,eAAe,GAAG,CAAC;AAAA,EAC3G;AAEA,QAAM,QAAQ,YAAY;AAC1B,QAAM,iBAAiB,qBAAqB,CAAC,IAAI;AACjD,QAAM,kBAAkB,uBAAuB;AAC/C,QAAM,kBAAkB,KAAK,MAAO,kBAAkB,IAAK,CAAC;AAE5D,MAAI,mBAAmB,GAAG;AACxB,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAEA,QAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,eAAe;AACtD,QAAM,YAAY,qBAAqB,KAAK,IAAI;AAChD,QAAM,wBAAwB,uBAAuB;AACrD,QAAM,wBAAwB,KAAK,MAAO,wBAAwB,IAAK,CAAC;AAExE,QAAM,SAAsB,CAAC;AAC7B,MAAI,SAAS;AAEb,SAAO,SAAS,MAAM,QAAQ;AAC5B,UAAM,QAAQ,MAAM,SAAS,QAAQ,KAAK,IAAI,SAAS,uBAAuB,MAAM,MAAM,CAAC;AAC3F,UAAM,cAAc,KAAK,OAAO,aAAa,MAAM,MAAM,MAAM,KAAK,KAAK,CAAC,CAAC;AAC3E,UAAM,QAAQ,YAAY,OAAO,OAAO,QAAQ,OAAO,WAAW;AAClE,WAAO,KAAK,EAAE,YAAY,OAAO,QAAQ,aAAa,OAAO,OAAO,eAAe,YAAY,CAAC;AAChG,cAAU,MAAM;AAAA,EAClB;AAEA,SAAO;AACT;AAEA,SAAS,YAAY,OAAe,KAAa,OAAe,eAA+B;AAC7F,SAAO,GAAG,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,aAAa;AAClD;AAYO,SAAS,WAAW,MAAkC;AAC3D,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,QAAQ,KAAK,MAAM,GAAG,KAAK;AACjC,QAAM,QAAQ,KAAK,MAAM,QAAQ,CAAC;AAClC,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,MAAI,QAAQ,KAAK,QAAQ,KAAK,QAAQ,MAAO,QAAO;AACpD,QAAM,MAAM,SAAS,MAAM,MAAM,GAAG,KAAK,GAAG,EAAE;AAC9C,QAAM,QAAQ,SAAS,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,EAAE;AACxD,QAAM,gBAAgB,MAAM,MAAM,QAAQ,CAAC;AAC3C,MAAI,MAAM,GAAG,KAAK,MAAM,KAAK,KAAK,MAAM,KAAK,QAAQ,KAAK,OAAO,MAAO,QAAO;AAC/E,SAAO,EAAE,OAAO,KAAK,OAAO,cAAc;AAC5C;AAKO,SAAS,cAAc,QAA4B;AACxD,QAAM,SAAS,KAAK,MAAM;AAC1B,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AAAA,EAChC;AACA,SAAO;AACT;AAeO,IAAM,kBAAN,MAAsB;AAAA,EAAtB;AACL,SAAQ,SAAS,oBAAI,IAAiC;AAAA;AAAA,EAEtD,IAAI,QAAqC;AACvC,UAAM,EAAE,OAAO,KAAK,OAAO,cAAc,IAAI;AAC7C,QAAI,MAAM,KAAK,OAAO,IAAI,KAAK;AAC/B,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,OAAO,IAAI,OAAO,GAAG;AAAA,IAC5B;AACA,UAAM,QAAQ,CAAC,IAAI,IAAI,GAAG;AAC1B,QAAI,MAAO,KAAI,IAAI,KAAK,aAAa;AACrC,UAAM,gBAAgB,IAAI;AAE1B,QAAI,kBAAkB,OAAO;AAC3B,aAAO;AAAA,QACL;AAAA,QACA,YAAY;AAAA,QACZ,aAAa;AAAA,QACb;AAAA,QACA;AAAA,QACA,UAAU;AAAA,MACZ;AAAA,IACF;AAEA,UAAM,QAAsB,CAAC;AAC7B,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,MAAM,IAAI,IAAI,CAAC;AACrB,UAAI,CAAC,KAAK;AACR,eAAO,EAAE,OAAO,YAAY,KAAK,aAAa,OAAO,OAAO,eAAe,UAAU,MAAM;AAAA,MAC7F;AACA,YAAM,KAAK,cAAc,GAAG,CAAC;AAAA,IAC/B;AACA,SAAK,OAAO,OAAO,KAAK;AACxB,UAAM,WAAW,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC;AACvD,UAAM,OAAO,IAAI,WAAW,QAAQ;AACpC,QAAI,MAAM;AACV,eAAW,KAAK,OAAO;AACrB,WAAK,IAAI,GAAG,GAAG;AACf,aAAO,EAAE;AAAA,IACX;AACA,WAAO;AAAA,MACL;AAAA,MACA,YAAY;AAAA,MACZ,aAAa;AAAA,MACb;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;;;ADjKA,IAAM,cAAc;AACpB,IAAM,mBAAmB;AAclB,SAAS,qBAAqB,OAAmC;AACtE,QAAM,SAAS,gBAAgB,KAAK;AACpC,QAAM,SAAyB,CAAC;AAEhC,aAAW,EAAE,YAAY,aAAa,OAAO,cAAc,KAAK,QAAQ;AACtE,UAAM,KAAK,OAAO,aAAa,gBAAgB;AAC/C,OAAG,QAAQ,OAAO,MAAM;AACxB,OAAG,KAAK;AACR,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,KAAK,GAAG,aAAa;AAAA,MACrB,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AA0BO,SAAS,qBACd,OACA,SACiB;AACjB,QAAM,EAAE,SAAS,WAAW,IAAI;AAChC,QAAM,aAAa,IAAI,gBAAgB;AAEvC,QAAM,YAAY,IAAI;AAAA,IACpB;AAAA,IACA,CAAC,WAAW;AACV,YAAM,OAAO,OAAO,WAAW,WAAW,SAAS,OAAO;AAC1D,YAAM,SAAS,WAAW,IAAI;AAC9B,UAAI,CAAC,OAAQ;AACb,YAAM,MAAM,WAAW,IAAI,MAAM;AACjC,UAAI,IAAI,SAAS,SAAS;AACxB,gBAAQ;AAAA,UACN,YAAY,IAAI;AAAA,UAChB,aAAa,IAAI;AAAA,UACjB,OAAO,IAAI;AAAA,UACX,eAAe,IAAI;AAAA,QACrB,CAAC;AAAA,MACH;AACA,UAAI,IAAI,YAAY,IAAI,QAAQ,YAAY;AAC1C,mBAAW,IAAI,IAAI;AAAA,MACrB;AAAA,IACF;AAAA,IACA;AAAA,MACE,0BAA0B;AAAA,MAC1B,mBAAmB;AAAA,MACnB,qBAAqB,CAAC,MAAwB;AAC5C,cAAM,IAAI,EAAE;AACZ,cAAM,IAAI,EAAE;AACZ,cAAM,SAAS;AACf,cAAM,QAAQ,KAAK,IAAI,SAAS,IAAI,SAAS;AAC7C,eAAO;AAAA,UACL,GAAG;AAAA,UACH,GAAG;AAAA,UACH,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,iBAAiB,KAAK,MAAM,IAAI,KAAK;AAAA,UACrC,kBAAkB,KAAK,MAAM,IAAI,KAAK;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,YAAU,MAAM;AAEhB,SAAO;AAAA,IACL,OAAO;AACL,gBAAU,KAAK;AACf,gBAAU,QAAQ;AAAA,IACpB;AAAA,EACF;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qrcode-transmitter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight browser library for transmitting binary data across multi-frame QR codes.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"test:run": "vitest run",
|
|
25
|
+
"example": "vite --config example/vite.config.ts"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"qr-scanner": "^1.4.2",
|
|
32
|
+
"qrcode-generator": "^2.0.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "^5.3.0",
|
|
37
|
+
"vite": "^6.0.0",
|
|
38
|
+
"vitest": "^2.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|