@blockcast/mmt-base 0.1.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/README.md +120 -0
- package/dist/.build-stamp +0 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mmtp.d.ts +271 -0
- package/dist/mmtp.d.ts.map +1 -0
- package/dist/mmtp.js +568 -0
- package/dist/mmtp.js.map +1 -0
- package/package.json +44 -0
- package/src/alfec.test.ts +176 -0
- package/src/index.ts +1 -0
- package/src/mmtp.test.ts +769 -0
- package/src/mmtp.ts +746 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Strict ISO/IEC 23008-1:2023/Amd 1:2025 Table C.3 RaptorQ signaling. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { MMTP_HEADER_SIZE, parseAlFecMessage } from "./mmtp.js";
|
|
4
|
+
|
|
5
|
+
function concat(...parts: Uint8Array[]): Uint8Array {
|
|
6
|
+
const output = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
|
|
7
|
+
let offset = 0;
|
|
8
|
+
for (const part of parts) {
|
|
9
|
+
output.set(part, offset);
|
|
10
|
+
offset += part.length;
|
|
11
|
+
}
|
|
12
|
+
return output;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function u16(value: number): Uint8Array {
|
|
16
|
+
return Uint8Array.of((value >>> 8) & 0xff, value & 0xff);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function u24(value: number): Uint8Array {
|
|
20
|
+
return Uint8Array.of((value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function u32(value: number): Uint8Array {
|
|
24
|
+
return Uint8Array.of(
|
|
25
|
+
(value >>> 24) & 0xff,
|
|
26
|
+
(value >>> 16) & 0xff,
|
|
27
|
+
(value >>> 8) & 0xff,
|
|
28
|
+
value & 0xff,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function raptorqOti(k: number, symbolSize: number, reserved = 0): Uint8Array {
|
|
33
|
+
const transferLength = k * symbolSize;
|
|
34
|
+
return Uint8Array.of(
|
|
35
|
+
Math.floor(transferLength / 0x1_0000_0000) & 0xff,
|
|
36
|
+
(transferLength >>> 24) & 0xff,
|
|
37
|
+
(transferLength >>> 16) & 0xff,
|
|
38
|
+
(transferLength >>> 8) & 0xff,
|
|
39
|
+
transferLength & 0xff,
|
|
40
|
+
reserved,
|
|
41
|
+
(symbolSize >>> 8) & 0xff,
|
|
42
|
+
symbolSize & 0xff,
|
|
43
|
+
1,
|
|
44
|
+
0,
|
|
45
|
+
1,
|
|
46
|
+
8,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildAlFecMessage(options: {
|
|
51
|
+
symbolSize?: number;
|
|
52
|
+
k?: number;
|
|
53
|
+
maximumK?: number;
|
|
54
|
+
p?: number;
|
|
55
|
+
depth?: number;
|
|
56
|
+
codingParams?: number;
|
|
57
|
+
otiReserved?: number;
|
|
58
|
+
repairFlowIdWidth?: 1 | 2;
|
|
59
|
+
} = {}): Uint8Array {
|
|
60
|
+
const symbolSize = options.symbolSize ?? 1312;
|
|
61
|
+
const k = options.k ?? 10;
|
|
62
|
+
const maximumK = options.maximumK ?? k;
|
|
63
|
+
const p = options.p ?? 2;
|
|
64
|
+
const depth = options.depth ?? 4;
|
|
65
|
+
const oti = raptorqOti(k, symbolSize, options.otiReserved);
|
|
66
|
+
const repairFlowId = options.repairFlowIdWidth === 1
|
|
67
|
+
? Uint8Array.of(0)
|
|
68
|
+
: u16(0);
|
|
69
|
+
const descriptor = concat(
|
|
70
|
+
Uint8Array.of(
|
|
71
|
+
1, // number_of_fec_flows
|
|
72
|
+
0, // fec_flow_id
|
|
73
|
+
0, // source_flow_id
|
|
74
|
+
1, // number_of_assets
|
|
75
|
+
),
|
|
76
|
+
u16(1), // packet_id
|
|
77
|
+
Uint8Array.of(options.codingParams ?? 0x10), // one-stage, ssbg0, no FFSRP, payload-ID mode 0
|
|
78
|
+
u16(symbolSize),
|
|
79
|
+
repairFlowId,
|
|
80
|
+
Uint8Array.of(2), // RaptorQ fec_code_id
|
|
81
|
+
Uint8Array.of(0x80 | 13), // private_flag + exact OTI/depth length
|
|
82
|
+
oti,
|
|
83
|
+
Uint8Array.of(depth),
|
|
84
|
+
u24(maximumK),
|
|
85
|
+
u24(p),
|
|
86
|
+
u32(1_000_000),
|
|
87
|
+
u32(k * symbolSize),
|
|
88
|
+
);
|
|
89
|
+
const messagePayload = concat(Uint8Array.of(0xff), u16(descriptor.length), descriptor);
|
|
90
|
+
return concat(
|
|
91
|
+
u16(0x0203),
|
|
92
|
+
Uint8Array.of(1),
|
|
93
|
+
u16(messagePayload.length),
|
|
94
|
+
messagePayload,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function asMulticastPacket(message: Uint8Array, byte1 = 0x02): Uint8Array {
|
|
99
|
+
const packet = new Uint8Array(MMTP_HEADER_SIZE + message.length);
|
|
100
|
+
packet[1] = byte1;
|
|
101
|
+
packet.set(message, MMTP_HEADER_SIZE);
|
|
102
|
+
return packet;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe("parseAlFecMessage", () => {
|
|
106
|
+
it("parses the exact current one-stage RaptorQ profile", () => {
|
|
107
|
+
const config = parseAlFecMessage(asMulticastPacket(buildAlFecMessage()));
|
|
108
|
+
expect(config).not.toBeNull();
|
|
109
|
+
expect(config).toEqual({
|
|
110
|
+
fecCodeId: 2,
|
|
111
|
+
fecPayloadIdMode: 0,
|
|
112
|
+
repairSymbolSize: 1312,
|
|
113
|
+
maximumK: 10,
|
|
114
|
+
maximumP: 2,
|
|
115
|
+
interleaveDepth: 4,
|
|
116
|
+
oti: raptorqOti(10, 1312),
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it.each([64, 1280, 1312, 1320])("accepts canonical Al=8 symbol size %i", symbolSize => {
|
|
121
|
+
expect(parseAlFecMessage(asMulticastPacket(
|
|
122
|
+
buildAlFecMessage({ symbolSize }),
|
|
123
|
+
))?.repairSymbolSize).toBe(symbolSize);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects the pre-amendment 8-bit repair_flow_id encoding", () => {
|
|
127
|
+
expect(parseAlFecMessage(asMulticastPacket(
|
|
128
|
+
buildAlFecMessage({ repairFlowIdWidth: 1 }),
|
|
129
|
+
))).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("rejects unsupported FEC payload ID mode 1", () => {
|
|
133
|
+
expect(parseAlFecMessage(asMulticastPacket(
|
|
134
|
+
buildAlFecMessage({ codingParams: 0x11 }),
|
|
135
|
+
))).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("rejects inferred or mismatched decoder geometry", () => {
|
|
139
|
+
expect(parseAlFecMessage(asMulticastPacket(
|
|
140
|
+
buildAlFecMessage({ depth: 0 }),
|
|
141
|
+
))).toBeNull();
|
|
142
|
+
expect(parseAlFecMessage(asMulticastPacket(
|
|
143
|
+
buildAlFecMessage({ otiReserved: 1 }),
|
|
144
|
+
))).toBeNull();
|
|
145
|
+
expect(parseAlFecMessage(asMulticastPacket(
|
|
146
|
+
buildAlFecMessage({ maximumK: 11 }),
|
|
147
|
+
))).toBeNull();
|
|
148
|
+
expect(parseAlFecMessage(asMulticastPacket(
|
|
149
|
+
buildAlFecMessage({ symbolSize: 1310 }),
|
|
150
|
+
))).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("requires exact message and descriptor boundaries", () => {
|
|
154
|
+
const wrongMessageLength = buildAlFecMessage();
|
|
155
|
+
wrongMessageLength[4] = wrongMessageLength[4]! - 1;
|
|
156
|
+
expect(parseAlFecMessage(asMulticastPacket(wrongMessageLength))).toBeNull();
|
|
157
|
+
|
|
158
|
+
const wrongDescriptorLength = buildAlFecMessage();
|
|
159
|
+
wrongDescriptorLength[7] = wrongDescriptorLength[7]! - 1;
|
|
160
|
+
expect(parseAlFecMessage(asMulticastPacket(wrongDescriptorLength))).toBeNull();
|
|
161
|
+
|
|
162
|
+
const trailing = concat(buildAlFecMessage(), Uint8Array.of(0));
|
|
163
|
+
expect(parseAlFecMessage(asMulticastPacket(trailing))).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("rejects the pre-migration high-bit packet_type encoding", () => {
|
|
167
|
+
expect(parseAlFecMessage(asMulticastPacket(buildAlFecMessage(), 0x08))).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("never throws on truncated input", () => {
|
|
171
|
+
const packet = asMulticastPacket(buildAlFecMessage());
|
|
172
|
+
for (let length = 0; length < packet.length; length++) {
|
|
173
|
+
expect(() => parseAlFecMessage(packet.subarray(0, length))).not.toThrow();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './mmtp.js'
|