@moqtap/codec 0.8.0 → 0.8.1

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.
Files changed (51) hide show
  1. package/dist/{chunk-5XZKO2U5.js → chunk-2XH6PHXR.js} +62 -51
  2. package/dist/{chunk-APTV6F72.js → chunk-3YAR3I3S.js} +67 -51
  3. package/dist/{chunk-BQHLUT37.cjs → chunk-7ENEWAMZ.cjs} +2 -6
  4. package/dist/{chunk-MD2OA7SE.js → chunk-BV6MITLO.js} +15 -8
  5. package/dist/{chunk-B56AG5HU.cjs → chunk-EVCPBFSR.cjs} +6 -4
  6. package/dist/{chunk-KNLPX3YA.js → chunk-H6DTAIWM.js} +6 -4
  7. package/dist/{chunk-J3ASFPCX.cjs → chunk-IADVBKEJ.cjs} +15 -8
  8. package/dist/{chunk-I55J3KXD.js → chunk-NH4L6NIY.js} +48 -33
  9. package/dist/{chunk-EQAYMCXO.cjs → chunk-PU2KV7VP.cjs} +48 -33
  10. package/dist/{chunk-HEO2JG6C.js → chunk-TMXRYOBS.js} +100 -92
  11. package/dist/{chunk-IEJULBKB.cjs → chunk-UA5XFP4O.cjs} +100 -92
  12. package/dist/{chunk-KTVN4P6J.cjs → chunk-UZO5Z2XT.cjs} +62 -51
  13. package/dist/{chunk-MH7537FB.cjs → chunk-VWXUT5R2.cjs} +67 -51
  14. package/dist/{chunk-JJEN2VG5.js → chunk-XQGEOFGE.js} +2 -6
  15. package/dist/{codec-BnamSx7L.d.cts → codec-N3g1lxQQ.d.cts} +1 -1
  16. package/dist/{codec-CFhUGTu2.d.ts → codec-v6Vloc7H.d.ts} +1 -1
  17. package/dist/draft12.cjs +2 -2
  18. package/dist/draft12.js +1 -1
  19. package/dist/draft14-session.d.cts +1 -1
  20. package/dist/draft14-session.d.ts +1 -1
  21. package/dist/draft14.cjs +2 -2
  22. package/dist/draft14.d.cts +3 -3
  23. package/dist/draft14.d.ts +3 -3
  24. package/dist/draft14.js +1 -1
  25. package/dist/draft15.cjs +2 -2
  26. package/dist/draft15.js +1 -1
  27. package/dist/draft16.cjs +2 -2
  28. package/dist/draft16.js +1 -1
  29. package/dist/draft17.cjs +2 -2
  30. package/dist/draft17.js +1 -1
  31. package/dist/draft18.cjs +2 -2
  32. package/dist/draft18.js +1 -1
  33. package/dist/draft19.cjs +2 -2
  34. package/dist/draft19.js +1 -1
  35. package/dist/index.cjs +14 -14
  36. package/dist/index.d.cts +2 -2
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +7 -7
  39. package/dist/session.d.cts +1 -1
  40. package/dist/session.d.ts +1 -1
  41. package/dist/{types-CVeKIk7A.d.cts → types-HI_bbXka.d.cts} +0 -2
  42. package/dist/{types-CVeKIk7A.d.ts → types-HI_bbXka.d.ts} +0 -2
  43. package/package.json +2 -2
  44. package/src/drafts/draft12/data-streams.ts +13 -4
  45. package/src/drafts/draft14/codec.ts +2 -6
  46. package/src/drafts/draft14/types.ts +0 -2
  47. package/src/drafts/draft15/data-streams.ts +30 -16
  48. package/src/drafts/draft16/codec.ts +116 -93
  49. package/src/drafts/draft17/codec.ts +72 -35
  50. package/src/drafts/draft18/codec.ts +89 -53
  51. package/src/drafts/draft19/codec.ts +94 -53
@@ -75,19 +75,23 @@ export function encodeDatagram(dg: DatagramObject): Uint8Array {
75
75
  w.writeVarInt(dg.trackAlias)
76
76
  w.writeVarInt(dg.groupId)
77
77
 
78
- // Datagram type flags:
78
+ // Datagram type flags, Table 5:
79
79
  // bit 0 (0x01): extensions present
80
80
  // bit 1 (0x02): end-of-group
81
81
  // bit 2 (0x04): object_id ABSENT when set
82
+ // bit 3 (0x08): publisher priority ABSENT when set
82
83
  // bit 5 (0x20): status (replaces payload)
83
84
  const extensionsPresent = (dgType & 0x01) !== 0
84
85
  const objectIdAbsent = (dgType & 0x04) !== 0
86
+ const defaultPriority = (dgType & 0x08) !== 0
85
87
  const isStatus = (dgType & 0x20) !== 0
86
88
 
87
89
  if (!objectIdAbsent) {
88
90
  w.writeVarInt(dg.objectId)
89
91
  }
90
- w.writeUint8(dg.publisherPriority)
92
+ if (!defaultPriority) {
93
+ w.writeUint8(dg.publisherPriority)
94
+ }
91
95
 
92
96
  if (extensionsPresent) {
93
97
  const extData = dg.extensionData ?? new Uint8Array(0)
@@ -141,29 +145,34 @@ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupSt
141
145
  const r = new BufferReader(bytes)
142
146
  const streamType = Number(r.readVarInt())
143
147
 
144
- // Draft-15 valid subgroup types: 0x10-0x17, 0x30-0x37
145
- // bit 0: extensions, bit 1: end-of-group, bit 2: explicit subgroup_id
148
+ // Draft-15 Table 6 assigns 0x10-0x15, 0x18-0x1D, 0x30-0x35 and 0x38-0x3D:
149
+ // the end-of-group marker at 0x08 doubles the range the previous drafts
150
+ // used. What the table leaves out is every Type whose two Subgroup ID mode
151
+ // bits are 0b11. Draft-16 names those reserved in prose; draft-15 only
152
+ // omits them, which comes to the same refusal by a different route.
146
153
  if (
147
- !((streamType >= 0x10 && streamType <= 0x17) || (streamType >= 0x30 && streamType <= 0x37))
154
+ !((streamType >= 0x10 && streamType <= 0x1f) || (streamType >= 0x30 && streamType <= 0x3f)) ||
155
+ (streamType & 0x06) === 0x06
148
156
  ) {
149
157
  return {
150
158
  ok: false,
151
159
  error: new DecodeError(
152
160
  'CONSTRAINT_VIOLATION',
153
- `Expected subgroup stream type 0x10-0x17/0x30-0x37, got 0x${streamType.toString(16)}`,
161
+ `Expected subgroup stream type 0x10-0x1F/0x30-0x3F, got 0x${streamType.toString(16)}`,
154
162
  0,
155
163
  ),
156
164
  }
157
165
  }
158
166
 
159
167
  // Decode type flags
160
- // Draft-15 bit layout:
161
- // bit 0: extensions present
162
- // bit 1: end-of-group
163
- // bit 2: explicit subgroup_id field present
168
+ // Draft-15 Table 6, over the 0x10 base:
169
+ // bit 0 (0x01): extensions present
170
+ // bit 1 (0x02): subgroup id is the first Object ID, not transmitted
171
+ // bit 2 (0x04): explicit subgroup_id field present
172
+ // bit 3 (0x08): contains end of group
164
173
  // bit 5 (0x20): no priority (>= 0x30)
165
174
  const extensionsPresent = (streamType & 0x01) !== 0
166
- const endOfGroup = (streamType & 0x02) !== 0
175
+ const endOfGroup = (streamType & 0x08) !== 0
167
176
  const hasSubgroupField = (streamType & 0x04) !== 0
168
177
  const hasPriority = streamType < 0x30
169
178
 
@@ -260,10 +269,12 @@ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
260
269
  // bit 0 (0x01): extensions present
261
270
  // bit 1 (0x02): end-of-group
262
271
  // bit 2 (0x04): object_id ABSENT when set
272
+ // bit 3 (0x08): publisher priority ABSENT when set
263
273
  // bit 5 (0x20): status (replaces payload with status varint)
264
274
  const extensionsPresent = (dgType & 0x01) !== 0
265
275
  const objectIdAbsent = (dgType & 0x04) !== 0
266
276
  const endOfGroup = (dgType & 0x02) !== 0
277
+ const defaultPriority = (dgType & 0x08) !== 0
267
278
  const isStatus = (dgType & 0x20) !== 0
268
279
 
269
280
  const trackAlias = r.readVarInt()
@@ -272,7 +283,10 @@ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
272
283
  if (!objectIdAbsent) {
273
284
  objectId = r.readVarInt()
274
285
  }
275
- const publisherPriority = r.readUint8()
286
+ let publisherPriority = 128 // default
287
+ if (!defaultPriority) {
288
+ publisherPriority = r.readUint8()
289
+ }
276
290
 
277
291
  let extensionData: Uint8Array | undefined
278
292
  if (extensionsPresent) {
@@ -531,8 +545,8 @@ export function createSubgroupStreamDecoder(): TransformStream<
531
545
 
532
546
  if (
533
547
  !(
534
- (streamType >= 0x10 && streamType <= 0x17) ||
535
- (streamType >= 0x30 && streamType <= 0x37)
548
+ (streamType >= 0x10 && streamType <= 0x1f) ||
549
+ (streamType >= 0x30 && streamType <= 0x3f)
536
550
  )
537
551
  ) {
538
552
  controller.error(
@@ -760,7 +774,7 @@ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStrea
760
774
  if (offset >= buffer.length) return
761
775
  const firstByte = buffer[offset]!
762
776
 
763
- if ((firstByte >= 0x10 && firstByte <= 0x17) || (firstByte >= 0x30 && firstByte <= 0x37)) {
777
+ if ((firstByte >= 0x10 && firstByte <= 0x1f) || (firstByte >= 0x30 && firstByte <= 0x3f)) {
764
778
  // Subgroup — delegate to subgroup decoder
765
779
  // We need to feed the full buffer including the type byte
766
780
  const decoder = createSubgroupStreamDecoder()
@@ -796,7 +810,7 @@ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStrea
796
810
  const firstByte = view[0]!
797
811
  let result: DecodeResult<Draft15DataStream>
798
812
 
799
- if ((firstByte >= 0x10 && firstByte <= 0x17) || (firstByte >= 0x30 && firstByte <= 0x37)) {
813
+ if ((firstByte >= 0x10 && firstByte <= 0x1f) || (firstByte >= 0x30 && firstByte <= 0x3f)) {
800
814
  result = decodeSubgroupStream(view)
801
815
  } else if (firstByte === 0x05) {
802
816
  result = decodeFetchStream(view)
@@ -311,116 +311,146 @@ function decodeTrackExtensions(reader: BufferReader): Draft16TrackExtensions {
311
311
  }
312
312
 
313
313
  function encodeParams(params: Draft16Params, writer: BufferWriter): void {
314
- let count = params.unknown ? params.unknown.length : 0
315
- if (params.delivery_timeout !== undefined) count++
316
- if (params.authorization_token !== undefined) count++
317
- if (params.max_cache_duration !== undefined) count++
318
- if (params.expires !== undefined) count++
319
- if (params.largest_object !== undefined) count++
320
- if (params.publisher_priority !== undefined) count++
321
- if (params.forward !== undefined) count++
322
- if (params.subscriber_priority !== undefined) count++
323
- if (params.subscription_filter !== undefined) count++
324
- if (params.group_order !== undefined) count++
325
- if (params.dynamic_groups !== undefined) count++
326
- if (params.new_group_request !== undefined) count++
327
- writer.writeVarInt(count)
314
+ // Draft-16 Section 1.4.2: "Key-Value-Pairs encode a Type value as a delta
315
+ // from the previous Type value, or from 0 if there is no previous Type
316
+ // value." That only defines a wire format if the order is fixed, which the
317
+ // same section fixes: "Parameters MUST be serialized in ascending order by
318
+ // Type." So the entries are collected, sorted, and written as differences.
319
+ const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
328
320
 
329
321
  if (params.delivery_timeout !== undefined) {
330
- writer.writeVarInt(PARAM_DELIVERY_TIMEOUT)
331
- writer.writeVarInt(params.delivery_timeout)
322
+ entries.push({
323
+ type: PARAM_DELIVERY_TIMEOUT,
324
+ encode: (w) => w.writeVarInt(params.delivery_timeout!),
325
+ })
332
326
  }
333
327
  if (params.authorization_token !== undefined) {
334
- writer.writeVarInt(PARAM_AUTHORIZATION_TOKEN)
335
- const tmpW = new BufferWriter(64)
336
- encodeAuthorizationToken(params.authorization_token, tmpW)
337
- const raw = tmpW.finish()
338
- writer.writeVarInt(raw.byteLength)
339
- writer.writeBytes(raw)
328
+ entries.push({
329
+ type: PARAM_AUTHORIZATION_TOKEN,
330
+ encode: (w) => {
331
+ const tmpW = new BufferWriter(64)
332
+ encodeAuthorizationToken(params.authorization_token!, tmpW)
333
+ const raw = tmpW.finish()
334
+ w.writeVarInt(raw.byteLength)
335
+ w.writeBytes(raw)
336
+ },
337
+ })
340
338
  }
341
339
  if (params.max_cache_duration !== undefined) {
342
- writer.writeVarInt(PARAM_MAX_CACHE_DURATION)
343
- writer.writeVarInt(params.max_cache_duration)
340
+ entries.push({
341
+ type: PARAM_MAX_CACHE_DURATION,
342
+ encode: (w) => w.writeVarInt(params.max_cache_duration!),
343
+ })
344
344
  }
345
345
  if (params.expires !== undefined) {
346
- writer.writeVarInt(PARAM_EXPIRES)
347
- writer.writeVarInt(params.expires)
346
+ entries.push({ type: PARAM_EXPIRES, encode: (w) => w.writeVarInt(params.expires!) })
348
347
  }
349
348
  if (params.largest_object !== undefined) {
350
- writer.writeVarInt(PARAM_LARGEST_OBJECT)
351
- const tmpW = new BufferWriter(16)
352
- tmpW.writeVarInt(params.largest_object.group)
353
- tmpW.writeVarInt(params.largest_object.object)
354
- const raw = tmpW.finish()
355
- writer.writeVarInt(raw.byteLength)
356
- writer.writeBytes(raw)
349
+ entries.push({
350
+ type: PARAM_LARGEST_OBJECT,
351
+ // Draft-16 has no per-parameter value encoding: a Key-Value-Pair whose
352
+ // Type is odd carries a Length, and 0x09 is odd. Drafts 17 and later
353
+ // replaced that with "the encoding is specified by each parameter
354
+ // definition", which is where the same field loses its length.
355
+ encode: (w) => {
356
+ const tmpW = new BufferWriter(16)
357
+ tmpW.writeVarInt(params.largest_object!.group)
358
+ tmpW.writeVarInt(params.largest_object!.object)
359
+ const raw = tmpW.finish()
360
+ w.writeVarInt(raw.byteLength)
361
+ w.writeBytes(raw)
362
+ },
363
+ })
357
364
  }
358
365
  if (params.publisher_priority !== undefined) {
359
- writer.writeVarInt(PARAM_PUBLISHER_PRIORITY)
360
- writer.writeVarInt(params.publisher_priority)
366
+ entries.push({
367
+ type: PARAM_PUBLISHER_PRIORITY,
368
+ encode: (w) => w.writeVarInt(params.publisher_priority!),
369
+ })
361
370
  }
362
371
  if (params.forward !== undefined) {
363
- writer.writeVarInt(PARAM_FORWARD)
364
- writer.writeVarInt(params.forward)
372
+ entries.push({ type: PARAM_FORWARD, encode: (w) => w.writeVarInt(params.forward!) })
365
373
  }
366
374
  if (params.subscriber_priority !== undefined) {
367
- writer.writeVarInt(PARAM_SUBSCRIBER_PRIORITY)
368
- writer.writeVarInt(params.subscriber_priority)
375
+ entries.push({
376
+ type: PARAM_SUBSCRIBER_PRIORITY,
377
+ encode: (w) => w.writeVarInt(params.subscriber_priority!),
378
+ })
369
379
  }
370
380
  if (params.subscription_filter !== undefined) {
371
- writer.writeVarInt(PARAM_SUBSCRIPTION_FILTER)
372
- const tmpW = new BufferWriter(32)
373
- const f = params.subscription_filter
374
- tmpW.writeVarInt(f.filter_type)
375
- if (f.filter_type === 3n || f.filter_type === 4n) {
376
- tmpW.writeVarInt(f.start_group!)
377
- tmpW.writeVarInt(f.start_object!)
378
- }
379
- if (f.filter_type === 4n) {
380
- tmpW.writeVarInt(f.end_group!)
381
- }
382
- const raw = tmpW.finish()
383
- writer.writeVarInt(raw.byteLength)
384
- writer.writeBytes(raw)
381
+ entries.push({
382
+ type: PARAM_SUBSCRIPTION_FILTER,
383
+ encode: (w) => {
384
+ const tmpW = new BufferWriter(32)
385
+ const f = params.subscription_filter!
386
+ tmpW.writeVarInt(f.filter_type)
387
+ if (f.filter_type === 3n || f.filter_type === 4n) {
388
+ tmpW.writeVarInt(f.start_group!)
389
+ tmpW.writeVarInt(f.start_object!)
390
+ }
391
+ if (f.filter_type === 4n) {
392
+ tmpW.writeVarInt(f.end_group!)
393
+ }
394
+ const raw = tmpW.finish()
395
+ w.writeVarInt(raw.byteLength)
396
+ w.writeBytes(raw)
397
+ },
398
+ })
385
399
  }
386
400
  if (params.group_order !== undefined) {
387
- writer.writeVarInt(PARAM_GROUP_ORDER)
388
- writer.writeVarInt(params.group_order)
401
+ entries.push({ type: PARAM_GROUP_ORDER, encode: (w) => w.writeVarInt(params.group_order!) })
389
402
  }
390
403
  if (params.dynamic_groups !== undefined) {
391
- writer.writeVarInt(PARAM_DYNAMIC_GROUPS)
392
- writer.writeVarInt(params.dynamic_groups)
404
+ entries.push({
405
+ type: PARAM_DYNAMIC_GROUPS,
406
+ encode: (w) => w.writeVarInt(params.dynamic_groups!),
407
+ })
393
408
  }
394
409
  if (params.new_group_request !== undefined) {
395
- writer.writeVarInt(PARAM_NEW_GROUP_REQUEST)
396
- writer.writeVarInt(params.new_group_request)
410
+ entries.push({
411
+ type: PARAM_NEW_GROUP_REQUEST,
412
+ encode: (w) => w.writeVarInt(params.new_group_request!),
413
+ })
397
414
  }
398
415
 
399
416
  if (params.unknown) {
400
417
  for (const u of params.unknown) {
401
418
  const id = BigInt(u.id)
402
- writer.writeVarInt(id)
403
- if (id % 2n === 0n) {
404
- const raw = hexToBytes(u.raw_hex)
405
- const tmpReader = new BufferReader(raw)
406
- const value = tmpReader.readVarInt()
407
- writer.writeVarInt(value)
408
- } else {
409
- const raw = hexToBytes(u.raw_hex)
410
- writer.writeVarInt(raw.byteLength)
411
- writer.writeBytes(raw)
412
- }
419
+ entries.push({
420
+ type: id,
421
+ encode: (w) => {
422
+ const raw = hexToBytes(u.raw_hex)
423
+ if (id % 2n === 0n) {
424
+ w.writeVarInt(new BufferReader(raw).readVarInt())
425
+ } else {
426
+ w.writeVarInt(raw.byteLength)
427
+ w.writeBytes(raw)
428
+ }
429
+ },
430
+ })
413
431
  }
414
432
  }
433
+
434
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
435
+
436
+ writer.writeVarInt(entries.length)
437
+ let prevType = 0n
438
+ for (const entry of entries) {
439
+ writer.writeVarInt(entry.type - prevType)
440
+ entry.encode(writer)
441
+ prevType = entry.type
442
+ }
415
443
  }
416
444
 
417
445
  function decodeParams(reader: BufferReader): Draft16Params {
418
446
  const count = Number(reader.readVarInt())
419
447
  const result: Draft16Params = {}
420
- const unknown: UnknownParam[] = []
448
+ let prevType = 0n
421
449
 
422
450
  for (let i = 0; i < count; i++) {
423
- const paramType = reader.readVarInt()
451
+ const delta = reader.readVarInt()
452
+ const paramType = prevType + delta
453
+ prevType = paramType
424
454
 
425
455
  if (paramType === PARAM_DELIVERY_TIMEOUT) {
426
456
  result.delivery_timeout = reader.readVarInt()
@@ -472,31 +502,24 @@ function decodeParams(reader: BufferReader): Draft16Params {
472
502
  const consumed = reader.offset - startOff
473
503
  if (consumed < length) reader.readBytes(length - consumed)
474
504
  result.subscription_filter = filter
475
- } else if (paramType % 2n === 0n) {
476
- const value = reader.readVarInt()
477
- const tmpWriter = new BufferWriter(16)
478
- tmpWriter.writeVarInt(value)
479
- const raw = tmpWriter.finish()
480
- unknown.push({
481
- id: `0x${paramType.toString(16)}`,
482
- length: raw.byteLength,
483
- raw_hex: bytesToHex(raw),
484
- })
485
505
  } else {
486
- const length = Number(reader.readVarInt())
487
- const bytes = reader.readBytes(length)
488
- unknown.push({
489
- id: `0x${paramType.toString(16)}`,
490
- length,
491
- raw_hex: bytesToHex(bytes),
492
- })
506
+ // Drafts 16 and later: "All Message Parameters MUST be defined in the
507
+ // negotiated version of MOQT or negotiated via Setup Parameters. An
508
+ // endpoint that receives an unknown Message Parameter MUST close the
509
+ // session with PROTOCOL_VIOLATION." (Section 9.2)
510
+ //
511
+ // There is no skipping an unknown one and carrying on: the value's
512
+ // encoding comes from its definition, so a receiver that does not know
513
+ // the Type does not know how many bytes it spans either. Drafts 11
514
+ // through 15 say the opposite and keep collecting them.
515
+ throw new DecodeError(
516
+ 'INVALID_PARAMETER',
517
+ `Unknown Message Parameter type 0x${paramType.toString(16)}`,
518
+ reader.offset,
519
+ )
493
520
  }
494
521
  }
495
522
 
496
- if (unknown.length > 0) {
497
- result.unknown = unknown
498
- }
499
-
500
523
  return result
501
524
  }
502
525
 
@@ -236,6 +236,43 @@ const PARAM_SUBSCRIPTION_FILTER = 0x21n
236
236
  const PARAM_GROUP_ORDER = 0x22n
237
237
  const PARAM_NEW_GROUP_REQUEST = 0x32n
238
238
 
239
+ /**
240
+ * Which messages each Message Parameter's own definition names.
241
+ *
242
+ * Section 9.3.1: "Each Message Parameter definition indicates the message types in
243
+ * which it can appear. If it appears in some other type of message, the
244
+ * receiving endpoint MUST close the connection with a PROTOCOL_VIOLATION."
245
+ * Drafts up to and including 16 end that sentence "it MUST be ignored", so this
246
+ * table has no counterpart there.
247
+ *
248
+ * A parameter type absent from this table is one no definition restricts. A
249
+ * type this draft does not define at all never reaches here: it is refused as
250
+ * unknown while its scope is still undecidable.
251
+ */
252
+ const PARAMETER_SCOPE = new Map<bigint, Set<string>>([
253
+ [0x02n, new Set(['publish_ok', 'subscribe', 'request_update'])],
254
+ [
255
+ 0x03n,
256
+ new Set([
257
+ 'publish',
258
+ 'subscribe',
259
+ 'request_update',
260
+ 'subscribe_namespace',
261
+ 'publish_namespace',
262
+ 'track_status',
263
+ 'fetch',
264
+ ]),
265
+ ],
266
+ [0x04n, new Set(['subscribe'])],
267
+ [0x08n, new Set(['subscribe_ok', 'publish', 'publish_ok', 'request_ok'])],
268
+ [0x09n, new Set(['subscribe_ok', 'publish', 'request_ok'])],
269
+ [0x10n, new Set(['subscribe', 'request_update', 'publish', 'publish_ok', 'subscribe_namespace'])],
270
+ [0x20n, new Set(['subscribe', 'fetch', 'request_update', 'publish_ok'])],
271
+ [0x21n, new Set(['subscribe', 'publish_ok', 'request_update'])],
272
+ [0x22n, new Set(['subscribe', 'publish_ok', 'fetch'])],
273
+ [0x32n, new Set(['publish_ok', 'subscribe', 'request_update'])],
274
+ ])
275
+
239
276
  function encodeParams(params: Draft17Params, writer: BufferWriter): void {
240
277
  // Collect and sort params by type
241
278
  const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
@@ -350,10 +387,9 @@ function encodeParams(params: Draft17Params, writer: BufferWriter): void {
350
387
  }
351
388
  }
352
389
 
353
- function decodeParams(reader: BufferReader): Draft17Params {
390
+ function decodeParams(reader: BufferReader, messageType: string): Draft17Params {
354
391
  const count = Number(reader.readVarInt())
355
392
  const result: Draft17Params = {}
356
- const unknown: UnknownParam[] = []
357
393
  let prevType = 0n
358
394
 
359
395
  for (let i = 0; i < count; i++) {
@@ -361,6 +397,15 @@ function decodeParams(reader: BufferReader): Draft17Params {
361
397
  const paramType = prevType + delta
362
398
  prevType = paramType
363
399
 
400
+ const scope = PARAMETER_SCOPE.get(paramType)
401
+ if (scope !== undefined && !scope.has(messageType)) {
402
+ throw new DecodeError(
403
+ 'CONSTRAINT_VIOLATION',
404
+ `Message Parameter 0x${paramType.toString(16)} may not appear in ${messageType}`,
405
+ reader.offset,
406
+ )
407
+ }
408
+
364
409
  if (paramType === PARAM_DELIVERY_TIMEOUT) {
365
410
  result.delivery_timeout = reader.readVarInt()
366
411
  } else if (paramType === PARAM_AUTHORIZATION_TOKEN) {
@@ -408,31 +453,23 @@ function decodeParams(reader: BufferReader): Draft17Params {
408
453
  } else if (paramType === PARAM_NEW_GROUP_REQUEST) {
409
454
  result.new_group_request = reader.readVarInt()
410
455
  } else {
411
- // Unknown parameter we don't know the encoding, protocol violation per spec
412
- // But for robustness, attempt even/odd heuristic
413
- if (paramType % 2n === 0n) {
414
- const value = reader.readVarInt()
415
- const tmpWriter = new BufferWriter(16)
416
- tmpWriter.writeVarInt(value)
417
- const raw = tmpWriter.finish()
418
- unknown.push({
419
- id: `0x${paramType.toString(16)}`,
420
- length: raw.byteLength,
421
- raw_hex: bytesToHex(raw),
422
- })
423
- } else {
424
- const length = Number(reader.readVarInt())
425
- const bytes = reader.readBytes(length)
426
- unknown.push({
427
- id: `0x${paramType.toString(16)}`,
428
- length,
429
- raw_hex: bytesToHex(bytes),
430
- })
431
- }
456
+ // Drafts 16 and later: "All Message Parameters MUST be defined in the
457
+ // negotiated version of MOQT or negotiated via Setup Options. An
458
+ // endpoint that receives an unknown Message Parameter MUST close the
459
+ // session with PROTOCOL_VIOLATION." (Section 9.3)
460
+ //
461
+ // There is no skipping an unknown one and carrying on: the value's
462
+ // encoding comes from its definition, so a receiver that does not know
463
+ // the Type does not know how many bytes it spans either. Drafts 11
464
+ // through 15 say the opposite and keep collecting them.
465
+ throw new DecodeError(
466
+ 'INVALID_PARAMETER',
467
+ `Unknown Message Parameter type 0x${paramType.toString(16)}`,
468
+ reader.offset,
469
+ )
432
470
  }
433
471
  }
434
472
 
435
- if (unknown.length > 0) result.unknown = unknown
436
473
  return result
437
474
  }
438
475
 
@@ -747,7 +784,7 @@ function decodeSubscribePayload(r: BufferReader): Draft17Message {
747
784
  const required_request_id_delta = r.readVarInt()
748
785
  const track_namespace = r.readTuple()
749
786
  const track_name = r.readString()
750
- const parameters = decodeParams(r)
787
+ const parameters = decodeParams(r, 'subscribe')
751
788
  return {
752
789
  type: 'subscribe',
753
790
  request_id,
@@ -760,7 +797,7 @@ function decodeSubscribePayload(r: BufferReader): Draft17Message {
760
797
 
761
798
  function decodeSubscribeOkPayload(r: BufferReader, payloadEnd: number): Draft17Message {
762
799
  const track_alias = r.readVarInt()
763
- const parameters = decodeParams(r)
800
+ const parameters = decodeParams(r, 'subscribe_ok')
764
801
  const track_properties = decodeTrackProperties(r, payloadEnd)
765
802
  return { type: 'subscribe_ok', track_alias, parameters, track_properties }
766
803
  }
@@ -768,7 +805,7 @@ function decodeSubscribeOkPayload(r: BufferReader, payloadEnd: number): Draft17M
768
805
  function decodeRequestUpdatePayload(r: BufferReader): Draft17Message {
769
806
  const request_id = r.readVarInt()
770
807
  const required_request_id_delta = r.readVarInt()
771
- const parameters = decodeParams(r)
808
+ const parameters = decodeParams(r, 'request_update')
772
809
  return {
773
810
  type: 'request_update',
774
811
  request_id,
@@ -783,7 +820,7 @@ function decodePublishPayload(r: BufferReader, payloadEnd: number): Draft17Messa
783
820
  const track_namespace = r.readTuple()
784
821
  const track_name = r.readString()
785
822
  const track_alias = r.readVarInt()
786
- const parameters = decodeParams(r)
823
+ const parameters = decodeParams(r, 'publish')
787
824
  const track_properties = decodeTrackProperties(r, payloadEnd)
788
825
  return {
789
826
  type: 'publish',
@@ -798,7 +835,7 @@ function decodePublishPayload(r: BufferReader, payloadEnd: number): Draft17Messa
798
835
  }
799
836
 
800
837
  function decodePublishOkPayload(r: BufferReader): Draft17Message {
801
- const parameters = decodeParams(r)
838
+ const parameters = decodeParams(r, 'publish_ok')
802
839
  return { type: 'publish_ok', parameters }
803
840
  }
804
841
 
@@ -813,7 +850,7 @@ function decodePublishNamespacePayload(r: BufferReader): Draft17Message {
813
850
  const request_id = r.readVarInt()
814
851
  const required_request_id_delta = r.readVarInt()
815
852
  const track_namespace = r.readTuple()
816
- const parameters = decodeParams(r)
853
+ const parameters = decodeParams(r, 'publish_namespace')
817
854
  return {
818
855
  type: 'publish_namespace',
819
856
  request_id,
@@ -838,7 +875,7 @@ function decodeSubscribeNamespacePayload(r: BufferReader): Draft17Message {
838
875
  const required_request_id_delta = r.readVarInt()
839
876
  const namespace_prefix = r.readTuple()
840
877
  const subscribe_options = r.readVarInt()
841
- const parameters = decodeParams(r)
878
+ const parameters = decodeParams(r, 'subscribe_namespace')
842
879
  return {
843
880
  type: 'subscribe_namespace',
844
881
  request_id,
@@ -889,7 +926,7 @@ function decodeFetchPayload(r: BufferReader): Draft17Message {
889
926
  joining = { joining_request_id, joining_start }
890
927
  }
891
928
 
892
- const parameters = decodeParams(r)
929
+ const parameters = decodeParams(r, 'fetch')
893
930
 
894
931
  return {
895
932
  type: 'fetch',
@@ -906,7 +943,7 @@ function decodeFetchOkPayload(r: BufferReader, payloadEnd: number): Draft17Messa
906
943
  const end_of_track = r.readUint8()
907
944
  const end_group = r.readVarInt()
908
945
  const end_object = r.readVarInt()
909
- const parameters = decodeParams(r)
946
+ const parameters = decodeParams(r, 'fetch_ok')
910
947
  const track_properties = decodeTrackProperties(r, payloadEnd)
911
948
  return {
912
949
  type: 'fetch_ok',
@@ -923,7 +960,7 @@ function decodeTrackStatusPayload(r: BufferReader): Draft17Message {
923
960
  const required_request_id_delta = r.readVarInt()
924
961
  const track_namespace = r.readTuple()
925
962
  const track_name = r.readString()
926
- const parameters = decodeParams(r)
963
+ const parameters = decodeParams(r, 'track_status')
927
964
  return {
928
965
  type: 'track_status',
929
966
  request_id,
@@ -935,7 +972,7 @@ function decodeTrackStatusPayload(r: BufferReader): Draft17Message {
935
972
  }
936
973
 
937
974
  function decodeRequestOkPayload(r: BufferReader): Draft17Message {
938
- const parameters = decodeParams(r)
975
+ const parameters = decodeParams(r, 'request_ok')
939
976
  return { type: 'request_ok', parameters }
940
977
  }
941
978