Last updated: 2026-09-18
QRTP is a stream-oriented codec to send data over lossy one-way channels such as QR code streams. It is designed to be compact and efficient to parse, while supporting large file transfers with minimal overhead. All numbers are encoded as little-endian.
QR code transfers are inherently unidirectional — the receiver has no back-channel to request retransmission. To work around this, QRTP uses fountain codes (Wirehair) that let the receiver recover the original data from any sufficiently large subset of packets, regardless of order or which specific packets were lost.
QR codes have severely limited capacity: a Level 40 QR code with low ECC carries at most 2,953 bytes of binary data, and Level 3 carries only 53 bytes. This forces a trade-off between header overhead and packet payload. QRTP provides two frame formats to optimize for different scenarios:
| Name | Value | Notes |
|---|---|---|
| Frame marker | 0x71 0x00 | ASCII "q\0" |
| Large packet | 368 bytes | QR level ≥ 13 with low ECC (Level 17 with high ECC) |
| Small packet | 40 bytes | QR level ≤ 12 |
| Wirehair header | 4 bytes | u32 block_id prefix |
| Wirehair block payload | packet_size − 4 bytes | 364 bytes with standard large packets |
| Wirehair max blocks | 64,000 | Per fountain |
| Segmented flag | 0x40 | Set on stream_type when the stream carries a segment_id; the stream header grows from 8 to 12 bytes |
| Last-stream flag | 0x80 | Set on stream_type of the last stream header in a full frame |
| Max frame buffer | 2,953 bytes | Max binary payload per QR (Level 40, low ECC) |
The full frame format is the most general. It can carry multiple streams with different packet sizes in a single QR code. The transfer_id distinguishes different transfers; only the bottom 15 bits are used (transfer_id & 0x7fff). The high bit is zero for full frames, differentiating them from squeezed frames.
Stream headers start immediately after the transfer_id. The last header has the 0x80 flag set on its stream_type byte so the parser knows when to stop reading headers.
0x00 71 00 "q\0" marker (u16 LE) 0x02 xx xx transfer_id & 0x7fff (u16 LE, high bit = 0) 0x04 --- stream headers (8 bytes each, start here) --- --- packets, packed in stream order ---
The squeezed format reduces header overhead for the common case of a single transfer stream. It is identified by the transfer_id having its high bit set (frame[3] & 0x80 != 0, or transfer_id & 0x8000 != 0). Since there is no explicit stream header section, the receiver infers that stream_id = 0.
Squeezed format packet_size depends on the total frame size. If frame_size − 8 ≥ 368, packet_size = 368. Otherwise, packet_size = 40. In QR code terms, large packet size is used from level 13 and up with low ECC (L17 with high ECC), and small packet size for levels below that.
QR codes smaller than 48 bytes are not supported by the current QRTP implementation.
0x00 71 00 "q\0" marker (u16 LE) 0x02 xx xx transfer_id | 0x8000 (u16 LE) 0x04 sS sS sS sS source_total_bytes (u32 LE) 0x08 --- packets, packet_size from frame size ---
Stream headers appear only in full frames, starting immediately after the transfer_id. Each header block is exactly 8 bytes long. The 0x80 flag on stream_type marks the final stream header section.
0x00 tt stream_type (u8). 0x80 = last stream flag 0x01 si stream_id (u8) 0x02 pc packet_count (u8) 0x03 pw packet_size_words (u8, packet_size = pw * 8) 0x04 sS sS sS sS source_total_bytes (u32 LE)
Stream type (bits 0–5) differentiates between kinds of streams: 0 for a data transfer stream and 1 for the network metadata stream. The two high bits are flags: 0x80 marks the last stream header before packet data, and 0x40 marks a stream that carries a segment_id (see Segmented streams).
Stream ID is a small integer to differentiate multiple active streams in the same transfer. The type of a stream ID must remain constant across all frames within a transfer.
Packet size in bytes is calculated as packet_size_words × 8. The packet size remains uniform within a given stream, but distinct streams within the same frame can utilize different packet sizes.
stream_id = 0, stream_type = 0.Important constraint: Encoders should avoid mixing Wirehair packet sizes within a single stream, as each size variant represents a separate fountain. If the packet size is changed mid-transfer, it restarts the stream.
Direct packets are a decoder-side tolerance. The shipping encoder always frames streams with Wirehair; a decoder still accepts a packet whose payload is the source verbatim when source_total_bytes is less than the packet size (at equality the message is a single Wirehair block, not direct mode).
The total length of an evaluated packet always matches the target stream's declared packet_size.
source_total_bytes < packet_size, decode-side): the packet embeds source data verbatim with terminal zero-padding, and no block marker is present.source_total_bytes >= packet_size): packets are a 4-byte block_id followed by the Wirehair payload. The lowest permitted Wirehair packet length is 12 bytes. This is what the encoder emits, including for tiny streams such as the 16-byte-packet network metadata stream.Wirehair: block_id | payload 0x00 bb bb bb bb block_id (u32 LE) 0x04 pp pp pp .. wirehair payload (packet_size - 4 bytes) Direct: source | zero-pad 0x00 dd dd dd .. source bytes verbatim 00 00 00 .. zero-padding to fill packet_size
QRTP employs Wirehair fountain codes to safely rebuild lost items from arbitrary subsets of blocks. The underlying block payload space equals packet_size − 4 bytes.
If the stream is longer than 64000 * payload_bytes, the stream is split into multiple fountains. In the multi-fountain mode, the block_id is interpreted as a fountain-aware block id, where the fountain id is block_id % fountain_count and the fountain block id is block_id / fountain_count.
The total number of fountains and the size of each fountain in bytes can be calculated from the stream header fields as follows:
max_fountain_size_bytes = payload_bytes * 64000 fountain_count = ceil(source_total_bytes / max_fountain_size_bytes) is_last_fountain = current_fountain_index == fountain_count - 1 if is_last_fountain: fountain_size_bytes = source_total_bytes % max_fountain_size_bytes else: fountain_size_bytes = max_fountain_size_bytes
Sometimes you want to send a frequently-updated file over a single QRTP stream. For example, if you are streaming video as 2-second segments, each segment is a separate file but part of the same stream. The stream_id is reused and the stream_type byte carries the segmented flag 0x40; the stream header then grows to 12 bytes with the segment_id as a u32 little-endian in bytes 8–11. Packet layout is unchanged: a packet is still block_id | payload (or verbatim source for a direct packet). It is not a stream type: there is no type 2, and a frame claiming one is rejected.
Stream header, segmented (12 bytes) 0x00 st stream_type | 0x40 (segmented flag) 0x01 si stream_id (u8) 0x02 pc packet_count (u8) 0x03 pw packet_size_words (u8, packet_size = pw * 8) 0x04 sS sS sS sS source_total_bytes (u32 LE) 0x08 gg gg gg gg segment_id (u32 LE) — present because 0x40 is set
Segmented streams do not support Squeezed Frame layouts; they must be sent using Full Frames with explicit stream headers. Segmented streams do not support multi-fountain large file transfers. When sending packets from multiple segments in a QRTP frame, each segment requires its own stream header, as the source_total_bytes may differ between segments. A descriptor whose segment_id differs from the previous one resets that stream's fountain.
Wirehair packet (unchanged by segmentation) 0x00 bb bb bb bb block_id (u32 LE) 0x04 pp pp pp .. wirehair payload (packet_size - 4 bytes) Direct packet (decode-side only: the shipping encoder always frames with Wirehair) 0x00 dd dd dd .. source bytes verbatim 00 00 00 .. zero-padding to fill packet_size
Transfer metadata — filename, absolute size, and context fields — is managed via a length-prefixed JSON string prefixed right before the payload binary stream:
0x00 ll ll ll ll metadata_length (u32 LE) 0x04 7b 22 6e .. JSON: {"name":"hello.txt","size":12345,"sha256",...,"enc":"gzip"} ff d8 ff .. raw payload bytes
If the optional "enc" field is present, it indicates the encoding applied to the payload. Currently only gzip encoding is supported.
The optional "sha256" field is the SHA-256 hash of the raw payload bytes, used for integrity verification before decoding. The receiver can compute the SHA-256 hash of the raw payload and compare it against this value to confirm successful transfer. If the hashes do not match, the receiver should discard the payload and report a transfer failure.
The core protocol operation is parsing a QRTP frame: identify the frame format, extract stream headers, and enumerate packets. The reference code blocks below provide complete, compliant implementation strategies across all supported environments.
Reads the initial frame components to classify the envelope type (Squeezed vs Full) and collect sizing offsets.
/// Parsed Stream info context structure
pub struct StreamInfo {
pub stream_type: u8,
pub stream_id: u8,
pub packet_count: u8,
pub packet_size: usize,
pub source_total_bytes: u32,
}
/// Parse a QRTP frame, returning structured stream info and the packet data start offset.
pub fn parse_qrtp_frame_header(frame: &[u8]) -> Result<(Vec<StreamInfo>, u16, usize), String> {
if frame.len() < 4 || frame[0..2] != [b'q', 0x00] {
return Err("Missing or invalid QRTP frame marker".to_string());
}
let transfer_word = u16::from_le_bytes(frame[2..4].try_into().unwrap());
let transfer_id = transfer_word & 0x7fff;
let is_squeezed = (transfer_word & 0x8000) != 0;
if is_squeezed {
if frame.len() < 8 {
return Err("Squeezed frame missing source size field".to_string());
}
let source_total_bytes = u32::from_le_bytes(frame[4..8].try_into().unwrap());
let payload_len = frame.len() - 8;
let packet_size = if payload_len >= 368 { 368 } else { 40 };
let packet_count = (payload_len / packet_size) as u8;
let squeezed_stream = StreamInfo {
stream_type: 0,
stream_id: 0,
packet_count,
packet_size,
source_total_bytes,
};
Ok((vec![squeezed_stream], transfer_id, 8))
} else {
let mut offset = 4;
let mut streams = Vec::new();
loop {
if offset + 8 > frame.len() {
return Err("Malformed frame: stream headers truncated".to_string());
}
let stream_type_raw = frame[offset];
let stream_id = frame[offset + 1];
let packet_count = frame[offset + 2];
let packet_size_words = frame[offset + 3];
let source_total_bytes = u32::from_le_bytes(frame[offset+4..offset+8].try_into().unwrap());
let is_last = (stream_type_raw & 0x80) != 0;
let is_segmented = (stream_type_raw & 0x40) != 0;
let stream_type = stream_type_raw & 0x3f;
let packet_size = (packet_size_words as usize) * 8;
streams.push(StreamInfo {
stream_type,
stream_id,
packet_count,
packet_size,
source_total_bytes,
});
offset += if is_segmented { 12 } else { 8 };
if is_last { break; }
}
Ok((streams, transfer_id, offset))
}
}
export function parseQrtpFrameHeader(data) {
const pkt = new Uint8Array(data);
if (pkt.length < 4 || pkt[0] !== 0x71 || pkt[1] !== 0) return null;
const tw = pkt[2] | (pkt[3] << 8);
const transferId = tw & 0x7fff;
const isSqueezed = (tw & 0x8000) !== 0;
if (isSqueezed) {
if (pkt.length < 8) return null;
const src = (pkt[4] | (pkt[5] << 8) | (pkt[6] << 16) | (pkt[7] << 24)) >>> 0;
const payloadLen = pkt.length - 8;
const pktSz = payloadLen >= 368 ? 368 : 40;
const count = Math.floor(payloadLen / pktSz);
return {
transferId,
squeezed: true,
payloadOffset: 8,
streams: [{
streamType: 0,
streamId: 0,
packetCount: count,
packetSizeBytes: pktSz,
sourceTotalBytes: src
}]
};
}
let offset = 4;
const streams = [];
while (offset + 8 <= pkt.length) {
const stRaw = pkt[offset];
const streamId = pkt[offset + 1];
const packetCount = pkt[offset + 2];
const pw = pkt[offset + 3];
const src = (pkt[offset+4] | (pkt[offset+5] << 8) | (pkt[offset+6] << 16) | (pkt[offset+7] << 24)) >>> 0;
streams.push({
streamType: stRaw & 0x3f,
segmented: (stRaw & 0x40) !== 0,
streamId,
packetCount,
packetSizeBytes: pw * 8,
sourceTotalBytes: src
});
offset += (stRaw & 0x40) !== 0 ? 12 : 8;
if ((stRaw & 0x80) !== 0) break;
}
return { transferId, squeezed: false, streams, payloadOffset: offset };
}
import struct
def parse_qrtp_frame_header(data: bytes):
if len(data) < 4 or data[0:2] != b'q\x00':
return None
tw = struct.unpack_from('<H', data, 2)[0]
transfer_id = tw & 0x7fff
is_squeezed = bool(tw & 0x8000)
if is_squeezed:
if len(data) < 8:
return None
src = struct.unpack_from('<I', data, 4)[0]
payload_len = len(data) - 8
pkt_size = 368 if payload_len >= 368 else 40
count = payload_len // pkt_size
return {
'transfer_id': transfer_id,
'squeezed': True,
'payload_offset': 8,
'streams': [{
'stream_type': 0, 'stream_id': 0, 'packet_count': count,
'packet_size': pkt_size, 'source_total_bytes': src
}]
}
offset = 4
streams = []
while offset + 8 <= len(data):
st_raw, stream_id, packet_count, pw = struct.unpack_from('<BBBB', data, offset)
src = struct.unpack_from('<I', data, offset + 4)[0]
streams.append({
'stream_type': st_raw & 0x3f,
'segmented': bool(st_raw & 0x40),
'stream_id': stream_id,
'packet_count': packet_count,
'packet_size': pw * 8,
'source_total_bytes': src
})
offset += 12 if st_raw & 0x40 else 8
if st_raw & 0x80:
break
return {'transfer_id': transfer_id, 'squeezed': False, 'streams': streams, 'payload_offset': offset}
package qrtp
import (
"encoding/binary"
"errors"
)
type StreamInfo struct {
StreamType uint8
StreamID uint8
PacketCount uint8
PacketSize int
SourceTotalBytes uint32
}
func ParseQrtpFrameHeader(data []byte) ([]StreamInfo, uint16, int, error) {
if len(data) < 4 || data[0] != 'q' || data[1] != 0 {
return nil, 0, 0, errors.New("invalid protocol header marker")
}
tw := binary.LittleEndian.Uint16(data[2:4])
transferID := tw & 0x7fff
isSqueezed := (tw & 0x8000) != 0
if isSqueezed {
if len(data) < 8 {
return nil, 0, 0, errors.New("truncated squeezed metadata")
}
src := binary.LittleEndian.Uint32(data[4:8])
payloadLen := len(data) - 8
pktSize := 40
if payloadLen >= 368 {
pktSize = 368
}
count := payloadLen / pktSize
return []StreamInfo{{
StreamType: 0,
StreamID: 0,
PacketCount: uint8(count),
PacketSize: pktSize,
SourceTotalBytes: src,
}}, transferID, 8, nil
}
offset := 4
var streams []StreamInfo
for offset+8 <= len(data) {
stRaw := data[offset]
src := binary.LittleEndian.Uint32(data[offset+4 : offset+8])
streams = append(streams, StreamInfo{
StreamType: stRaw & 0x3f,
Segmented: stRaw&0x40 != 0,
StreamID: data[offset+1],
PacketCount: data[offset+2],
PacketSize: int(data[offset+3]) * 8,
SourceTotalBytes: src,
})
if stRaw&0x40 != 0 {
offset += 12
} else {
offset += 8
}
if stRaw&0x80 != 0 {
break
}
}
return streams, transferID, offset, nil
}
Processes sequentially packed chunks based on target stream attributes, handling differences between Wirehair and Direct mode.
// Process packet buffers directly after resolving header state
// 'offset' begins exactly where parse_qrtp_frame_header finished
pub fn process_payload_packets(frame: &[u8], streams: &[StreamInfo], mut offset: usize) {
for stream in streams {
let pkt_size = stream.packet_size;
let src_total = stream.source_total_bytes as usize;
for _ in 0..stream.packet_count {
if offset + pkt_size > frame.len() { break; }
let pkt = &frame[offset..offset + pkt_size];
// Wirehair mode condition: packet_size <= source_total_bytes
let use_wirehair = pkt_size <= src_total;
if use_wirehair && pkt_size >= 12 {
let mut block_id = u32::from_le_bytes(pkt[..4].try_into().unwrap());
let payload = &pkt[4..];
if stream.segmented {
// Segmented stream: first 2 bytes of block_id are segment_id, next 2 bytes are fountain-aware block_id
let segment_id = u16::from_le_bytes(pkt[0..2].try_into().unwrap());
block_id = u16::from_le_bytes(pkt[2..4].try_into().unwrap()) as u32;
// Use segment_id and block_id for choosing the decoder...
}
// Pass components into Wirehair decoding routine...
} else {
// Direct mode: extract raw data verbatim
let mut actual_data_len = std::cmp::min(pkt_size, src_total);
let mut start_offset = 0;
if stream.stream_type == 0x02 {
// Segmented stream: first 2 bytes are segment_id, followed by source data
let segment_id = u16::from_le_bytes(pkt[0..2].try_into().unwrap());
start_offset = 2;
actual_data_len = std::cmp::min(actual_data_len - 2, src_total);
// Use segment_id for choosing the processing logic...
}
// Record the plaintext payload chunk directly...
let valid_data = &pkt[start_offset..actual_data_len];
// ... process valid_data as needed ...
}
offset += pkt_size;
}
}
}
export function processPayloadPackets(pkt, streams, startOffset) {
let offset = startOffset;
for (const stream of streams) {
const pktSize = stream.packetSizeBytes;
const srcTotal = stream.sourceTotalBytes;
for (let i = 0; i < stream.packetCount; i++) {
if (offset + pktSize > pkt.length) break;
const packetBuffer = pkt.slice(offset, offset + pktSize);
const dv = new DataView(packetBuffer.buffer, packetBuffer.byteOffset, packetBuffer.byteLength);
const useWirehair = pktSize <= srcTotal;
if (useWirehair && pktSize >= 12) {
let blockId = dv.getUint32(0, true);
if (stream.streamType === 0x02) {
const segmentId = dv.getUint16(0, true);
blockId = dv.getUint16(2, true);
// updateWirehairDecoder(segmentId);
}
const payload = packetBuffer.slice(4);
// processWirehairBlock(blockId, payload);
} else {
const startOffset = stream.streamType === 0x02 ? 2 : 0;
const segmentId = stream.streamType === 0x02 ? dv.getUint16(0, true) : null;
const validLen = Math.min(pktSize - startOffset, srcTotal);
const directData = packetBuffer.slice(startOffset, startOffset + validLen);
// processDirectBlock(directData);
}
offset += pktSize;
}
}
}
def process_payload_packets(data: bytes, streams: list, start_offset: int):
offset = start_offset
for stream in streams:
pkt_sz = stream['packet_size']
src_total = stream['source_total_bytes']
for _ in range(stream['packet_count']):
if offset + pkt_sz > len(data):
break
pkt = data[offset : offset + pkt_sz]
use_wirehair = pkt_sz <= src_total
if use_wirehair and pkt_sz >= 12:
block_id = struct.unpack_from('<I', pkt, 0)[0]
if stream['stream_type'] == 0x02:
segment_id = struct.unpack_from('<H', pkt, 0)[0]
block_id = struct.unpack_from('<H', pkt, 2)[0]
# update_wirehair_decoder(segment_id)
payload = pkt[4:]
# feed_wirehair_decoder(block_id, payload)
else:
start_offset = 2 if stream['stream_type'] == 0x02 else 0
segment_id = struct.unpack_from('<H', pkt, 0)[0] if stream['stream_type'] == 0x02 else None
valid_len = min(pkt_sz - start_offset, src_total)
direct_data = pkt[start_offset : start_offset + valid_len]
# process_direct_data(direct_data)
offset += pkt_sz
func ProcessPayloadPackets(data []byte, streams []StreamInfo, startOffset int) {
offset := startOffset
for _, stream := range streams {
pktSz := stream.PacketSize
srcTotal := int(stream.SourceTotalBytes)
for i := 0; i < int(stream.PacketCount); i++ {
if offset+pktSz > len(data) {
break
}
pkt := data[offset : offset+pktSz]
useWirehair := pktSz <= srcTotal
if useWirehair && pktSz >= 12 {
blockID := binary.LittleEndian.Uint32(pkt[:4])
payload := pkt[4:]
if stream.StreamType == 0x02 {
segmentID := binary.LittleEndian.Uint16(pkt[0:2])
blockID := binary.LittleEndian.Uint16(pkt[2:4])
// updateWirehairDecoder(segmentID)
}
// ingestWirehair(blockID, payload)
_, _ = blockID, payload
} else {
startOffset := 0
validLen := pktSz
if stream.StreamType == 0x02 {
startOffset = 2
segmentID := binary.LittleEndian.Uint16(pkt[0:2])
validLen -= 2
}
if srcTotal < validLen {
validLen = srcTotal
}
directData := pkt[startOffset:validLen]
_ = directData
}
offset += pktSz
}
}
}
Generates standardized frame vectors, opting for Squeezed compression configurations where single-stream rules allow.
pub fn create_qrtp_frame(
transfer_id: u16,
streams: &[StreamInfo],
packets_payloads: &[Vec<u8>]
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"q\x00");
// Squeezed logic checks: single stream, stream_id == 0, packet size matches standard values
if streams.len() == 1 && streams[0].stream_id == 0 && streams[0].stream_type == 0 && (streams[0].packet_size == 368 || streams[0].packet_size == 40) {
out.extend_from_slice(&((transfer_id & 0x7fff) | 0x8000).to_le_bytes());
out.extend_from_slice(&streams[0].source_total_bytes.to_le_bytes());
for pkt in packets_payloads {
out.extend_from_slice(pkt);
}
return out;
}
// Default Full Frame Execution fallback path
out.extend_from_slice(&(transfer_id & 0x7fff).to_le_bytes());
for (i, stream) in streams.iter().enumerate() {
let is_last = i == streams.len() - 1;
let stream_type_byte = if is_last { stream.stream_type | 0x80 } else { stream.stream_type };
out.push(stream_type_byte);
out.push(stream.stream_id);
out.push(stream.packet_count);
out.push((stream.packet_size / 8) as u8);
out.extend_from_slice(&stream.source_total_bytes.to_le_bytes());
}
for pkt in packets_payloads {
out.extend_from_slice(pkt);
}
out
}
export function createQrtpFrame(transferId, streams, packetsPayloads) {
const buffers = [];
// Check squeezed criteria optimization
if (streams.len === 1 && streams[0].streamId === 0 && streams[0].streamType === 0 && (streams[0].packetSizeBytes === 368 || streams[0].packetSizeBytes === 40)) {
const markerAndId = new Uint8Array(4);
markerAndId[0] = 0x71; markerAndId[1] = 0x00;
const tw = (transferId & 0x7fff) | 0x8000;
markerAndId[2] = tw & 0xff; markerAndId[3] = (tw >> 8) & 0xff;
buffers.push(markerAndId);
const srcBuf = new Uint8Array(4);
const src = streams[0].sourceTotalBytes;
srcBuf[0] = src & 0xff; srcBuf[1] = (src >> 8) & 0xff;
srcBuf[2] = (src >> 16) & 0xff; srcBuf[3] = (src >> 24) & 0xff;
buffers.push(srcBuf);
for (const pkt of packetsPayloads) buffers.push(pkt);
return mergeBuffers(buffers);
}
const header = new bytearray([0x71, 0x00, transferId & 0xff, (transferId >> 8) & 0x7f]);
buffers.push(header);
for (let i = 0; i < streams.length; i++) {
const s = streams[i];
const isLast = i === streams.length - 1;
const stByte = isLast ? (s.streamType | 0x80) : s.streamType;
const sHeader = new Uint8Array(8);
sHeader[0] = stByte; sHeader[1] = s.streamId; sHeader[2] = s.packetCount; sHeader[3] = Math.floor(s.packetSizeBytes / 8);
sHeader[4] = s.sourceTotalBytes & 0xff; sHeader[5] = (s.sourceTotalBytes >> 8) & 0xff;
sHeader[6] = (s.sourceTotalBytes >> 16) & 0xff; sHeader[7] = (s.sourceTotalBytes >> 24) & 0xff;
buffers.push(sHeader);
}
for (const pkt of packetsPayloads) buffers.push(pkt);
return mergeBuffers(buffers);
}
Gentile function helper to simplify buffer array composition
function mergeBuffers(arr) {
let total = arr.reduce((acc, b) => acc + b.length, 0);
let out = new Uint8Array(total), offset = 0;
for (const b of arr) { out.set(b, offset); offset += b.length; }
return out;
}
def create_qrtp_frame(transfer_id, streams, packets_payloads):
buf = bytearray(b'q\x00')
# Optimization path validation for squeezed targets
if len(streams) == 1 and streams[0]['stream_id'] == 0 and streams[0]['stream_type'] == 0 and streams[0]['packet_size'] in (368, 40):
buf += struct.pack('<HI', (transfer_id & 0x7fff) | 0x8000, streams[0]['source_total_bytes'])
for pkt in packets_payloads:
buf += pkt
return bytes(buf)
# Standard full layout execution flow
buf += struct.pack('<H', transfer_id & 0x7fff)
for i, stream in enumerate(streams):
is_last = (i == len(streams) - 1)
st_byte = (stream['stream_type'] | 0x80) if is_last else stream['stream_type']
buf += struct.pack('<BBBB', st_byte, stream['stream_id'], stream['packet_count'], stream['packet_size'] // 8)
buf += struct.pack('<I', stream['source_total_bytes'])
for pkt in packets_payloads:
buf += pkt
return bytes(buf)
func CreateQrtpFrame(transferID uint16, streams []StreamInfo, packetsPayloads [][]byte) []byte {
var buf []byte
buf = append(buf, 'q', 0)
if len(streams) == 1 && streams[0].StreamID == 0 && streams[0].StreamType == 0 && (streams[0].PacketSize == 368 || streams[0].PacketSize == 40) {
tw := (transferID & 0x7fff) | 0x8000
tBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(tBytes, tw)
buf = append(buf, tBytes...)
sBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(sBytes, streams[0].SourceTotalBytes)
buf = append(buf, sBytes...)
for _, pkt := range packetsPayloads {
buf = append(buf, pkt...)
}
return buf
}
tBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(tBytes, transferID&0x7fff)
buf = append(buf, tBytes...)
for i, stream := range streams {
isLast := i == len(streams)-1
stByte := stream.StreamType
if isLast {
stByte |= 0x80
}
buf = append(buf, stByte, stream.StreamID, stream.PacketCount, uint8(stream.PacketSize/8))
sBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(sBytes, stream.SourceTotalBytes)
buf = append(buf, sBytes...)
}
for _, pkt := range packetsPayloads {
buf = append(buf, pkt...)
}
return buf
}