@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.
- package/README.md +1 -0
- package/announced.d.ts +111 -21
- package/announced.d.ts.map +1 -1
- package/announced.js +213 -40
- package/announced.js.map +1 -1
- package/broadcast.d.ts +80 -25
- package/broadcast.d.ts.map +1 -1
- package/broadcast.js +270 -49
- package/broadcast.js.map +1 -1
- package/connection/accept.d.ts +6 -0
- package/connection/accept.d.ts.map +1 -1
- package/connection/accept.js +42 -13
- package/connection/accept.js.map +1 -1
- package/connection/browser.d.ts +5 -0
- package/connection/browser.d.ts.map +1 -0
- package/connection/browser.js +31 -0
- package/connection/browser.js.map +1 -0
- package/connection/connect.d.ts +43 -1
- package/connection/connect.d.ts.map +1 -1
- package/connection/connect.js +118 -123
- package/connection/connect.js.map +1 -1
- package/connection/established.d.ts +43 -16
- package/connection/established.d.ts.map +1 -1
- package/connection/established.js.map +1 -1
- package/connection/handshake.d.ts +12 -1
- package/connection/handshake.d.ts.map +1 -1
- package/connection/handshake.js +30 -14
- package/connection/handshake.js.map +1 -1
- package/connection/index.d.ts +8 -0
- package/connection/index.d.ts.map +1 -1
- package/connection/index.js +6 -0
- package/connection/index.js.map +1 -1
- package/connection/reload.d.ts +66 -9
- package/connection/reload.d.ts.map +1 -1
- package/connection/reload.js +192 -66
- package/connection/reload.js.map +1 -1
- package/connection/stats.d.ts +92 -0
- package/connection/stats.d.ts.map +1 -0
- package/connection/stats.js +35 -0
- package/connection/stats.js.map +1 -0
- package/connection/transport.d.ts +10 -0
- package/connection/transport.d.ts.map +1 -0
- package/connection/transport.js +12 -0
- package/connection/transport.js.map +1 -0
- package/consume.d.ts +44 -0
- package/consume.d.ts.map +1 -0
- package/consume.js +60 -0
- package/consume.js.map +1 -0
- package/datagram.d.ts +23 -0
- package/datagram.d.ts.map +1 -0
- package/datagram.js +3 -0
- package/datagram.js.map +1 -0
- package/error.d.ts +76 -0
- package/error.d.ts.map +1 -0
- package/error.js +107 -0
- package/error.js.map +1 -0
- package/group.d.ts +143 -38
- package/group.d.ts.map +1 -1
- package/group.js +270 -68
- package/group.js.map +1 -1
- package/ietf/adapter.d.ts +2 -2
- package/ietf/adapter.d.ts.map +1 -1
- package/ietf/adapter.js +20 -10
- package/ietf/adapter.js.map +1 -1
- package/ietf/aliases.d.ts +73 -5
- package/ietf/aliases.d.ts.map +1 -1
- package/ietf/aliases.js +121 -14
- package/ietf/aliases.js.map +1 -1
- package/ietf/cluster.d.ts +141 -0
- package/ietf/cluster.d.ts.map +1 -0
- package/ietf/cluster.js +204 -0
- package/ietf/cluster.js.map +1 -0
- package/ietf/connection.d.ts +39 -6
- package/ietf/connection.d.ts.map +1 -1
- package/ietf/connection.js +73 -12
- package/ietf/connection.js.map +1 -1
- package/ietf/fetch.d.ts +17 -0
- package/ietf/fetch.d.ts.map +1 -1
- package/ietf/fetch.js +21 -0
- package/ietf/fetch.js.map +1 -1
- package/ietf/filter.d.ts +87 -0
- package/ietf/filter.d.ts.map +1 -0
- package/ietf/filter.js +292 -0
- package/ietf/filter.js.map +1 -0
- package/ietf/index.d.ts +2 -0
- package/ietf/index.d.ts.map +1 -1
- package/ietf/index.js +3 -0
- package/ietf/index.js.map +1 -1
- package/ietf/namespace.d.ts +6 -0
- package/ietf/namespace.d.ts.map +1 -1
- package/ietf/namespace.js +27 -2
- package/ietf/namespace.js.map +1 -1
- package/ietf/object.d.ts +54 -6
- package/ietf/object.d.ts.map +1 -1
- package/ietf/object.js +178 -25
- package/ietf/object.js.map +1 -1
- package/ietf/parameters.d.ts +35 -10
- package/ietf/parameters.d.ts.map +1 -1
- package/ietf/parameters.js +236 -45
- package/ietf/parameters.js.map +1 -1
- package/ietf/priority.d.ts +5 -0
- package/ietf/priority.d.ts.map +1 -0
- package/ietf/priority.js +10 -0
- package/ietf/priority.js.map +1 -0
- package/ietf/properties.d.ts +13 -2
- package/ietf/properties.d.ts.map +1 -1
- package/ietf/properties.js +65 -8
- package/ietf/properties.js.map +1 -1
- package/ietf/publish.d.ts.map +1 -1
- package/ietf/publish.js +13 -3
- package/ietf/publish.js.map +1 -1
- package/ietf/publish_namespace.d.ts +16 -2
- package/ietf/publish_namespace.d.ts.map +1 -1
- package/ietf/publish_namespace.js +25 -6
- package/ietf/publish_namespace.js.map +1 -1
- package/ietf/publisher.d.ts +40 -7
- package/ietf/publisher.d.ts.map +1 -1
- package/ietf/publisher.js +751 -134
- package/ietf/publisher.js.map +1 -1
- package/ietf/request.js +1 -1
- package/ietf/request.js.map +1 -1
- package/ietf/solicit.d.ts +41 -0
- package/ietf/solicit.d.ts.map +1 -0
- package/ietf/solicit.js +47 -0
- package/ietf/solicit.js.map +1 -0
- package/ietf/subscribe.d.ts +41 -2
- package/ietf/subscribe.d.ts.map +1 -1
- package/ietf/subscribe.js +103 -22
- package/ietf/subscribe.js.map +1 -1
- package/ietf/subscribe_namespace.d.ts +21 -7
- package/ietf/subscribe_namespace.d.ts.map +1 -1
- package/ietf/subscribe_namespace.js +31 -14
- package/ietf/subscribe_namespace.js.map +1 -1
- package/ietf/subscriber.d.ts +20 -7
- package/ietf/subscriber.d.ts.map +1 -1
- package/ietf/subscriber.js +512 -142
- package/ietf/subscriber.js.map +1 -1
- package/ietf/version.d.ts +7 -1
- package/ietf/version.d.ts.map +1 -1
- package/ietf/version.js +7 -0
- package/ietf/version.js.map +1 -1
- package/index.d.ts +10 -5
- package/index.d.ts.map +1 -1
- package/index.js +10 -5
- package/index.js.map +1 -1
- package/internal.d.ts +29 -0
- package/internal.d.ts.map +1 -0
- package/internal.js +8 -0
- package/internal.js.map +1 -0
- package/lite/announce.d.ts +62 -15
- package/lite/announce.d.ts.map +1 -1
- package/lite/announce.js +230 -94
- package/lite/announce.js.map +1 -1
- package/lite/connection.d.ts +69 -20
- package/lite/connection.d.ts.map +1 -1
- package/lite/connection.js +128 -81
- package/lite/connection.js.map +1 -1
- package/lite/datagram.d.ts +23 -0
- package/lite/datagram.d.ts.map +1 -0
- package/lite/datagram.js +57 -0
- package/lite/datagram.js.map +1 -0
- package/lite/datagram_stream.d.ts +8 -0
- package/lite/datagram_stream.d.ts.map +1 -0
- package/lite/datagram_stream.js +44 -0
- package/lite/datagram_stream.js.map +1 -0
- package/lite/fetch.d.ts.map +1 -1
- package/lite/fetch.js +2 -2
- package/lite/fetch.js.map +1 -1
- package/lite/index.d.ts +3 -0
- package/lite/index.d.ts.map +1 -1
- package/lite/index.js +3 -0
- package/lite/index.js.map +1 -1
- package/lite/priority.d.ts +67 -0
- package/lite/priority.d.ts.map +1 -0
- package/lite/priority.js +114 -0
- package/lite/priority.js.map +1 -0
- package/lite/probe.d.ts +18 -2
- package/lite/probe.d.ts.map +1 -1
- package/lite/probe.js +24 -22
- package/lite/probe.js.map +1 -1
- package/lite/publisher.d.ts +14 -7
- package/lite/publisher.d.ts.map +1 -1
- package/lite/publisher.js +406 -84
- package/lite/publisher.js.map +1 -1
- package/lite/setup.d.ts +83 -6
- package/lite/setup.d.ts.map +1 -1
- package/lite/setup.js +203 -31
- package/lite/setup.js.map +1 -1
- package/lite/stream.d.ts +12 -3
- package/lite/stream.d.ts.map +1 -1
- package/lite/stream.js +7 -4
- package/lite/stream.js.map +1 -1
- package/lite/subscribe.d.ts +37 -23
- package/lite/subscribe.d.ts.map +1 -1
- package/lite/subscribe.js +70 -55
- package/lite/subscribe.js.map +1 -1
- package/lite/subscriber.d.ts +29 -23
- package/lite/subscriber.d.ts.map +1 -1
- package/lite/subscriber.js +689 -112
- package/lite/subscriber.js.map +1 -1
- package/lite/track.d.ts +25 -13
- package/lite/track.d.ts.map +1 -1
- package/lite/track.js +60 -35
- package/lite/track.js.map +1 -1
- package/lite/version.d.ts +39 -4
- package/lite/version.d.ts.map +1 -1
- package/lite/version.js +139 -17
- package/lite/version.js.map +1 -1
- package/mock.d.ts +53 -3
- package/mock.d.ts.map +1 -1
- package/mock.js +110 -21
- package/mock.js.map +1 -1
- package/{lite/origin.d.ts → origin.d.ts} +25 -1
- package/origin.d.ts.map +1 -0
- package/{lite/origin.js → origin.js} +25 -1
- package/origin.js.map +1 -0
- package/package.json +6 -4
- package/path.d.ts +67 -0
- package/path.d.ts.map +1 -1
- package/path.js +128 -0
- package/path.js.map +1 -1
- package/stream.d.ts +94 -4
- package/stream.d.ts.map +1 -1
- package/stream.js +128 -17
- package/stream.js.map +1 -1
- package/time.d.ts +109 -86
- package/time.d.ts.map +1 -1
- package/time.js +80 -30
- package/time.js.map +1 -1
- package/track.d.ts +298 -47
- package/track.d.ts.map +1 -1
- package/track.js +721 -109
- package/track.js.map +1 -1
- package/util/timeout.d.ts +5 -0
- package/util/timeout.d.ts.map +1 -0
- package/util/timeout.js +18 -0
- package/util/timeout.js.map +1 -0
- package/util/utf8.d.ts +3 -0
- package/util/utf8.d.ts.map +1 -0
- package/util/utf8.js +7 -0
- package/util/utf8.js.map +1 -0
- package/varint.d.ts +14 -3
- package/varint.d.ts.map +1 -1
- package/varint.js +23 -12
- package/varint.js.map +1 -1
- package/zod.d.ts +5 -5
- package/zod.d.ts.map +1 -1
- package/zod.js.map +1 -1
- package/bandwidth.d.ts +0 -11
- package/bandwidth.d.ts.map +0 -1
- package/bandwidth.js +0 -7
- package/bandwidth.js.map +0 -1
- package/connection/cert.test.d.ts +0 -2
- package/connection/cert.test.d.ts.map +0 -1
- package/connection/cert.test.js +0 -26
- package/connection/cert.test.js.map +0 -1
- package/connection/reload.test.d.ts +0 -2
- package/connection/reload.test.d.ts.map +0 -1
- package/connection/reload.test.js +0 -39
- package/connection/reload.test.js.map +0 -1
- package/group.test.d.ts +0 -2
- package/group.test.d.ts.map +0 -1
- package/group.test.js +0 -66
- package/group.test.js.map +0 -1
- package/ietf/aliases.test.d.ts +0 -2
- package/ietf/aliases.test.d.ts.map +0 -1
- package/ietf/aliases.test.js +0 -30
- package/ietf/aliases.test.js.map +0 -1
- package/ietf/ietf.test.d.ts +0 -2
- package/ietf/ietf.test.d.ts.map +0 -1
- package/ietf/ietf.test.js +0 -873
- package/ietf/ietf.test.js.map +0 -1
- package/integration.test.d.ts +0 -2
- package/integration.test.d.ts.map +0 -1
- package/integration.test.js +0 -90
- package/integration.test.js.map +0 -1
- package/lite/origin.d.ts.map +0 -1
- package/lite/origin.js.map +0 -1
- package/lite/subscriber.test.d.ts +0 -2
- package/lite/subscriber.test.d.ts.map +0 -1
- package/lite/subscriber.test.js +0 -30
- package/lite/subscriber.test.js.map +0 -1
- package/lite/track.test.d.ts +0 -2
- package/lite/track.test.d.ts.map +0 -1
- package/lite/track.test.js +0 -53
- package/lite/track.test.js.map +0 -1
- package/path.test.d.ts +0 -2
- package/path.test.d.ts.map +0 -1
- package/path.test.js +0 -169
- package/path.test.js.map +0 -1
- package/stream.test.d.ts +0 -2
- package/stream.test.d.ts.map +0 -1
- package/stream.test.js +0 -210
- package/stream.test.js.map +0 -1
- package/track.test.d.ts +0 -2
- package/track.test.d.ts.map +0 -1
- package/track.test.js +0 -56
- package/track.test.js.map +0 -1
- package/util/error.d.ts +0 -3
- package/util/error.d.ts.map +0 -1
- package/util/error.js +0 -9
- package/util/error.js.map +0 -1
- package/util/index.d.ts +0 -2
- package/util/index.d.ts.map +0 -1
- package/util/index.js +0 -3
- package/util/index.js.map +0 -1
- package/varint.test.d.ts +0 -2
- package/varint.test.d.ts.map +0 -1
- package/varint.test.js +0 -106
- 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 {
|
|
6
|
-
import {
|
|
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 {
|
|
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.
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
123
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
130
|
-
|
|
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,
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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}
|
|
168
|
-
track.
|
|
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
|
|
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
|
-
*
|
|
301
|
+
* Handles a FETCH stream by serving one group as bare frame records (lite-05+).
|
|
184
302
|
*
|
|
185
303
|
* @internal
|
|
186
304
|
*/
|
|
187
|
-
async
|
|
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
|
-
|
|
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
|
|
213
|
-
|
|
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 (!
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
void this.#runGroup(sub,
|
|
390
|
+
end = Math.max(end, group.sequence + 1);
|
|
391
|
+
void this.#runGroup({ sub, group, timescale, priority, unsubscribed });
|
|
226
392
|
}
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
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
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* @
|
|
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 #
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
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 (
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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 (
|
|
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
|
-
|
|
327
|
-
|
|
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
|
|
331
|
-
|
|
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
|