@blockcast/mmt-container 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.
@@ -0,0 +1,416 @@
1
+ /**
2
+ * @mmt/container - MFU Fragment Reassembler
3
+ *
4
+ * Handles out-of-order delivery of fragmented MFUs (Media Fragment Units)
5
+ * according to ISO/IEC 23008-1:2023 Section 9.2.3.3
6
+ *
7
+ * Fragmentation Indicator (FI) values:
8
+ * - 0: Complete MFU (single packet, no fragmentation)
9
+ * - 1: First fragment
10
+ * - 2: Middle fragment
11
+ * - 3: Last fragment
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ import {
17
+ type MMTPPacket,
18
+ unwrapMMTP,
19
+ parseMPUHeader,
20
+ isFecRepairPacket,
21
+ MPU_HEADER_SIZE,
22
+ MFU_DU_HEADER_SIZE,
23
+ } from './mmtp.js'
24
+
25
+ /**
26
+ * MFU Fragment for reassembly
27
+ */
28
+ export interface MFUFragment {
29
+ fragmentationIndicator: number
30
+ mpuSequenceNumber: number
31
+ fragmentCounter: number
32
+ data: Uint8Array
33
+ timestamp: bigint
34
+ receivedAt: number
35
+ /** Random Access Point flag from MMTP header - indicates keyframe */
36
+ rapFlag: boolean
37
+ }
38
+
39
+ /**
40
+ * Reassembled MFU result
41
+ */
42
+ export interface ReassembledMFU {
43
+ mpuSequenceNumber: number
44
+ data: Uint8Array
45
+ timestamp: bigint
46
+ fragmentCount: number
47
+ /** Random Access Point flag - indicates keyframe (from first fragment) */
48
+ rapFlag: boolean
49
+ }
50
+
51
+ /**
52
+ * Reassembler statistics
53
+ */
54
+ export interface ReassemblerStats {
55
+ totalFragments: number
56
+ reassembledMFUs: number
57
+ timedOutMFUs: number
58
+ outOfOrderFragments: number
59
+ }
60
+
61
+ export interface MFUReassemblerOptions {
62
+ /** Timeout in milliseconds for incomplete MFUs (default: 1000) */
63
+ timeoutMs?: number
64
+ /** Maximum buffered MFUs before eviction (default: 100) */
65
+ maxBuffered?: number
66
+ }
67
+
68
+ /**
69
+ * MFU Reassembler for out-of-order fragment delivery
70
+ * Based on ISO/IEC 23008-1:2023 Section 9.2.3.3
71
+ */
72
+ export class MFUReassembler {
73
+ #buffers: Map<number, MFUFragment[]> = new Map()
74
+ #timeouts: Map<number, ReturnType<typeof setTimeout>> = new Map()
75
+ #timeoutMs: number
76
+ #maxBuffered: number
77
+
78
+ /** Statistics for debugging */
79
+ readonly stats: ReassemblerStats = {
80
+ totalFragments: 0,
81
+ reassembledMFUs: 0,
82
+ timedOutMFUs: 0,
83
+ outOfOrderFragments: 0,
84
+ }
85
+
86
+ constructor(options?: MFUReassemblerOptions) {
87
+ this.#timeoutMs = options?.timeoutMs ?? 1000
88
+ this.#maxBuffered = options?.maxBuffered ?? 100
89
+ }
90
+
91
+ /**
92
+ * Add a fragment and return reassembled MFU if complete
93
+ */
94
+ addFragment(fragment: MFUFragment): ReassembledMFU | null {
95
+ this.stats.totalFragments++
96
+
97
+ const { mpuSequenceNumber, fragmentationIndicator, fragmentCounter } = fragment
98
+
99
+ // Handle complete MFU (FI=0)
100
+ if (fragmentationIndicator === 0) {
101
+ this.stats.reassembledMFUs++
102
+ return {
103
+ mpuSequenceNumber,
104
+ data: fragment.data,
105
+ timestamp: fragment.timestamp,
106
+ fragmentCount: 1,
107
+ rapFlag: fragment.rapFlag,
108
+ }
109
+ }
110
+
111
+ // Get or create fragment buffer for this MPU
112
+ let fragments = this.#buffers.get(mpuSequenceNumber)
113
+ if (!fragments) {
114
+ fragments = []
115
+ this.#buffers.set(mpuSequenceNumber, fragments)
116
+ this.#setTimeout(mpuSequenceNumber)
117
+ }
118
+
119
+ // Check for duplicate fragment
120
+ const existingIdx = fragments.findIndex((f) => f.fragmentCounter === fragmentCounter)
121
+ if (existingIdx >= 0) {
122
+ fragments[existingIdx] = fragment
123
+ } else {
124
+ fragments.push(fragment)
125
+ }
126
+
127
+ // Track out-of-order delivery
128
+ if (fragments.length > 1) {
129
+ const lastFI = fragments[fragments.length - 2]!.fragmentationIndicator
130
+ if (fragmentationIndicator !== lastFI + 1) {
131
+ this.stats.outOfOrderFragments++
132
+ }
133
+ }
134
+
135
+ // Enforce memory limits
136
+ this.#enforceMemoryLimit()
137
+
138
+ // Try to reassemble
139
+ return this.#tryReassemble(mpuSequenceNumber)
140
+ }
141
+
142
+ #tryReassemble(mpuSequenceNumber: number): ReassembledMFU | null {
143
+ const fragments = this.#buffers.get(mpuSequenceNumber)
144
+ if (!fragments || fragments.length === 0) {
145
+ return null
146
+ }
147
+
148
+ // Sort by fragment counter
149
+ fragments.sort((a, b) => a.fragmentCounter - b.fragmentCounter)
150
+
151
+ const firstFI = fragments[0]!.fragmentationIndicator
152
+ const lastFI = fragments[fragments.length - 1]!.fragmentationIndicator
153
+
154
+ // Need first (FI=1) and last (FI=3) fragments
155
+ if (firstFI !== 1 || lastFI !== 3) {
156
+ return null
157
+ }
158
+
159
+ // Verify middle fragments have FI=2
160
+ for (let i = 1; i < fragments.length - 1; i++) {
161
+ if (fragments[i]!.fragmentationIndicator !== 2) {
162
+ return null
163
+ }
164
+ }
165
+
166
+ // Reassemble: concatenate all fragment data
167
+ const totalSize = fragments.reduce((sum, f) => sum + f.data.length, 0)
168
+ const data = new Uint8Array(totalSize)
169
+
170
+ let offset = 0
171
+ for (const fragment of fragments) {
172
+ data.set(fragment.data, offset)
173
+ offset += fragment.data.length
174
+ }
175
+
176
+ // Cleanup
177
+ this.#clearTimeout(mpuSequenceNumber)
178
+ this.#buffers.delete(mpuSequenceNumber)
179
+ this.stats.reassembledMFUs++
180
+
181
+ return {
182
+ mpuSequenceNumber,
183
+ data,
184
+ timestamp: fragments[0]!.timestamp,
185
+ fragmentCount: fragments.length,
186
+ rapFlag: fragments[0]!.rapFlag,
187
+ }
188
+ }
189
+
190
+ #setTimeout(mpuSequenceNumber: number): void {
191
+ const timeoutId = setTimeout(() => {
192
+ this.#onTimeout(mpuSequenceNumber)
193
+ }, this.#timeoutMs)
194
+ this.#timeouts.set(mpuSequenceNumber, timeoutId)
195
+ }
196
+
197
+ #clearTimeout(mpuSequenceNumber: number): void {
198
+ const timeoutId = this.#timeouts.get(mpuSequenceNumber)
199
+ if (timeoutId !== undefined) {
200
+ clearTimeout(timeoutId)
201
+ this.#timeouts.delete(mpuSequenceNumber)
202
+ }
203
+ }
204
+
205
+ #onTimeout(mpuSequenceNumber: number): void {
206
+ const fragments = this.#buffers.get(mpuSequenceNumber)
207
+ if (!fragments) return
208
+
209
+ this.stats.timedOutMFUs++
210
+ this.#buffers.delete(mpuSequenceNumber)
211
+ this.#timeouts.delete(mpuSequenceNumber)
212
+ }
213
+
214
+ #enforceMemoryLimit(): void {
215
+ if (this.#buffers.size <= this.#maxBuffered) return
216
+
217
+ const sequences = Array.from(this.#buffers.keys()).sort((a, b) => a - b)
218
+ const toRemove = sequences.slice(0, sequences.length - this.#maxBuffered)
219
+
220
+ for (const mpuSeq of toRemove) {
221
+ this.#clearTimeout(mpuSeq)
222
+ this.#buffers.delete(mpuSeq)
223
+ this.stats.timedOutMFUs++
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Flush all buffered fragments
229
+ */
230
+ flush(): void {
231
+ for (const mpuSeq of this.#buffers.keys()) {
232
+ this.#clearTimeout(mpuSeq)
233
+ }
234
+ this.#buffers.clear()
235
+ this.#timeouts.clear()
236
+ }
237
+
238
+ /**
239
+ * Get number of pending MFUs
240
+ */
241
+ get pendingCount(): number {
242
+ return this.#buffers.size
243
+ }
244
+
245
+ /**
246
+ * Get current buffer status
247
+ */
248
+ getStatus(): {
249
+ bufferedMFUs: number
250
+ pendingFragments: number
251
+ stats: ReassemblerStats
252
+ } {
253
+ let pendingFragments = 0
254
+ for (const fragments of this.#buffers.values()) {
255
+ pendingFragments += fragments.length
256
+ }
257
+
258
+ return {
259
+ bufferedMFUs: this.#buffers.size,
260
+ pendingFragments,
261
+ stats: { ...this.stats },
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Create MFU fragment from MMTP packet for reassembly
268
+ */
269
+ export function createFragment(mmtpPacket: Uint8Array): MFUFragment {
270
+ const { header, payload } = unwrapMMTP(mmtpPacket)
271
+ const mpuHeader = parseMPUHeader(payload)
272
+ const { mpuSequenceNumber, fragmentationIndicator, fragmentCounter } = mpuHeader
273
+
274
+ // MFU DU header is present only for FI = 0 (complete) or FI = 1 (first)
275
+ const hasMfuHeader = fragmentationIndicator === 0 || fragmentationIndicator === 1
276
+ const headerSize = MPU_HEADER_SIZE + (hasMfuHeader ? MFU_DU_HEADER_SIZE : 0)
277
+
278
+ if (payload.length < headerSize) {
279
+ throw new Error(`Payload too small for headers: ${payload.length} bytes (need ${headerSize})`)
280
+ }
281
+
282
+ const data = payload.subarray(headerSize)
283
+
284
+ return {
285
+ fragmentationIndicator,
286
+ mpuSequenceNumber,
287
+ fragmentCounter,
288
+ data,
289
+ timestamp: header.timestamp,
290
+ receivedAt: typeof performance !== 'undefined' ? performance.now() : Date.now(),
291
+ rapFlag: header.rapFlag,
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Process MMTP packet with fragment reassembly
297
+ */
298
+ export function processMMTPWithReassembly(
299
+ mmtpPacket: Uint8Array,
300
+ reassembler: MFUReassembler,
301
+ ): ReassembledMFU | null {
302
+ // Skip FEC repair packets
303
+ if (isFecRepairPacket(mmtpPacket)) {
304
+ return null
305
+ }
306
+
307
+ const fragment = createFragment(mmtpPacket)
308
+ return reassembler.addFragment(fragment)
309
+ }
310
+
311
+ // =============================================================================
312
+ // Inline Tests (vitest)
313
+ // =============================================================================
314
+
315
+ if (import.meta.vitest) {
316
+ const { describe, test, expect } = import.meta.vitest
317
+
318
+ describe('MFUReassembler', () => {
319
+ test('should return complete MFU for FI=0', () => {
320
+ const reassembler = new MFUReassembler()
321
+ const result = reassembler.addFragment({
322
+ fragmentationIndicator: 0,
323
+ mpuSequenceNumber: 1,
324
+ fragmentCounter: 0,
325
+ data: new Uint8Array([1, 2, 3, 4]),
326
+ timestamp: 1000n,
327
+ receivedAt: 0,
328
+ rapFlag: true,
329
+ })
330
+ expect(result).not.toBeNull()
331
+ expect(result?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
332
+ expect(result?.fragmentCount).toBe(1)
333
+ expect(result?.rapFlag).toBe(true)
334
+ })
335
+
336
+ test('should buffer and reassemble 2 fragments', () => {
337
+ const reassembler = new MFUReassembler()
338
+
339
+ // First fragment (FI=1)
340
+ const result1 = reassembler.addFragment({
341
+ fragmentationIndicator: 1,
342
+ mpuSequenceNumber: 1,
343
+ fragmentCounter: 0,
344
+ data: new Uint8Array([1, 2]),
345
+ timestamp: 1000n,
346
+ receivedAt: 0,
347
+ rapFlag: true,
348
+ })
349
+ expect(result1).toBeNull()
350
+
351
+ // Last fragment (FI=3)
352
+ const result2 = reassembler.addFragment({
353
+ fragmentationIndicator: 3,
354
+ mpuSequenceNumber: 1,
355
+ fragmentCounter: 1,
356
+ data: new Uint8Array([3, 4]),
357
+ timestamp: 1000n,
358
+ receivedAt: 0,
359
+ rapFlag: false,
360
+ })
361
+ expect(result2).not.toBeNull()
362
+ expect(result2?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
363
+ expect(result2?.fragmentCount).toBe(2)
364
+ expect(result2?.rapFlag).toBe(true) // Should use first fragment's rapFlag
365
+ })
366
+
367
+ test('should handle out-of-order fragments', () => {
368
+ const reassembler = new MFUReassembler()
369
+
370
+ // Last fragment first (FI=3)
371
+ reassembler.addFragment({
372
+ fragmentationIndicator: 3,
373
+ mpuSequenceNumber: 1,
374
+ fragmentCounter: 1,
375
+ data: new Uint8Array([3, 4]),
376
+ timestamp: 1000n,
377
+ receivedAt: 0,
378
+ rapFlag: false,
379
+ })
380
+
381
+ // Then first fragment (FI=1)
382
+ const result = reassembler.addFragment({
383
+ fragmentationIndicator: 1,
384
+ mpuSequenceNumber: 1,
385
+ fragmentCounter: 0,
386
+ data: new Uint8Array([1, 2]),
387
+ timestamp: 1000n,
388
+ receivedAt: 0,
389
+ rapFlag: true,
390
+ })
391
+
392
+ expect(result).not.toBeNull()
393
+ expect(result?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
394
+ expect(result?.rapFlag).toBe(true)
395
+ })
396
+
397
+ test('should track pending count', () => {
398
+ const reassembler = new MFUReassembler()
399
+ expect(reassembler.pendingCount).toBe(0)
400
+
401
+ reassembler.addFragment({
402
+ fragmentationIndicator: 1,
403
+ mpuSequenceNumber: 1,
404
+ fragmentCounter: 0,
405
+ data: new Uint8Array([1]),
406
+ timestamp: 1000n,
407
+ receivedAt: 0,
408
+ rapFlag: false,
409
+ })
410
+ expect(reassembler.pendingCount).toBe(1)
411
+
412
+ reassembler.flush()
413
+ expect(reassembler.pendingCount).toBe(0)
414
+ })
415
+ })
416
+ }
package/src/mmtp.ts ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @mmt/container - MMTP Parser
3
+ *
4
+ * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2
5
+ *
6
+ * MMTP header format (12 bytes):
7
+ * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)
8
+ * - Byte 1: packet_type(6) | reserved(2)
9
+ * - Bytes 2-3: packet_id (big-endian)
10
+ * - Bytes 4-7: timestamp (big-endian, 32-bit)
11
+ * - Bytes 8-11: sequence_number (big-endian)
12
+ *
13
+ * FEC_type values:
14
+ * - 0: No FEC / source packet
15
+ * - 1: FEC mode 0 (source with FEC payload ID)
16
+ * - 2: FEC repair packet mode 0
17
+ * - 3: FEC repair packet mode 1
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+
22
+ export interface MMTPHeader {
23
+ version: number
24
+ packetId: number
25
+ timestamp: bigint
26
+ sequenceNumber: number
27
+ payloadLength: number
28
+ fecType: number
29
+ /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */
30
+ rapFlag: boolean
31
+ }
32
+
33
+ export interface MPUHeader {
34
+ mpuSequenceNumber: number
35
+ fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last
36
+ fragmentCounter: number
37
+ }
38
+
39
+ export interface MMTPPacket {
40
+ header: MMTPHeader
41
+ payload: Uint8Array
42
+ }
43
+
44
+ export const MMTP_HEADER_SIZE = 12
45
+ export const MPU_HEADER_SIZE = 8
46
+ export const MFU_DU_HEADER_SIZE = 14
47
+
48
+ /**
49
+ * Parse MMTP header from buffer
50
+ */
51
+ export function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {
52
+ if (buffer.length < MMTP_HEADER_SIZE) {
53
+ throw new Error(`MMTP packet too small: ${buffer.length} bytes`)
54
+ }
55
+
56
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
57
+
58
+ // Extract FEC type from byte 0, bits 3-4
59
+ const fecType = (buffer[0]! >> 3) & 0x03
60
+
61
+ // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)
62
+ const rapFlag = (buffer[0]! & 0x01) === 1
63
+
64
+ // Skip bytes 0-1 (flags and packet_type)
65
+ let offset = 2
66
+
67
+ // Packet ID (2 bytes, big-endian)
68
+ const packetId = view.getUint16(offset, false)
69
+ offset += 2
70
+
71
+ // Timestamp (4 bytes, big-endian)
72
+ const timestamp = BigInt(view.getUint32(offset, false))
73
+ offset += 4
74
+
75
+ // Sequence number (4 bytes, big-endian)
76
+ const sequenceNumber = view.getUint32(offset, false)
77
+ offset += 4
78
+
79
+ return {
80
+ version: 1,
81
+ packetId,
82
+ timestamp,
83
+ sequenceNumber,
84
+ payloadLength: buffer.length - offset,
85
+ fecType,
86
+ rapFlag,
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Unwrap MMTP packet to extract payload
92
+ */
93
+ export function unwrapMMTP(buffer: Uint8Array): MMTPPacket {
94
+ const header = parseMMTPHeader(buffer)
95
+ const payload = buffer.subarray(MMTP_HEADER_SIZE)
96
+
97
+ return { header, payload }
98
+ }
99
+
100
+ /**
101
+ * Parse MPU header from payload
102
+ * ISO/IEC 23008-1:2023 Section 9.2.3.3
103
+ */
104
+ export function parseMPUHeader(payload: Uint8Array): MPUHeader {
105
+ if (payload.length < MPU_HEADER_SIZE) {
106
+ throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)
107
+ }
108
+
109
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)
110
+
111
+ // MPU sequence number (4 bytes, big-endian)
112
+ const mpuSequenceNumber = view.getUint32(0, false)
113
+
114
+ // Fragmentation indicator (byte 5, bits 0-1)
115
+ const fragmentationIndicator = payload[5]! & 0x03
116
+
117
+ // Fragment counter (bytes 6-7, big-endian)
118
+ const fragmentCounter = view.getUint16(6, false)
119
+
120
+ return {
121
+ mpuSequenceNumber,
122
+ fragmentationIndicator,
123
+ fragmentCounter,
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Extract NAL unit payload from MMTP packet
129
+ */
130
+ export function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {
131
+ const { payload } = unwrapMMTP(mmtpPacket)
132
+
133
+ if (payload.length < MPU_HEADER_SIZE) {
134
+ throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)
135
+ }
136
+
137
+ // Parse MPU header to check fragmentation indicator
138
+ const fragmentationIndicator = payload[5]! & 0x03
139
+
140
+ // MFU DU header is present only for FI = 0 (complete) or FI = 1 (first)
141
+ const hasMfuHeader = fragmentationIndicator === 0 || fragmentationIndicator === 1
142
+ const headerSize = MPU_HEADER_SIZE + (hasMfuHeader ? MFU_DU_HEADER_SIZE : 0)
143
+
144
+ if (payload.length < headerSize) {
145
+ throw new Error(`Payload too small for headers: ${payload.length} bytes (need ${headerSize})`)
146
+ }
147
+
148
+ return payload.subarray(headerSize)
149
+ }
150
+
151
+ /**
152
+ * Extract timestamp from MMTP packet
153
+ */
154
+ export function extractTimestamp(mmtpPacket: Uint8Array): bigint {
155
+ const { header } = unwrapMMTP(mmtpPacket)
156
+ return header.timestamp
157
+ }
158
+
159
+ /**
160
+ * Check if MMTP packet is an FEC repair packet
161
+ * FEC_type 2 and 3 are repair packets
162
+ */
163
+ export function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {
164
+ if (mmtpPacket.length < 1) return false
165
+ const fecType = (mmtpPacket[0]! >> 3) & 0x03
166
+ return fecType >= 2
167
+ }
168
+
169
+ /**
170
+ * Check if MMTP packet has FEC payload ID (FEC_type 1 or higher)
171
+ */
172
+ export function hasFecPayloadId(mmtpPacket: Uint8Array): boolean {
173
+ if (mmtpPacket.length < 1) return false
174
+ const fecType = (mmtpPacket[0]! >> 3) & 0x03
175
+ return fecType >= 1
176
+ }
177
+
178
+ // =============================================================================
179
+ // Inline Tests (vitest)
180
+ // =============================================================================
181
+
182
+ if (import.meta.vitest) {
183
+ const { describe, test, expect } = import.meta.vitest
184
+
185
+ describe('MMTP Parser', () => {
186
+ // Helper to create a minimal MMTP packet
187
+ function createTestPacket(opts: {
188
+ fecType?: number
189
+ packetId?: number
190
+ timestamp?: number
191
+ sequenceNumber?: number
192
+ payload?: Uint8Array
193
+ }): Uint8Array {
194
+ const fecType = opts.fecType ?? 0
195
+ const packetId = opts.packetId ?? 1
196
+ const timestamp = opts.timestamp ?? 12345
197
+ const sequenceNumber = opts.sequenceNumber ?? 1
198
+ const payload = opts.payload ?? new Uint8Array([0x00, 0x00, 0x00, 0x01])
199
+
200
+ const header = new Uint8Array(12)
201
+ const view = new DataView(header.buffer)
202
+
203
+ // Byte 0: V=1, C=0, FEC_type, r=0, X=0, R=0
204
+ header[0] = (1 << 6) | (fecType << 3)
205
+ // Byte 1: packet_type=0 (MPU)
206
+ header[1] = 0
207
+ view.setUint16(2, packetId, false)
208
+ view.setUint32(4, timestamp, false)
209
+ view.setUint32(8, sequenceNumber, false)
210
+
211
+ const result = new Uint8Array(header.length + payload.length)
212
+ result.set(header, 0)
213
+ result.set(payload, header.length)
214
+ return result
215
+ }
216
+
217
+ describe('parseMMTPHeader', () => {
218
+ test('should parse MMTP header correctly', () => {
219
+ const packet = createTestPacket({
220
+ packetId: 42,
221
+ timestamp: 1000000,
222
+ sequenceNumber: 100,
223
+ })
224
+ const header = parseMMTPHeader(packet)
225
+ expect(header.packetId).toBe(42)
226
+ expect(header.timestamp).toBe(1000000n)
227
+ expect(header.sequenceNumber).toBe(100)
228
+ expect(header.fecType).toBe(0)
229
+ })
230
+
231
+ test('should extract FEC type', () => {
232
+ const packet = createTestPacket({ fecType: 2 })
233
+ const header = parseMMTPHeader(packet)
234
+ expect(header.fecType).toBe(2)
235
+ })
236
+
237
+ test('should throw on too small packet', () => {
238
+ expect(() => parseMMTPHeader(new Uint8Array(10))).toThrow()
239
+ })
240
+ })
241
+
242
+ describe('isFecRepairPacket', () => {
243
+ test('should return false for source packets (FEC type 0)', () => {
244
+ const packet = createTestPacket({ fecType: 0 })
245
+ expect(isFecRepairPacket(packet)).toBe(false)
246
+ })
247
+
248
+ test('should return false for FEC mode 0 source (FEC type 1)', () => {
249
+ const packet = createTestPacket({ fecType: 1 })
250
+ expect(isFecRepairPacket(packet)).toBe(false)
251
+ })
252
+
253
+ test('should return true for repair packets (FEC type 2)', () => {
254
+ const packet = createTestPacket({ fecType: 2 })
255
+ expect(isFecRepairPacket(packet)).toBe(true)
256
+ })
257
+
258
+ test('should return true for repair packets (FEC type 3)', () => {
259
+ const packet = createTestPacket({ fecType: 3 })
260
+ expect(isFecRepairPacket(packet)).toBe(true)
261
+ })
262
+ })
263
+
264
+ describe('hasFecPayloadId', () => {
265
+ test('should return false for no FEC (type 0)', () => {
266
+ const packet = createTestPacket({ fecType: 0 })
267
+ expect(hasFecPayloadId(packet)).toBe(false)
268
+ })
269
+
270
+ test('should return true for FEC type 1+', () => {
271
+ expect(hasFecPayloadId(createTestPacket({ fecType: 1 }))).toBe(true)
272
+ expect(hasFecPayloadId(createTestPacket({ fecType: 2 }))).toBe(true)
273
+ expect(hasFecPayloadId(createTestPacket({ fecType: 3 }))).toBe(true)
274
+ })
275
+ })
276
+ })
277
+ }