@norskvideo/moq-net 0.1.8 → 0.2.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.
- package/README.md +2 -2
- package/announce.d.ts +7 -0
- package/announce.d.ts.map +1 -0
- package/announce.js +8 -0
- package/announce.js.map +1 -0
- package/announced.d.ts +49 -91
- package/announced.d.ts.map +1 -1
- package/announced.js +21 -156
- package/announced.js.map +1 -1
- package/bandwidth.d.ts +163 -0
- package/bandwidth.d.ts.map +1 -0
- package/bandwidth.js +304 -0
- package/bandwidth.js.map +1 -0
- package/bandwidth_api.d.ts +7 -0
- package/bandwidth_api.d.ts.map +1 -0
- package/bandwidth_api.js +8 -0
- package/bandwidth_api.js.map +1 -0
- package/broadcast.d.ts +44 -35
- package/broadcast.d.ts.map +1 -1
- package/broadcast.js +104 -60
- package/broadcast.js.map +1 -1
- package/connection/accept.d.ts +16 -1
- package/connection/accept.d.ts.map +1 -1
- package/connection/accept.js +52 -28
- package/connection/accept.js.map +1 -1
- package/connection/browser.d.ts.map +1 -1
- package/connection/browser.js +9 -7
- package/connection/browser.js.map +1 -1
- package/connection/connect.d.ts +30 -6
- package/connection/connect.d.ts.map +1 -1
- package/connection/connect.js +110 -54
- package/connection/connect.js.map +1 -1
- package/connection/established.d.ts +17 -21
- package/connection/established.d.ts.map +1 -1
- package/connection/established.js.map +1 -1
- package/connection/forward.d.ts +2 -0
- package/connection/forward.d.ts.map +1 -0
- package/connection/forward.js +173 -0
- package/connection/forward.js.map +1 -0
- package/connection/handshake.d.ts +1 -0
- package/connection/handshake.d.ts.map +1 -1
- package/connection/handshake.js +5 -2
- package/connection/handshake.js.map +1 -1
- package/connection/index.d.ts +5 -5
- package/connection/index.d.ts.map +1 -1
- package/connection/index.js +4 -5
- package/connection/index.js.map +1 -1
- package/connection/pool.d.ts +186 -0
- package/connection/pool.d.ts.map +1 -0
- package/connection/pool.js +361 -0
- package/connection/pool.js.map +1 -0
- package/connection/reload.d.ts +14 -93
- package/connection/reload.d.ts.map +1 -1
- package/connection/reload.js +217 -81
- package/connection/reload.js.map +1 -1
- package/connection/stats.d.ts +3 -26
- package/connection/stats.d.ts.map +1 -1
- package/connection/stats.js.map +1 -1
- package/connection/transport.d.ts +0 -7
- package/connection/transport.d.ts.map +1 -1
- package/consume.d.ts +1 -43
- package/consume.d.ts.map +1 -1
- package/consume.js +1 -1
- package/consume.js.map +1 -1
- package/error.d.ts +180 -29
- package/error.d.ts.map +1 -1
- package/error.js +331 -16
- package/error.js.map +1 -1
- package/errors.d.ts +7 -0
- package/errors.d.ts.map +1 -0
- package/errors.js +8 -0
- package/errors.js.map +1 -0
- package/group.d.ts +9 -43
- package/group.d.ts.map +1 -1
- package/group.js +284 -69
- package/group.js.map +1 -1
- package/hop.d.ts +115 -0
- package/hop.d.ts.map +1 -0
- package/hop.js +119 -0
- package/hop.js.map +1 -0
- package/ietf/adapter.d.ts +5 -1
- package/ietf/adapter.d.ts.map +1 -1
- package/ietf/adapter.js +105 -60
- package/ietf/adapter.js.map +1 -1
- package/ietf/aliases.d.ts +1 -78
- package/ietf/aliases.d.ts.map +1 -1
- package/ietf/cluster.d.ts +9 -123
- package/ietf/cluster.d.ts.map +1 -1
- package/ietf/cluster.js +84 -44
- package/ietf/cluster.js.map +1 -1
- package/ietf/connection.d.ts +6 -72
- package/ietf/connection.d.ts.map +1 -1
- package/ietf/connection.js +62 -56
- package/ietf/connection.js.map +1 -1
- package/ietf/error.d.ts +11 -0
- package/ietf/error.d.ts.map +1 -0
- package/ietf/error.js +193 -0
- package/ietf/error.js.map +1 -0
- package/ietf/fetch.d.ts +7 -20
- package/ietf/fetch.d.ts.map +1 -1
- package/ietf/fetch.js +52 -22
- package/ietf/fetch.js.map +1 -1
- package/ietf/filter.d.ts +2 -0
- package/ietf/filter.d.ts.map +1 -1
- package/ietf/filter.js +10 -1
- package/ietf/filter.js.map +1 -1
- package/ietf/goaway.d.ts.map +1 -1
- package/ietf/goaway.js +22 -5
- package/ietf/goaway.js.map +1 -1
- package/ietf/hidden.d.ts +2 -0
- package/ietf/hidden.d.ts.map +1 -0
- package/ietf/hidden.js +30 -0
- package/ietf/hidden.js.map +1 -0
- package/ietf/index.d.ts +2 -0
- package/ietf/index.d.ts.map +1 -1
- package/ietf/index.js +2 -0
- package/ietf/index.js.map +1 -1
- package/ietf/object.d.ts +15 -8
- package/ietf/object.d.ts.map +1 -1
- package/ietf/object.js +51 -36
- package/ietf/object.js.map +1 -1
- package/ietf/parameters.d.ts +14 -2
- package/ietf/parameters.d.ts.map +1 -1
- package/ietf/parameters.js +97 -29
- package/ietf/parameters.js.map +1 -1
- package/ietf/properties.d.ts +1 -0
- package/ietf/properties.d.ts.map +1 -1
- package/ietf/properties.js +14 -0
- package/ietf/properties.js.map +1 -1
- package/ietf/publish.d.ts +19 -2
- package/ietf/publish.d.ts.map +1 -1
- package/ietf/publish.js +40 -6
- package/ietf/publish.js.map +1 -1
- package/ietf/publish_namespace.d.ts +25 -0
- package/ietf/publish_namespace.d.ts.map +1 -1
- package/ietf/publish_namespace.js +63 -0
- package/ietf/publish_namespace.js.map +1 -1
- package/ietf/publisher.d.ts +1 -82
- package/ietf/publisher.d.ts.map +1 -1
- package/ietf/publisher.js +483 -235
- package/ietf/publisher.js.map +1 -1
- package/ietf/solicit.d.ts +1 -40
- package/ietf/solicit.d.ts.map +1 -1
- package/ietf/subscribe.d.ts +8 -6
- package/ietf/subscribe.d.ts.map +1 -1
- package/ietf/subscribe.js +33 -27
- package/ietf/subscribe.js.map +1 -1
- package/ietf/subscribe_namespace.d.ts +8 -2
- package/ietf/subscribe_namespace.d.ts.map +1 -1
- package/ietf/subscribe_namespace.js +18 -8
- package/ietf/subscribe_namespace.js.map +1 -1
- package/ietf/subscriber.d.ts +1 -65
- package/ietf/subscriber.d.ts.map +1 -1
- package/ietf/subscriber.js +337 -123
- package/ietf/subscriber.js.map +1 -1
- package/ietf/token.d.ts +2 -0
- package/ietf/token.d.ts.map +1 -0
- package/ietf/token.js +99 -0
- package/ietf/token.js.map +1 -0
- package/ietf/track.d.ts +4 -0
- package/ietf/track.d.ts.map +1 -1
- package/ietf/track.js +6 -20
- package/ietf/track.js.map +1 -1
- package/ietf/version.d.ts +12 -1
- package/ietf/version.d.ts.map +1 -1
- package/ietf/version.js +13 -0
- package/ietf/version.js.map +1 -1
- package/index.d.ts +12 -7
- package/index.d.ts.map +1 -1
- package/index.js +10 -5
- package/index.js.map +1 -1
- package/internal.d.ts +115 -1
- package/internal.d.ts.map +1 -1
- package/internal.js +108 -0
- package/internal.js.map +1 -1
- package/lite/announce.d.ts +60 -10
- package/lite/announce.d.ts.map +1 -1
- package/lite/announce.js +176 -31
- package/lite/announce.js.map +1 -1
- package/lite/connection.d.ts +9 -59
- package/lite/connection.d.ts.map +1 -1
- package/lite/connection.js +34 -35
- package/lite/connection.js.map +1 -1
- package/lite/datagram.d.ts +3 -2
- package/lite/datagram.d.ts.map +1 -1
- package/lite/datagram.js +7 -8
- package/lite/datagram.js.map +1 -1
- package/lite/fetch.d.ts +15 -1
- package/lite/fetch.d.ts.map +1 -1
- package/lite/fetch.js +39 -7
- package/lite/fetch.js.map +1 -1
- package/lite/goaway.d.ts.map +1 -1
- package/lite/goaway.js +7 -1
- package/lite/goaway.js.map +1 -1
- package/lite/group.d.ts +30 -12
- package/lite/group.d.ts.map +1 -1
- package/lite/group.js +68 -26
- package/lite/group.js.map +1 -1
- package/lite/message.d.ts +2 -2
- package/lite/message.d.ts.map +1 -1
- package/lite/message.js +14 -5
- package/lite/message.js.map +1 -1
- package/lite/priority.d.ts +1 -61
- package/lite/priority.d.ts.map +1 -1
- package/lite/priority.js +4 -5
- package/lite/priority.js.map +1 -1
- package/lite/publisher.d.ts +1 -69
- package/lite/publisher.d.ts.map +1 -1
- package/lite/publisher.js +630 -262
- package/lite/publisher.js.map +1 -1
- package/lite/setup.d.ts +8 -8
- package/lite/setup.d.ts.map +1 -1
- package/lite/setup.js +33 -30
- package/lite/setup.js.map +1 -1
- package/lite/subscribe.d.ts +71 -17
- package/lite/subscribe.d.ts.map +1 -1
- package/lite/subscribe.js +205 -53
- package/lite/subscribe.js.map +1 -1
- package/lite/subscriber.d.ts +15 -56
- package/lite/subscriber.d.ts.map +1 -1
- package/lite/subscriber.js +451 -247
- package/lite/subscriber.js.map +1 -1
- package/lite/track.d.ts +4 -10
- package/lite/track.d.ts.map +1 -1
- package/lite/track.js +34 -29
- package/lite/track.js.map +1 -1
- package/lite/version.d.ts +42 -6
- package/lite/version.d.ts.map +1 -1
- package/lite/version.js +126 -10
- package/lite/version.js.map +1 -1
- package/origin.d.ts +256 -29
- package/origin.d.ts.map +1 -1
- package/origin.js +1427 -37
- package/origin.js.map +1 -1
- package/package.json +8 -3
- package/path.d.ts +25 -7
- package/path.d.ts.map +1 -1
- package/path.js +5 -3
- package/path.js.map +1 -1
- package/stream.d.ts +73 -14
- package/stream.d.ts.map +1 -1
- package/stream.js +372 -141
- package/stream.js.map +1 -1
- package/tail.d.ts +18 -0
- package/tail.d.ts.map +1 -0
- package/tail.js +167 -0
- package/tail.js.map +1 -0
- package/time.d.ts +15 -2
- package/time.d.ts.map +1 -1
- package/time.js +28 -9
- package/time.js.map +1 -1
- package/track.d.ts +211 -83
- package/track.d.ts.map +1 -1
- package/track.js +816 -205
- package/track.js.map +1 -1
- package/util/abort.d.ts +2 -0
- package/util/abort.d.ts.map +1 -0
- package/util/abort.js +20 -0
- package/util/abort.js.map +1 -0
- package/util/log.d.ts +5 -0
- package/util/log.d.ts.map +1 -0
- package/util/log.js +17 -0
- package/util/log.js.map +1 -0
- package/util/u64.d.ts +39 -0
- package/util/u64.d.ts.map +1 -0
- package/util/u64.js +83 -0
- package/util/u64.js.map +1 -0
- package/util/varint.d.ts +29 -0
- package/util/varint.d.ts.map +1 -0
- package/util/varint.js +198 -0
- package/util/varint.js.map +1 -0
- package/varint.d.ts +10 -6
- package/varint.d.ts.map +1 -1
- package/varint.js +40 -237
- package/varint.js.map +1 -1
- package/wire.d.ts +80 -0
- package/wire.d.ts.map +1 -0
- package/wire.js +32 -0
- package/wire.js.map +1 -0
- package/zod.d.ts +1 -1
- package/zod.d.ts.map +1 -1
- package/zod.js.map +1 -1
- package/mock.d.ts +0 -66
- package/mock.d.ts.map +0 -1
- package/mock.js +0 -243
- package/mock.js.map +0 -1
package/lite/publisher.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/* @ts-self-types="./publisher.d.ts" */
|
|
2
|
-
import { Signal } from "@norskvideo/moq-signals";
|
|
3
|
-
import { error, reason } from "../error.js";
|
|
4
|
-
import
|
|
2
|
+
import { race, Signal } from "@norskvideo/moq-signals";
|
|
3
|
+
import { error, NotFound, reason, StreamCode, StreamError } from "../error.js";
|
|
4
|
+
import { Cost, routesEqual } from "../hop.js";
|
|
5
|
+
import { hiddenBelow, hooks, presented } from "../internal.js";
|
|
5
6
|
import { Writer } from "../stream.js";
|
|
6
|
-
import { Timescale } from "../time.js";
|
|
7
|
+
import { Milli, Timescale } from "../time.js";
|
|
8
|
+
import { wireOf } from "../wire.js";
|
|
7
9
|
import { AnnounceInit, AnnounceOk, encodeAnnounceBroadcast } from "./announce.js";
|
|
8
10
|
import { Datagram as DatagramMessage } from "./datagram.js";
|
|
9
11
|
import * as DatagramStream from "./datagram_stream.js";
|
|
10
12
|
import { Group as GroupMessage } from "./group.js";
|
|
11
13
|
import { Priority, sendOrder } from "./priority.js";
|
|
12
14
|
import { Probe } from "./probe.js";
|
|
13
|
-
import { encodeSubscribeResponse, SubscribeEnd, SubscribeOk, SubscribeStart, SubscribeUpdate, } from "./subscribe.js";
|
|
15
|
+
import { encodeSubscribeResponse, exclusiveGroupEnd, SubscribeEnd, SubscribeOk, SubscribeStart, SubscribeUpdate, } from "./subscribe.js";
|
|
14
16
|
import { TrackInfo as TrackInfoMessage } from "./track.js";
|
|
15
|
-
import { hasAnnounceId, hasAnnounceOk, hasDatagrams, hasProbeRtt, Version } from "./version.js";
|
|
17
|
+
import { hasAnnounceId, hasAnnounceOk, hasDatagrams, hasProbeRtt, hasRouteCost, hasStreamCount, resolvesStart, Version, } from "./version.js";
|
|
16
18
|
const PROBE_INTERVAL = 100; // ms
|
|
17
19
|
const PROBE_MAX_AGE = 10_000; // ms
|
|
18
20
|
const PROBE_MAX_DELTA = 0.25;
|
|
@@ -34,6 +36,175 @@ function supportsTrackStream(version) {
|
|
|
34
36
|
return true;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* The frames of `sequence` a subscription asked for, as a start index and an inclusive end.
|
|
41
|
+
*
|
|
42
|
+
* The frame bounds qualify the start and end group only; every other group is served whole.
|
|
43
|
+
* Which groups are served at all is the subscriber's read cursor (`replaceGroups`),
|
|
44
|
+
* applied when a group is popped rather than re-checked here.
|
|
45
|
+
*
|
|
46
|
+
* The serving loop calls this synchronously after the pop, before any SUBSCRIBE_UPDATE can
|
|
47
|
+
* change `bounds`. Nothing downstream trims the frame range: it is a wire request,
|
|
48
|
+
* deliberately decoupled from the receiver's local read cursor.
|
|
49
|
+
*/
|
|
50
|
+
function frameRange(bounds, sequence) {
|
|
51
|
+
return {
|
|
52
|
+
start: bounds.startGroup === sequence ? bounds.startFrame : 0,
|
|
53
|
+
end: bounds.endGroup === sequence ? bounds.endFrame : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The subscribe stream's control half, decoded ahead of the serving loop.
|
|
58
|
+
*
|
|
59
|
+
* Decoding runs on its own and publishes each full subscription update immediately, so a
|
|
60
|
+
* blocked response write cannot delay re-ranking streams already in flight. It separately
|
|
61
|
+
* stores only the latest range state for the serving loop, which owns the local track cursor
|
|
62
|
+
* and frame bounds. That keeps a group pop and its frame-range snapshot one indivisible step.
|
|
63
|
+
*
|
|
64
|
+
* Reading ahead makes control-first ordering hold for a burst. Coalescing bounds memory while
|
|
65
|
+
* preserving the newest state decoded before the next group pop. The Rust publisher gets the
|
|
66
|
+
* same ordering from `poll_decode_maybe`, which decodes straight out of the reader's buffer;
|
|
67
|
+
* nothing here can decode synchronously, so it reads ahead instead.
|
|
68
|
+
*/
|
|
69
|
+
class SubscriptionControls {
|
|
70
|
+
#writer;
|
|
71
|
+
#update;
|
|
72
|
+
// Sticky, first one wins: null once the stream is over, an Error once it failed.
|
|
73
|
+
#end;
|
|
74
|
+
#ended;
|
|
75
|
+
#resolveEnd;
|
|
76
|
+
#changed = new Signal(0);
|
|
77
|
+
/** Settles once decoding stops, so teardown can wait for it rather than leaving it running. */
|
|
78
|
+
decoding;
|
|
79
|
+
constructor({ reader, writer, version, apply }) {
|
|
80
|
+
this.#writer = writer;
|
|
81
|
+
this.#ended = new Promise((resolve) => {
|
|
82
|
+
this.#resolveEnd = resolve;
|
|
83
|
+
});
|
|
84
|
+
this.decoding = this.#decode(reader, version, apply);
|
|
85
|
+
// Our own half going away ends the loop too, and has to reach it the same way: the
|
|
86
|
+
// loop looks at nothing else.
|
|
87
|
+
void writer.closed.then(() => this.#finish(null), (err) => this.#finish(error(err)));
|
|
88
|
+
}
|
|
89
|
+
/** The next control to apply, or undefined while the peer is quiet. */
|
|
90
|
+
take() {
|
|
91
|
+
const update = this.#update;
|
|
92
|
+
this.#update = undefined;
|
|
93
|
+
// An update decoded before the stream ended still applies before the sticky end.
|
|
94
|
+
if (update)
|
|
95
|
+
return { kind: "update", update };
|
|
96
|
+
if (this.#end === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
return this.#end === null ? { kind: "done" } : { kind: "error", error: this.#end };
|
|
99
|
+
}
|
|
100
|
+
/** Calls `fn` once {@link take} may answer differently. */
|
|
101
|
+
changed(fn) {
|
|
102
|
+
return this.#changed.changed(fn);
|
|
103
|
+
}
|
|
104
|
+
/** Returns false when peer departure supersedes a blocked response write. */
|
|
105
|
+
async response(pending) {
|
|
106
|
+
// `#ended` lives as long as the stream, so it is raced as-is rather than mapped per call.
|
|
107
|
+
const result = await race([
|
|
108
|
+
pending.then(() => ({ kind: "sent" }), (err) => ({ kind: "error", error: error(err) })),
|
|
109
|
+
this.#ended,
|
|
110
|
+
]);
|
|
111
|
+
if (result !== null && !(result instanceof Error)) {
|
|
112
|
+
if (result.kind === "sent")
|
|
113
|
+
return true;
|
|
114
|
+
throw result.error;
|
|
115
|
+
}
|
|
116
|
+
// The race leaves the blocked encode running, so reset the writable half too.
|
|
117
|
+
this.#writer.reset(result ?? new StreamError(StreamCode.Cancel, { message: "cancel" }));
|
|
118
|
+
if (result)
|
|
119
|
+
throw result;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
/** Settles once the stream is over: `null` when it ended cleanly, or the failure. */
|
|
123
|
+
get ended() {
|
|
124
|
+
return this.#ended;
|
|
125
|
+
}
|
|
126
|
+
#finish(end) {
|
|
127
|
+
if (this.#end !== undefined)
|
|
128
|
+
return;
|
|
129
|
+
this.#end = end;
|
|
130
|
+
this.#resolveEnd(end);
|
|
131
|
+
this.#changed.update((value) => value + 1);
|
|
132
|
+
}
|
|
133
|
+
async #decode(reader, version, apply) {
|
|
134
|
+
try {
|
|
135
|
+
while (this.#end === undefined) {
|
|
136
|
+
const update = await SubscribeUpdate.decodeMaybe(reader, version);
|
|
137
|
+
if (!update)
|
|
138
|
+
break;
|
|
139
|
+
apply(update);
|
|
140
|
+
this.#update = update;
|
|
141
|
+
this.#changed.update((value) => value + 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
this.#finish(error(err));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.#finish(null);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// A microtask is too short: decoding one framed update crosses several awaits, each of which
|
|
152
|
+
// can requeue behind the serving continuation. A task boundary lets the decoder finish whatever
|
|
153
|
+
// the transport already delivered before the next group pop. Updates are rare, so groups do not
|
|
154
|
+
// pay this scheduling cost on the normal path.
|
|
155
|
+
const yieldToControls = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
156
|
+
// Register both readiness sources in the same turn after the caller observed neither ready.
|
|
157
|
+
// The winner disposes both registrations, so an idle subscription accumulates nothing no
|
|
158
|
+
// matter how many times it wakes.
|
|
159
|
+
function waitForSubscription(controls, subscriber) {
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
let settled = false;
|
|
162
|
+
const dispose = [];
|
|
163
|
+
const wake = () => {
|
|
164
|
+
if (settled)
|
|
165
|
+
return;
|
|
166
|
+
settled = true;
|
|
167
|
+
for (const close of dispose)
|
|
168
|
+
close();
|
|
169
|
+
resolve();
|
|
170
|
+
};
|
|
171
|
+
dispose.push(controls.changed(wake), hooks.groupChanged(subscriber, wake));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The budget to serve a peer with, given what its wire could tell us.
|
|
176
|
+
*
|
|
177
|
+
* A version without the field decodes as `0`, which is indistinguishable from a peer
|
|
178
|
+
* genuinely asking for the live edge. Serving that as real time would discard backlog
|
|
179
|
+
* a legacy subscriber never declined, so fall back to a window wide enough not to drop
|
|
180
|
+
* and leave enforcement to the receiver, as the IETF path does for the same reason.
|
|
181
|
+
*/
|
|
182
|
+
function servingMaxAge(version, requested) {
|
|
183
|
+
return carriesMaxAge(version) ? (requested ?? 0) : Number.MAX_SAFE_INTEGER;
|
|
184
|
+
}
|
|
185
|
+
/** Whether this version's SUBSCRIBE carries Subscriber Max Age at all. */
|
|
186
|
+
function carriesMaxAge(version) {
|
|
187
|
+
return version !== Version.DRAFT_01 && version !== Version.DRAFT_02;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Position a subscription's read cursor for the wire serving it.
|
|
191
|
+
*
|
|
192
|
+
* On lite-06 there is nothing to do: the cursor is floored at the group the subscription
|
|
193
|
+
* named (or 0), and its Max Age decides what above the floor is worth delivering.
|
|
194
|
+
*
|
|
195
|
+
* Pre-06 wires are the exception: their drafts define an absent `Group Start` as the
|
|
196
|
+
* latest group, so say so explicitly rather than letting the budget reach back. Lite-03/04/05
|
|
197
|
+
* carry a Max Age, but there it is a staleness tolerance only; lite-01/02 additionally get
|
|
198
|
+
* an unbounded budget so nothing is dropped under them (see {@link servingMaxAge}), which
|
|
199
|
+
* must not read as a request to replay the whole cache on join.
|
|
200
|
+
*/
|
|
201
|
+
function positionCursor(track, version, startGroup) {
|
|
202
|
+
if (resolvesStart(version) || startGroup !== undefined)
|
|
203
|
+
return;
|
|
204
|
+
const latest = track.latest();
|
|
205
|
+
if (latest !== undefined)
|
|
206
|
+
hooks.replaceGroups(track, { start: { included: latest } });
|
|
207
|
+
}
|
|
37
208
|
/**
|
|
38
209
|
* Handles publishing broadcasts and managing their lifecycle.
|
|
39
210
|
*
|
|
@@ -46,58 +217,45 @@ export class Publisher {
|
|
|
46
217
|
// can detect loops and prefer shorter paths. Created by Connection and
|
|
47
218
|
// shared with Subscriber, which can optionally use it to filter out its
|
|
48
219
|
// own announcements.
|
|
49
|
-
|
|
220
|
+
hop;
|
|
50
221
|
#quic;
|
|
51
222
|
// The one writer for the outbound datagram stream (getWriter locks it), acquired once at
|
|
52
223
|
// construction when this version + transport carry datagrams, released in close(). Its
|
|
53
224
|
// presence is the gate: undefined means datagrams aren't served on this connection. All
|
|
54
225
|
// subscriptions share it, since a second getWriter on the same stream would throw.
|
|
55
226
|
#datagramWriter;
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
#
|
|
227
|
+
// Originated advertisements this session forwards.
|
|
228
|
+
#advertised;
|
|
229
|
+
#publish;
|
|
59
230
|
// TRACK_INFO is immutable per track, so resolve it from the application once
|
|
60
231
|
// (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
|
-
//
|
|
63
|
-
|
|
232
|
+
// and reuse it for every later TRACK request of the same track. Keyed by the
|
|
233
|
+
// routing front rather than the path: immutability holds for one broadcast, and a
|
|
234
|
+
// republish puts a different one on the path, so its entries must not be reused.
|
|
235
|
+
// A rejected lookup is evicted so a retry can re-probe.
|
|
236
|
+
#trackInfo = new WeakMap();
|
|
64
237
|
/**
|
|
65
238
|
* Creates a new Publisher instance.
|
|
66
239
|
* @param quic - The WebTransport session to use
|
|
67
240
|
* @param version - Negotiated protocol version
|
|
68
|
-
* @param origin -
|
|
241
|
+
* @param origin - Hop id shared with the Subscriber
|
|
242
|
+
* @param publish - The origin whose broadcasts this session serves; omit to publish nothing
|
|
69
243
|
*
|
|
70
244
|
* @internal
|
|
71
245
|
*/
|
|
72
|
-
constructor(quic, version,
|
|
246
|
+
constructor(quic, version, hop, publish) {
|
|
73
247
|
this.#quic = quic;
|
|
74
248
|
this.version = version;
|
|
75
|
-
this.
|
|
249
|
+
this.hop = hop;
|
|
250
|
+
const origin = publish && wireOf(publish);
|
|
251
|
+
this.#advertised = origin?.advertised ?? new Signal(new Map());
|
|
252
|
+
this.#publish = publish;
|
|
76
253
|
// Grab the datagram writer up front when the transport carries datagrams (no group
|
|
77
254
|
// fallback, so it stays undefined otherwise). One writer for all subscriptions.
|
|
78
255
|
if (hasDatagrams(version)) {
|
|
79
256
|
this.#datagramWriter = DatagramStream.datagramWriter(quic);
|
|
80
257
|
}
|
|
81
258
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Publishes a broadcast with any associated tracks.
|
|
84
|
-
* @param name - The broadcast to publish
|
|
85
|
-
*/
|
|
86
|
-
publish(path, broadcast) {
|
|
87
|
-
this.#broadcasts.mutate((broadcasts) => {
|
|
88
|
-
if (!broadcasts)
|
|
89
|
-
throw new Error("closed");
|
|
90
|
-
broadcasts.set(path, broadcast);
|
|
91
|
-
});
|
|
92
|
-
// Remove the broadcast from the lookup when it's closed, unless the path was republished.
|
|
93
|
-
void broadcast.closed.then(() => {
|
|
94
|
-
this.#broadcasts.mutate((broadcasts) => {
|
|
95
|
-
if (broadcasts?.get(path) === broadcast) {
|
|
96
|
-
broadcasts.delete(path);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
259
|
/**
|
|
102
260
|
* Handles an announce interest message.
|
|
103
261
|
* @param msg - The announce interest message
|
|
@@ -107,99 +265,133 @@ export class Publisher {
|
|
|
107
265
|
*/
|
|
108
266
|
async runAnnounce(msg, stream) {
|
|
109
267
|
console.debug(`announce: prefix=${msg.prefix}`);
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!broadcasts)
|
|
114
|
-
return; // closed
|
|
115
|
-
for (const name of broadcasts.keys()) {
|
|
116
|
-
const suffix = Path.stripPrefix(msg.prefix, name);
|
|
117
|
-
if (suffix === null)
|
|
118
|
-
continue;
|
|
119
|
-
console.debug(`announce: broadcast=${name} active=true`);
|
|
120
|
-
active.add(suffix);
|
|
121
|
-
}
|
|
268
|
+
// Keyed by suffix, valued by identity plus route, so a republish diffs as
|
|
269
|
+
// ended-then-active and a re-price as a restart.
|
|
270
|
+
let active = new Map();
|
|
122
271
|
// Lite06+: announce ids. Every active we send implicitly assigns the next
|
|
123
|
-
// per-stream ordinal; ended
|
|
272
|
+
// per-stream ordinal; ended/restart reference the id instead of repeating the path.
|
|
124
273
|
let nextAnnounceId = 0n;
|
|
125
274
|
const announceIds = new Map();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
275
|
+
const wireHops = (route) => {
|
|
276
|
+
if (hasAnnounceOk(this.version))
|
|
277
|
+
return route.hops;
|
|
278
|
+
return [...route.hops, this.hop];
|
|
279
|
+
};
|
|
280
|
+
// What the peer decodes for a route: pre-lite-06 wires carry no cost, so a re-price
|
|
281
|
+
// there must not restart.
|
|
282
|
+
const onWire = (route) => (hasRouteCost(this.version) ? route : { ...route, cost: Cost.zero });
|
|
283
|
+
const announce = async (suffix, route) => {
|
|
284
|
+
console.debug(`announce: broadcast=${suffix} active=true`);
|
|
285
|
+
if (hasAnnounceId(this.version))
|
|
286
|
+
announceIds.set(suffix, nextAnnounceId++);
|
|
287
|
+
await encodeAnnounceBroadcast(stream.writer, { status: "active", suffix, hops: wireHops(route), cost: route.cost }, this.version);
|
|
288
|
+
};
|
|
289
|
+
const restart = async (suffix, route) => {
|
|
290
|
+
if (!hasAnnounceId(this.version)) {
|
|
291
|
+
await retract(suffix);
|
|
292
|
+
await announce(suffix, route);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const id = announceIds.get(suffix);
|
|
296
|
+
if (id === undefined) {
|
|
297
|
+
await announce(suffix, route);
|
|
298
|
+
return;
|
|
132
299
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
300
|
+
console.debug(`announce: broadcast=${suffix} restart=true`);
|
|
301
|
+
await encodeAnnounceBroadcast(stream.writer, { status: "restart", id, hops: wireHops(route), cost: route.cost }, this.version);
|
|
302
|
+
};
|
|
303
|
+
// Lite06+ retracts by announce id; older versions repeat the path (ended announces
|
|
304
|
+
// don't need hops).
|
|
305
|
+
const retract = async (suffix) => {
|
|
306
|
+
console.debug(`announce: broadcast=${suffix} active=false`);
|
|
307
|
+
if (!hasAnnounceId(this.version)) {
|
|
308
|
+
await encodeAnnounceBroadcast(stream.writer, { status: "ended", suffix }, this.version);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const id = announceIds.get(suffix);
|
|
312
|
+
announceIds.delete(suffix);
|
|
313
|
+
if (id === undefined)
|
|
314
|
+
return; // never announced
|
|
315
|
+
await encodeAnnounceBroadcast(stream.writer, { status: "endedId", id }, this.version);
|
|
316
|
+
};
|
|
317
|
+
// Subscribe BEFORE writing anything: every encode below awaits the wire, and a publish
|
|
318
|
+
// landing in that window only notifies the listeners already registered. One created
|
|
319
|
+
// afterwards would sleep through it, leaving the change unannounced until something
|
|
320
|
+
// unrelated moved.
|
|
321
|
+
// TODO Make a better helper within Signals.
|
|
322
|
+
let dispose;
|
|
323
|
+
let changed = new Promise((resolve) => {
|
|
324
|
+
dispose = this.#advertised.changed(resolve);
|
|
325
|
+
});
|
|
326
|
+
// A hidden route stays off the wire unless the request opted in.
|
|
327
|
+
const carries = (covered) => msg.hidden || !hiddenBelow(msg.prefix, covered);
|
|
328
|
+
try {
|
|
329
|
+
const initial = this.#advertised.peek();
|
|
330
|
+
if (!initial)
|
|
331
|
+
return; // closed
|
|
332
|
+
for (const [name, snap] of presented(msg.prefix, initial, carries)) {
|
|
333
|
+
active.set(name, snap);
|
|
334
|
+
}
|
|
335
|
+
switch (this.version) {
|
|
336
|
+
case Version.DRAFT_01:
|
|
337
|
+
case Version.DRAFT_02: {
|
|
338
|
+
for (const suffix of active.keys()) {
|
|
339
|
+
console.debug(`announce: broadcast=${suffix} active=true`);
|
|
138
340
|
}
|
|
341
|
+
const init = new AnnounceInit([...active.keys()]);
|
|
342
|
+
await init.encode(stream.writer, this.version);
|
|
139
343
|
break;
|
|
140
344
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
345
|
+
default: {
|
|
346
|
+
if (!hasAnnounceOk(this.version)) {
|
|
347
|
+
for (const [suffix, snap] of active) {
|
|
348
|
+
await announce(suffix, snap.route);
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
const ok = new AnnounceOk(this.hop, active.size);
|
|
353
|
+
await ok.encode(stream.writer, this.version);
|
|
354
|
+
for (const [suffix, snap] of active) {
|
|
355
|
+
await announce(suffix, snap.route);
|
|
148
356
|
}
|
|
149
|
-
|
|
357
|
+
break;
|
|
150
358
|
}
|
|
151
|
-
break;
|
|
152
359
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const newActive = new Set();
|
|
169
|
-
for (const name of broadcasts.keys()) {
|
|
170
|
-
const suffix = Path.stripPrefix(msg.prefix, name);
|
|
171
|
-
if (suffix === null)
|
|
172
|
-
continue; // Not our prefix.
|
|
173
|
-
newActive.add(suffix);
|
|
174
|
-
}
|
|
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.
|
|
177
|
-
for (const added of newActive.difference(active)) {
|
|
178
|
-
console.debug(`announce: broadcast=${added} active=true`);
|
|
179
|
-
const hops = hasAnnounceOk(this.version) ? [] : [this.origin];
|
|
180
|
-
if (hasAnnounceId(this.version)) {
|
|
181
|
-
announceIds.set(added, nextAnnounceId++);
|
|
360
|
+
for (;;) {
|
|
361
|
+
const advertised = await race([changed, stream.reader.closed]);
|
|
362
|
+
dispose();
|
|
363
|
+
if (!advertised)
|
|
364
|
+
break;
|
|
365
|
+
// Re-arm before reading, so an advertise that lands while we write is not lost.
|
|
366
|
+
changed = new Promise((resolve) => {
|
|
367
|
+
dispose = this.#advertised.changed(resolve);
|
|
368
|
+
});
|
|
369
|
+
const latest = this.#advertised.peek();
|
|
370
|
+
if (!latest)
|
|
371
|
+
break;
|
|
372
|
+
const updated = new Map();
|
|
373
|
+
for (const [name, snap] of presented(msg.prefix, latest, carries)) {
|
|
374
|
+
updated.set(name, snap);
|
|
182
375
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
for (const removed of active.difference(newActive)) {
|
|
188
|
-
console.debug(`announce: broadcast=${removed} active=false`);
|
|
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);
|
|
376
|
+
for (const [suffix, snap] of active) {
|
|
377
|
+
const cur = updated.get(suffix);
|
|
378
|
+
if (!cur || cur.identity !== snap.identity)
|
|
379
|
+
await retract(suffix);
|
|
195
380
|
}
|
|
196
|
-
|
|
197
|
-
|
|
381
|
+
for (const [suffix, snap] of updated) {
|
|
382
|
+
const prev = active.get(suffix);
|
|
383
|
+
if (!prev || prev.identity !== snap.identity) {
|
|
384
|
+
await announce(suffix, snap.route);
|
|
385
|
+
}
|
|
386
|
+
else if (!routesEqual(onWire(prev.route), onWire(snap.route))) {
|
|
387
|
+
await restart(suffix, snap.route);
|
|
388
|
+
}
|
|
198
389
|
}
|
|
390
|
+
active = updated;
|
|
199
391
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
dispose();
|
|
203
395
|
}
|
|
204
396
|
}
|
|
205
397
|
/**
|
|
@@ -210,27 +402,37 @@ export class Publisher {
|
|
|
210
402
|
* @internal
|
|
211
403
|
*/
|
|
212
404
|
async runSubscribe(msg, stream) {
|
|
213
|
-
|
|
214
|
-
|
|
405
|
+
let front;
|
|
406
|
+
try {
|
|
407
|
+
front =
|
|
408
|
+
this.#publish &&
|
|
409
|
+
(wireOf(this.#publish).local(msg.broadcast) ?? (await wireOf(this.#publish).demand(msg.broadcast)));
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
stream.writer.reset(error(err));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!front) {
|
|
215
416
|
console.debug(`publish unknown: broadcast=${msg.broadcast}`);
|
|
216
|
-
stream.writer.reset(new
|
|
417
|
+
stream.writer.reset(new NotFound(`broadcast ${msg.broadcast}`));
|
|
217
418
|
return;
|
|
218
419
|
}
|
|
219
|
-
const
|
|
420
|
+
const endGroup = exclusiveGroupEnd(msg.endGroup);
|
|
421
|
+
const track = wireOf(front).subscribe(msg.track, {
|
|
220
422
|
priority: msg.priority,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
423
|
+
maxAge: Milli(servingMaxAge(this.version, msg.maxAge)),
|
|
424
|
+
groups: {
|
|
425
|
+
start: msg.startGroup === undefined ? undefined : { included: msg.startGroup },
|
|
426
|
+
end: endGroup === undefined ? undefined : { excluded: endGroup },
|
|
427
|
+
},
|
|
225
428
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
track.startAt(startGroup);
|
|
229
|
-
track.endAt(msg.endGroup);
|
|
429
|
+
positionCursor(track, this.version, msg.startGroup);
|
|
430
|
+
hooks.replaceGroups(track, { end: endGroup === undefined ? undefined : { excluded: endGroup } });
|
|
230
431
|
// The best-effort datagram loop, started once serving begins. It parks when the
|
|
231
432
|
// track finishes (recvDatagram returns undefined), so #runTrack alone ends the
|
|
232
433
|
// subscription; awaited during teardown so it doesn't outlive the subscription.
|
|
233
434
|
let datagrams = Promise.resolve();
|
|
435
|
+
let controls;
|
|
234
436
|
try {
|
|
235
437
|
let timescale = Timescale.MILLI;
|
|
236
438
|
if (supportsTrackStream(this.version)) {
|
|
@@ -250,51 +452,57 @@ export class Publisher {
|
|
|
250
452
|
// Older drafts acknowledge with SUBSCRIBE_OK and stream frames verbatim.
|
|
251
453
|
const ok = new SubscribeOk({
|
|
252
454
|
priority: msg.priority,
|
|
253
|
-
|
|
254
|
-
maxLatency: msg.maxLatency,
|
|
455
|
+
maxAge: msg.maxAge,
|
|
255
456
|
startGroup: msg.startGroup,
|
|
256
457
|
endGroup: msg.endGroup,
|
|
257
458
|
});
|
|
258
459
|
await encodeSubscribeResponse(stream.writer, { ok }, this.version);
|
|
259
460
|
}
|
|
260
461
|
console.debug(`publish ok: broadcast=${msg.broadcast} track=${track.name}`);
|
|
261
|
-
const serving = this.#runTrack(msg.id, msg.broadcast, track, stream.writer, timescale);
|
|
262
462
|
// Serve datagrams concurrently with groups whenever the transport carries them
|
|
263
463
|
// (the writer exists iff so). No group fallback: otherwise they simply aren't sent.
|
|
264
464
|
if (this.#datagramWriter) {
|
|
265
465
|
datagrams = this.#runDatagrams(msg.id, track, timescale);
|
|
266
466
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
console.debug(`subscribe update: broadcast=${msg.broadcast} track=${track.name}`);
|
|
467
|
+
controls = new SubscriptionControls({
|
|
468
|
+
reader: stream.reader,
|
|
469
|
+
writer: stream.writer,
|
|
470
|
+
version: this.version,
|
|
471
|
+
apply: (update) => {
|
|
472
|
+
const end = exclusiveGroupEnd(update.endGroup);
|
|
274
473
|
track.update({
|
|
275
|
-
priority:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
474
|
+
priority: update.priority,
|
|
475
|
+
maxAge: Milli(servingMaxAge(this.version, update.maxAge)),
|
|
476
|
+
groups: {
|
|
477
|
+
start: update.startGroup === undefined ? undefined : { included: update.startGroup },
|
|
478
|
+
end: end === undefined ? undefined : { excluded: end },
|
|
479
|
+
},
|
|
280
480
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
await this.#runTrack(track, stream.writer, controls, {
|
|
484
|
+
sub: msg.id,
|
|
485
|
+
broadcast: msg.broadcast,
|
|
486
|
+
timescale,
|
|
487
|
+
bounds: {
|
|
488
|
+
startGroup: msg.startGroup,
|
|
489
|
+
startFrame: msg.startFrame,
|
|
490
|
+
endGroup: msg.endGroup,
|
|
491
|
+
endFrame: msg.endFrame,
|
|
492
|
+
},
|
|
493
|
+
});
|
|
286
494
|
console.debug(`publish done: broadcast=${msg.broadcast} track=${track.name}`);
|
|
287
495
|
stream.close();
|
|
288
496
|
track.close();
|
|
289
|
-
// track.close ends the datagram loop
|
|
290
|
-
await datagrams;
|
|
497
|
+
// Closing the stream ends the decoder and track.close ends the datagram loop.
|
|
498
|
+
await Promise.all([datagrams, controls.decoding]);
|
|
291
499
|
}
|
|
292
500
|
catch (err) {
|
|
293
501
|
const e = error(err);
|
|
294
502
|
console.warn(`publish error: broadcast=${msg.broadcast} track=${track.name} error=${reason(e)}`);
|
|
295
503
|
track.close(e);
|
|
296
504
|
stream.abort(e);
|
|
297
|
-
await datagrams;
|
|
505
|
+
await Promise.all([datagrams, controls?.decoding]);
|
|
298
506
|
}
|
|
299
507
|
}
|
|
300
508
|
/**
|
|
@@ -307,10 +515,19 @@ export class Publisher {
|
|
|
307
515
|
stream.writer.reset(new Error("fetch requires moq-lite-05 or newer"));
|
|
308
516
|
return;
|
|
309
517
|
}
|
|
310
|
-
|
|
311
|
-
|
|
518
|
+
let front;
|
|
519
|
+
try {
|
|
520
|
+
front =
|
|
521
|
+
this.#publish &&
|
|
522
|
+
(wireOf(this.#publish).local(msg.broadcast) ?? (await wireOf(this.#publish).demand(msg.broadcast)));
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
stream.writer.reset(error(err));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!front) {
|
|
312
529
|
console.debug(`fetch unknown: broadcast=${msg.broadcast}`);
|
|
313
|
-
stream.writer.reset(new
|
|
530
|
+
stream.writer.reset(new NotFound(`broadcast ${msg.broadcast}`));
|
|
314
531
|
return;
|
|
315
532
|
}
|
|
316
533
|
// The subscriber opened this stream, so its send order only ranked the request. Rank the
|
|
@@ -318,10 +535,15 @@ export class Publisher {
|
|
|
318
535
|
stream.writer.setPriority(sendOrder({ priority: msg.priority }));
|
|
319
536
|
let group;
|
|
320
537
|
try {
|
|
321
|
-
// The timescale is immutable, so serve exactly what TRACK_INFO advertised.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
await
|
|
538
|
+
// The timescale is immutable, so serve exactly what TRACK_INFO advertised. Both
|
|
539
|
+
// come off the same front, so the metadata and the frames are one generation.
|
|
540
|
+
const info = await this.#resolveTrackInfo(front, msg.track);
|
|
541
|
+
group = await wireOf(front).fetchGroup(msg.track, msg.group, { priority: msg.priority });
|
|
542
|
+
await this.#runFetchGroup(group, stream.writer, {
|
|
543
|
+
timescale: Timescale(info.timescale),
|
|
544
|
+
start: msg.startFrame,
|
|
545
|
+
end: msg.endFrame,
|
|
546
|
+
});
|
|
325
547
|
console.debug(`fetch done: broadcast=${msg.broadcast} track=${msg.track} group=${msg.group}`);
|
|
326
548
|
stream.close();
|
|
327
549
|
group.close();
|
|
@@ -342,70 +564,172 @@ export class Publisher {
|
|
|
342
564
|
*
|
|
343
565
|
* @internal
|
|
344
566
|
*/
|
|
345
|
-
async #runTrack(
|
|
567
|
+
async #runTrack(track, stream, controls, serving) {
|
|
568
|
+
const { sub, broadcast, timescale, bounds } = serving;
|
|
346
569
|
// Lite-05+ resolves the range on the subscribe stream: SUBSCRIBE_START once the
|
|
347
570
|
// first group is known, SUBSCRIBE_END when the track finishes.
|
|
348
571
|
const emitRange = supportsTrackStream(this.version);
|
|
349
572
|
let startSent = false;
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
573
|
+
let endSent = false;
|
|
574
|
+
// Lite-07+ counts the group streams in SUBSCRIBE_END, so it goes out only once every
|
|
575
|
+
// served group has opened its stream or given up, and a cap holding groups back
|
|
576
|
+
// delays it until they are released.
|
|
577
|
+
const countStreams = hasStreamCount(this.version);
|
|
578
|
+
let streams = 0;
|
|
579
|
+
const opening = new Set();
|
|
580
|
+
// The track's exclusive final boundary. A Rust subscriber feeds SUBSCRIBE_END
|
|
581
|
+
// straight into finish_at, so it must name the track's boundary (which counts
|
|
582
|
+
// datagram sequences too), not the delivered range: a subscription cap can hold
|
|
583
|
+
// produced groups back. The latest() fallback covers a subscription torn down
|
|
584
|
+
// before the producer declared it.
|
|
585
|
+
const boundary = () => track.final() ?? (track.latest() ?? -1) + 1;
|
|
586
|
+
// Before lite-07, SUBSCRIBE_END names that boundary, which a cap can hold groups back
|
|
587
|
+
// from, so it goes out as soon as the producer finishes and the subscription keeps
|
|
588
|
+
// serving whatever a later cap raise releases (see the Rust publisher's Recv::Boundary).
|
|
589
|
+
const sendEnd = async () => {
|
|
590
|
+
endSent = true;
|
|
591
|
+
if (!emitRange)
|
|
592
|
+
return true;
|
|
593
|
+
return controls.response((async () => {
|
|
594
|
+
// A group that gives up before its stream opens is never counted.
|
|
595
|
+
if (countStreams)
|
|
596
|
+
while (opening.size > 0)
|
|
597
|
+
await Promise.all(opening);
|
|
598
|
+
const end = new SubscribeEnd(boundary(), streams);
|
|
599
|
+
await encodeSubscribeResponse(stream, { end }, this.version);
|
|
600
|
+
})());
|
|
601
|
+
};
|
|
354
602
|
// One ranking for the whole subscription, shared by every group it serves.
|
|
355
603
|
const priority = new Priority(track);
|
|
604
|
+
// Every group this subscription started serving, until its stream finishes or resets.
|
|
605
|
+
const groups = new Set();
|
|
356
606
|
// 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
|
|
358
|
-
//
|
|
607
|
+
// a track that ran out of groups still has to flush the ones already queued, and the
|
|
608
|
+
// caller FINs the subscribe stream to say so.
|
|
359
609
|
let finished = false;
|
|
360
610
|
let unsubscribe;
|
|
361
611
|
const unsubscribed = new Promise((resolve) => {
|
|
362
612
|
unsubscribe = resolve;
|
|
363
613
|
});
|
|
364
|
-
void stream.closed.then(() => {
|
|
365
|
-
if (!finished)
|
|
366
|
-
unsubscribe();
|
|
367
|
-
},
|
|
368
|
-
// A reset is always the peer.
|
|
369
|
-
() => unsubscribe());
|
|
370
614
|
try {
|
|
371
615
|
for (;;) {
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
616
|
+
// Control before data, matching the Rust publisher: every control decoded while
|
|
617
|
+
// the loop was parked applies to the next pop, never to one already made. This
|
|
618
|
+
// drain is synchronous, so nothing the decoder holds can land between the pop
|
|
619
|
+
// below and its frame range.
|
|
620
|
+
const control = controls.take();
|
|
621
|
+
if (control) {
|
|
622
|
+
switch (control.kind) {
|
|
623
|
+
case "done":
|
|
624
|
+
// The subscriber left. Its queued groups are pointless now, which
|
|
625
|
+
// the finally below acts on since `finished` stays false.
|
|
626
|
+
return;
|
|
627
|
+
case "error":
|
|
628
|
+
throw control.error;
|
|
629
|
+
case "update": {
|
|
630
|
+
const update = control.update;
|
|
631
|
+
console.debug(`subscribe update: broadcast=${broadcast} track=${track.name}`);
|
|
632
|
+
hooks.replaceGroups(track, {
|
|
633
|
+
start: update.startGroup === undefined ? undefined : { included: update.startGroup },
|
|
634
|
+
end: update.endGroup === undefined ? undefined : { included: update.endGroup },
|
|
635
|
+
});
|
|
636
|
+
bounds.startGroup = update.startGroup;
|
|
637
|
+
bounds.startFrame = update.startFrame;
|
|
638
|
+
bounds.endGroup = update.endGroup;
|
|
639
|
+
bounds.endFrame = update.endFrame;
|
|
640
|
+
await yieldToControls();
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Exactly-once arrival-order serving. This synchronous package-internal pop
|
|
646
|
+
// and frameRange call are the operation's linearization point.
|
|
647
|
+
const recv = hooks.tryRecvGroup(track);
|
|
648
|
+
switch (recv.kind) {
|
|
649
|
+
case "error":
|
|
650
|
+
throw recv.error;
|
|
651
|
+
case "idle":
|
|
652
|
+
// Before lite-07, an end declared ahead of the live edge goes out as
|
|
653
|
+
// soon as it is known, while the remaining groups are still being
|
|
654
|
+
// produced. The lite-07 count is not final until those groups open.
|
|
655
|
+
if (!endSent && !countStreams && track.final() !== undefined) {
|
|
656
|
+
if (!(await sendEnd()))
|
|
657
|
+
return;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
await waitForSubscription(controls, track);
|
|
661
|
+
continue;
|
|
662
|
+
case "boundary":
|
|
663
|
+
// The producer finished but is still holding groups above the cap.
|
|
664
|
+
// Declare the boundary, then wait for an update to release them.
|
|
665
|
+
if (!endSent && !countStreams) {
|
|
666
|
+
if (!(await sendEnd()))
|
|
667
|
+
return;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
await waitForSubscription(controls, track);
|
|
671
|
+
continue;
|
|
672
|
+
case "done": {
|
|
673
|
+
if (!endSent) {
|
|
674
|
+
if (!(await sendEnd()))
|
|
675
|
+
return;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
// The FIN tells the subscriber every group is accounted for, so it waits
|
|
679
|
+
// until each group stream finished or reset. The subscriber leaving
|
|
680
|
+
// instead cancels whatever is still queued.
|
|
681
|
+
const drained = Symbol("drained");
|
|
682
|
+
const end = await Promise.race([Promise.all(groups).then(() => drained), controls.ended]);
|
|
683
|
+
if (end instanceof Error)
|
|
684
|
+
throw end;
|
|
685
|
+
if (end !== drained)
|
|
686
|
+
return;
|
|
687
|
+
finished = true;
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
381
690
|
}
|
|
691
|
+
const group = recv.group;
|
|
692
|
+
const range = frameRange(bounds, group.sequence);
|
|
382
693
|
if (emitRange && !startSent) {
|
|
383
694
|
startSent = true;
|
|
384
695
|
// SUBSCRIBE_START promises nothing below this sequence will be delivered.
|
|
385
696
|
// Arrival-order serving could later surface a straggler below the first
|
|
386
697
|
// group, so pin the floor to what was announced.
|
|
387
|
-
|
|
388
|
-
|
|
698
|
+
hooks.replaceGroups(track, {
|
|
699
|
+
start: { included: group.sequence },
|
|
700
|
+
end: bounds.endGroup === undefined ? undefined : { included: bounds.endGroup },
|
|
701
|
+
});
|
|
702
|
+
if (!(await controls.response(encodeSubscribeResponse(stream, { start: new SubscribeStart(group.sequence) }, this.version))))
|
|
703
|
+
return;
|
|
389
704
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
705
|
+
const options = {
|
|
706
|
+
sub,
|
|
707
|
+
group,
|
|
708
|
+
timescale,
|
|
709
|
+
priority,
|
|
710
|
+
unsubscribed,
|
|
711
|
+
start: range.start,
|
|
712
|
+
end: range.end,
|
|
713
|
+
};
|
|
714
|
+
// `opening` settles when the stream opens so the lite-07 count can be sent.
|
|
715
|
+
// `groups` covers the serve too, so the FIN still waits for every stream to
|
|
716
|
+
// finish or reset, including one that has not opened yet.
|
|
717
|
+
const opened = this.#openGroup(options);
|
|
718
|
+
const task = opened.then(async (writer) => {
|
|
719
|
+
if (!writer)
|
|
720
|
+
return;
|
|
721
|
+
streams += 1;
|
|
722
|
+
await this.#serveGroup(writer, options);
|
|
723
|
+
});
|
|
724
|
+
groups.add(task);
|
|
725
|
+
void task.finally(() => groups.delete(task));
|
|
726
|
+
opening.add(opened);
|
|
727
|
+
void opened.finally(() => opening.delete(opened));
|
|
395
728
|
}
|
|
396
|
-
console.debug(`publish close: broadcast=${broadcast} track=${track.name}`);
|
|
397
|
-
finished = true;
|
|
398
|
-
track.close();
|
|
399
|
-
stream.close();
|
|
400
|
-
}
|
|
401
|
-
catch (err) {
|
|
402
|
-
const e = error(err);
|
|
403
|
-
console.warn(`publish error: broadcast=${broadcast} track=${track.name} error=${reason(e)}`);
|
|
404
|
-
unsubscribe();
|
|
405
|
-
track.close(e);
|
|
406
|
-
stream.reset(e);
|
|
407
729
|
}
|
|
408
730
|
finally {
|
|
731
|
+
if (!finished)
|
|
732
|
+
unsubscribe();
|
|
409
733
|
priority.close();
|
|
410
734
|
}
|
|
411
735
|
}
|
|
@@ -416,7 +740,11 @@ export class Publisher {
|
|
|
416
740
|
*/
|
|
417
741
|
async runTrackInfo(msg, stream) {
|
|
418
742
|
try {
|
|
419
|
-
const
|
|
743
|
+
const front = this.#publish &&
|
|
744
|
+
(wireOf(this.#publish).local(msg.broadcast) ?? (await wireOf(this.#publish).demand(msg.broadcast)));
|
|
745
|
+
if (!front)
|
|
746
|
+
throw new NotFound(`broadcast ${msg.broadcast}`);
|
|
747
|
+
const info = await this.#resolveTrackInfo(front, msg.track);
|
|
420
748
|
await info.encode(stream.writer, this.version);
|
|
421
749
|
console.debug(`track info: broadcast=${msg.broadcast} track=${msg.track}`);
|
|
422
750
|
stream.close();
|
|
@@ -427,34 +755,33 @@ export class Publisher {
|
|
|
427
755
|
}
|
|
428
756
|
}
|
|
429
757
|
// Resolve (and cache) a track's immutable TRACK_INFO by asking the application.
|
|
430
|
-
// `
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
758
|
+
// `resolveTrackInfo` triggers a TrackRequest the app answers with accept(TrackInfo);
|
|
759
|
+
// only the immutable properties are needed (not the groups). Cached because they're
|
|
760
|
+
// fixed for the track's lifetime. Rejects if the track is unavailable.
|
|
761
|
+
#resolveTrackInfo(front, track) {
|
|
762
|
+
let tracks = this.#trackInfo.get(front);
|
|
763
|
+
if (!tracks) {
|
|
764
|
+
tracks = new Map();
|
|
765
|
+
this.#trackInfo.set(front, tracks);
|
|
766
|
+
}
|
|
767
|
+
const cached = tracks.get(track);
|
|
768
|
+
if (cached !== undefined)
|
|
438
769
|
return cached;
|
|
439
770
|
const pending = (async () => {
|
|
440
|
-
const
|
|
441
|
-
if (!published)
|
|
442
|
-
throw new Error("not found");
|
|
443
|
-
const info = await published.track(track).info();
|
|
771
|
+
const info = await wireOf(front).resolveTrackInfo(track);
|
|
444
772
|
return new TrackInfoMessage({
|
|
445
773
|
priority: info.priority,
|
|
446
|
-
|
|
447
|
-
// Publisher Max Latency: the publisher's retention bound, advertised so
|
|
774
|
+
// Publisher Max Age: the publisher's retention bound, advertised so
|
|
448
775
|
// relays re-serve with the same window.
|
|
449
|
-
|
|
776
|
+
maxAge: info.maxAge,
|
|
450
777
|
// Lite05 mandates per-frame timestamps. Advertise the track's timescale;
|
|
451
|
-
// `#
|
|
778
|
+
// `#serveGroup` emits each frame converted to it.
|
|
452
779
|
timescale: info.timescale,
|
|
453
780
|
});
|
|
454
781
|
})();
|
|
455
782
|
// Don't poison the cache on failure: a later request may succeed.
|
|
456
|
-
pending.catch(() =>
|
|
457
|
-
|
|
783
|
+
pending.catch(() => tracks.delete(track));
|
|
784
|
+
tracks.set(track, pending);
|
|
458
785
|
return pending;
|
|
459
786
|
}
|
|
460
787
|
/**
|
|
@@ -475,9 +802,9 @@ export class Publisher {
|
|
|
475
802
|
const datagram = await track.recvDatagram();
|
|
476
803
|
if (!datagram)
|
|
477
804
|
return; // Track finished; #runTrack tears the subscription down.
|
|
478
|
-
// Convert the timestamp to the track's advertised timescale, matching #
|
|
805
|
+
// Convert the timestamp to the track's advertised timescale, matching #serveGroup.
|
|
479
806
|
const ts = Math.round(datagram.timestamp.as(timescale));
|
|
480
|
-
const body = new DatagramMessage(sub, datagram.sequence, ts, datagram.payload).encode();
|
|
807
|
+
const body = new DatagramMessage(sub, datagram.sequence, ts, datagram.payload).encode(this.version);
|
|
481
808
|
// No group fallback: drop anything that doesn't fit a single datagram.
|
|
482
809
|
if (body.byteLength > maxSize) {
|
|
483
810
|
console.debug(`dropping oversize datagram: sub=${sub} size=${body.byteLength} max=${maxSize}`);
|
|
@@ -494,10 +821,18 @@ export class Publisher {
|
|
|
494
821
|
}
|
|
495
822
|
// Serialize a fetched group's frames onto the FETCH stream as bare records: each a
|
|
496
823
|
// zigzag-delta timestamp (at the track's advertised timescale) followed by size + bytes.
|
|
497
|
-
async #runFetchGroup(group, stream, timescale) {
|
|
824
|
+
async #runFetchGroup(group, stream, { timescale, start: startFrame, end: endFrame }) {
|
|
825
|
+
// The response carries no header, so the receiver numbers the first frame it gets
|
|
826
|
+
// as `startFrame`. Skipping the head here is the only thing keeping those numbers
|
|
827
|
+
// honest; a group that ends before we reach it can't be served at all.
|
|
828
|
+
for (let i = 0; i < startFrame; i++) {
|
|
829
|
+
if (!(await race([group.readFrame(), stream.closed]))) {
|
|
830
|
+
throw new Error(`fetch group ended at frame ${i}, before the requested start ${startFrame}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
498
833
|
let prevTs = 0n;
|
|
499
|
-
for (
|
|
500
|
-
const frame = await
|
|
834
|
+
for (let index = startFrame; endFrame === undefined || index <= endFrame; index++) {
|
|
835
|
+
const frame = await race([group.readFrame(), stream.closed]);
|
|
501
836
|
if (!frame)
|
|
502
837
|
break;
|
|
503
838
|
const ts = BigInt(Math.round(frame.timestamp.as(timescale)));
|
|
@@ -508,13 +843,13 @@ export class Publisher {
|
|
|
508
843
|
}
|
|
509
844
|
}
|
|
510
845
|
/**
|
|
511
|
-
*
|
|
846
|
+
* Opens the unidirectional stream for one group, or closes the group and resolves
|
|
847
|
+
* `undefined` when it cannot get one.
|
|
512
848
|
*
|
|
513
849
|
* @internal
|
|
514
850
|
*/
|
|
515
|
-
async #
|
|
516
|
-
const {
|
|
517
|
-
const msg = new GroupMessage(sub, group.sequence);
|
|
851
|
+
async #openGroup(options) {
|
|
852
|
+
const { group, priority, unsubscribed } = options;
|
|
518
853
|
try {
|
|
519
854
|
// The transport drains streams by send order, so this is what makes a high-priority
|
|
520
855
|
// track (and a newer group within it) win the link when there isn't room for both.
|
|
@@ -524,55 +859,92 @@ export class Publisher {
|
|
|
524
859
|
// in the order we asked, which is oldest-first, exactly backwards for live media.
|
|
525
860
|
// Failing here drops the group and lets the next one compete for the next slot.
|
|
526
861
|
const stream = await Writer.tryOpen(this.#quic, {
|
|
862
|
+
version: this.version,
|
|
527
863
|
sendOrder: priority.rank(group.sequence),
|
|
528
864
|
cancel: unsubscribed,
|
|
529
865
|
waitUntilAvailable: false,
|
|
530
866
|
});
|
|
531
|
-
if (!stream)
|
|
867
|
+
if (!stream)
|
|
532
868
|
group.close(new Error("no stream slot"));
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
869
|
+
return stream;
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
group.close(error(err));
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Serves one group on the stream {@link #openGroup} opened for it.
|
|
878
|
+
*
|
|
879
|
+
* @internal
|
|
880
|
+
*/
|
|
881
|
+
async #serveGroup(stream, options) {
|
|
882
|
+
const { sub, group, timescale, priority, start: startFrame, end: endFrame } = options;
|
|
883
|
+
// This model holds whole groups, so frame `startFrame` is always reachable unless
|
|
884
|
+
// the group ends first. Declaring it up front keeps the stream self-describing.
|
|
885
|
+
const msg = new GroupMessage({ subscribe: sub, sequence: group.sequence, frameStart: startFrame });
|
|
886
|
+
// Everything past this point runs inside the cleanup scope, so a failure never leaves
|
|
887
|
+
// a finished group's stream being ranked.
|
|
888
|
+
try {
|
|
889
|
+
// A SUBSCRIBE_UPDATE re-ranks the subscription, so a group already on the wire
|
|
890
|
+
// follows it too rather than keeping a stale rank until it finishes.
|
|
891
|
+
priority.add(stream, group.sequence);
|
|
892
|
+
await hooks.guardGroup(group, async () => {
|
|
541
893
|
await stream.u53(0); // stream type
|
|
542
|
-
await msg.encode(stream);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
894
|
+
await msg.encode(stream, this.version);
|
|
895
|
+
});
|
|
896
|
+
// Lite05+ prefixes every frame with a zigzag-delta timestamp at the track's
|
|
897
|
+
// advertised timescale; older drafts omit it.
|
|
898
|
+
const timestamps = supportsTrackStream(this.version);
|
|
899
|
+
let prevTs = 0n;
|
|
900
|
+
// Whether the cursor ever reached the requested start, which decides how the
|
|
901
|
+
// end of the group is read below.
|
|
902
|
+
let reached = startFrame === 0;
|
|
903
|
+
for (;;) {
|
|
904
|
+
const read = await race([hooks.readGroupFrame(group), stream.closed]);
|
|
905
|
+
if (!read) {
|
|
906
|
+
// The group ended before the frame the subscriber asked to start
|
|
907
|
+
// at, so this publisher can't serve the range at all. FINning here
|
|
908
|
+
// would claim an empty group under that index; reset so it reads
|
|
909
|
+
// as the gap it is.
|
|
910
|
+
if (!reached)
|
|
911
|
+
throw new Error(`group ended before frame ${startFrame}`);
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
// A group that ends exactly at the start is a valid, empty range.
|
|
916
|
+
if (read.sequence + 1 >= startFrame)
|
|
917
|
+
reached = true;
|
|
918
|
+
// Frames below the requested start were excluded, and the receiver
|
|
919
|
+
// numbers what it gets from `startFrame`.
|
|
920
|
+
if (read.sequence < startFrame)
|
|
921
|
+
continue;
|
|
922
|
+
if (endFrame !== undefined && read.sequence > endFrame)
|
|
550
923
|
break;
|
|
551
924
|
if (timestamps) {
|
|
552
925
|
// 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));
|
|
926
|
+
const ts = BigInt(Math.round(read.frame.timestamp.as(timescale)));
|
|
927
|
+
await hooks.guardGroup(group, () => stream.u62(zigzag(ts - prevTs)));
|
|
555
928
|
prevTs = ts;
|
|
556
929
|
}
|
|
557
|
-
await stream.u53(frame.payload.byteLength);
|
|
558
|
-
await stream.write(frame.payload);
|
|
930
|
+
await hooks.guardGroup(group, () => stream.u53(read.frame.payload.byteLength));
|
|
931
|
+
await hooks.guardGroup(group, () => stream.write(read.frame.payload));
|
|
932
|
+
}
|
|
933
|
+
finally {
|
|
934
|
+
read.complete();
|
|
559
935
|
}
|
|
560
|
-
stream.close();
|
|
561
|
-
group.close();
|
|
562
|
-
}
|
|
563
|
-
catch (err) {
|
|
564
|
-
const e = error(err);
|
|
565
|
-
stream.reset(e);
|
|
566
|
-
group.close(e);
|
|
567
|
-
}
|
|
568
|
-
finally {
|
|
569
|
-
priority.remove(stream);
|
|
570
936
|
}
|
|
937
|
+
stream.close();
|
|
938
|
+
group.close();
|
|
571
939
|
}
|
|
572
940
|
catch (err) {
|
|
573
941
|
const e = error(err);
|
|
942
|
+
stream.reset(e);
|
|
574
943
|
group.close(e);
|
|
575
944
|
}
|
|
945
|
+
finally {
|
|
946
|
+
priority.remove(stream);
|
|
947
|
+
}
|
|
576
948
|
}
|
|
577
949
|
/**
|
|
578
950
|
* Handles a probe stream by periodically reporting estimated bitrate.
|
|
@@ -605,7 +977,7 @@ export class Publisher {
|
|
|
605
977
|
try {
|
|
606
978
|
for (;;) {
|
|
607
979
|
const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), PROBE_INTERVAL));
|
|
608
|
-
const result = await
|
|
980
|
+
const result = await race([timeout, stream.reader.closed]);
|
|
609
981
|
if (result !== "timeout")
|
|
610
982
|
break;
|
|
611
983
|
// The two fields are independent on the wire, each using 0 for
|
|
@@ -658,12 +1030,8 @@ export class Publisher {
|
|
|
658
1030
|
}
|
|
659
1031
|
}
|
|
660
1032
|
close() {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
broadcast.close();
|
|
664
|
-
}
|
|
665
|
-
return undefined;
|
|
666
|
-
});
|
|
1033
|
+
// The broadcasts belong to the origin, which outlives this session; closing here
|
|
1034
|
+
// only drops the borrow. The peer sees the unannounce when the streams die.
|
|
667
1035
|
// Release the datagram writer's lock so the stream can be torn down.
|
|
668
1036
|
this.#datagramWriter?.releaseLock();
|
|
669
1037
|
this.#datagramWriter = undefined;
|