@norskvideo/moq-net 0.1.2 → 0.1.4

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 (310) hide show
  1. package/README.md +1 -0
  2. package/announced.d.ts +111 -21
  3. package/announced.d.ts.map +1 -1
  4. package/announced.js +213 -40
  5. package/announced.js.map +1 -1
  6. package/broadcast.d.ts +80 -25
  7. package/broadcast.d.ts.map +1 -1
  8. package/broadcast.js +270 -49
  9. package/broadcast.js.map +1 -1
  10. package/connection/accept.d.ts +6 -0
  11. package/connection/accept.d.ts.map +1 -1
  12. package/connection/accept.js +42 -13
  13. package/connection/accept.js.map +1 -1
  14. package/connection/browser.d.ts +5 -0
  15. package/connection/browser.d.ts.map +1 -0
  16. package/connection/browser.js +31 -0
  17. package/connection/browser.js.map +1 -0
  18. package/connection/connect.d.ts +43 -1
  19. package/connection/connect.d.ts.map +1 -1
  20. package/connection/connect.js +118 -123
  21. package/connection/connect.js.map +1 -1
  22. package/connection/established.d.ts +43 -16
  23. package/connection/established.d.ts.map +1 -1
  24. package/connection/established.js.map +1 -1
  25. package/connection/handshake.d.ts +12 -1
  26. package/connection/handshake.d.ts.map +1 -1
  27. package/connection/handshake.js +30 -14
  28. package/connection/handshake.js.map +1 -1
  29. package/connection/index.d.ts +8 -0
  30. package/connection/index.d.ts.map +1 -1
  31. package/connection/index.js +6 -0
  32. package/connection/index.js.map +1 -1
  33. package/connection/reload.d.ts +66 -9
  34. package/connection/reload.d.ts.map +1 -1
  35. package/connection/reload.js +192 -66
  36. package/connection/reload.js.map +1 -1
  37. package/connection/stats.d.ts +92 -0
  38. package/connection/stats.d.ts.map +1 -0
  39. package/connection/stats.js +35 -0
  40. package/connection/stats.js.map +1 -0
  41. package/connection/transport.d.ts +10 -0
  42. package/connection/transport.d.ts.map +1 -0
  43. package/connection/transport.js +12 -0
  44. package/connection/transport.js.map +1 -0
  45. package/consume.d.ts +44 -0
  46. package/consume.d.ts.map +1 -0
  47. package/consume.js +60 -0
  48. package/consume.js.map +1 -0
  49. package/datagram.d.ts +23 -0
  50. package/datagram.d.ts.map +1 -0
  51. package/datagram.js +3 -0
  52. package/datagram.js.map +1 -0
  53. package/error.d.ts +76 -0
  54. package/error.d.ts.map +1 -0
  55. package/error.js +107 -0
  56. package/error.js.map +1 -0
  57. package/group.d.ts +143 -38
  58. package/group.d.ts.map +1 -1
  59. package/group.js +270 -68
  60. package/group.js.map +1 -1
  61. package/ietf/adapter.d.ts +2 -2
  62. package/ietf/adapter.d.ts.map +1 -1
  63. package/ietf/adapter.js +20 -10
  64. package/ietf/adapter.js.map +1 -1
  65. package/ietf/aliases.d.ts +73 -5
  66. package/ietf/aliases.d.ts.map +1 -1
  67. package/ietf/aliases.js +121 -14
  68. package/ietf/aliases.js.map +1 -1
  69. package/ietf/cluster.d.ts +141 -0
  70. package/ietf/cluster.d.ts.map +1 -0
  71. package/ietf/cluster.js +204 -0
  72. package/ietf/cluster.js.map +1 -0
  73. package/ietf/connection.d.ts +39 -6
  74. package/ietf/connection.d.ts.map +1 -1
  75. package/ietf/connection.js +73 -12
  76. package/ietf/connection.js.map +1 -1
  77. package/ietf/fetch.d.ts +17 -0
  78. package/ietf/fetch.d.ts.map +1 -1
  79. package/ietf/fetch.js +21 -0
  80. package/ietf/fetch.js.map +1 -1
  81. package/ietf/filter.d.ts +87 -0
  82. package/ietf/filter.d.ts.map +1 -0
  83. package/ietf/filter.js +292 -0
  84. package/ietf/filter.js.map +1 -0
  85. package/ietf/index.d.ts +2 -0
  86. package/ietf/index.d.ts.map +1 -1
  87. package/ietf/index.js +3 -0
  88. package/ietf/index.js.map +1 -1
  89. package/ietf/namespace.d.ts +6 -0
  90. package/ietf/namespace.d.ts.map +1 -1
  91. package/ietf/namespace.js +27 -2
  92. package/ietf/namespace.js.map +1 -1
  93. package/ietf/object.d.ts +54 -6
  94. package/ietf/object.d.ts.map +1 -1
  95. package/ietf/object.js +178 -25
  96. package/ietf/object.js.map +1 -1
  97. package/ietf/parameters.d.ts +35 -10
  98. package/ietf/parameters.d.ts.map +1 -1
  99. package/ietf/parameters.js +236 -45
  100. package/ietf/parameters.js.map +1 -1
  101. package/ietf/priority.d.ts +5 -0
  102. package/ietf/priority.d.ts.map +1 -0
  103. package/ietf/priority.js +10 -0
  104. package/ietf/priority.js.map +1 -0
  105. package/ietf/properties.d.ts +13 -2
  106. package/ietf/properties.d.ts.map +1 -1
  107. package/ietf/properties.js +65 -8
  108. package/ietf/properties.js.map +1 -1
  109. package/ietf/publish.d.ts.map +1 -1
  110. package/ietf/publish.js +13 -3
  111. package/ietf/publish.js.map +1 -1
  112. package/ietf/publish_namespace.d.ts +16 -2
  113. package/ietf/publish_namespace.d.ts.map +1 -1
  114. package/ietf/publish_namespace.js +25 -6
  115. package/ietf/publish_namespace.js.map +1 -1
  116. package/ietf/publisher.d.ts +40 -7
  117. package/ietf/publisher.d.ts.map +1 -1
  118. package/ietf/publisher.js +751 -134
  119. package/ietf/publisher.js.map +1 -1
  120. package/ietf/request.js +1 -1
  121. package/ietf/request.js.map +1 -1
  122. package/ietf/solicit.d.ts +41 -0
  123. package/ietf/solicit.d.ts.map +1 -0
  124. package/ietf/solicit.js +47 -0
  125. package/ietf/solicit.js.map +1 -0
  126. package/ietf/subscribe.d.ts +41 -2
  127. package/ietf/subscribe.d.ts.map +1 -1
  128. package/ietf/subscribe.js +103 -22
  129. package/ietf/subscribe.js.map +1 -1
  130. package/ietf/subscribe_namespace.d.ts +21 -7
  131. package/ietf/subscribe_namespace.d.ts.map +1 -1
  132. package/ietf/subscribe_namespace.js +31 -14
  133. package/ietf/subscribe_namespace.js.map +1 -1
  134. package/ietf/subscriber.d.ts +20 -7
  135. package/ietf/subscriber.d.ts.map +1 -1
  136. package/ietf/subscriber.js +512 -142
  137. package/ietf/subscriber.js.map +1 -1
  138. package/ietf/version.d.ts +7 -1
  139. package/ietf/version.d.ts.map +1 -1
  140. package/ietf/version.js +7 -0
  141. package/ietf/version.js.map +1 -1
  142. package/index.d.ts +10 -5
  143. package/index.d.ts.map +1 -1
  144. package/index.js +10 -5
  145. package/index.js.map +1 -1
  146. package/internal.d.ts +29 -0
  147. package/internal.d.ts.map +1 -0
  148. package/internal.js +8 -0
  149. package/internal.js.map +1 -0
  150. package/lite/announce.d.ts +62 -15
  151. package/lite/announce.d.ts.map +1 -1
  152. package/lite/announce.js +230 -94
  153. package/lite/announce.js.map +1 -1
  154. package/lite/connection.d.ts +69 -20
  155. package/lite/connection.d.ts.map +1 -1
  156. package/lite/connection.js +128 -81
  157. package/lite/connection.js.map +1 -1
  158. package/lite/datagram.d.ts +23 -0
  159. package/lite/datagram.d.ts.map +1 -0
  160. package/lite/datagram.js +57 -0
  161. package/lite/datagram.js.map +1 -0
  162. package/lite/datagram_stream.d.ts +8 -0
  163. package/lite/datagram_stream.d.ts.map +1 -0
  164. package/lite/datagram_stream.js +44 -0
  165. package/lite/datagram_stream.js.map +1 -0
  166. package/lite/fetch.d.ts.map +1 -1
  167. package/lite/fetch.js +2 -2
  168. package/lite/fetch.js.map +1 -1
  169. package/lite/index.d.ts +3 -0
  170. package/lite/index.d.ts.map +1 -1
  171. package/lite/index.js +3 -0
  172. package/lite/index.js.map +1 -1
  173. package/lite/priority.d.ts +67 -0
  174. package/lite/priority.d.ts.map +1 -0
  175. package/lite/priority.js +114 -0
  176. package/lite/priority.js.map +1 -0
  177. package/lite/probe.d.ts +18 -2
  178. package/lite/probe.d.ts.map +1 -1
  179. package/lite/probe.js +24 -22
  180. package/lite/probe.js.map +1 -1
  181. package/lite/publisher.d.ts +14 -7
  182. package/lite/publisher.d.ts.map +1 -1
  183. package/lite/publisher.js +406 -84
  184. package/lite/publisher.js.map +1 -1
  185. package/lite/setup.d.ts +83 -6
  186. package/lite/setup.d.ts.map +1 -1
  187. package/lite/setup.js +203 -31
  188. package/lite/setup.js.map +1 -1
  189. package/lite/stream.d.ts +12 -3
  190. package/lite/stream.d.ts.map +1 -1
  191. package/lite/stream.js +7 -4
  192. package/lite/stream.js.map +1 -1
  193. package/lite/subscribe.d.ts +37 -23
  194. package/lite/subscribe.d.ts.map +1 -1
  195. package/lite/subscribe.js +70 -55
  196. package/lite/subscribe.js.map +1 -1
  197. package/lite/subscriber.d.ts +29 -23
  198. package/lite/subscriber.d.ts.map +1 -1
  199. package/lite/subscriber.js +689 -112
  200. package/lite/subscriber.js.map +1 -1
  201. package/lite/track.d.ts +25 -13
  202. package/lite/track.d.ts.map +1 -1
  203. package/lite/track.js +60 -35
  204. package/lite/track.js.map +1 -1
  205. package/lite/version.d.ts +39 -4
  206. package/lite/version.d.ts.map +1 -1
  207. package/lite/version.js +139 -17
  208. package/lite/version.js.map +1 -1
  209. package/mock.d.ts +53 -3
  210. package/mock.d.ts.map +1 -1
  211. package/mock.js +110 -21
  212. package/mock.js.map +1 -1
  213. package/{lite/origin.d.ts → origin.d.ts} +25 -1
  214. package/origin.d.ts.map +1 -0
  215. package/{lite/origin.js → origin.js} +25 -1
  216. package/origin.js.map +1 -0
  217. package/package.json +6 -4
  218. package/path.d.ts +67 -0
  219. package/path.d.ts.map +1 -1
  220. package/path.js +128 -0
  221. package/path.js.map +1 -1
  222. package/stream.d.ts +94 -4
  223. package/stream.d.ts.map +1 -1
  224. package/stream.js +128 -17
  225. package/stream.js.map +1 -1
  226. package/time.d.ts +109 -86
  227. package/time.d.ts.map +1 -1
  228. package/time.js +80 -30
  229. package/time.js.map +1 -1
  230. package/track.d.ts +298 -47
  231. package/track.d.ts.map +1 -1
  232. package/track.js +721 -109
  233. package/track.js.map +1 -1
  234. package/util/timeout.d.ts +5 -0
  235. package/util/timeout.d.ts.map +1 -0
  236. package/util/timeout.js +18 -0
  237. package/util/timeout.js.map +1 -0
  238. package/util/utf8.d.ts +3 -0
  239. package/util/utf8.d.ts.map +1 -0
  240. package/util/utf8.js +7 -0
  241. package/util/utf8.js.map +1 -0
  242. package/varint.d.ts +14 -3
  243. package/varint.d.ts.map +1 -1
  244. package/varint.js +23 -12
  245. package/varint.js.map +1 -1
  246. package/zod.d.ts +5 -5
  247. package/zod.d.ts.map +1 -1
  248. package/zod.js.map +1 -1
  249. package/bandwidth.d.ts +0 -11
  250. package/bandwidth.d.ts.map +0 -1
  251. package/bandwidth.js +0 -7
  252. package/bandwidth.js.map +0 -1
  253. package/connection/cert.test.d.ts +0 -2
  254. package/connection/cert.test.d.ts.map +0 -1
  255. package/connection/cert.test.js +0 -26
  256. package/connection/cert.test.js.map +0 -1
  257. package/connection/reload.test.d.ts +0 -2
  258. package/connection/reload.test.d.ts.map +0 -1
  259. package/connection/reload.test.js +0 -39
  260. package/connection/reload.test.js.map +0 -1
  261. package/group.test.d.ts +0 -2
  262. package/group.test.d.ts.map +0 -1
  263. package/group.test.js +0 -66
  264. package/group.test.js.map +0 -1
  265. package/ietf/aliases.test.d.ts +0 -2
  266. package/ietf/aliases.test.d.ts.map +0 -1
  267. package/ietf/aliases.test.js +0 -30
  268. package/ietf/aliases.test.js.map +0 -1
  269. package/ietf/ietf.test.d.ts +0 -2
  270. package/ietf/ietf.test.d.ts.map +0 -1
  271. package/ietf/ietf.test.js +0 -873
  272. package/ietf/ietf.test.js.map +0 -1
  273. package/integration.test.d.ts +0 -2
  274. package/integration.test.d.ts.map +0 -1
  275. package/integration.test.js +0 -90
  276. package/integration.test.js.map +0 -1
  277. package/lite/origin.d.ts.map +0 -1
  278. package/lite/origin.js.map +0 -1
  279. package/lite/subscriber.test.d.ts +0 -2
  280. package/lite/subscriber.test.d.ts.map +0 -1
  281. package/lite/subscriber.test.js +0 -30
  282. package/lite/subscriber.test.js.map +0 -1
  283. package/lite/track.test.d.ts +0 -2
  284. package/lite/track.test.d.ts.map +0 -1
  285. package/lite/track.test.js +0 -53
  286. package/lite/track.test.js.map +0 -1
  287. package/path.test.d.ts +0 -2
  288. package/path.test.d.ts.map +0 -1
  289. package/path.test.js +0 -169
  290. package/path.test.js.map +0 -1
  291. package/stream.test.d.ts +0 -2
  292. package/stream.test.d.ts.map +0 -1
  293. package/stream.test.js +0 -210
  294. package/stream.test.js.map +0 -1
  295. package/track.test.d.ts +0 -2
  296. package/track.test.d.ts.map +0 -1
  297. package/track.test.js +0 -56
  298. package/track.test.js.map +0 -1
  299. package/util/error.d.ts +0 -3
  300. package/util/error.d.ts.map +0 -1
  301. package/util/error.js +0 -9
  302. package/util/error.js.map +0 -1
  303. package/util/index.d.ts +0 -2
  304. package/util/index.d.ts.map +0 -1
  305. package/util/index.js +0 -3
  306. package/util/index.js.map +0 -1
  307. package/varint.test.d.ts +0 -2
  308. package/varint.test.d.ts.map +0 -1
  309. package/varint.test.js +0 -106
  310. package/varint.test.js.map +0 -1
package/lite/publisher.js CHANGED
@@ -1,17 +1,39 @@
1
1
  /* @ts-self-types="./publisher.d.ts" */
2
2
  import { Signal } from "@norskvideo/moq-signals";
3
+ import { error, reason } from "../error.js";
3
4
  import * as Path from "../path.js";
4
5
  import { Writer } from "../stream.js";
5
- import { error } from "../util/error.js";
6
- import { Announce, AnnounceInit } from "./announce.js";
6
+ import { Timescale } from "../time.js";
7
+ import { AnnounceInit, AnnounceOk, encodeAnnounceBroadcast } from "./announce.js";
8
+ import { Datagram as DatagramMessage } from "./datagram.js";
9
+ import * as DatagramStream from "./datagram_stream.js";
7
10
  import { Group as GroupMessage } from "./group.js";
11
+ import { Priority, sendOrder } from "./priority.js";
8
12
  import { Probe } from "./probe.js";
9
- import { encodeSubscribeResponse, SubscribeEnd, SubscribeOk, SubscribeUpdate } from "./subscribe.js";
10
- import { TrackInfo } from "./track.js";
11
- import { hasTrackStream, Version } from "./version.js";
13
+ import { encodeSubscribeResponse, SubscribeEnd, SubscribeOk, SubscribeStart, SubscribeUpdate, } from "./subscribe.js";
14
+ import { TrackInfo as TrackInfoMessage } from "./track.js";
15
+ import { hasAnnounceId, hasAnnounceOk, hasDatagrams, hasProbeRtt, Version } from "./version.js";
12
16
  const PROBE_INTERVAL = 100; // ms
13
17
  const PROBE_MAX_AGE = 10_000; // ms
14
18
  const PROBE_MAX_DELTA = 0.25;
19
+ const PROBE_RTT_DELTA = 0.25;
20
+ /** Map a signed delta to an unsigned zigzag varint value (mirrors Rust `VarInt::from_zigzag`). */
21
+ function zigzag(delta) {
22
+ return delta >= 0n ? delta << 1n : (-delta << 1n) - 1n;
23
+ }
24
+ // The TRACK stream, implicit SUBSCRIBE acceptance, and SUBSCRIBE_START/END are
25
+ // all lite-05+.
26
+ function supportsTrackStream(version) {
27
+ switch (version) {
28
+ case Version.DRAFT_01:
29
+ case Version.DRAFT_02:
30
+ case Version.DRAFT_03:
31
+ case Version.DRAFT_04:
32
+ return false;
33
+ default:
34
+ return true;
35
+ }
36
+ }
15
37
  /**
16
38
  * Handles publishing broadcasts and managing their lifecycle.
17
39
  *
@@ -26,9 +48,19 @@ export class Publisher {
26
48
  // own announcements.
27
49
  origin;
28
50
  #quic;
51
+ // The one writer for the outbound datagram stream (getWriter locks it), acquired once at
52
+ // construction when this version + transport carry datagrams, released in close(). Its
53
+ // presence is the gate: undefined means datagrams aren't served on this connection. All
54
+ // subscriptions share it, since a second getWriter on the same stream would throw.
55
+ #datagramWriter;
29
56
  // Our published broadcasts.
30
57
  // It's a signal so we can live update any announce streams.
31
58
  #broadcasts = new Signal(new Map());
59
+ // TRACK_INFO is immutable per track, so resolve it from the application once
60
+ // (via a throwaway subscribe whose info() resolves when the app calls accept)
61
+ // and reuse it for every later TRACK request of the same track. Keyed by
62
+ // `broadcast\0track`. A rejected lookup is evicted so a retry can re-probe.
63
+ #trackInfo = new Map();
32
64
  /**
33
65
  * Creates a new Publisher instance.
34
66
  * @param quic - The WebTransport session to use
@@ -41,6 +73,11 @@ export class Publisher {
41
73
  this.#quic = quic;
42
74
  this.version = version;
43
75
  this.origin = origin;
76
+ // Grab the datagram writer up front when the transport carries datagrams (no group
77
+ // fallback, so it stays undefined otherwise). One writer for all subscriptions.
78
+ if (hasDatagrams(version)) {
79
+ this.#datagramWriter = DatagramStream.datagramWriter(quic);
80
+ }
44
81
  }
45
82
  /**
46
83
  * Publishes a broadcast with any associated tracks.
@@ -52,10 +89,12 @@ export class Publisher {
52
89
  throw new Error("closed");
53
90
  broadcasts.set(path, broadcast);
54
91
  });
55
- // Remove the broadcast from the lookup when it's closed.
56
- void broadcast.closed.finally(() => {
92
+ // Remove the broadcast from the lookup when it's closed, unless the path was republished.
93
+ void broadcast.closed.then(() => {
57
94
  this.#broadcasts.mutate((broadcasts) => {
58
- broadcasts?.delete(path);
95
+ if (broadcasts?.get(path) === broadcast) {
96
+ broadcasts.delete(path);
97
+ }
59
98
  });
60
99
  });
61
100
  }
@@ -80,6 +119,10 @@ export class Publisher {
80
119
  console.debug(`announce: broadcast=${name} active=true`);
81
120
  active.add(suffix);
82
121
  }
122
+ // Lite06+: announce ids. Every active we send implicitly assigns the next
123
+ // per-stream ordinal; ended references the id instead of repeating the path.
124
+ let nextAnnounceId = 0n;
125
+ const announceIds = new Map();
83
126
  switch (this.version) {
84
127
  case Version.DRAFT_01:
85
128
  case Version.DRAFT_02: {
@@ -87,13 +130,26 @@ export class Publisher {
87
130
  await init.encode(stream.writer, this.version);
88
131
  break;
89
132
  }
90
- default:
91
- // Draft03+: send individual Announce messages for initial state.
133
+ default: {
134
+ if (!hasAnnounceOk(this.version)) {
135
+ // Draft03/04: send individual Announce messages, stamping our origin as a hop.
136
+ for (const suffix of active) {
137
+ await encodeAnnounceBroadcast(stream.writer, { status: "active", suffix, hops: [this.origin] }, this.version);
138
+ }
139
+ break;
140
+ }
141
+ // Report our origin id once via AnnounceOk and the count of initial announces
142
+ // that follow; the subscriber stamps our origin onto each hop chain, so we omit it.
143
+ const ok = new AnnounceOk(this.origin, active.size);
144
+ await ok.encode(stream.writer, this.version);
92
145
  for (const suffix of active) {
93
- const wire = new Announce({ suffix, active: true, hops: [this.origin] });
94
- await wire.encode(stream.writer, this.version);
146
+ if (hasAnnounceId(this.version)) {
147
+ announceIds.set(suffix, nextAnnounceId++);
148
+ }
149
+ await encodeAnnounceBroadcast(stream.writer, { status: "active", suffix, hops: [] }, this.version);
95
150
  }
96
151
  break;
152
+ }
97
153
  }
98
154
  // Wait for updates to the broadcasts.
99
155
  for (;;) {
@@ -116,18 +172,30 @@ export class Publisher {
116
172
  continue; // Not our prefix.
117
173
  newActive.add(suffix);
118
174
  }
119
- // Announce any new broadcasts.
175
+ // Announce any new broadcasts. Lite05+ reports our origin once via AnnounceOk, so
176
+ // the subscriber stamps it onto each hop chain; older versions stamp it here.
120
177
  for (const added of newActive.difference(active)) {
121
178
  console.debug(`announce: broadcast=${added} active=true`);
122
- const wire = new Announce({ suffix: added, active: true, hops: [this.origin] });
123
- await wire.encode(stream.writer, this.version);
179
+ const hops = hasAnnounceOk(this.version) ? [] : [this.origin];
180
+ if (hasAnnounceId(this.version)) {
181
+ announceIds.set(added, nextAnnounceId++);
182
+ }
183
+ await encodeAnnounceBroadcast(stream.writer, { status: "active", suffix: added, hops }, this.version);
124
184
  }
125
- // Announce any removed broadcasts.
126
- // Ended announces don't need hops — the peer matches on path only.
185
+ // Announce any removed broadcasts. Lite06+ retracts by announce id;
186
+ // older versions repeat the path (ended announces don't need hops).
127
187
  for (const removed of active.difference(newActive)) {
128
188
  console.debug(`announce: broadcast=${removed} active=false`);
129
- const wire = new Announce({ suffix: removed, active: false });
130
- await wire.encode(stream.writer, this.version);
189
+ if (hasAnnounceId(this.version)) {
190
+ const id = announceIds.get(removed);
191
+ announceIds.delete(removed);
192
+ if (id === undefined)
193
+ continue; // never announced
194
+ await encodeAnnounceBroadcast(stream.writer, { status: "endedId", id }, this.version);
195
+ }
196
+ else {
197
+ await encodeAnnounceBroadcast(stream.writer, { status: "ended", suffix: removed }, this.version);
198
+ }
131
199
  }
132
200
  // NOTE: This is kind of a hack that won't work with a rapid UNANNOUNCE/ANNOUNCE cycle.
133
201
  // However, our client doesn't do that anyway.
@@ -148,51 +216,122 @@ export class Publisher {
148
216
  stream.writer.reset(new Error("not found"));
149
217
  return;
150
218
  }
151
- const track = broadcast.subscribe(msg.track, msg.priority);
219
+ const track = broadcast.subscribe(msg.track, {
220
+ priority: msg.priority,
221
+ ordered: msg.ordered,
222
+ latencyMax: msg.maxLatency,
223
+ startGroup: msg.startGroup,
224
+ endGroup: msg.endGroup,
225
+ });
226
+ const startGroup = msg.startGroup ?? track.latest();
227
+ if (startGroup !== undefined)
228
+ track.startAt(startGroup);
229
+ track.endAt(msg.endGroup);
230
+ // The best-effort datagram loop, started once serving begins. It parks when the
231
+ // track finishes (recvDatagram returns undefined), so #runTrack alone ends the
232
+ // subscription; awaited during teardown so it doesn't outlive the subscription.
233
+ let datagrams = Promise.resolve();
152
234
  try {
153
- // moq-lite-05+ trims SUBSCRIBE_OK to the resolved start group; the publisher
154
- // properties move to TRACK_INFO. Earlier versions echo the priority inline.
155
- const info = hasTrackStream(this.version)
156
- ? new SubscribeOk({ startGroup: msg.startGroup ?? 0 })
157
- : new SubscribeOk({ priority: msg.priority });
158
- await encodeSubscribeResponse(stream.writer, { ok: info }, this.version);
235
+ let timescale = Timescale.MILLI;
236
+ if (supportsTrackStream(this.version)) {
237
+ // Lite-05+ accepts implicitly: no SUBSCRIBE_OK (the immutable
238
+ // properties live in TRACK_INFO), and the resolved range arrives as
239
+ // SUBSCRIBE_START / SUBSCRIBE_END emitted from #runTrack.
240
+ //
241
+ // The timescale is an immutable property, so serving MUST use exactly
242
+ // what TRACK_INFO advertised. It comes from the producer's accept(), so
243
+ // they always agree. Awaiting info() also surfaces a rejected track
244
+ // (accept never called, track closed) as an error here, which resets the
245
+ // stream.
246
+ const info = await track.info();
247
+ timescale = info.timescale;
248
+ }
249
+ else {
250
+ // Older drafts acknowledge with SUBSCRIBE_OK and stream frames verbatim.
251
+ const ok = new SubscribeOk({
252
+ priority: msg.priority,
253
+ ordered: msg.ordered,
254
+ maxLatency: msg.maxLatency,
255
+ startGroup: msg.startGroup,
256
+ endGroup: msg.endGroup,
257
+ });
258
+ await encodeSubscribeResponse(stream.writer, { ok }, this.version);
259
+ }
159
260
  console.debug(`publish ok: broadcast=${msg.broadcast} track=${track.name}`);
160
- const serving = this.#runTrack(msg.id, msg.broadcast, track, stream.writer);
261
+ const serving = this.#runTrack(msg.id, msg.broadcast, track, stream.writer, timescale);
262
+ // Serve datagrams concurrently with groups whenever the transport carries them
263
+ // (the writer exists iff so). No group fallback: otherwise they simply aren't sent.
264
+ if (this.#datagramWriter) {
265
+ datagrams = this.#runDatagrams(msg.id, track, timescale);
266
+ }
161
267
  for (;;) {
162
268
  const decode = SubscribeUpdate.decodeMaybe(stream.reader, this.version);
163
269
  const result = await Promise.any([serving, decode]);
164
270
  if (!result)
165
271
  break;
166
272
  if (result instanceof SubscribeUpdate) {
167
- console.debug(`subscribe update: broadcast=${msg.broadcast} track=${track.name} priority=${result.priority}`);
168
- track.updatePriority(result.priority);
273
+ console.debug(`subscribe update: broadcast=${msg.broadcast} track=${track.name}`);
274
+ track.update({
275
+ priority: result.priority,
276
+ ordered: result.ordered,
277
+ latencyMax: result.maxLatency,
278
+ startGroup: result.startGroup,
279
+ endGroup: result.endGroup,
280
+ });
281
+ if (result.startGroup !== undefined)
282
+ track.startAt(result.startGroup);
283
+ track.endAt(result.endGroup);
169
284
  }
170
285
  }
171
286
  console.debug(`publish done: broadcast=${msg.broadcast} track=${track.name}`);
172
287
  stream.close();
173
288
  track.close();
289
+ // track.close ends the datagram loop; wait so it doesn't leak past teardown.
290
+ await datagrams;
174
291
  }
175
292
  catch (err) {
176
293
  const e = error(err);
177
- console.warn(`publish error: broadcast=${msg.broadcast} track=${track.name} error=${e.message}`);
294
+ console.warn(`publish error: broadcast=${msg.broadcast} track=${track.name} error=${reason(e)}`);
178
295
  track.close(e);
179
296
  stream.abort(e);
297
+ await datagrams;
180
298
  }
181
299
  }
182
300
  /**
183
- * Serves a Track stream (moq-lite-05+): replies with the track's immutable TRACK_INFO.
301
+ * Handles a FETCH stream by serving one group as bare frame records (lite-05+).
184
302
  *
185
303
  * @internal
186
304
  */
187
- async runTrackInfo(msg, stream) {
305
+ async runFetch(msg, stream) {
306
+ if (!supportsTrackStream(this.version)) {
307
+ stream.writer.reset(new Error("fetch requires moq-lite-05 or newer"));
308
+ return;
309
+ }
188
310
  const broadcast = this.#broadcasts.peek()?.get(msg.broadcast);
189
311
  if (!broadcast) {
190
- throw new Error("not found");
312
+ console.debug(`fetch unknown: broadcast=${msg.broadcast}`);
313
+ stream.writer.reset(new Error("not found"));
314
+ return;
315
+ }
316
+ // The subscriber opened this stream, so its send order only ranked the request. Rank the
317
+ // response here, on the same scale as the group streams it competes with.
318
+ stream.writer.setPriority(sendOrder({ priority: msg.priority }));
319
+ let group;
320
+ try {
321
+ // The timescale is immutable, so serve exactly what TRACK_INFO advertised.
322
+ const info = await this.#resolveTrackInfo(msg.broadcast, msg.track);
323
+ group = await broadcast.track(msg.track).fetchGroup(msg.group, { priority: msg.priority });
324
+ await this.#runFetchGroup(group, stream.writer, Timescale(info.timescale));
325
+ console.debug(`fetch done: broadcast=${msg.broadcast} track=${msg.track} group=${msg.group}`);
326
+ stream.close();
327
+ group.close();
328
+ }
329
+ catch (err) {
330
+ const e = error(err);
331
+ console.warn(`fetch error: broadcast=${msg.broadcast} track=${msg.track} group=${msg.group} error=${reason(e)}`);
332
+ group?.close(e);
333
+ stream.abort(e);
191
334
  }
192
- // The publisher properties aren't tracked in the browser model yet; report
193
- // defaults with a millisecond timescale matching the wall-clock frame timestamps.
194
- const info = new TrackInfo({ timescale: 1000 });
195
- await info.encode(stream.writer, this.version);
196
335
  }
197
336
  /**
198
337
  * Runs a track and sends its data to the stream.
@@ -203,72 +342,220 @@ export class Publisher {
203
342
  *
204
343
  * @internal
205
344
  */
206
- async #runTrack(sub, broadcast, track, stream) {
345
+ async #runTrack(sub, broadcast, track, stream, timescale) {
346
+ // Lite-05+ resolves the range on the subscribe stream: SUBSCRIBE_START once the
347
+ // first group is known, SUBSCRIBE_END when the track finishes.
348
+ const emitRange = supportsTrackStream(this.version);
349
+ let startSent = false;
350
+ // The exclusive end of the delivered range. recvGroup is arrival-ordered rather than
351
+ // sequence-ordered, so this tracks the max and not the last group seen. 0 is already
352
+ // the encoding for a track that produced no groups.
353
+ let end = 0;
354
+ // One ranking for the whole subscription, shared by every group it serves.
355
+ const priority = new Priority(track);
356
+ // Cancels groups still queued for a stream slot. Only the subscriber leaving counts:
357
+ // a track that ran out of groups still has to flush the ones already queued, and we
358
+ // FIN the subscribe stream ourselves below to say so.
359
+ let finished = false;
360
+ let unsubscribe;
361
+ const unsubscribed = new Promise((resolve) => {
362
+ unsubscribe = resolve;
363
+ });
364
+ void stream.closed.then(() => {
365
+ if (!finished)
366
+ unsubscribe();
367
+ },
368
+ // A reset is always the peer.
369
+ () => unsubscribe());
207
370
  try {
208
- let lastSequence;
209
- let trackEnded = false;
210
371
  for (;;) {
372
+ // Exactly-once serving, not the sequence cursor: on a relay, a burst can be
373
+ // ingested micro-reordered by the upstream leg, and a sequence cursor would
374
+ // permanently skip the older group even though it is cached and in demand.
375
+ // Staleness is the latency window's job (cache expiry), not arrival order's.
211
376
  const next = track.recvGroup();
212
- const res = await Promise.race([
213
- next.then((group) => ({ group })),
214
- stream.closed.then(() => "closed"),
215
- ]);
216
- if (res === "closed") {
377
+ const group = await Promise.race([next, stream.closed]);
378
+ if (!group) {
217
379
  next.then((group) => group?.close()).catch(() => { });
218
380
  break;
219
381
  }
220
- if (!res.group) {
221
- trackEnded = true;
222
- break;
382
+ if (emitRange && !startSent) {
383
+ startSent = true;
384
+ // SUBSCRIBE_START promises nothing below this sequence will be delivered.
385
+ // Arrival-order serving could later surface a straggler below the first
386
+ // group, so pin the floor to what was announced.
387
+ track.startAt(group.sequence);
388
+ await encodeSubscribeResponse(stream, { start: new SubscribeStart(group.sequence) }, this.version);
223
389
  }
224
- lastSequence = res.group.sequence;
225
- void this.#runGroup(sub, res.group);
390
+ end = Math.max(end, group.sequence + 1);
391
+ void this.#runGroup({ sub, group, timescale, priority, unsubscribed });
226
392
  }
227
- // moq-lite-05+: signal end-of-track before the subscribe stream FINs.
228
- if (trackEnded && hasTrackStream(this.version)) {
229
- const end = new SubscribeEnd({ group: lastSequence ?? 0 });
230
- await encodeSubscribeResponse(stream, { end }, this.version);
393
+ if (emitRange) {
394
+ await encodeSubscribeResponse(stream, { end: new SubscribeEnd(end) }, this.version);
231
395
  }
232
396
  console.debug(`publish close: broadcast=${broadcast} track=${track.name}`);
397
+ finished = true;
233
398
  track.close();
234
399
  stream.close();
235
400
  }
236
401
  catch (err) {
237
402
  const e = error(err);
238
- console.warn(`publish error: broadcast=${broadcast} track=${track.name} error=${e.message}`);
403
+ console.warn(`publish error: broadcast=${broadcast} track=${track.name} error=${reason(e)}`);
404
+ unsubscribe();
239
405
  track.close(e);
240
406
  stream.reset(e);
241
407
  }
408
+ finally {
409
+ priority.close();
410
+ }
242
411
  }
243
412
  /**
244
- * Runs a group and sends its frames to the stream.
245
- * @param sub - The subscription ID
246
- * @param group - The group to run
413
+ * Answers a TRACK stream (0x6) with a single TRACK_INFO, then FINs.
414
+ *
415
+ * @internal
416
+ */
417
+ async runTrackInfo(msg, stream) {
418
+ try {
419
+ const info = await this.#resolveTrackInfo(msg.broadcast, msg.track);
420
+ await info.encode(stream.writer, this.version);
421
+ console.debug(`track info: broadcast=${msg.broadcast} track=${msg.track}`);
422
+ stream.close();
423
+ }
424
+ catch (err) {
425
+ console.debug(`track unknown: broadcast=${msg.broadcast} track=${msg.track}`);
426
+ stream.writer.reset(error(err));
427
+ }
428
+ }
429
+ // Resolve (and cache) a track's immutable TRACK_INFO by asking the application.
430
+ // `broadcast.track(name).info()` triggers a TrackRequest the app answers with
431
+ // accept(TrackInfo); only the immutable properties are needed (not the groups).
432
+ // Cached because they're fixed for the track's lifetime. Rejects if the broadcast
433
+ // or track is unavailable.
434
+ #resolveTrackInfo(broadcast, track) {
435
+ const key = `${broadcast}\0${track}`;
436
+ const cached = this.#trackInfo.get(key);
437
+ if (cached)
438
+ return cached;
439
+ const pending = (async () => {
440
+ const published = this.#broadcasts.peek()?.get(broadcast);
441
+ if (!published)
442
+ throw new Error("not found");
443
+ const info = await published.track(track).info();
444
+ return new TrackInfoMessage({
445
+ priority: info.priority,
446
+ ordered: info.ordered,
447
+ // Publisher Max Latency: the publisher's retention bound, advertised so
448
+ // relays re-serve with the same window.
449
+ latencyMax: info.latencyMax,
450
+ // Lite05 mandates per-frame timestamps. Advertise the track's timescale;
451
+ // `#runGroup` emits each frame converted to it.
452
+ timescale: info.timescale,
453
+ });
454
+ })();
455
+ // Don't poison the cache on failure: a later request may succeed.
456
+ pending.catch(() => this.#trackInfo.delete(key));
457
+ this.#trackInfo.set(key, pending);
458
+ return pending;
459
+ }
460
+ /**
461
+ * Forwards a track's datagrams best-effort over QUIC datagrams (lite-05 §6.4), parallel to
462
+ * its groups. Each datagram is dropped (there is no group fallback) if the encoded body
463
+ * doesn't fit the transport's datagram limit or the send fails. Returns once the track
464
+ * finishes; a failure never tears down the subscription.
247
465
  *
248
466
  * @internal
249
467
  */
250
- async #runGroup(sub, group) {
468
+ async #runDatagrams(sub, track, timescale) {
469
+ const writer = this.#datagramWriter;
470
+ if (!writer)
471
+ return; // Only reached with a writer (see the #datagramWriter gate).
472
+ const maxSize = DatagramStream.maxDatagramSize(this.#quic);
473
+ try {
474
+ for (;;) {
475
+ const datagram = await track.recvDatagram();
476
+ if (!datagram)
477
+ return; // Track finished; #runTrack tears the subscription down.
478
+ // Convert the timestamp to the track's advertised timescale, matching #runGroup.
479
+ const ts = Math.round(datagram.timestamp.as(timescale));
480
+ const body = new DatagramMessage(sub, datagram.sequence, ts, datagram.payload).encode();
481
+ // No group fallback: drop anything that doesn't fit a single datagram.
482
+ if (body.byteLength > maxSize) {
483
+ console.debug(`dropping oversize datagram: sub=${sub} size=${body.byteLength} max=${maxSize}`);
484
+ continue;
485
+ }
486
+ await writer.ready;
487
+ await writer.write(body);
488
+ }
489
+ }
490
+ catch (err) {
491
+ // Best-effort: a datagram send failure stops sending but never fails the subscription.
492
+ console.debug(`datagram send stopped: sub=${sub} error=${reason(err)}`);
493
+ }
494
+ }
495
+ // Serialize a fetched group's frames onto the FETCH stream as bare records: each a
496
+ // zigzag-delta timestamp (at the track's advertised timescale) followed by size + bytes.
497
+ async #runFetchGroup(group, stream, timescale) {
498
+ let prevTs = 0n;
499
+ for (;;) {
500
+ const frame = await Promise.race([group.readFrame(), stream.closed]);
501
+ if (!frame)
502
+ break;
503
+ const ts = BigInt(Math.round(frame.timestamp.as(timescale)));
504
+ await stream.u62(zigzag(ts - prevTs));
505
+ prevTs = ts;
506
+ await stream.u53(frame.payload.byteLength);
507
+ await stream.write(frame.payload);
508
+ }
509
+ }
510
+ /**
511
+ * Serves one group on its own unidirectional stream.
512
+ *
513
+ * @internal
514
+ */
515
+ async #runGroup(options) {
516
+ const { sub, group, timescale, priority, unsubscribed } = options;
251
517
  const msg = new GroupMessage(sub, group.sequence);
252
518
  try {
253
- const stream = await Writer.open(this.#quic);
254
- await stream.u8(0); // stream type
255
- await msg.encode(stream);
519
+ // The transport drains streams by send order, so this is what makes a high-priority
520
+ // track (and a newer group within it) win the link when there isn't room for both.
521
+ //
522
+ // One stream per group is faster than a peer at its limit can retire them, so this
523
+ // is the one path that doesn't wait for a slot: the transport would serve the opens
524
+ // in the order we asked, which is oldest-first, exactly backwards for live media.
525
+ // Failing here drops the group and lets the next one compete for the next slot.
526
+ const stream = await Writer.tryOpen(this.#quic, {
527
+ sendOrder: priority.rank(group.sequence),
528
+ cancel: unsubscribed,
529
+ waitUntilAvailable: false,
530
+ });
531
+ if (!stream) {
532
+ group.close(new Error("no stream slot"));
533
+ return;
534
+ }
535
+ // Everything past this point runs inside the cleanup scope, so a failure never leaves
536
+ // a finished group's stream being ranked.
256
537
  try {
257
- // moq-lite-05+ stamps each frame with a wall-clock millisecond timestamp, sent
258
- // as a zigzag delta from the previous frame (the first frame is a delta from 0).
259
- let prevTimestamp = 0n;
538
+ // A SUBSCRIBE_UPDATE re-ranks the subscription, so a group already on the wire
539
+ // follows it too rather than keeping a stale rank until it finishes.
540
+ priority.add(stream, group.sequence);
541
+ await stream.u53(0); // stream type
542
+ await msg.encode(stream);
543
+ // Lite05+ prefixes every frame with a zigzag-delta timestamp at the track's
544
+ // advertised timescale; older drafts omit it.
545
+ const timestamps = supportsTrackStream(this.version);
546
+ let prevTs = 0n;
260
547
  for (;;) {
261
548
  const frame = await Promise.race([group.readFrame(), stream.closed]);
262
549
  if (!frame)
263
550
  break;
264
- if (hasTrackStream(this.version)) {
265
- const now = BigInt(Date.now());
266
- const delta = now - prevTimestamp;
267
- prevTimestamp = now;
268
- await stream.u62((delta << 1n) ^ (delta >> 63n));
551
+ if (timestamps) {
552
+ // Convert each frame to the track's advertised timescale.
553
+ const ts = BigInt(Math.round(frame.timestamp.as(timescale)));
554
+ await stream.u62(zigzag(ts - prevTs));
555
+ prevTs = ts;
269
556
  }
270
- await stream.u53(frame.byteLength);
271
- await stream.write(frame);
557
+ await stream.u53(frame.payload.byteLength);
558
+ await stream.write(frame.payload);
272
559
  }
273
560
  stream.close();
274
561
  group.close();
@@ -278,6 +565,9 @@ export class Publisher {
278
565
  stream.reset(e);
279
566
  group.close(e);
280
567
  }
568
+ finally {
569
+ priority.remove(stream);
570
+ }
281
571
  }
282
572
  catch (err) {
283
573
  const e = error(err);
@@ -299,36 +589,65 @@ export class Publisher {
299
589
  stream.close();
300
590
  return;
301
591
  }
302
- let lastSentBitrate;
592
+ let lastSent;
303
593
  let lastSentTime;
594
+ // Whether a metric moved enough to be worth another report. Gaining or
595
+ // losing a value always counts; both unknown never does.
596
+ const moved = (prev, next, threshold = 0) => {
597
+ if (prev === undefined && next === undefined)
598
+ return false;
599
+ if (prev === undefined || next === undefined)
600
+ return true;
601
+ if (prev === 0)
602
+ return next !== 0;
603
+ return Math.abs(next - prev) / prev >= threshold;
604
+ };
304
605
  try {
305
606
  for (;;) {
306
607
  const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), PROBE_INTERVAL));
307
608
  const result = await Promise.race([timeout, stream.reader.closed]);
308
609
  if (result !== "timeout")
309
610
  break;
611
+ // The two fields are independent on the wire, each using 0 for
612
+ // unknown, so a transport exposing only one still has something to
613
+ // report. Anything this version can't carry is dropped here rather
614
+ // than by the encoder, so it reads as unknown to every check below.
310
615
  const stats = await quic.getStats();
311
- const bitrate = stats.estimatedSendRate;
312
- if (bitrate == null)
313
- continue;
616
+ // `smoothedRtt` is a DOMHighResTimeStamp, i.e. a double, but the wire
617
+ // carries whole milliseconds and the varint encoder throws on a
618
+ // fractional value. Round before it ever reaches `Probe`.
619
+ const rtt = stats.smoothedRtt != null ? Math.round(stats.smoothedRtt) : undefined;
620
+ const report = new Probe({
621
+ bitrate: stats.estimatedSendRate ?? undefined,
622
+ rtt: hasProbeRtt(this.version) ? rtt : undefined,
623
+ });
624
+ // Nothing left to report. Say so once if it retracts a value the peer
625
+ // is still holding, then stay quiet rather than repeating "unknown"
626
+ // every time the max age comes around.
627
+ if (report.bitrate === undefined && report.rtt === undefined) {
628
+ const retracts = lastSent !== undefined && (lastSent.bitrate !== undefined || lastSent.rtt !== undefined);
629
+ if (!retracts)
630
+ continue;
631
+ }
314
632
  let shouldSend;
315
- if (lastSentBitrate === undefined || lastSentTime === undefined) {
633
+ if (lastSent === undefined || lastSentTime === undefined) {
316
634
  shouldSend = true;
317
635
  }
318
- else if (lastSentBitrate === 0) {
319
- shouldSend = bitrate > 0;
320
- }
321
636
  else {
322
637
  const elapsed = performance.now() - lastSentTime;
638
+ // The bitrate threshold decays to zero as the last report ages: a
639
+ // stale estimate is worth refreshing for a smaller move.
323
640
  const t = Math.max(PROBE_INTERVAL, Math.min(PROBE_MAX_AGE, elapsed));
324
641
  const range = PROBE_MAX_AGE - PROBE_INTERVAL;
325
642
  const threshold = (PROBE_MAX_DELTA * (PROBE_MAX_AGE - t)) / range;
326
- const change = Math.abs(bitrate - lastSentBitrate) / lastSentBitrate;
327
- shouldSend = change >= threshold;
643
+ shouldSend =
644
+ elapsed >= PROBE_MAX_AGE ||
645
+ moved(lastSent.bitrate, report.bitrate, threshold) ||
646
+ moved(lastSent.rtt, report.rtt, PROBE_RTT_DELTA);
328
647
  }
329
648
  if (shouldSend) {
330
- await new Probe(bitrate).encode(stream.writer, this.version);
331
- lastSentBitrate = bitrate;
649
+ await report.encode(stream.writer, this.version);
650
+ lastSent = report;
332
651
  lastSentTime = performance.now();
333
652
  }
334
653
  }
@@ -345,6 +664,9 @@ export class Publisher {
345
664
  }
346
665
  return undefined;
347
666
  });
667
+ // Release the datagram writer's lock so the stream can be torn down.
668
+ this.#datagramWriter?.releaseLock();
669
+ this.#datagramWriter = undefined;
348
670
  }
349
671
  }
350
672
  //# sourceMappingURL=publisher.js.map