UNPKG

@davidcal/fec-raptorq

Version:

Node.js wrapper for RaptorQ forward error correction

212 lines (179 loc) 5.96 kB
import { spawn } from "child_process"; import { throw_error } from "../uoe/throw_error.js"; import { error_user_payload } from "../uoe/error_user_payload.js"; import { create_promise } from "../uoe/create_promise.js"; const decode_blocks = ({ binary_path }, input) => { const process = spawn(binary_path, ["--decode"], { stdio: ["pipe", "pipe", "pipe"], }); let [block_prom, block_res, block_rej] = create_promise(); const block_queue = []; let iterator_waiting = false; let stream_ended = false; const blocks = { async *[Symbol.asyncIterator]() { while (true) { if (block_queue.length > 0) { const block = block_queue.shift(); if (block === null) break; // End of stream yield block; } else if (stream_ended) { break; } else { iterator_waiting = true; try { const result = await block_prom; iterator_waiting = false; if (result === null) break; // End of stream yield result; // Create new promise for next block [block_prom, block_res, block_rej] = create_promise(); } catch (error) { iterator_waiting = false; throw error; } } } } }; let buffer = []; process.stdout.on('data', (chunk) => { // Binary now outputs blocks with format: [SBN: 1 byte][Block Size: 4 bytes, little-endian][Block Data: variable] buffer.push(...chunk); // Process complete blocks - we now know exact block sizes from the size header while (buffer.length >= 5) { // Need at least SBN + size header // Read SBN (1 byte) const sbn = buffer[0]; // Read block size (4 bytes, little-endian) const block_size = buffer[1] | (buffer[2] << 8) | (buffer[3] << 16) | (buffer[4] << 24); // Check if we have the complete block const total_block_length = 5 + block_size; // 1 (SBN) + 4 (size) + block_size (data) if (buffer.length < total_block_length) { break; // Wait for more data } // Extract block data const block_data = new Uint8Array(buffer.slice(5, total_block_length)); const block = { sbn: sbn, data: block_data }; // Send block to iterator if (iterator_waiting) { block_res(block); iterator_waiting = false; [block_prom, block_res, block_rej] = create_promise(); } else { block_queue.push(block); } // Remove processed block from buffer buffer.splice(0, total_block_length); } }); process.stdout.on('end', () => { stream_ended = true; if (iterator_waiting) { block_res(null); // Signal end of stream } else { block_queue.push(null); // Signal end of stream } }); process.stderr.on('data', (chunk) => { const message = chunk.toString().trim(); if (message.toLowerCase().includes('error') || message.toLowerCase().includes('failed')) { const error = new Error(`RaptorQ decoding error: ${message}`); block_rej(error); } }); process.on('error', (error) => { const wrapped_error = new Error(`Failed to spawn RaptorQ process: ${error.message}`); block_rej(wrapped_error); }); process.on('close', (code) => { if (code !== 0) { const error = new Error(`RaptorQ process exited with code ${code}`); block_rej(error); } else { stream_ended = true; if (iterator_waiting) { block_res(null); } else { block_queue.push(null); } } }); // Handle the async writing of OTI and symbols (async () => { try { const oti = input.oti; if (!(oti instanceof Uint8Array) || oti.length !== 12) { throw new Error('OTI must be a 12-byte Uint8Array'); } process.stdin.write(oti); for await (const symbol of input.encoding_packets) { if (!(symbol instanceof Uint8Array)) { throw new Error('Each symbol must be a Uint8Array'); } process.stdin.write(symbol); } process.stdin.end(); } catch (error) { const wrapped_error = new Error(`Error writing to RaptorQ decoder: ${error.message}`); block_rej(wrapped_error); process.kill(); } })(); return { blocks }; }; const decode_combined = ({ binary_path }, input) => { return new Promise(async (resolve, reject) => { try { // Get blocks from the binary const blocks_result = decode_blocks({ binary_path }, input); // Collect all blocks const blocks_map = new Map(); for await (const block of blocks_result.blocks) { blocks_map.set(block.sbn, block.data); } // Sort blocks by SBN and combine const sorted_sbns = Array.from(blocks_map.keys()).sort((a, b) => a - b); const combined_blocks = sorted_sbns.map(sbn => blocks_map.get(sbn)); // Calculate total length and combine const total_length = combined_blocks.reduce((sum, block) => sum + block.length, 0); const result = new Uint8Array(total_length); let offset = 0; for (const block of combined_blocks) { result.set(block, offset); offset += block.length; } resolve(result); } catch (error) { reject(error); } }); }; export const decode = ({ binary_path }, { usage, oti, encoding_packets }) => { usage ??= {}; usage.output_format ??= "combined"; if (false || !(oti instanceof Uint8Array) || oti.length !== 12 ) { throw_error(error_user_payload("Provided oti must be 12-byte Uint8Array.")); } if (false || !encoding_packets || typeof encoding_packets[Symbol.asyncIterator] !== "function" ) { throw_error(error_user_payload("Provided encoding_packets must be iterable.")); } if (false || !["combined", "blocks"].includes(usage.output_format) ) { throw_error(error_user_payload("Provided output_format must be \"combined\" or \"blocks\".")); } if (usage.output_format === "blocks") { return decode_blocks({ binary_path }, { oti, encoding_packets }); } else { return decode_combined({ binary_path }, { oti, encoding_packets }); } };