cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/release-build-and-order
goq / portal / frame-envelope.mjs
frame-envelope.mjs 3.8K
1export const FRAME_ENVELOPE_HEADER_LENGTH = 40;2export const MAX_FRAME_PAYLOAD_LENGTH = 16 * 1024 * 1024;3export const MAX_FRAME_DIMENSION = 8192;4export const MAX_FRAME_PIXELS = 7680 * 4320;56const MAGIC = [0x53, 0x47, 0x46, 0x52]; // SGFR7const VERSION = 1;8// Flag bits mirror sigil-protocol's FrameFlags (crates/sigil-protocol/src/media.rs)9// exactly; the Rust channel encoder derives its values from that type.10const KNOWN_FLAGS = 0b00000111;11const KEYFRAME_FLAG = 1 << 0;12const CODEC_CONFIG_FLAG = 1 << 1;13const DISCONTINUITY_FLAG = 1 << 2;14const OPTIONAL_U64_NONE = 0xffffffffffffffffn;15const OPTIONAL_I64_NONE = -0x8000000000000000n;16const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);17const CODECS = Object.freeze({ 1: 'h264' });1819function exactOptionalUnsigned(value, field) {20 if (value === OPTIONAL_U64_NONE) return null;21 if (value > MAX_SAFE_BIGINT) {22 throw new Error(`${field} exceeds JavaScript's exact integer range`);23 }24 return Number(value);25}2627function exactOptionalTimestamp(value) {28 if (value === OPTIONAL_I64_NONE) return null;29 if (value < 0n || value > MAX_SAFE_BIGINT) {30 throw new Error('frame PTS is outside the supported exact range');31 }32 return Number(value);33}3435/**36 * Parse the fixed v1 Rust→webview frame envelope without allocating or copying37 * its encoded payload. Every byte in the header and the total message length is38 * validated before a payload view is returned.39 */40export function parseFrameEnvelope(message) {41 if (!(message instanceof ArrayBuffer)) {42 throw new TypeError('frame channel message must be an ArrayBuffer');43 }44 if (message.byteLength < FRAME_ENVELOPE_HEADER_LENGTH) {45 throw new Error('frame channel message is shorter than its header');46 }4748 const bytes = new Uint8Array(message);49 const view = new DataView(message);50 for (let index = 0; index < MAGIC.length; index++) {51 if (bytes[index] !== MAGIC[index]) throw new Error('invalid frame channel magic');52 }53 if (bytes[4] !== VERSION) throw new Error(`unsupported frame channel version: ${bytes[4]}`);5455 const codec = CODECS[bytes[5]];56 if (!codec) throw new Error(`unsupported frame channel codec: ${bytes[5]}`);57 const flags = bytes[6];58 if ((flags & ~KNOWN_FLAGS) !== 0) throw new Error('frame channel has unknown flag bits');59 if ((flags & CODEC_CONFIG_FLAG) !== 0 && (flags & KEYFRAME_FLAG) === 0) {60 throw new Error('frame channel codec configuration requires a keyframe');61 }62 if (bytes[7] !== 0) throw new Error('frame channel reserved byte must be zero');6364 const width = view.getUint16(8, false);65 const height = view.getUint16(10, false);66 if (67 width === 0 ||68 height === 0 ||69 width > MAX_FRAME_DIMENSION ||70 height > MAX_FRAME_DIMENSION ||71 width * height > MAX_FRAME_PIXELS72 ) {73 throw new Error(`invalid frame channel dimensions: ${width}x${height}`);74 }7576 const payloadLength = view.getUint32(12, false);77 if (payloadLength === 0 || payloadLength > MAX_FRAME_PAYLOAD_LENGTH) {78 throw new Error(`invalid frame channel payload length: ${payloadLength}`);79 }80 const expectedLength = FRAME_ENVELOPE_HEADER_LENGTH + payloadLength;81 if (message.byteLength !== expectedLength) {82 throw new Error(`frame channel length mismatch: expected ${expectedLength}, got ${message.byteLength}`);83 }8485 const sequence = exactOptionalUnsigned(view.getBigUint64(16, false), 'frame sequence');86 const captureTimestampMicros = exactOptionalUnsigned(87 view.getBigUint64(24, false),88 'capture timestamp',89 );90 const ptsMicros = exactOptionalTimestamp(view.getBigInt64(32, false));9192 return {93 width,94 height,95 codec,96 keyframe: (flags & KEYFRAME_FLAG) !== 0,97 discontinuity: (flags & DISCONTINUITY_FLAG) !== 0,98 codecConfig: (flags & CODEC_CONFIG_FLAG) !== 0,99 sequence,100 captureTimestampMicros,101 ptsMicros,102 data: new Uint8Array(message, FRAME_ENVELOPE_HEADER_LENGTH, payloadLength),103 };104}