-
-import { merge, type SliceData } from '~~/utils/slicing'
+import { binaryToBlock, createDecoder } from '~~/utils/lt-code'
+import { toUint8Array } from 'js-base64'
import { scan } from 'qr-scanner-wechat'
+import { useBytesRate } from '~/composables/timeseries'
const props = withDefaults(defineProps<{
speed?: number
@@ -8,12 +10,33 @@ const props = withDefaults(defineProps<{
height?: number
}>(), {
speed: 34,
- width: 512,
- height: 512,
+ width: 1080,
+ height: 1080,
})
-const { bytesReceived: validBytesReceivedInLastSecond, currentFormatted: currentValidBandwidthFormatted } = useBandwidth()
-const { bytesReceived: bytesReceivedInLastSecond, currentFormatted: currentBandwidthFormatted } = useBandwidth()
+enum CameraSignalStatus {
+ Waiting,
+ Ready,
+}
+
+const bytesReceived = ref(0)
+const totalValidBytesReceived = ref(0)
+
+const { formatted: currentValidBytesSpeedFormatted } = useBytesRate(totalValidBytesReceived, {
+ interval: 250,
+ timeWindow: 1000,
+ type: 'counter',
+ sampleRate: 50,
+ maxDataPoints: 100,
+})
+
+const { formatted: currentBytesFormatted } = useBytesRate(bytesReceived, {
+ interval: 250,
+ timeWindow: 1000,
+ type: 'counter',
+ sampleRate: 50,
+ maxDataPoints: 100,
+})
const { devices } = useDevicesList({
requestPermissions: true,
@@ -23,6 +46,7 @@ const { devices } = useDevicesList({
},
})
+const cameraSignalStatus = ref(CameraSignalStatus.Waiting)
const cameras = computed(() => devices.value.filter(i => i.kind === 'videoinput'))
const selectedCamera = ref(cameras.value[0]?.deviceId)
@@ -31,7 +55,7 @@ watchEffect(() => {
selectedCamera.value = cameras.value[0]?.deviceId
})
-const results = defineModel>('results', { default: new Set() })
+// const results = defineModel>('results', { default: new Set() })
let stream: MediaStream | undefined
@@ -54,7 +78,14 @@ onMounted(async () => {
}, { immediate: true })
useIntervalFn(
- () => scanFrame(),
+ async () => {
+ try {
+ await scanFrame()
+ }
+ catch (e) {
+ error.value = e
+ }
+ },
() => props.speed,
)
})
@@ -83,13 +114,38 @@ async function connectCamera() {
}
}
-const chunks: SliceData[] = reactive([])
+const decoder = ref(createDecoder())
+const k = ref(0)
+const bytes = ref(0)
+const checksum = ref(0)
+const cached = new Set()
+const startTime = ref(0)
+const endTime = ref(0)
-const length = computed(() => chunks.find(i => i?.[1])?.[1] || 0)
-const id = computed(() => chunks.find(i => i?.[0])?.[0] || 0)
-const picked = computed(() => Array.from({ length: length.value }, (_, idx) => chunks[idx]))
const dataUrl = ref()
const dots = useTemplateRef('dots')
+const status = ref([])
+const decodedBlocks = computed(() => status.value.filter(i => i === 1).length)
+const receivedBytes = computed(() => decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0))
+
+function getStatus() {
+ const array = Array.from({ length: k.value }, () => 0)
+ for (let i = 0; i < k.value; i++) {
+ if (decoder.value.decodedData[i] != null)
+ array[i] = 1
+ }
+ for (const block of decoder.value.encodedBlocks) {
+ for (const i of block.indices) {
+ if (array[i] === 0 || array[i]! > block.indices.length) {
+ array[i] = block.indices.length
+ }
+ else {
+ console.warn(`Unexpected block #${i} status: ${array[i]}`)
+ }
+ }
+ }
+ return array
+}
function pluse(index: number) {
const el = dots.value?.[index]
@@ -97,7 +153,7 @@ function pluse(index: number) {
return
el.style.transition = 'none'
el.style.transform = 'scale(1.3)'
- el.style.filter = 'hue-rotate(90deg)'
+ el.style.filter = 'hue-rotate(-90deg)'
// // force reflow
void el.offsetWidth
el.style.transition = 'transform 0.3s, filter 0.3s'
@@ -110,53 +166,84 @@ async function scanFrame() {
const canvas = document.createElement('canvas')
canvas.width = video.value!.videoWidth
canvas.height = video.value!.videoHeight
+ if (video.value!.videoWidth === 0 || video.value!.videoHeight === 0) {
+ cameraSignalStatus.value = CameraSignalStatus.Waiting
+ return
+ }
+
+ cameraSignalStatus.value = CameraSignalStatus.Ready
const ctx = canvas.getContext('2d')!
ctx.drawImage(video.value!, 0, 0, canvas.width, canvas.height)
- const result = await scan(canvas)
- if (result?.text) {
- setFps()
- results.value.add(result.text)
- const data = JSON.parse(result.text) as SliceData
- if (Array.isArray(data)) {
- if (data[0] !== id.value) {
- chunks.length = 0
- dataUrl.value = undefined
- }
+ const result = await scan(canvas)
+ if (!result.text)
+ return
- // Bandwidth calculation
- {
- const chunkSize = data[4].length
+ setFps()
+ bytesReceived.value += result.text.length
+ totalValidBytesReceived.value = decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0)
- if (!chunks[data[2]]) {
- validBytesReceivedInLastSecond.value += chunkSize
- }
+ // Do not process the same QR code twice
+ if (cached.has(result.text))
+ return
- bytesReceivedInLastSecond.value += chunkSize
- }
+ error.value = undefined
+ const binary = toUint8Array(result.text)
+ const data = binaryToBlock(binary)
+ // Data set changed, reset decoder
+ if (checksum.value !== data.checksum) {
+ decoder.value = createDecoder()
+ checksum.value = data.checksum
+ bytes.value = data.bytes
+ k.value = data.k
+ startTime.value = performance.now()
+ endTime.value = 0
+ cached.clear()
+ }
+ // The previous data set is already decoded, skip for any new blocks
+ else if (endTime.value) {
+ return
+ }
- chunks[data[2]] = data
- pluse(data[2])
-
- if (!length.value)
- return
- if (picked.value.every(i => !!i)) {
- try {
- const merged = merge(picked.value as SliceData[])
- dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
- }
- catch (e) {
- error.value = e
- }
- }
- }
+ cached.add(result.text)
+ k.value = data.k
+ data.indices.map(i => pluse(i))
+ const success = decoder.value.addBlock(data)
+ status.value = getStatus()
+ if (success) {
+ endTime.value = performance.now()
+ const merged = decoder.value.getDecoded()!
+ dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
}
+ // console.log({ data })
+ // if (Array.isArray(data)) {
+ // if (data[0] !== id.value) {
+ // chunks.length = 0
+ // dataUrl.value = undefined
+ // }
+
+ //
+
+ // chunks[data[2]] = data
+ // pluse(data[2])
+
+ // if (!length.value)
+ // return
+ // if (picked.value.every(i => !!i)) {
+ // try {
+ // const merged = merge(picked.value as SliceData[])
+ // dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
+ // }
+ // catch (e) {
+ // error.value = e
+ // }
+ // }
+ // }
}
-watch(() => results.value.size, (size) => {
- if (!size)
- chunks.length = 0
-})
+function now() {
+ return performance.now()
+}
@@ -166,51 +253,102 @@ watch(() => results.value.size, (size) => {
v-for="item of cameras" :key="item.deviceId" :class="{
'text-blue': selectedCamera === item.deviceId,
}"
- class="border rounded-md px2 py1 text-sm shadow-sm"
+ px2 py1 text-sm shadow-sm
+ border="~ gray/25 rounded-lg"
@click="selectedCamera = item.deviceId"
>
{{ item.label }}
-
-
-
-
+
+
+
+
+ Checksum: {{ checksum }}
+ Indices: {{ k }}
+ Decoded: {{ decodedBlocks }}
+ Received blocks: {{ decoder.encodedCount }}
+ Expected bytes: {{ (bytes / 1024).toFixed(2) }} KB
+ Received bytes: {{ (receivedBytes / 1024).toFixed(2) }} KB ({{ bytes === 0 ? 0 : (receivedBytes / bytes * 100).toFixed(2) }}%)
+ Timepassed: {{ (((endTime || now()) - startTime) / 1000).toFixed(2) }} s
+ Average bitrate: {{ (receivedBytes / 1024 / ((endTime || now()) - startTime) * 1000).toFixed(2) }} Kbps
+
+
+
+
+
+
+
+ {{ ([0, 1, 2].includes(x)) ? '' : x }}
+
+
+
+
+
+
+
- Download
+
+
+
+
-
+
-
-
- {{ picked.filter(p => !!p).length }} / {{ length }}
+
+
+
+ {{ (receivedBytes / 1024).toFixed(2) }} / {{ (bytes / 1024).toFixed(2) }} KB ({{ (receivedBytes / bytes * 100).toFixed(2) }}%)
No Data
-
- {{ shutterCount }} | {{ fps.toFixed(0) }} hz | {{ currentValidBandwidthFormatted }} ({{ currentBandwidthFormatted }})
+
+
+ {{ fps.toFixed(0) }} hz | {{ currentValidBytesSpeedFormatted }} ({{ currentBytesFormatted }})
+
+
diff --git a/app/composables/bandwidth.ts b/app/composables/bandwidth.ts
deleted file mode 100644
index e25adc9..0000000
--- a/app/composables/bandwidth.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-export function useBandwidth() {
- const kiloBytesFormatter = new Intl.NumberFormat('en-US', {
- style: 'unit',
- unit: 'kilobyte-per-second',
- unitDisplay: 'short',
- })
-
- const bytesReceived = ref(0)
- const current = ref(0)
- const animationFrameId = ref
(null)
- const lastUpdateTime = ref(0)
-
- const currentFormatted = computed(() => {
- return kiloBytesFormatter.format(current.value)
- })
-
- function updateBandwidth(timestamp: number) {
- const now = timestamp
- const elapsedTime = now - lastUpdateTime.value
-
- if (elapsedTime >= 1000) {
- // Calculate bandwidth for the last second
- current.value = Number.parseFloat(
- (
- (bytesReceived.value / 1024)
- / (elapsedTime / 1000)
- ).toFixed(2),
- )
-
- // Reset for the next second
- bytesReceived.value = 0
-
- lastUpdateTime.value = now
- }
-
- requestAnimationFrame(updateBandwidth)
- }
-
- onMounted(() => {
- animationFrameId.value = requestAnimationFrame(updateBandwidth)
- })
-
- onUnmounted(() => {
- if (animationFrameId.value !== null) {
- cancelAnimationFrame(animationFrameId.value)
- }
- })
-
- return {
- bytesReceived,
- current,
- currentFormatted,
- }
-}
diff --git a/app/composables/timeseries.ts b/app/composables/timeseries.ts
new file mode 100644
index 0000000..07c2ab2
--- /dev/null
+++ b/app/composables/timeseries.ts
@@ -0,0 +1,163 @@
+import type { ComputedRef, Ref } from 'vue'
+import { computed, ref } from 'vue'
+
+interface TimeSeriesOptions {
+ type: 'counter' | 'gauge'
+ interval: number
+ maxDataPoints: number
+}
+
+interface DataPoint {
+ time: number
+ value: number
+}
+
+function timeSeriesOptions(options?: Partial): TimeSeriesOptions {
+ if (!options) {
+ return {
+ type: 'gauge',
+ interval: 250,
+ maxDataPoints: 100,
+ }
+ }
+
+ return {
+ type: options?.type ?? 'gauge',
+ interval: options?.interval ?? 250,
+ maxDataPoints: options?.maxDataPoints ?? 100,
+ }
+}
+
+export function useTimeSeries(
+ input: Ref | ComputedRef,
+ opts?: Partial,
+) {
+ const options = timeSeriesOptions(opts)
+
+ const timeSeries = ref([])
+ const lastValue = ref(0)
+ const lastSampleTime = ref(0)
+
+ onAnimationFrame((now) => {
+ if (now - lastSampleTime.value < options.interval) {
+ return
+ }
+
+ const currentValue = input.value
+
+ switch (options.type) {
+ case 'counter':
+ // eslint-disable-next-line no-case-declarations
+ const diff = Math.max(0, currentValue - lastValue.value)
+
+ timeSeries.value.push({ time: now, value: diff })
+ lastValue.value = currentValue
+ break
+ case 'gauge':
+ timeSeries.value.push({ time: now, value: currentValue })
+ break
+ }
+
+ // GC
+ if (timeSeries.value.length > options.maxDataPoints) {
+ const excessPoints = timeSeries.value.length - options.maxDataPoints
+ timeSeries.value.splice(0, excessPoints)
+ }
+
+ lastSampleTime.value = now
+ })
+
+ return timeSeries
+}
+
+interface BytesRateOption {
+ sampleRate: number
+ timeWindow: number
+}
+
+function bytesRateOptions(options?: Partial): BytesRateOption {
+ if (!options) {
+ return {
+ sampleRate: 1000,
+ timeWindow: 10000,
+ }
+ }
+
+ return {
+ sampleRate: options?.sampleRate ?? 1000,
+ timeWindow: options?.timeWindow ?? 10000,
+ }
+}
+
+export function useBytesRate(
+ input: Ref | ComputedRef,
+ opts?: Partial,
+) {
+ const options = bytesRateOptions(opts)
+
+ const timeSeriesOpts = timeSeriesOptions(opts)
+ const timeSeries = useTimeSeries(input, timeSeriesOpts)
+
+ const bytesRate = ref(0)
+ const lastUpdateTime = ref(0)
+
+ onAnimationFrame((now) => {
+ if (now - lastUpdateTime.value < options.sampleRate) {
+ return
+ }
+
+ const cutoffTime = now - options.timeWindow
+ const relevantPoints = timeSeries.value.filter(point => point.time > cutoffTime)
+
+ if (relevantPoints.length < 2) {
+ bytesRate.value = 0
+ }
+
+ else {
+ const oldestPoint = relevantPoints[0]!
+ const newestPoint = relevantPoints[relevantPoints.length - 1]!
+ const timeDiff = newestPoint.time - oldestPoint.time
+ const valueDiff = relevantPoints.reduce((sum, point) => sum + point.value, 0)
+
+ // bytes per second
+ bytesRate.value = timeDiff > 0 ? (valueDiff / timeDiff) * 1000 : 0
+ }
+
+ lastUpdateTime.value = now
+ })
+
+ const kiloBytesFormatter = new Intl.NumberFormat('en-US', {
+ style: 'unit',
+ unit: 'kilobyte-per-second',
+ unitDisplay: 'short',
+ })
+
+ const formatted = computed(() => {
+ const kiloBytesPerSecond = bytesRate.value / 1024
+ return kiloBytesFormatter.format(Number.parseFloat(kiloBytesPerSecond.toFixed(2)))
+ })
+
+ return {
+ bytesRate,
+ formatted,
+ }
+}
+
+export function onAnimationFrame(callback: (timestamp: number) => void) {
+ const animationFrameId = ref(null)
+
+ function onAnimationFrame(timestamp: number) {
+ callback(timestamp)
+ animationFrameId.value = requestAnimationFrame(onAnimationFrame)
+ }
+
+ onMounted(() => {
+ animationFrameId.value = requestAnimationFrame(onAnimationFrame)
+ })
+
+ onUnmounted(() => {
+ if (animationFrameId.value !== null) {
+ cancelAnimationFrame(animationFrameId.value)
+ }
+ })
+}
diff --git a/app/pages/index.vue b/app/pages/index.vue
index ef33f30..a546b92 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,6 +1,4 @@
@@ -61,8 +57,13 @@ async function onFileChange(file?: File) {
/>
-
-
+
+
const speed = ref(16)
-
-const results = ref(new Set())
-
+
Speed
@@ -20,18 +18,18 @@ const results = ref(new Set())
w-full flex-1
/>
-
+
+
diff --git a/test/lt-blocks.test.ts b/test/lt-blocks.test.ts
new file mode 100644
index 0000000..aaef2c1
--- /dev/null
+++ b/test/lt-blocks.test.ts
@@ -0,0 +1,53 @@
+import { expect, it } from 'vitest'
+import { createDecoder, createEncoder } from '../utils/lt-code'
+
+function randomBuffer(length: number) {
+ return new Uint8Array(length).map(() => Math.floor(Math.random() * 256))
+}
+
+function createEncoderWithIndices(count: number) {
+ const buffer = randomBuffer(count * 100 - 5)
+ const encoder = createEncoder(buffer, 100)
+ expect(encoder.k).toBe(count)
+
+ return {
+ buffer,
+ encoder,
+ }
+}
+
+it('cross-blocks resolving 1', () => {
+ const { buffer, encoder } = createEncoderWithIndices(3)
+
+ const decoder = createDecoder()
+ decoder.addBlock(encoder.createBlock([0, 1, 2]))
+ decoder.addBlock(encoder.createBlock([1, 2]))
+ // Here we know [0]
+ decoder.addBlock(encoder.createBlock([0, 2]))
+
+ const data = decoder.getDecoded()
+ expect(data).toBeDefined()
+ expect(data).toEqual(buffer)
+})
+
+// TODO: get it work
+it.skip('cross-blocks resolving 2', () => {
+ const { buffer, encoder } = createEncoderWithIndices(5)
+
+ const decoder = createDecoder()
+ decoder.addBlock(encoder.createBlock([0, 1, 2, 3, 4]))
+ decoder.addBlock(encoder.createBlock([0, 1]))
+ expect(decoder.decodedCount).toBe(0)
+ // Here we know [0, 1] and [2, 3, 4]
+ decoder.addBlock(encoder.createBlock([2, 3]))
+ // Here we can have [4]
+ expect(decoder.decodedCount).toBe(1)
+ decoder.addBlock(encoder.createBlock([1]))
+ // Here we can have [0] and [1]
+ expect(decoder.decodedCount).toBe(3)
+ decoder.addBlock(encoder.createBlock([4]))
+
+ const data = decoder.getDecoded()
+ expect(data).toBeDefined()
+ expect(data).toEqual(buffer)
+})
diff --git a/test/lt.test.ts b/test/lt.test.ts
index ea97019..5236916 100644
--- a/test/lt.test.ts
+++ b/test/lt.test.ts
@@ -1,73 +1,81 @@
import fs from 'node:fs/promises'
import { join } from 'node:path'
-import { describe, expect, it } from 'vitest'
-import { binaryToBlock, blockToBinary, createDecoder, encodeFountain } from '../utils/lt-codes'
+import { fromUint8Array, toUint8Array } from 'js-base64'
+import { expect, it } from 'vitest'
+import { binaryToBlock, blockToBinary, createDecoder, createEncoder } from '../utils/lt-code'
-describe('lt-codes', () => {
- it('slice binary', async () => {
- const input = (await fs.readFile(join('test', 'SampleJPGImage_100kbmb.jpg'), null)).buffer
- const data = new Uint32Array(input)
+const list: {
+ name: string
+ data: Uint8Array
+ only?: boolean
+ repeats?: number
+ size?: number
+}[] = [
+ {
+ name: 'generated-1',
+ data: new Uint8Array(1).fill(23),
+ // only: true,
+ // repeats: 0,
+ },
+ {
+ name: 'generated-2',
+ data: new Uint8Array(1000).fill(1),
+ },
+ {
+ name: 'generated-3',
+ data: new Uint8Array(1031).fill(1),
+ },
+ {
+ name: 'sample-jpg',
+ data: new Uint8Array((await fs.readFile(join('test', 'SampleJPGImage_100kbmb.jpg'), null)).buffer),
+ size: 1200,
+ },
+]
- const decoder = createDecoder()
- let count = 0
- for (const block of encodeFountain(data, 1000)) {
- count += 1
- if (count > 1000)
- throw new Error('Too many blocks')
- const binary = blockToBinary(block)
- // Use the binary to transfer
- const back = binaryToBlock(binary)
- const result = decoder.addBlock([back])
- if (result)
- break
- }
-
- const result = decoder.getDecoded()!
- expect(result).toBeDefined()
- expect(result).toBeInstanceOf(Uint32Array)
- expect(result.length).toBe(data.length)
- })
-
- it(`allow loss and disorder`, async () => {
- const input = (await fs.readFile(join('test', 'SampleJPGImage_100kbmb.jpg'), null)).buffer
- const data = new Uint32Array(input)
+for (const item of list) {
+ let test = it
+ if (item.only)
+ test = (it as any).only
+ const {
+ data,
+ name,
+ repeats = 10,
+ size = 1000,
+ } = item
+ test(`slice binary: ${name} (size: ${size})`, { repeats }, async () => {
+ const encoder = createEncoder(data, size)
const decoder = createDecoder()
- let count = 0
- const packets: Uint32Array[] = []
-
- // Encode the data
- for (const block of encodeFountain(data, 1000)) {
- count += 1
- if (count > 1000)
- break
- packets.push(blockToBinary(block))
- }
-
- // Dissupt the order of packets
- packets.sort(() => Math.random() - 0.5)
- // Simulate 50% of packet loss
- packets.length = Math.floor(packets.length * 0.5)
+ let count = 0
- count = 0
- // Decode the data
- for (const packet of packets) {
+ for (const block of encoder.fountain()) {
count += 1
- if (count > 500)
- throw new Error('Too many blocks')
+ const rate = count / encoder.k
+ if (rate > 10) {
+ throw new Error('Too many blocks, aborting')
+ }
+ const binary = blockToBinary(block)
+ const str = fromUint8Array(binary)
+ // Use the str to transfer
- // Use the binary to transfer
- const back = binaryToBlock(packet)
- const result = decoder.addBlock([back])
+ const b2 = toUint8Array(str)
+ const back = binaryToBlock(b2)
+ const result = decoder.addBlock(back)
if (result)
break
}
const result = decoder.getDecoded()!
expect(result).toBeDefined()
- expect(result).toBeInstanceOf(Uint32Array)
+ expect(result).toBeInstanceOf(Uint8Array)
expect(result.length).toBe(data.length)
+
+ expect(
+ +(count / encoder.k * 100).toFixed(2),
+ 'Data rate should be less than 200%',
+ )
+ .toBeLessThan(250) // TODO: target 180%
})
-})
+}
diff --git a/utils/lt-code/checksum.ts b/utils/lt-code/checksum.ts
new file mode 100644
index 0000000..70cfaa8
--- /dev/null
+++ b/utils/lt-code/checksum.ts
@@ -0,0 +1,30 @@
+function generateCRCTable() {
+ const crcTable = new Uint32Array(256)
+ for (let i = 0; i < 256; i++) {
+ let crc = i
+ for (let j = 8; j > 0; j--) {
+ if (crc & 1) {
+ crc = (crc >>> 1) ^ 0xEDB88320 // Polynomial used in CRC-32
+ }
+ else {
+ crc = crc >>> 1
+ }
+ }
+ crcTable[i] = crc >>> 0
+ }
+ return crcTable
+}
+const crcTable = /* @__PURE__ */ generateCRCTable()
+/**
+ * Get checksum of the data using CRC32 and XOR with k to ensure uniqueness for different chunking sizes
+ */
+
+export function getChecksum(uint8Array: Uint8Array, k: number): number {
+ let crc = 0xFFFFFFFF // Initial value
+ for (let i = 0; i < uint8Array.length; i++) {
+ const byte = uint8Array[i]!
+ crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xFF]!
+ }
+
+ return (crc ^ k ^ 0xFFFFFFFF) >>> 0 // Final XOR value and ensure 32-bit unsigned
+}
diff --git a/utils/lt-code/decoder.ts b/utils/lt-code/decoder.ts
new file mode 100644
index 0000000..95c0cbf
--- /dev/null
+++ b/utils/lt-code/decoder.ts
@@ -0,0 +1,123 @@
+import type { EncodedBlock } from './shared'
+import { getChecksum } from './checksum'
+import { xorUint8Array } from './shared'
+
+export function createDecoder(blocks?: EncodedBlock[]) {
+ return new LtDecoder(blocks)
+}
+
+export class LtDecoder {
+ public decodedData: (Uint8Array | undefined)[] = []
+ public decodedCount = 0
+ public encodedCount = 0
+ public encodedBlocks: Set = new Set()
+ public meta: EncodedBlock = undefined!
+
+ constructor(blocks?: EncodedBlock[]) {
+ if (blocks) {
+ for (const block of blocks) {
+ this.addBlock(block)
+ }
+ }
+ }
+
+ // Add block and decode them on the fly
+ addBlock(block: EncodedBlock): boolean {
+ if (!this.meta) {
+ this.meta = block
+ this.decodedData = Array.from({ length: this.meta.k })
+ }
+
+ if (block.checksum !== this.meta.checksum) {
+ throw new Error('Adding block with different checksum')
+ }
+ this.encodedBlocks.add(block)
+ this.encodedCount += 1
+
+ this.propagateDecoded()
+
+ return this.decodedCount === this.meta.k
+ }
+
+ propagateDecoded() {
+ let changed = false
+ for (const block of this.encodedBlocks) {
+ let { data, indices } = block
+
+ // We already have all the data from this block
+ if (indices.every(index => this.decodedData[index] != null)) {
+ this.encodedBlocks.delete(block)
+ continue
+ }
+
+ // XOR the data
+ for (const index of indices) {
+ if (this.decodedData[index] != null) {
+ block.data = data = xorUint8Array(data, this.decodedData[index]!)
+ block.indices = indices = indices.filter(i => i !== index)
+ changed = true
+ }
+ }
+
+ if (indices.length === 1 && this.decodedData[indices[0]!] == null) {
+ this.decodedData[indices[0]!] = block.data
+ this.decodedCount++
+ this.encodedBlocks.delete(block)
+ changed = true
+ }
+ }
+
+ for (const block of this.encodedBlocks) {
+ const { data, indices } = block
+
+ // Use 1x2x3 XOR 2x3 to get 1
+ if (indices.length >= 3) {
+ const lowerBlocks = Array.from(this.encodedBlocks).filter(i => i.indices.length === indices.length - 1)
+ for (const lower of lowerBlocks) {
+ const extraIndices = indices.filter(i => !lower.indices.includes(i))
+ if (extraIndices.length === 1 && this.decodedData[extraIndices[0]!] == null) {
+ const extraData = xorUint8Array(data, lower.data)
+ const extraIndex = extraIndices[0]!
+ this.decodedData[extraIndex] = extraData
+ this.decodedCount++
+ this.encodedBlocks.delete(lower)
+ changed = true
+ }
+ }
+ }
+ }
+
+ // If making some progress, continue
+ if (changed) {
+ this.propagateDecoded()
+ }
+ }
+
+ getDecoded(): Uint8Array | undefined {
+ if (this.decodedCount !== this.meta.k) {
+ return
+ }
+ if (this.decodedData.some(block => block == null)) {
+ return
+ }
+ const indiceSize = this.meta.data.length
+ const blocks = this.decodedData as Uint8Array[]
+ const decodedData = new Uint8Array(this.meta.bytes)
+ blocks.forEach((block, i) => {
+ const start = i * indiceSize
+ if (start + indiceSize > decodedData.length) {
+ for (let j = 0; j < decodedData.length - start; j++) {
+ decodedData[start + j] = block[j]!
+ }
+ }
+ else {
+ decodedData.set(block, i * indiceSize)
+ }
+ })
+ const checksum = getChecksum(decodedData, this.meta.k)
+ if (checksum !== this.meta.checksum) {
+ throw new Error('Checksum mismatch')
+ }
+ return decodedData
+ }
+}
diff --git a/utils/lt-code/encoder.ts b/utils/lt-code/encoder.ts
new file mode 100644
index 0000000..bd6ba56
--- /dev/null
+++ b/utils/lt-code/encoder.ts
@@ -0,0 +1,99 @@
+import type { EncodedBlock } from './shared'
+import { getChecksum } from './checksum'
+
+export function createEncoder(data: Uint8Array, indiceSize: number) {
+ return new LtEncoder(data, indiceSize)
+}
+
+export class LtEncoder {
+ public readonly k: number
+ public readonly indices: Uint8Array[]
+ public readonly checksum: number
+ public readonly bytes: number
+
+ constructor(
+ public readonly data: Uint8Array,
+ public readonly indiceSize: number,
+ ) {
+ this.indices = sliceData(data, indiceSize)
+ this.k = this.indices.length
+ this.checksum = getChecksum(data, this.k)
+ this.bytes = data.length
+ }
+
+ createBlock(indices: number[]): EncodedBlock {
+ const data = new Uint8Array(this.indiceSize)
+ for (const index of indices) {
+ const indice = this.indices[index]!
+ for (let i = 0; i < this.indiceSize; i++) {
+ data[i] = data[i]! ^ indice[i]!
+ }
+ }
+
+ return {
+ k: this.k,
+ bytes: this.bytes,
+ checksum: this.checksum,
+ indices,
+ data,
+ }
+ }
+
+ /**
+ * Generate random encoded blocks that **never** ends
+ */
+ *fountain(): Generator {
+ while (true) {
+ const degree = getRandomDegree(this.k)
+ const selectedIndices = getRandomIndices(this.k, degree)
+ yield this.createBlock(selectedIndices)
+ }
+ }
+}
+
+function sliceData(data: Uint8Array, blockSize: number): Uint8Array[] {
+ const blocks: Uint8Array[] = []
+ for (let i = 0; i < data.length; i += blockSize) {
+ const block = new Uint8Array(blockSize)
+ block.set(data.slice(i, i + blockSize))
+ blocks.push(block)
+ }
+ return blocks
+}
+
+// Use Ideal Soliton Distribution to select degree
+function getRandomDegree(k: number): number {
+ const probabilities: number[] = Array.from({ length: k }, () => 0)
+
+ // Calculate the probabilities of the Ideal Soliton Distribution
+ probabilities[0] = 1 / k // P(1) = 1/k
+ for (let d = 2; d <= k; d++) {
+ probabilities[d - 1] = 1 / (d * (d - 1))
+ }
+
+ // Accumulate the probabilities to generate the cumulative distribution
+ const cumulativeProbabilities: number[] = probabilities.reduce((acc, p, index) => {
+ acc.push(p + (acc[index - 1] || 0))
+ return acc
+ }, [] as number[])
+
+ // Generate a random number between [0,1] and select the corresponding degree in the cumulative probabilities
+ const randomValue = Math.random()
+ for (let i = 0; i < cumulativeProbabilities.length; i++) {
+ if (randomValue < cumulativeProbabilities[i]!) {
+ return i + 1
+ }
+ }
+
+ return k // Theoretically, this line should never be reached
+}
+
+// Randomly select indices of degree number of original data blocks
+function getRandomIndices(k: number, degree: number): number[] {
+ const indices: Set = new Set()
+ while (indices.size < degree) {
+ const randomIndex = Math.floor(Math.random() * k)
+ indices.add(randomIndex)
+ }
+ return Array.from(indices)
+}
diff --git a/utils/lt-code/index.ts b/utils/lt-code/index.ts
new file mode 100644
index 0000000..c54e34b
--- /dev/null
+++ b/utils/lt-code/index.ts
@@ -0,0 +1,3 @@
+export * from './decoder'
+export * from './encoder'
+export * from './shared'
diff --git a/utils/lt-code/shared.ts b/utils/lt-code/shared.ts
new file mode 100644
index 0000000..a33691d
--- /dev/null
+++ b/utils/lt-code/shared.ts
@@ -0,0 +1,71 @@
+export interface EncodedHeader {
+ /**
+ * Number of original data blocks
+ */
+ k: number
+ /**
+ * Data length for Uint8Array data
+ */
+ bytes: number
+ /**
+ * Checksum, CRC32 and XOR of k
+ */
+ checksum: number
+}
+
+export interface EncodedBlock extends EncodedHeader {
+ indices: number[]
+ data: Uint8Array
+}
+
+export function blockToBinary(block: EncodedBlock): Uint8Array {
+ const { k, bytes, checksum, indices, data } = block
+ const header = new Uint32Array([
+ indices.length,
+ ...indices,
+ k,
+ bytes,
+ checksum,
+ ])
+ const binary = new Uint8Array(header.length * 4 + data.length)
+ let offset = 0
+ binary.set(new Uint8Array(header.buffer), offset)
+ offset += header.length * 4
+ binary.set(data, offset)
+ return binary
+}
+
+export function binaryToBlock(binary: Uint8Array): EncodedBlock {
+ const degree = new Uint32Array(binary.buffer, 0, 4)[0]!
+ const headerRest = Array.from(new Uint32Array(binary.buffer, 4, degree + 3))
+ const indices = headerRest.slice(0, degree)
+ const [
+ k,
+ bytes,
+ checksum,
+ ] = headerRest.slice(degree) as [number, number, number]
+ const data = binary.slice(4 * (degree + 4))
+ return {
+ k,
+ bytes,
+ checksum,
+ indices,
+ data,
+ }
+}
+
+export function xorUint8Array(a: Uint8Array, b: Uint8Array): Uint8Array {
+ const result = new Uint8Array(a.length)
+ for (let i = 0; i < a.length; i++) {
+ result[i] = a[i]! ^ b[i]!
+ }
+ return result
+}
+
+export function stringToUint8Array(str: string): Uint8Array {
+ const data = new Uint8Array(str.length)
+ for (let i = 0; i < str.length; i++) {
+ data[i] = str.charCodeAt(i)
+ }
+ return data
+}
diff --git a/utils/lt-codes.ts b/utils/lt-codes.ts
deleted file mode 100644
index 89c563f..0000000
--- a/utils/lt-codes.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-interface EncodingMeta {
- /**
- * Number of original data blocks
- */
- k: number
- /**
- * Data length
- */
- length: number
- /**
- * Checksum
- */
- sum: number
-}
-
-interface EncodingBlock extends EncodingMeta {
- indices: number[]
- data: Uint32Array
-}
-
-export function blockToBinary(block: EncodingBlock): Uint32Array {
- const { k, length, sum, indices, data } = block
- const header = [
- indices.length,
- ...indices,
- k,
- length,
- sum,
- ]
- const binary = new Uint32Array(header.length + data.length)
- binary.set(header)
- binary.set(data, header.length)
- return binary
-}
-
-export function binaryToBlock(binary: Uint32Array): EncodingBlock {
- const degree = binary[0]!
- const indices = Array.from(binary.slice(1, degree + 1))
- const [
- k,
- length,
- sum,
- ] = Array.from(binary.slice(degree + 1)) as [number, number, number]
- const data = binary.slice(degree + 1 + 3)
- return {
- k,
- length,
- sum,
- indices,
- data,
- }
-}
-
-// CRC32 checksum
-function checksum(data: Uint32Array): number {
- let crc = 0xFFFFFFFF
- for (let i = 0; i < data.length; i++) {
- crc = crc ^ data[i]!
- for (let j = 0; j < 8; j++) {
- crc = crc & 1 ? (crc >>> 1) ^ 0xEDB88320 : crc >>> 1
- }
- }
- return crc ^ 0xFFFFFFFF
-}
-
-// Use Ideal Soliton Distribution to select degree
-function getRandomDegree(k: number): number {
- const probability = Math.random()
- let degree = 1
-
- for (let d = 2; d <= k; d++) {
- const prob = 1 / (d * (d - 1))
- if (probability < prob) {
- degree = d
- break
- }
- }
-
- return degree
-}
-
-// Randomly select indices of degree number of original data blocks
-function getRandomIndices(k: number, degree: number): number[] {
- const indices: Set = new Set()
- while (indices.size < degree) {
- const randomIndex = Math.floor(Math.random() * k)
- indices.add(randomIndex)
- }
- return Array.from(indices)
-}
-
-function xorUint32Array(a: Uint32Array, b: Uint32Array): Uint32Array {
- const result = new Uint32Array(a.length)
- for (let i = 0; i < a.length; i++) {
- result[i] = a[i]! ^ b[i]!
- }
- return result
-}
-
-function sliceData(data: Uint32Array, blockSize: number): Uint32Array[] {
- const blocks: Uint32Array[] = []
- for (let i = 0; i < data.length; i += blockSize) {
- const block = new Uint32Array(blockSize)
- block.set(data.slice(i, i + blockSize))
- blocks.push(block)
- }
- return blocks
-}
-
-export function *encodeFountain(data: Uint32Array, indiceSize: number): Generator {
- const sum = checksum(data)
- const indices = sliceData(data, indiceSize)
- const k = indices.length
- const meta: EncodingMeta = {
- k,
- length: data.length,
- sum,
- }
-
- while (true) {
- const degree = getRandomDegree(k)
- const selectedIndices = getRandomIndices(k, degree)
- let encodedData = new Uint32Array(indiceSize)
-
- for (const index of selectedIndices) {
- encodedData = xorUint32Array(encodedData, indices[index]!)
- }
-
- yield {
- ...meta,
- indices: selectedIndices,
- data: encodedData,
- }
- }
-}
-
-export function createDecoder(blocks?: EncodingBlock[]) {
- return new LtDecoder(blocks)
-}
-
-export class LtDecoder {
- public decodedData: (Uint32Array | undefined)[] = []
- public decodedCount = 0
- public encodedBlocks: Set = new Set()
- public meta: EncodingBlock = undefined!
-
- constructor(blocks?: EncodingBlock[]) {
- if (blocks) {
- this.addBlock(blocks)
- }
- }
-
- // Add blocks and decode them on the fly
- addBlock(blocks: EncodingBlock[]): boolean {
- if (!blocks.length) {
- return false
- }
-
- if (!this.meta) {
- this.meta = blocks[0]!
- }
-
- for (const block of blocks) {
- this.encodedBlocks.add(block)
- }
-
- for (const block of this.encodedBlocks) {
- let { data, indices } = block
-
- for (const index of indices) {
- if (this.decodedData[index] != null) {
- data = xorUint32Array(data, this.decodedData[index]!)
- indices = indices.filter(i => i !== index)
- }
- }
-
- block.data = data
- block.indices = indices
-
- if (indices.length === 1) {
- this.decodedData[indices[0]!] = data
- this.decodedCount++
- this.encodedBlocks.delete(block)
- }
- }
-
- return this.decodedCount === this.meta.k
- }
-
- getDecoded(): Uint32Array | undefined {
- if (this.decodedCount !== this.meta.k) {
- return
- }
- if (this.decodedData.some(block => block == null)) {
- return
- }
- const indiceSize = this.meta.data.length
- const blocks = this.decodedData as Uint32Array[]
- const decodedData = new Uint32Array(this.meta.length)
- blocks.forEach((block, i) => {
- const start = i * indiceSize
- if (start + indiceSize > decodedData.length) {
- for (let j = 0; j < decodedData.length - start; j++) {
- decodedData[start + j] = block[j]!
- }
- }
- else {
- decodedData.set(block, i * indiceSize)
- }
- })
- const sum = checksum(decodedData)
- if (sum !== this.meta.sum) {
- throw new Error('Checksum mismatch')
- }
- return decodedData
- }
-}
-
-export function tringToUint32Array(str: string): Uint32Array {
- const data = new Uint32Array(str.length)
- for (let i = 0; i < str.length; i++) {
- data[i] = str.charCodeAt(i)
- }
- return data
-}